Bağımlılık Olmadan Gerçekten Modüler Kod Oluşturma

Yayınlanan: 2022-03-11

Yazılım geliştirmek harika ama… Sanırım hepimiz bunun biraz duygusal bir rollercoaster olabileceği konusunda hemfikiriz. Başlangıçta her şey harika. Saatlerce olmasa da birkaç gün içinde birbiri ardına yeni özellikler ekliyorsunuz. Bir yuvarlanıyorsun!

Birkaç ay ileri sararsanız geliştirme hızınız düşer. Eskisi kadar sıkı çalışmadığınız için mi? Pek sayılmaz. Birkaç ay daha ileri saralım ve geliştirme hızınız daha da düşer. Bu proje üzerinde çalışmak artık eğlenceli değil ve sıkıcı hale geldi.

Daha da kötüleşiyor. Uygulamanızda birden fazla hata keşfetmeye başlarsınız. Çoğu zaman, bir hatayı çözmek iki yenisini yaratır. Bu noktada şarkı söylemeye başlayabilirsiniz:

Kodda 99 küçük hata var. 99 küçük böcek. Birini indir, etrafına yama yap,

…Kodda 127 küçük hata var.

Şimdi bu proje üzerinde çalışmak konusunda nasıl hissediyorsunuz? Eğer benim gibiyseniz, muhtemelen motivasyonunuzu kaybetmeye başlarsınız. Mevcut kodda yapılan her değişikliğin öngörülemeyen sonuçları olabileceğinden, bu uygulamayı geliştirmek sadece bir acıdır.

Bu deneyim yazılım dünyasında yaygındır ve neden bu kadar çok programcının kaynak kodunu atıp her şeyi yeniden yazmak istediğini açıklayabilir.

Yazılım Geliştirmenin Zaman İçinde Yavaşlamasının Nedenleri

Peki bu sorunun nedeni nedir?

Bunun ana nedeni artan karmaşıklıktır. Deneyimlerime göre, genel karmaşıklığa en büyük katkı, yazılım projelerinin büyük çoğunluğunda her şeyin bağlantılı olduğu gerçeğidir. Her sınıfın sahip olduğu bağımlılıklar nedeniyle, e-posta gönderen sınıfta bazı kodları değiştirirseniz, kullanıcılarınız aniden kayıt olamaz. Nedenmiş? Çünkü kayıt kodunuz e-posta gönderen koda bağlıdır. Artık hataları tanıtmadan hiçbir şeyi değiştiremezsiniz. Tüm bağımlılıkları izlemek mümkün değildir.

İşte karşınızda; sorunlarımızın gerçek nedeni, kodumuzun sahip olduğu tüm bağımlılıklardan kaynaklanan karmaşıklığı arttırmaktır.

Büyük Çamur Topu ve Nasıl Azaltılır

İşin garibi, bu konu yıllardır biliniyor. Bu, “büyük çamur yumağı” adı verilen yaygın bir anti-kalıptır. Yıllar boyunca birçok farklı şirkette çalıştığım hemen hemen tüm projelerde bu tür bir mimari gördüm.

Peki bu anti-kalıp tam olarak nedir? Basitçe söylemek gerekirse, her bir elementin diğer elementlerle bağımlılığı olduğunda büyük bir çamur yumağı elde edersiniz. Aşağıda, iyi bilinen açık kaynak projesi Apache Hadoop'tan bağımlılıkların bir grafiğini görebilirsiniz. Büyük çamur yumağını (ya da daha doğrusu büyük iplik yumağını) görselleştirmek için bir daire çizer ve projeden sınıfları eşit olarak bunun üzerine yerleştirirsiniz. Birbirine bağlı olan her bir sınıf çifti arasına bir çizgi çizin. Artık sorunlarınızın kaynağını görebilirsiniz.

Birkaç düzine düğüm ve bunları birbirine bağlayan yüzlerce çizgi ile Apache Hadoop'un "büyük çamur yumağı"nın bir görselleştirmesi.

Apache Hadoop'un "büyük çamur topu"

Modüler Kodlu Çözüm

Bu yüzden kendime bir soru sordum: Karmaşıklığı azaltmak ve yine de projenin başındaki gibi eğlenmek mümkün olabilir mi? Gerçeği söylemek gerekirse, tüm karmaşıklığı ortadan kaldıramazsınız. Yeni özellikler eklemek istiyorsanız, her zaman kod karmaşıklığını artırmanız gerekecektir. Bununla birlikte, karmaşıklık taşınabilir ve ayrılabilir.

