Tek Sorumluluk İlkesi: Büyük Kodun Tarifi
Yayınlanan: 2022-03-11Harika kod olarak gördüğümüz şey ne olursa olsun, her zaman basit bir kalite gerektirir: kodun bakımı yapılabilir olmalıdır. Uygun girinti, düzgün değişken adları, %100 test kapsamı vb. sizi ancak bir yere kadar götürebilir. Bakımı yapılamayan ve değişen gereksinimlere görece kolaylıkla uyum sağlayamayan herhangi bir kod, yalnızca modasının geçmesini bekleyen kodlardır. Bir prototip, bir kavram kanıtı veya minimum uygulanabilir bir ürün oluşturmaya çalışırken harika kod yazmamız gerekmeyebilir, ancak diğer tüm durumlarda her zaman bakımı yapılabilir kod yazmalıyız. Bu, yazılım mühendisliği ve tasarımının temel bir kalitesi olarak düşünülmesi gereken bir şeydir.
Bu yazıda, Tek Sorumluluk İlkesinin ve onun etrafında dönen bazı tekniklerin kodunuza bu kaliteyi nasıl verebileceğini tartışacağım. Harika kod yazmak bir sanattır, ancak bazı ilkeler her zaman geliştirme çalışmanıza sağlam ve sürdürülebilir yazılım üretmek için ihtiyaç duyduğu yönü vermenize yardımcı olabilir.
Model Her Şeydir
Bazı yeni MVC (MVP, MVVM veya diğer M**) çerçeveleriyle ilgili hemen hemen her kitap, kötü kod örnekleriyle doludur. Bu örnekler, çerçevenin neler sunabileceğini göstermeye çalışır. Ama aynı zamanda yeni başlayanlar için kötü tavsiyeler veriyorlar. “Diyelim ki modellerimiz için bu ORM X'e sahibiz, görüşlerimiz için Y motorunu tasarlıyoruz ve hepsini yönetecek kontrolörlerimiz olacak” gibi örnekler, devasa kontrolörlerden başka bir şey sağlamaz.
Her ne kadar bu kitapların savunmasında olsalar da, örnekler, çerçevelerine ne kadar kolay başlayabileceğinizi göstermek içindir. Yazılım tasarımını öğretmeleri amaçlanmamıştır. Ancak bu örnekleri izleyen okuyucular, projelerinde tek parça kod parçalarına sahip olmanın ne kadar verimsiz olduğunu ancak yıllar sonra fark ederler.
Modeller, uygulamanızın kalbidir. Uygulama mantığınızın geri kalanından ayrılmış modelleriniz varsa, uygulamanız ne kadar karmaşık olursa olsun bakım çok daha kolay olacaktır. Karmaşık uygulamalar için bile, iyi bir model uygulaması son derece anlamlı kodla sonuçlanabilir. Ve bunu başarmak için, modellerinizin yalnızca yapmaları gerekeni yaptığından emin olarak başlayın ve etrafında oluşturulan uygulamanın ne yaptığıyla ilgilenmeyin. Ayrıca, temeldeki veri depolama katmanının ne olduğuyla ilgilenmez: uygulamanız bir SQL veritabanına mı dayanıyor, yoksa her şeyi metin dosyalarında mı saklıyor?
Bu makaleye devam ederken, endişenin ayrılması konusunda kodun ne kadar harika olduğunu anlayacaksınız.
Tek Sorumluluk İlkesi
Muhtemelen SOLID ilkelerini duymuşsunuzdur: tek sorumluluk, açık-kapalı, liskov ikamesi, arayüz ayrımı ve bağımlılığın tersine çevrilmesi. İlk harf olan S, Tek Sorumluluk İlkesini (SRP) temsil eder ve önemi göz ardı edilemez. İyi kod için gerekli ve yeterli bir koşul olduğunu bile iddia ediyorum. Aslında, kötü yazılmış herhangi bir kodda, her zaman birden fazla sorumluluğa sahip bir sınıf bulabilirsiniz - form1.cs veya index.php birkaç bin satır kod içeren nadir bir şey değildir ve hepimiz muhtemelen görmüş veya yapmıştır.
C#'daki bir örneğe bakalım (ASP.NET MVC ve Entity framework). Bir C# geliştiricisi olmasanız bile, biraz OOP deneyimi ile kolayca takip edebileceksiniz.
public class OrderController { ... public ActionResult CreateForm() { /* * View data preparations */ return View(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } using (var context = new DataContext()) { var order = new Order(); // Create order from request context.Orders.Add(order); // Reserve ordered goods …(Huge logic here)... context.SaveChanges(); //Send email with order details for customer } return RedirectToAction("Index"); } ... (many more methods like Create here) }
Bu olağan bir OrderController sınıfıdır, Create yöntemi gösterilmektedir. Bunun gibi kontrolörlerde, Order sınıfının kendisinin bir request parametresi olarak kullanıldığı durumları sıklıkla görüyorum. Ancak özel istek sınıflarını kullanmayı tercih ederim. Yine, SRP!
Yukarıdaki kod parçacığında, denetleyicinin Order nesnesini depolamak, e-posta göndermek vb. dahil ancak bunlarla sınırlı olmamak üzere "sipariş verme" hakkında çok fazla şey bildiğine dikkat edin. Bu, tek bir sınıf için çok fazla iş demektir. Her küçük değişiklik için geliştiricinin tüm denetleyici kodunu değiştirmesi gerekir. Ve başka bir Denetleyicinin de sipariş oluşturması gerektiğinde, geliştiriciler çoğu zaman kodu kopyalayıp yapıştırmaya başvuracaktır. Denetleyiciler yalnızca genel süreci kontrol etmeli ve aslında sürecin her bir mantığını barındırmamalıdır.
Ama bugün bu devasa denetleyicileri yazmayı bıraktığımız gün!
Önce tüm iş mantığını denetleyiciden çıkaralım ve onu bir OrderService sınıfına taşıyalım:
public class OrderService { public void Create(OrderCreateRequest request) { // all actions for order creating here } } public class OrderController { public OrderController() { this.service = new OrderService(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } this.service.Create(request); return RedirectToAction("Index"); }
Bu yapıldığında, kontrolör artık sadece yapmak istediği şeyi yapar: süreci kontrol eder. Yalnızca görünümler, OrderService ve OrderRequest sınıfları hakkında bilgi sahibidir - bu, isteklerini yönetmek ve yanıtları göndermek olan işini yapması için gereken en az bilgi kümesidir.
Bu şekilde, kontrolör kodunu nadiren değiştireceksiniz. Görünümler, istek nesneleri ve hizmetler gibi diğer bileşenler, iş gereksinimlerine bağlı olduklarından ancak denetleyicilere bağlı olmadığı için değişebilir.
SRP'nin konusu budur ve bu ilkeyi karşılayan kod yazmak için birçok teknik vardır. Buna bir örnek, bağımlılık enjeksiyonudur (test edilebilir kod yazmak için de yararlı olan bir şey).
Bağımlılık Enjeksiyonu
Bağımlılık Enjeksiyonu olmadan Tek Sorumluluk İlkesine dayalı büyük bir proje hayal etmek zor. OrderService sınıfımıza tekrar bir göz atalım:
public class OrderService { public void Create(...) { // Creating the order(and let's forget about reserving here, it's not important for following examples) // Sending an email to client with order details var smtp = new SMTP(); // Setting smtp.Host, UserName, Password and other parameters smtp.Send(); } }
Bu kod çalışır, ancak oldukça ideal değildir. OrderService sınıfı oluşturma yönteminin nasıl çalıştığını anlamak için SMTP'nin inceliklerini anlamak zorunda kalırlar. Ve yine, kopyala-yapıştır, bu SMTP kullanımını ihtiyaç duyulan her yerde çoğaltmanın tek çıkış yoludur. Ancak küçük bir yeniden düzenleme ile bu değişebilir:
public class OrderService { private SmtpMailer mailer; public OrderService() { this.mailer = new SmtpMailer(); } public void Create(...) { // Creating the order // Sending an email to client with order details this.mailer.Send(...); } } public class SmtpMailer { public void Send(string to, string subject, string body) { // SMTP stuff will be only here } }
Zaten çok daha iyi! Ancak OrderService sınıfı hala e-posta gönderme hakkında çok şey biliyor. E-posta göndermek için tam olarak SmtpMailer sınıfına ihtiyaç duyar. Peki ya gelecekte değiştirmek istersek? Gönderilen e-postanın içeriğini geliştirme ortamımıza göndermek yerine özel bir günlük dosyasına yazdırmak istersek ne olur? OrderService sınıfımızı birim testi yapmak istersek ne olur? Bir arayüz IMailer oluşturarak yeniden düzenlemeye devam edelim:

public interface IMailer { void Send(string to, string subject, string body); }
SmtpMailer bu arayüzü uygulayacaktır. Ayrıca uygulamamız bir IoC kapsayıcı kullanacak ve onu IMailer'in SmtpMailer sınıfı tarafından uygulanması için yapılandırabiliriz. OrderService daha sonra aşağıdaki gibi değiştirilebilir:
public sealed class OrderService: IOrderService { private IOrderRepository repository; private IMailer mailer; public OrderService(IOrderRepository repository, IMailer mailer) { this.repository = repository; this.mailer = mailer; } public void Create(...) { var order = new Order(); // fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.) this.repository.Save(order); this.mailer.Send(<orders user email>, <subject>, <body with order details>); } }
Şimdi bir yerlere varıyoruz! Bu şansı başka bir değişiklik yapmak için de kullandım. OrderService artık tüm siparişlerimizi depolayan bileşenle etkileşim kurmak için IOrderRepository arayüzüne güveniyor. Artık bu arayüzün nasıl uygulandığı ve hangi depolama teknolojisinin ona güç verdiği umurunda değil. Artık OrderService sınıfı sadece sipariş iş mantığı ile ilgilenen koda sahiptir.
Bu şekilde, bir test kullanıcısı e-posta gönderirken yanlış davranan bir şey bulursa, geliştirici tam olarak nereye bakacağını bilir: SmtpMailer sınıfı. İndirimlerle ilgili bir sorun varsa, geliştirici yine nereye bakacağını bilir: OrderService (veya SRP'yi gönülden benimsediyseniz, o zaman İndirim Hizmeti olabilir) sınıf kodu.
Olay Odaklı Mimari
Ancak yine de OrderService.Create yöntemini sevmiyorum:
public void Create(...) { var order = new Order(); ... this.repository.Save(order); this.mailer.Send(<orders user email>, <subject>, <body with order details>); }
Bir e-posta göndermek, ana sipariş oluşturma akışının tam olarak bir parçası değildir. Uygulama e-postayı gönderemese bile sipariş yine de doğru şekilde oluşturulur. Ayrıca, başarılı bir sipariş verdikten sonra e-posta almaktan vazgeçmelerini sağlayan kullanıcı ayarları alanına yeni bir seçenek eklemeniz gereken bir durum hayal edin. Bunu OrderService sınıfımıza dahil etmek için bir bağımlılık olan IUserParametersService tanıtmamız gerekecek. Karışıma yerelleştirmeyi ekleyin ve başka bir bağımlılığınız daha var, ITranslator (kullanıcının seçtiği dilde doğru e-posta iletileri üretmek için). Bu eylemlerin birçoğu gereksizdir, özellikle bu kadar çok bağımlılığı ekleme ve ekrana sığmayan bir kurucu ile sonuçlanma fikri. Bunun harika bir örneğini Magento'nun kod tabanında (PHP ile yazılmış popüler bir e-ticaret CMS'si) 32 bağımlılığa sahip bir sınıfta buldum!
Bazen bu mantığın nasıl ayrılacağını anlamak zordur ve Magento'nun sınıfı muhtemelen bu durumlardan birinin kurbanıdır. Bu yüzden olaya dayalı yolu seviyorum:
namespace <base namespace>.Events { [Serializable] public class OrderCreated { private readonly Order order; public OrderCreated(Order order) { this.order = order; } public Order GetOrder() { return this.order; } } }
Bir sipariş oluşturulduğunda, doğrudan OrderService sınıfından bir e-posta göndermek yerine OrderCreated özel olay sınıfı oluşturulur ve bir olay oluşturulur. Uygulamanın bir yerinde olay işleyicileri yapılandırılacaktır. Bunlardan biri müşteriye bir e-posta gönderecek.
namespace <base namespace>.EventHandlers { public class OrderCreatedEmailSender : IEventHandler<OrderCreated> { public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator) { // this class depend on all stuff which it need to send an email. } public void Handle(OrderCreated event) { this.mailer.Send(...); } } }
OrderCreated sınıfı, bilerek Serileştirilebilir olarak işaretlenmiştir. Bu olayı hemen işleyebilir veya bir kuyrukta (Redis, ActiveMQ veya başka bir şey) seri hale getirebilir ve web isteklerini işleyenden ayrı bir işlem/iş parçacığında işleyebiliriz. Bu makalede yazar, olay güdümlü mimarinin ne olduğunu ayrıntılı olarak açıklamaktadır (lütfen OrderController içindeki iş mantığına dikkat etmeyin).
Bazıları, siparişi oluşturduğunuzda neler olup bittiğini anlamanın artık zor olduğunu iddia edebilir. Ama bu gerçeklerden daha fazla olamaz. Böyle hissediyorsanız, IDE'nizin işlevselliğinden yararlanmanız yeterlidir. IDE'de OrderCreated sınıfının tüm kullanımlarını bularak event ile ilgili tüm aksiyonları görebiliriz.
Ancak ne zaman Bağımlılık Enjeksiyonu kullanmalıyım ve ne zaman Olaya dayalı bir yaklaşım kullanmalıyım? Bu soruyu cevaplamak her zaman kolay değildir, ancak size yardımcı olabilecek basit bir kural, uygulama içindeki tüm ana faaliyetleriniz için Dependency Injection'ı ve tüm ikincil eylemler için Olaya dayalı yaklaşımı kullanmaktır. Örneğin, IOrderRepository ile OrderService sınıfı içinde bir sipariş oluşturmak gibi şeylerle Dependecy Injection'ı kullanın ve ana sipariş oluşturma akışının önemli bir parçası olmayan bir şey olan e-posta gönderimini bazı olay işleyicilerine devredin.
Çözüm
Çok ağır bir kontrolörle başladık, sadece bir sınıf ve ayrıntılı bir sınıf koleksiyonu ile bitirdik. Bu değişikliklerin avantajları, örneklerden oldukça açıktır. Ancak, bu örnekleri iyileştirmenin hala birçok yolu var. Örneğin, OrderService.Create yöntemi kendi sınıfına taşınabilir: OrderCreator. Sipariş oluşturma, Tek Sorumluluk İlkesini izleyen bağımsız bir iş mantığı birimi olduğundan, kendi bağımlılıkları ile kendi sınıfına sahip olması doğaldır. Aynı şekilde emir kaldırma ve emir iptali de kendi sınıflarında uygulanabilir.
Bu makaledeki ilk örneğe benzer bir şekilde, yüksek düzeyde eşleştirilmiş kod yazdığımda, gereksinimdeki herhangi bir küçük değişiklik kolayca kodun diğer bölümlerinde birçok değişikliğe yol açabilir. SRP, geliştiricilerin, her sınıfın kendi işine sahip olduğu, ayrıştırılmış kod yazmasına yardımcı olur. Bu işin özellikleri değişirse, geliştirici yalnızca o belirli sınıfta değişiklik yapar. Değişikliğin, tüm uygulamayı bozma olasılığı daha düşüktür, çünkü diğer sınıflar, elbette ilk etapta bozulmadıkları sürece, işlerini eskisi gibi yapıyor olmalıdır.
Bu teknikleri kullanarak önceden kod geliştirmek ve Tek Sorumluluk İlkesini takip etmek göz korkutucu bir görev gibi görünebilir, ancak proje büyüdükçe ve geliştirme devam ettikçe çabalar kesinlikle işe yarayacaktır.