Bir geliştirme yaparken başladığımız yer genelde veriyi/veri yapısını temsil edecek olan entity objelerini oluşturmak oluyor. Bundan sonrası ise bu entity ile kayıt oluşturma/güncelleme/silme/okuma(CRUD) işlemlerini yapacak servisleri oluşturmak olabiliyor. Ve hemen bu entity’ye bağlı repository, service ve controller’ları oluşturup dışarıya açabiliyoruz. Belki DTO’lar da oluşturabiliyoruz. Artık kullanıma hazır gibi, değil mi?
Bu noktada biraz daha duralım. Aslına bakıldığında entity’ler yazılımın state’ini saklamamıza yardımcı olan araçlardır. Peki state nasıl oluşuyor? State’ler belirli aksiyonlar sonucunda oluşuyor/değişiyor. Örneğin bir kullanıcı fatura kaydediyor, bu faturayı onaylıp muhasebeye iletiyor, faturayı iptal ediyor vb. Aslına bakarsanız bunları birer davranış olarak ifade edebiliriz. Ve görülüyorki aslında, biz yazılım geliştirirken kullanıcı davranışlarını temsil edecek/uygulayacak geliştirmeler yapıyoruz. Yazılımı bu davranışlara uygun şekilde organize edebilirsek Domain Driven Desing yaklaşımı tarafından ortaya atılan ve çok önemli olduğunu düşündüğüm Ubiquitous Language(Ortak Dil!) ciddi anlamda sağlanabilir.
Standart olarak oluşturduğumuz CRUD işlemleri bazen işleri kolaylaştırsa da, bazen ciddi karmaşa oluşturabilmekte. Özellikle yazılımın business logic ağırlıklı kısımlarında bu yaklaşım business logic ile ilgili bölümün uygulamanın diğer katmanlarına yayılmasına sebep olabiliyor. Ya da uygulamanın diğer parçaları tarafından manipüle edilebilir olmasına sebep oluyor.
Örneğin aşağıdaki gibi bir servisimiz olduğunu düşünelim. Invoice entity’sinin “status” adında bir field’i var. Bu status bilgisine göre Invoice onaylı ya da iptal edilmiş duruma geçebiliyor. Kaydın durumunun onaylı/iptal olması son durum olarak kabul ediliyor. Bu şekilde ya da buna benzer kullanımlarda, bu servis içerisinde çeşitli durumlara göre yapılacak işlemler bu servisin soyutlaması gereken iş kurallarını bu metodu çağıran yerlere taşımaya sebep olmakta.
private Address shipmentAddress; private Status status; public enum Status{ NEW, APPROVED, CANCELED} } @Service public class InvoiceService { private final InvoiceRepository repository; public Invoice update(Invoice invoice){ return repository.save(invoice); } ...create ...delete ...get }
Bunun yanında, iş kuralları update metodu içerisinde bir şekilde encapsule edilse bile Invoice entity’sinin doğrudan taşınması yine ciddi manada bir problem oluşturabiliyor. Bu servisin sorumluluğunda olan iş kurallarına bağlı olarak verinin saklanması gerekirken entity’nin tüm detayları dışarıdan kontrol edilebilir olduğu için yine servisin iş kuralları/sorumlulukları farkında olmadan dışarıya açılmış olabiliyor.
Peki neler yapabiliriz?
- Davranış odaklı yazılım geliştirme
- Data abstraction / Encapsulation
- Side Effect / Functional programming / Immutability
Davranış odaklı yazılım geliştirme
Yukarıda yazılımın state’nin belirli aksiyonlarda oluştuğundan/değiştiğinden bahsetmiştim. Bir faturanın onaylanmasını “kullanıcı davranışı” olarak niteleyebiliriz. Onaylamak, iptal etmek vb. gibi davranışları aşağıdaki gibi simüle edebilirsek yazılım ile business domaini arasında ortak bir dil oluşturmaya katkı sağlarız. Davranışın sorumluluklarını sınırlamış oluruz.
@Service public class InvoiceService { private final InvoiceRepository repository; public Invoice approveInvoice(final String invoiceId){ ....business validations invoice.setStatus(APPROVED); return repository.save(invoice); } }
Buna benzer şekilde kullanıcılar tarafından yapılan her davranışın servislerde davranışa uygun giriş noktası/noktaları olması hem yazılımın bakımını kolaylaştırır hem de hataların kolayca anlaşılıp çözülmesini kolaylaştırır. Aynı zamanda davranış odaklı bakıldığında fonksiyonları cohesion açısından da uygun şekilde gruplanmasına ciddi katkı sağlayacaktır.
Data abstraction / Encapsulation
Entity yazılımın state’ini temsil eder demiştim. Bu state’i soyutlarsak yazılımın state’indeki yapısal değişiklikler dış dünyayı etkilemez. Örneğin bir zamanda state’i iki parçadan oluşan hale getirebiliriz, data abstraction ile bunun etkilerini ortadan kaldırmış oluruz. Bunun dışında dışarı ile alış-veriş yapılan verinin kontrolünü ne kadar iyi yaparsak, dışarıdan alınan verinin davranışı değiştirecek etkileri o kadar bertaraf edilmiş olur. Örneğin “status” dışarıdan alınsaydı method içerisinde “status”a bağlı yapılan if’ler manipülasyona açık hale gelirdi. Peki bunu nasıl sağlayacağız? DTO objeleri niteliğindeki class’lar bize yardımcı olabilir.
@Builder public class InvoiceWriteDTO { private final String customerId; private final String shipmentAddressId; } public class InvoiceReadDTO extends InvoiceWriteDTO { private String id; private String customerNameTitle; private String shipmentAddressId; private Status status; } @Service public class InvoiceService { private final InvoiceRepository repository; public InvoiceReadDTO createANewInvoice(final InvoiceWriteDTO invoice){ ....business validations final var invoice = new Invoice(); invoice.setStatus(APPROVED); invoice.set... return mapToReadDTO(repository.save(invoice)); } }
Artık bir create methodum var ve bu methodumun business datasını tamamen soyutladım. Aynı zamanda dış dünya ile alışveriş yapacağım tüm veriyi de kısıtladım. Kullanıcının bir fatura oluşturmak için yazılıma iletmesi gereken veri yapısı tamamen net ve kısıtlı. Aynı zamanda görebileceği veri yapısı da net ve kısıtlı.
Side Effect / Immutability / Functional programming
Davranışlardan, dışarıdan yapılacak manipülasyonların engellenmesinden bahsettim. Bu konunun diğer bir boyutunu ise “side effect” teşkil etmekte. Methodlar ve servisler arası taşınan mutable objeler side effect problemini doğurabilir. Bu da yine data abstraction/encapsulation kurallarını bozar. Fonksiyonel yaklaşımına göre bir metod her ne olursa olsun aynı parametreler ile çağırıldığında hep aynı sonucu dönmesi gereklidir.
@Service public class InvoiceService { private final InvoiceRepository repository; public InvoiceReadDTO createANewInvoice(final InvoiceWriteDTO invoice){ ....business validations final var invoice = new Invoice(); invoice.setStatus(APPROVED); invoice.set... invoiceDetailService.caculateTotalAmount(invoide); return mapToReadDTO(repository.save(invoice)); } } @Service public class InvoiceDetailService { public void calculateTotalAmount(Invoice invoice){ invoice.setStatus(NEW) } }
Yukarıda “invoiceDetailService.calculateTotalAmount” metodu içerisindeki status değişikliği bir side effect oluşturur ve davranışı manipüle eder. Mutable objeler ile yapılan işlemlerde bu tarz problemlerin oluşmamasına dikkat edilmelidir. Bu problemi aşağıdaki gibi çözebiliriz;
@Service public class InvoiceDetailService { public BigDecimal calculateTotalAmount(Invoice invoice){ ... return totalAmount; } }
Bu problemden kaçınmak için Java 8 ile gelen listelerde Stream API tercih edilebilir. Aynı zamanda olabildiğince immutable objeler ile çalışmalıyız.
Sonuç olarak;
Geliştirmelerimizi olabildiğince kullanıcı davranışlarına göre gruplamak ve bu fonksiyonları olabildiğince encapsulate etmek bakımı kolay, daha anlaşılabilir yazılımlar geliştirmemize yardımcı olacaktır.