Diğer Endüstriler Bu Sorunu Nasıl Çözüyor?

Mekanik endüstrisini düşünün. Bazı küçük mekanik atölyeler makineler üretirken, bir dizi standart eleman satın alırlar, birkaç özel eleman yaratırlar ve bunları bir araya getirirler. Bu bileşenleri tamamen ayrı ayrı yapabilirler ve sonunda her şeyi bir araya getirerek sadece birkaç ince ayar yapabilirler. Bu nasıl mümkün olabilir? Cıvata boyutları gibi belirlenmiş endüstri standartları ve montaj deliklerinin boyutu ve aralarındaki mesafe gibi ön kararlarla her bir elemanın nasıl birbirine uyacağını bilirler.

Fiziksel bir mekanizmanın teknik diyagramı ve parçalarının nasıl bir araya geldiği. Parçalar, daha sonra eklenecekleri sırayla numaralandırılmıştır, ancak bu sıra soldan sağa 5, 3, 4, 1, 2 şeklindedir.

Yukarıdaki montajdaki her eleman, nihai ürün veya diğer parçaları hakkında hiçbir bilgisi olmayan ayrı bir şirket tarafından sağlanabilir. Her modüler eleman spesifikasyonlara göre üretildiği sürece, nihai cihazı planlandığı gibi oluşturabileceksiniz.

Bunu yazılım endüstrisinde çoğaltabilir miyiz?

Tabiki yapabiliriz! Arayüzler ve kontrol prensibinin tersine çevrilmesi kullanılarak; en iyi yanı, bu yaklaşımın herhangi bir nesne yönelimli dilde kullanılabilmesidir: Java, C#, Swift, TypeScript, JavaScript, PHP—liste uzayıp gidiyor. Bu yöntemi uygulamak için süslü bir çerçeveye ihtiyacınız yok. Sadece birkaç basit kurala bağlı kalmanız ve disiplinli kalmanız gerekiyor.

Kontrolün Tersine Çevirilmesi Arkadaşınızdır

Kontrolün tersine çevrilmesini ilk duyduğumda, hemen bir çözüm bulduğumu fark ettim. Mevcut bağımlılıkları alıp arayüzleri kullanarak tersine çevirme kavramıdır. Arabirimler, yöntemlerin basit bildirimleridir. Herhangi bir somut uygulama sağlamazlar. Sonuç olarak, iki öğe arasında, bunların nasıl bağlanacağı konusunda bir anlaşma olarak kullanılabilirler. İsterseniz modüler konektörler olarak kullanılabilirler. Bir öğe arabirimi ve başka bir öğe bunun için uygulamayı sağladığı sürece, birbirleri hakkında hiçbir şey bilmeden birlikte çalışabilirler. Bu dahice.

Modüler kod oluşturmak için sistemimizi nasıl ayırabileceğimizi basit bir örnek üzerinde görelim. Aşağıdaki diyagramlar basit Java uygulamaları olarak uygulanmıştır. Bunları bu GitHub deposunda bulabilirsiniz.

Sorun

Sadece bir Main sınıfı, üç servis ve tek bir Util sınıfından oluşan çok basit bir uygulamamız olduğunu varsayalım. Bu unsurlar birçok yönden birbirine bağlıdır. Aşağıda, “büyük çamur yumağı” yaklaşımını kullanan bir uygulamayı görebilirsiniz. Sınıflar sadece birbirini çağırır. Sıkıca bağlılar ve diğerlerine dokunmadan bir öğeyi kolayca çıkaramazsınız. Bu stil kullanılarak oluşturulan uygulamalar, başlangıçta hızla büyümenize olanak tanır. Bir şeylerle kolayca oynayabileceğiniz için bu tarzın kavram kanıtı projeleri için uygun olduğuna inanıyorum. Bununla birlikte, üretime hazır çözümler için uygun değildir, çünkü bakım bile tehlikeli olabilir ve herhangi bir tek değişiklik öngörülemeyen hatalar oluşturabilir. Aşağıdaki şema, bu büyük çamur mimarisi topunu göstermektedir.

Main, her biri Util kullanan A, B ve C hizmetlerini kullanır. Hizmet C, Hizmet A'yı da kullanır.

Bağımlılık Enjeksiyonu Neden Her Şeyi Yanlış Anladı?

Daha iyi bir yaklaşım arayışında, bağımlılık enjeksiyonu adı verilen bir teknik kullanabiliriz. Bu yöntem, tüm bileşenlerin arabirimler aracılığıyla kullanılması gerektiğini varsayar. Öğeleri ayrıştırdığına dair iddialar okudum, ama gerçekten öyle mi? Hayır. Aşağıdaki şemaya bir göz atın.

Önceki mimari ancak bağımlılık enjeksiyonu ile. Artık Main, ilgili hizmetleri tarafından uygulanan Arayüz Hizmeti A, B ve C'yi kullanır. A ve C Hizmetleri, Util tarafından uygulanan Arabirim Hizmeti B ve Arabirim Util'i kullanır. Servis C ayrıca Arayüz Servis A'yı kullanır. Her servis, arayüzü ile birlikte bir unsur olarak kabul edilir.

Mevcut durumla koca bir çamur yumağı arasındaki tek fark, artık sınıfları doğrudan çağırmak yerine onları arayüzleri üzerinden çağırmamızdır. Elemanları birbirinden ayırmayı biraz iyileştirir. Örneğin, Service A farklı bir projede yeniden kullanmak istiyorsanız, bunu Service A kendisini, Interface A ile birlikte Interface B ve Interface Util alarak yapabilirsiniz. Gördüğünüz gibi, Service A hala diğer unsurlara bağlı. Sonuç olarak, bir yerde kodu değiştirmek ve başka bir yerde davranışı bozmakla ilgili sorunlar yaşamaya devam ediyoruz. Service B ve Interface B değiştirirseniz, buna bağlı olan tüm öğeleri değiştirmeniz gerekeceği sorununu yine de yaratır. Bu yaklaşım hiçbir şeyi çözmez; bence, öğelerin üstüne sadece bir arayüz katmanı ekler. Asla herhangi bir bağımlılık enjekte etmemelisiniz, bunun yerine onlardan bir kez ve herkesten kurtulmalısınız. Bağımsızlık için acele edin!

Modüler Kod Çözümü

Bağımlılıkların tüm ana baş ağrılarını çözdüğüne inandığım yaklaşım, bağımlılıkları hiç kullanmayarak yapıyor. Bir bileşen ve onun dinleyicisini yaratırsınız. Dinleyici basit bir arayüzdür. Geçerli öğenin dışından bir yöntemi çağırmanız gerektiğinde, dinleyiciye bir yöntem eklemeniz ve bunun yerine onu çağırmanız yeterlidir. Öğenin yalnızca dosyaları kullanmasına, paketindeki yöntemleri çağırmasına ve ana çerçeve veya diğer kullanılan kitaplıklar tarafından sağlanan sınıfları kullanmasına izin verilir. Aşağıda, eleman mimarisini kullanmak için değiştirilmiş uygulamanın bir diyagramını görebilirsiniz.

Öğe mimarisini kullanmak için değiştirilmiş uygulamanın bir diyagramı. Main, Util'i ve üç hizmeti de kullanır. Main ayrıca her servis için o servis tarafından kullanılan bir dinleyici uygular. Dinleyici ve hizmet birlikte bir unsur olarak kabul edilir.

Lütfen bu mimaride yalnızca Main sınıfının birden çok bağımlılığı olduğunu unutmayın. Tüm öğeleri birbirine bağlar ve uygulamanın iş mantığını kapsar.

Hizmetler ise tamamen bağımsız unsurlardır. Artık her hizmeti bu uygulamadan çıkarabilir ve başka bir yerde yeniden kullanabilirsiniz. Başka hiçbir şeye bağımlı değiller. Ama bekleyin, daha iyi olacak: Davranışlarını değiştirmediğiniz sürece bu hizmetleri bir daha değiştirmenize gerek yok. Bu hizmetler yapması gerekeni yaptığı sürece, zamanın sonuna kadar el değmeden bırakılabilir. Profesyonel bir yazılım mühendisi tarafından veya herhangi birinin şimdiye kadar pişirdiği en kötü spagetti kodundan uzlaşan bir kodlayıcı tarafından, goto ifadeleriyle karıştırılarak oluşturulabilirler. Önemli değil, çünkü mantıkları kapsüllenmiş. Ne kadar korkunç olursa olsun, asla diğer sınıflara yayılmayacak. Bu aynı zamanda size bir projedeki işi birden fazla geliştirici arasında bölme gücü verir; burada her bir geliştirici, bir diğerini kesmeye gerek kalmadan veya hatta diğer geliştiricilerin varlığını bilmeden kendi bileşeni üzerinde bağımsız olarak çalışabilir.

Son olarak, son projenizin başında olduğu gibi, bir kez daha bağımsız kod yazmaya başlayabilirsiniz.

Eleman Kalıbı

Tekrarlanabilir bir şekilde oluşturabilmemiz için yapısal eleman desenini tanımlayalım.

Elemanın en basit versiyonu iki şeyden oluşur: Bir ana eleman sınıfı ve bir dinleyici. Bir öğe kullanmak istiyorsanız, dinleyiciyi uygulamanız ve ana sınıfa çağrı yapmanız gerekir. İşte en basit konfigürasyonun bir diyagramı:

Bir uygulama içindeki tek bir öğenin ve dinleyicisinin diyagramı. Daha önce olduğu gibi, Uygulama, Uygulama tarafından uygulanan dinleyicisini kullanan öğeyi kullanır.

Açıkçası, sonunda öğeye daha fazla karmaşıklık eklemeniz gerekecek, ancak bunu kolayca yapabilirsiniz. Mantık sınıflarınızın hiçbirinin projedeki diğer dosyalara bağlı olmadığından emin olun. Bu öğede yalnızca ana çerçeveyi, içe aktarılan kitaplıkları ve diğer dosyaları kullanabilirler. Görüntüler, görünümler, sesler vb. gibi varlık dosyaları söz konusu olduğunda, gelecekte yeniden kullanımlarının kolay olması için öğeler içinde kapsüllenmeleri gerekir. Tüm klasörü başka bir projeye kolayca kopyalayabilirsiniz ve işte burada!

Aşağıda, daha gelişmiş bir öğeyi gösteren örnek bir grafik görebilirsiniz. Kullandığı bir görünümden oluştuğuna ve diğer uygulama dosyalarına bağlı olmadığına dikkat edin. Bağımlılıkları kontrol etmenin basit bir yöntemini bilmek istiyorsanız, içe aktarma bölümüne bakmanız yeterlidir. Geçerli öğenin dışından herhangi bir dosya var mı? Öyleyse, bu bağımlılıkları öğeye taşıyarak veya dinleyiciye uygun bir çağrı ekleyerek kaldırmanız gerekir.

Daha karmaşık bir öğenin basit bir diyagramı. Burada "element" kelimesinin daha geniş anlamı altı bölümden oluşmaktadır: Görünüm; Mantık A, B ve C; eleman; ve Öğe Dinleyici. Son ikisi ile Uygulama arasındaki ilişkiler öncekiyle aynıdır, ancak iç Öğe ayrıca Mantık A ve C'yi kullanır. Mantık C, Mantık A ve B'yi kullanır. Mantık A, Mantık B ve Görünüm'ü kullanır.

Java'da oluşturulmuş basit bir “Merhaba Dünya” örneğine de bir göz atalım.

 public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = "Hello World of Elements!"; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String[] args) { App app = new App(); app.start(); } }

İlk olarak, çıktıyı yazdıran yöntemi belirtmek için ElementListener tanımlarız. Elemanın kendisi aşağıda tanımlanmıştır. Öğede sayHello çağrıldığında, ElementListener kullanarak bir mesaj yazdırır. Öğenin printOutput yönteminin uygulanmasından tamamen bağımsız olduğuna dikkat edin. Konsola, fiziksel bir yazıcıya veya süslü bir kullanıcı arayüzüne yazdırılabilir. Öğe bu uygulamaya bağlı değildir. Bu soyutlama sayesinde bu eleman farklı uygulamalarda kolaylıkla tekrar kullanılabilir.

Şimdi ana App sınıfına bir göz atın. Dinleyiciyi uygular ve elemanı somut uygulama ile birleştirir. Artık kullanmaya başlayabiliriz.

Bu örneği burada JavaScript'te de çalıştırabilirsiniz.

Eleman Mimarisi

Büyük ölçekli uygulamalarda eleman deseninin kullanımına bir göz atalım. Küçük bir projede göstermek başka, gerçek dünyaya uygulamak başka.

Kullanmayı sevdiğim full-stack bir web uygulamasının yapısı şu şekilde:

 src ├── client │ ├── app │ └── elements │ └── server ├── app └── elements

Bir kaynak kod klasöründe, başlangıçta istemci ve sunucu dosyalarını böldük. Tarayıcı ve arka uç sunucusu olmak üzere iki farklı ortamda çalıştıkları için yapılması makul bir şeydir.

Daha sonra her katmandaki kodu app ve element adındaki klasörlere böldük. Öğeler, bağımsız bileşenlere sahip klasörlerden oluşurken, uygulama klasörü tüm öğeleri birbirine bağlar ve tüm iş mantığını saklar.

Bu şekilde, öğeler farklı projeler arasında yeniden kullanılabilirken, uygulamaya özel tüm karmaşıklık tek bir klasörde kapsüllenir ve çoğu zaman basit öğelere yapılan çağrılara indirgenir.

Uygulamalı Örnek

Uygulamanın her zaman teoriden üstün olduğuna inanarak, Node.js ve TypeScript'te oluşturulmuş gerçek hayattan bir örneğe bakalım.

Gerçek Hayat Örneği

Daha gelişmiş çözümler için başlangıç ​​noktası olarak kullanılabilecek çok basit bir web uygulamasıdır. Kapsamlı bir yapısal eleman deseni kullanmasının yanı sıra eleman mimarisini takip eder.

Vurgulardan, ana sayfanın bir öğe olarak ayırt edildiğini görebilirsiniz. Bu sayfa kendi görünümünü içerir. Örneğin, yeniden kullanmak istediğinizde, tüm klasörü kopyalayıp farklı bir projeye bırakabilirsiniz. Sadece her şeyi birbirine bağlayın ve hazırsınız.

Bu, bugün kendi uygulamanızda öğeleri tanıtmaya başlayabileceğinizi gösteren temel bir örnektir. Bağımsız bileşenleri ayırt etmeye başlayabilir ve mantıklarını ayırabilirsiniz. Şu anda üzerinde çalıştığınız kodun ne kadar dağınık olduğu önemli değil.

Daha Hızlı Geliştirin, Daha Sık Yeniden Kullanın!

Umarım bu yeni araç seti ile daha kolay bakımı daha kolay olan kodlar geliştirebilirsiniz. Öğe modelini pratikte kullanmaya başlamadan önce, tüm ana noktaları hızlıca özetleyelim:

  • Yazılımda birçok sorun, birden çok bileşen arasındaki bağımlılıklar nedeniyle ortaya çıkar.

  • Bir yerde değişiklik yaparak, başka bir yerde öngörülemeyen davranışlar sergileyebilirsiniz.

Üç yaygın mimari yaklaşım şunlardır:

  • Büyük çamur topu. Hızlı geliştirme için harika, ancak istikrarlı üretim amaçları için çok iyi değil.

  • Bağımlılık enjeksiyonu. Kaçınmanız gereken yarı pişmiş bir çözüm.

  • Eleman mimarisi. Bu çözüm, bağımsız bileşenler oluşturmanıza ve bunları başka projelerde yeniden kullanmanıza olanak tanır. İstikrarlı üretim sürümleri için bakımı yapılabilir ve mükemmeldir.

Temel eleman kalıbı, tüm ana yöntemlere sahip bir ana sınıfın yanı sıra dış dünya ile iletişime izin veren basit bir arayüz olan bir dinleyiciden oluşur.

Tam yığın eleman mimarisini elde etmek için önce ön ucunuzu arka uç kodundan ayırırsınız. Ardından, bir uygulama ve öğeler için her birinde bir klasör oluşturursunuz. Öğeler klasörü, tüm bağımsız öğelerden oluşurken, uygulama klasörü her şeyi birbirine bağlar.

Şimdi gidip kendi öğelerinizi oluşturmaya ve paylaşmaya başlayabilirsiniz. Uzun vadede bakımı kolay ürünler oluşturmanıza yardımcı olacaktır. İyi şanslar ve ne yarattığınızı bana bildirin!

Ayrıca, kodunuzu zamanından önce optimize ettiğinizi fark ederseniz, Toptaler Kevin Bloch'un Erken Optimizasyonun Lanetinden Nasıl Kaçınılır başlıklı makaleyi okuyun.

İlgili: JS En İyi Uygulamaları: TypeScript ve Dependency Injection ile bir Discord Botu Oluşturun