JavaScript Tasarım Modelleri için Kapsamlı Kılavuz
Yayınlanan: 2022-03-11İyi bir JavaScript geliştiricisi olarak temiz, sağlıklı ve sürdürülebilir kod yazmaya çalışırsınız. Benzersiz olsa da benzersiz çözümler gerektirmeyen ilginç zorlukları çözersiniz. Kendinizi daha önce çözdüğünüz tamamen farklı bir sorunun çözümüne benzeyen kod yazarken bulmuşsunuzdur. Bunu bilmiyor olabilirsiniz, ancak bir JavaScript tasarım deseni kullandınız. Tasarım kalıpları, yazılım tasarımında yaygın olarak ortaya çıkan sorunlara yeniden kullanılabilir çözümlerdir.
Herhangi bir dilin ömrü boyunca, bu tür birçok yeniden kullanılabilir çözüm, o dilin topluluğundan çok sayıda geliştirici tarafından yapılır ve test edilir. Pek çok geliştiricinin bu birleşik deneyiminden dolayı, bu tür çözümler çok faydalıdır, çünkü aynı zamanda elimizdeki sorunu çözerken aynı zamanda optimize edilmiş bir şekilde kod yazmamıza yardımcı olurlar.
Tasarım kalıplarından elde ettiğimiz başlıca faydalar şunlardır:
- Bunlar kanıtlanmış çözümlerdir: Tasarım kalıpları çoğu geliştirici tarafından sıklıkla kullanıldığı için, çalıştıklarından emin olabilirsiniz. Sadece bu da değil, birçok kez revize edildiğinden ve muhtemelen optimizasyonların uygulandığından emin olabilirsiniz.
- Kolayca yeniden kullanılabilirler: Tasarım desenleri, belirli bir soruna bağlı olmadıklarından, birden çok belirli sorunu çözmek için değiştirilebilen yeniden kullanılabilir bir çözümü belgelemektedir.
- Etkileyicidirler: Tasarım desenleri büyük bir çözümü oldukça zarif bir şekilde açıklayabilir.
- İletişimi kolaylaştırırlar: Geliştiriciler tasarım kalıplarına aşina olduklarında, belirli bir problemin olası çözümleri hakkında birbirleriyle daha kolay iletişim kurabilirler.
- Kodu yeniden düzenleme ihtiyacını önlerler: Bir uygulama tasarım kalıpları göz önünde bulundurularak yazılırsa, genellikle daha sonra kodu yeniden düzenlemeniz gerekmez, çünkü belirli bir soruna doğru tasarım kalıbını uygulamak zaten en uygunudur. çözüm.
- Kod tabanının boyutunu küçültürler: Tasarım kalıpları genellikle zarif ve optimal çözümler olduğundan, genellikle diğer çözümlerden daha az kod gerektirir.
Bu noktada devreye girmeye hazır olduğunuzu biliyorum, ancak tasarım kalıpları hakkında her şeyi öğrenmeden önce, bazı JavaScript temellerini gözden geçirelim.
JavaScript'in Kısa Tarihi
JavaScript, günümüzde web geliştirme için en popüler programlama dillerinden biridir. Başlangıçta, ilk web tarayıcılarından biri için istemci tarafı komut dosyası dili olarak bilinen çeşitli görüntülenen HTML öğeleri için bir tür "yapıştırıcı" olarak yapıldı. Netscape Navigator olarak adlandırılan, o sırada yalnızca statik HTML görüntüleyebilir. Tahmin edebileceğiniz gibi, böyle bir betik dili fikri, o zamanlar tarayıcı geliştirme endüstrisindeki Netscape Communications (bugün Mozilla), Microsoft ve diğerleri gibi büyük oyuncular arasında tarayıcı savaşlarına yol açtı.
Büyük oyuncuların her biri bu betik dilinin kendi uygulamalarını zorlamak istedi, bu nedenle Netscape JavaScript'i (aslında Brendan Eich yaptı), Microsoft JScript'i yaptı vb. Gördüğünüz gibi, bu uygulamalar arasındaki farklar harikaydı, bu nedenle web tarayıcıları için geliştirme, bir web sayfasıyla birlikte gelen en iyi görüntülenen çıkartmalarla tarayıcı başına yapıldı. Geliştirme sürecini birleştirecek ve web sayfalarının oluşturulmasını basitleştirecek bir standart, tarayıcılar arası bir çözüme ihtiyacımız olduğu çok geçmeden anlaşıldı. Buldukları şeye ECMAScript denir.
ECMAScript, tüm modern tarayıcıların desteklemeye çalıştığı standartlaştırılmış bir betik dili özelliğidir ve ECMAScript'in birden çok uygulaması (lehçe diyebilirsiniz) vardır. En popüler olanı bu makalenin konusu olan JavaScript'tir. ECMAScript ilk yayınından bu yana birçok önemli şeyi standart hale getirdi ve ayrıntılarla daha fazla ilgilenenler için, ECMAScript'in her sürümü için Wikipedia'da bulunan standartlaştırılmış öğelerin ayrıntılı bir listesi var. ECMAScript sürüm 6 (ES6) ve üstü için tarayıcı desteği hala eksiktir ve tam olarak desteklenebilmesi için ES5'e aktarılması gerekir.
JavaScript Nedir?
Bu makalenin içeriğini tam olarak anlamak için JavaScript tasarım kalıplarına dalmadan önce bilmemiz gereken bazı çok önemli dil özelliklerine bir giriş yapalım. Birisi size “JavaScript nedir?” şu satırlarda bir yere cevap verebilirsiniz:
JavaScript, en yaygın olarak web sayfaları için bir betik dili olarak bilinen birinci sınıf işlevlere sahip hafif, yorumlanmış, nesne yönelimli bir programlama dilidir.
Yukarıda bahsedilen tanım, JavaScript kodunun düşük bellek ayak izine sahip olduğunu, C++ ve Java gibi popüler dillere benzer bir sözdizimi ile uygulanması kolay ve öğrenmesi kolay olduğunu söylemek anlamına gelir. Bu bir betik dilidir, yani kodunun derlenmek yerine yorumlanmasıdır. Prosedürel, nesne yönelimli ve işlevsel programlama stilleri desteği vardır, bu da onu geliştiriciler için çok esnek kılar.
Buraya kadar, kulağa diğer diller gibi gelen tüm özelliklere bir göz attık, o halde JavaScript'in diğer dillere göre nelere özel olduğuna bir göz atalım. Birkaç özelliği listeleyeceğim ve neden özel ilgiyi hak ettiklerini açıklamak için elimden gelenin en iyisini yapacağım.
JavaScript Birinci Sınıf İşlevleri Destekler
JavaScript'e yeni başladığımda, C/C++ arka planından geldiğim için bu özelliği kavramak benim için zordu. JavaScript, fonksiyonlara birinci sınıf vatandaş muamelesi yapar, yani fonksiyonları diğer herhangi bir değişken gibi diğer fonksiyonlara parametre olarak geçirebilirsiniz.
// we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })
JavaScript Prototip Tabanlıdır
Diğer birçok nesne yönelimli dilde olduğu gibi, JavaScript de nesneleri destekler ve nesneler hakkında düşünüldüğünde akla gelen ilk terimlerden biri sınıflar ve kalıtımdır. Dil, sade dil biçimindeki sınıfları desteklemediği, bunun yerine prototip tabanlı veya örnek tabanlı kalıtım adı verilen bir şey kullandığı için biraz zorlaştığı yer burasıdır.
Tam şimdi, ES6'da, resmi sınıf terimi tanıtıldı, bu, tarayıcıların hala bunu desteklemediği anlamına geliyor (hatırlarsanız, yazarken, tam olarak desteklenen son ECMAScript sürümü 5.1'dir). Bununla birlikte, "sınıf" teriminin JavaScript'e dahil edilmiş olmasına rağmen, kaputun altında prototip tabanlı kalıtımı kullanmaya devam ettiğini belirtmek önemlidir.
Prototip tabanlı programlama, davranışın yeniden kullanımının (kalıtım olarak bilinir), prototip görevi gören delegasyonlar aracılığıyla var olan nesneleri yeniden kullanma süreci aracılığıyla gerçekleştirildiği bir nesne yönelimli programlama stilidir. Bu özellik birçok JavaScript tasarım modelinde kullanıldığından, makalenin tasarım desenleri bölümüne geldiğimizde bununla daha fazla ayrıntıya dalacağız.
JavaScript Olay Döngüleri
JavaScript ile çalışma deneyiminiz varsa, geri arama işlevi terimine kesinlikle aşinasınızdır. Terime aşina olmayanlar için, geri arama işlevi, başka bir işleve parametre olarak gönderilen bir işlevdir (unutmayın, JavaScript işlevleri birinci sınıf vatandaşlar olarak değerlendirir) ve bir olay tetiklendikten sonra yürütülür. Bu genellikle fare tıklaması veya klavye düğmesine basılması gibi olaylara abone olmak için kullanılır.
Bir dinleyicinin bağlı olduğu bir olay her başlatıldığında (aksi takdirde olay kaybolur), FIFO tarzında (ilk giren ilk çıkar) eşzamanlı olarak işlenen bir mesaj kuyruğuna bir mesaj gönderilir. ). Buna olay döngüsü denir.
Kuyruktaki mesajların her birinin kendisiyle ilişkilendirilmiş bir işlevi vardır. Bir mesaj kuyruğa alındığında, çalışma zamanı, başka bir mesajı işlemeden önce işlevi tamamen yürütür. Başka bir deyişle, bir işlev başka işlev çağrıları içeriyorsa, bunların tümü kuyruktan yeni bir mesaj işlenmeden önce gerçekleştirilir. Buna çalıştırma-tamamlama denir.
while (queue.waitForMessage()) { queue.processNextMessage(); }
queue.waitForMessage()
eşzamanlı olarak yeni iletileri bekler. İşlenmekte olan mesajların her birinin kendi yığını vardır ve yığın boşalana kadar işlenir. Bittiğinde, varsa kuyruktan yeni bir mesaj işlenir.
JavaScript'in engellemediğini de duymuş olabilirsiniz, yani eşzamansız bir işlem gerçekleştirilirken program, eşzamansız işlemin tamamlanmasını beklerken, ana işlemi engellemeden, kullanıcı girdisi almak gibi diğer şeyleri işleyebilir. yürütme iş parçacığı. Bu JavaScript'in çok kullanışlı bir özelliğidir ve sadece bu konu üzerine koca bir makale yazılabilir; ancak, bu makalenin kapsamı dışındadır.
Tasarım Kalıpları Nelerdir?
Daha önce de söylediğim gibi, tasarım kalıpları, yazılım tasarımında yaygın olarak ortaya çıkan sorunlara yeniden kullanılabilir çözümlerdir. Tasarım desenlerinin bazı kategorilerine bir göz atalım.
Proto-kalıplar
Bir desen nasıl oluşturulur? Diyelim ki yaygın olarak meydana gelen bir sorunu fark ettiniz ve bu soruna küresel olarak tanınmayan ve belgelenmeyen kendi benzersiz çözümünüz var. Bu sorunla her karşılaştığınızda bu çözümü kullanırsınız ve yeniden kullanılabilir olduğunu ve geliştirici topluluğunun bundan faydalanabileceğini düşünürsünüz.
Bir anda kalıp mı oluyor? Neyse ki, hayır. Çoğu zaman, iyi kod yazma uygulamalarına sahip olabilir ve aslında bir kalıp olmadığı halde kalıp gibi görünen bir şeyi bir şeyle karıştırabilir.
Tanıdığınızı düşündüğünüz şeyin aslında bir tasarım deseni olduğunu nasıl bilebilirsiniz?
Bununla ilgili diğer geliştiricilerin görüşlerini alarak, bir kalıp oluşturma sürecini bilerek ve mevcut kalıplarla kendinizi iyi tanıyarak. Bir kalıbın tam teşekküllü bir kalıp haline gelmeden önce geçmesi gereken bir aşama vardır ve buna proto-kalıp denir.
Bir proto-kalıp, kalıbın yararlı olduğu ve doğru sonuçlar verdiği çeşitli geliştiriciler ve senaryolar tarafından belirli bir test döneminden geçerse , bir kalıptır. Toplum tarafından tam teşekküllü bir modelin tanınması için yapılması gereken - çoğu bu makalenin kapsamı dışında kalan - oldukça fazla miktarda çalışma ve belge vardır.
Anti-desenler
Bir tasarım deseni iyi uygulamayı temsil ettiğinden, bir anti-desen kötü uygulamayı temsil eder.
Bir anti-kalıp örneği, Object
sınıfı prototipini değiştirmek olabilir. JavaScript'teki hemen hemen tüm nesneler Object
devralır (JavaScript'in prototip tabanlı devralma kullandığını unutmayın), bu nedenle bu prototipi değiştirdiğiniz bir senaryo hayal edin. Object
prototipinde yapılan değişiklikler, bu prototipten miras alınan tüm nesnelerde görülecektir; bu çoğu JavaScript nesnesi olacaktır . Bu gerçekleşmeyi bekleyen bir felaket.
Yukarıda bahsedilene benzer başka bir örnek, sahip olmadığınız nesneleri değiştirmektir. Bunun bir örneği, uygulama boyunca birçok senaryoda kullanılan bir nesneden bir işlevi geçersiz kılmak olabilir. Büyük bir ekiple çalışıyorsanız, bunun neden olacağı kafa karışıklığını bir düşünün; adlandırma çakışmaları, uyumsuz uygulamalar ve bakım kabuslarıyla hızla karşılaşırsınız.
Tüm iyi uygulamalar ve çözümler hakkında bilgi sahibi olmak ne kadar faydalıysa, kötü olanları da bilmek çok önemlidir. Bu şekilde, onları tanıyabilir ve hatayı önceden yapmaktan kaçınabilirsiniz.
Tasarım Modeli Kategorizasyonu
Tasarım desenleri birçok şekilde kategorize edilebilir, ancak en popüler olanı şudur:
- Yaratıcı tasarım desenleri
- Yapısal tasarım desenleri
- Davranışsal tasarım kalıpları
- Eşzamanlılık tasarım desenleri
- Mimari tasarım desenleri
Yaratıcı Tasarım Desenleri
Bu modeller, temel bir yaklaşıma kıyasla nesne oluşturmayı optimize eden nesne oluşturma mekanizmalarıyla ilgilenir. Nesne oluşturmanın temel biçimi, tasarım sorunlarına veya tasarıma ek karmaşıklığa neden olabilir. Yaratıcı tasarım kalıpları, nesne oluşturmayı bir şekilde kontrol ederek bu sorunu çözer. Bu kategorideki popüler tasarım desenlerinden bazıları şunlardır:
- Fabrika yöntemi
- soyut fabrika
- inşaatçı
- Prototip
- tekton
Yapısal Tasarım Desenleri
Bu modeller nesne ilişkileriyle ilgilenir. Sistemin bir parçası değişirse, tüm sistemin de onunla birlikte değişmesine gerek kalmamasını sağlarlar. Bu kategorideki en popüler desenler:
- adaptör
- Köprü
- kompozit
- Dekoratör
- Cephe
- sinek ağırlığı
- vekil
Davranışsal Tasarım Modelleri
Bu tür modeller, bir sistemdeki farklı nesneler arasındaki iletişimi tanır, uygular ve geliştirir. Bir sistemin farklı bölümlerinin senkronize bilgilere sahip olmasını sağlamaya yardımcı olurlar. Bu kalıpların popüler örnekleri:
- sorumluluk zinciri
- Emretmek
- yineleyici
- arabulucu
- hatıra
- Gözlemci
- Belirtmek, bildirmek
- strateji
- Ziyaretçi
Eşzamanlılık Tasarım Kalıpları
Bu tür tasarım desenleri, çok iş parçacıklı programlama paradigmalarıyla ilgilenir. Popüler olanlardan bazıları:
- aktif nesne
- Nükleer reaksiyon
- zamanlayıcı
Mimari Tasarım Desenleri
Mimari amaçlar için kullanılan tasarım desenleri. En ünlülerinden bazıları şunlardır:
- MVC (Model-Görünüm-Denetleyici)
- MVP (Model-Görünüm-Sunucu)
- MVVM (Model-Görünüm-ViewModel)
Aşağıdaki bölümde, daha iyi anlaşılması için verilen örneklerle yukarıda bahsedilen tasarım modellerinden bazılarına daha yakından bakacağız.
Tasarım Desen Örnekleri
Tasarım modellerinin her biri, belirli bir problem tipine belirli bir çözüm tipini temsil eder. Her zaman en uygun olan evrensel bir kalıp seti yoktur. Belirli bir kalıbın ne zaman faydalı olacağını ve gerçek değer sağlayıp sağlamayacağını öğrenmemiz gerekiyor. En uygun oldukları kalıplara ve senaryolara aşina olduğumuzda, belirli bir kalıbın belirli bir problem için uygun olup olmadığını kolayca belirleyebiliriz.
Belirli bir soruna yanlış kalıbın uygulanmasının gereksiz kod karmaşıklığı, performans üzerinde gereksiz ek yük ve hatta yeni bir anti-kalıp ortaya çıkması gibi istenmeyen etkilere yol açabileceğini unutmayın.
Bunların hepsi, kodumuza bir tasarım deseni uygulamayı düşünürken göz önünde bulundurulması gereken önemli şeylerdir. Kişisel olarak faydalı bulduğum ve her üst düzey JavaScript geliştiricisinin aşina olması gerektiğine inandığım bazı tasarım modellerine göz atacağız.
Yapıcı Modeli
Klasik nesne yönelimli dilleri düşünürken, yapıcı, bir nesneyi bir dizi varsayılan ve/veya gönderilen değerlerle başlatan bir sınıftaki özel bir işlevdir.
JavaScript'te nesne oluşturmanın yaygın yolları, aşağıdaki üç yoldur:
// either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();
Bir nesne oluşturduktan sonra, bu nesnelere özellikler eklemenin (ES3'ten beri) dört yolu vardır. Bunlar aşağıdaki gibidir:
// supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });
Nesne oluşturmanın en popüler yolu küme parantezleri ve özellik eklemek için nokta gösterimi veya köşeli parantezlerdir. JavaScript ile herhangi bir deneyimi olan herkes bunları kullanmıştır.
JavaScript'in yerel sınıfları desteklemediğinden daha önce bahsetmiştik, ancak bir işlev çağrısına ön eklenmiş "yeni" bir anahtar kelime kullanarak yapıcıları destekler. Bu şekilde, işlevi bir kurucu olarak kullanabilir ve özelliklerini klasik bir dil yapılandırıcısında yaptığımız gibi başlatabiliriz.
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
Ancak, burada hala iyileştirme için yer var. Hatırlarsanız, JavaScript'in prototip tabanlı kalıtım kullandığından daha önce bahsetmiştim. Önceki yaklaşımdaki sorun, writesCode
yönteminin, Person
yapıcısının her bir örneği için yeniden tanımlanmasıdır. Yöntemi işlev prototipine ayarlayarak bundan kaçınabiliriz:
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
Artık, Person
yapıcısının her iki örneği de writesCode()
yönteminin paylaşılan bir örneğine erişebilir.

Modül Kalıbı
Tuhaflıklar söz konusu olduğunda, JavaScript asla şaşırtmaz. JavaScript'e özgü bir başka şey de (en azından nesne yönelimli diller söz konusu olduğunda), JavaScript'in erişim değiştiricileri desteklememesidir. Klasik bir OOP dilinde, bir kullanıcı bir sınıf tanımlar ve üyeleri için erişim haklarını belirler. JavaScript, basit biçiminde ne sınıfları ne de erişim değiştiricileri desteklemediğinden, JavaScript geliştiricileri gerektiğinde bu davranışı taklit etmenin bir yolunu buldular.
Modül desen özelliklerine geçmeden önce, kapatma kavramından bahsedelim. Kapatma , üst işlev kapandıktan sonra bile üst kapsama erişimi olan bir işlevdir. Kapsam belirleme yoluyla erişim değiştiricilerinin davranışını taklit etmemize yardımcı olurlar. Bunu bir örnek üzerinden gösterelim:
// we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());
Gördüğünüz gibi, IIFE'yi kullanarak, sayaç değişkenini çağrılan ve kapatılan ancak yine de onu artıran alt işlev tarafından erişilebilen bir işleve bağladık. Sayaç değişkenine fonksiyon ifadesinin dışından erişemediğimiz için, kapsam manipülasyonu ile onu özel hale getirdik.
Kapakları kullanarak özel ve genel bölümleri olan nesneler oluşturabiliriz. Bunlara modüller denir ve bir nesnenin belirli kısımlarını gizlemek istediğimizde ve yalnızca modülün kullanıcısına bir arabirimi göstermek istediğimizde çok kullanışlıdır. Bunu bir örnekle gösterelim:
// through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());
Bu kalıbın sunduğu en kullanışlı şey, klasik nesne yönelimli bir arka plandan gelen geliştiricilere çok benzeyen bir kavram olan bir nesnenin özel ve genel bölümlerinin net bir şekilde ayrılmasıdır.
Ancak, her şey o kadar mükemmel değil. Bir üyenin görünürlüğünü değiştirmek istediğinizde, genel ve özel bölümlere erişimin farklı doğası nedeniyle, bu üyeyi kullanmış olduğunuz her yerde kodu değiştirmeniz gerekir. Ayrıca, oluşturulduktan sonra nesneye eklenen yöntemler, nesnenin özel üyelerine erişemez.
Açığa Çıkan Modül Kalıbı
Bu model, yukarıda gösterildiği gibi modül modelinde yapılan bir iyileştirmedir. Temel fark, tüm nesne mantığını modülün özel kapsamına yazmamız ve ardından anonim bir nesne döndürerek genel olmasını istediğimiz kısımları göstermemizdir. Özel üyeleri ilgili genel üyelerle eşleştirirken özel üyelerin adlarını da değiştirebiliriz.
// we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());
Açıklayıcı modül kalıbı, bir modül kalıbı uygulayabileceğimiz en az üç yoldan biridir. Açıklayıcı modül kalıbı ile modül kalıbının diğer varyantları arasındaki farklar, öncelikle genel üyelere nasıl başvurulacağıyla ilgilidir. Sonuç olarak, açığa çıkaran modül modelinin kullanımı ve değiştirilmesi çok daha kolaydır; ancak, RMP nesnelerini bir miras zincirinde prototip olarak kullanmak gibi belirli senaryolarda kırılgan olabilir. Sorunlu durumlar şunlardır:
- Bir kamu işlevine atıfta bulunan özel bir işlevimiz varsa, özel işlev işlevin özel uygulamasına atıfta bulunmaya devam edeceğinden, genel işlevi geçersiz kılamayız, böylece sistemimize bir hata girer.
- Özel bir değişkene işaret eden bir genel üyemiz varsa ve genel üyeyi modülün dışından geçersiz kılmaya çalışırsak, diğer işlevler yine de değişkenin özel değerine atıfta bulunarak sistemimize bir hata getirir.
Tekli Desen
Singleton modeli, bir sınıfın tam olarak bir örneğine ihtiyacımız olduğunda senaryolarda kullanılır. Örneğin, bir şey için bazı konfigürasyonlar içeren bir nesneye ihtiyacımız var. Bu durumlarda, sistemin herhangi bir yerinde yapılandırma nesnesi gerektiğinde yeni bir nesne oluşturmak gerekli değildir.
var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);
Örnekte görebileceğiniz gibi, oluşturulan rasgele sayı ve gönderilen yapılandırma değerleri her zaman aynıdır.
Singleton değerini almak için erişim noktasının yalnızca bir tane olması ve çok iyi bilinmesi gerektiğine dikkat etmek önemlidir. Bu kalıbı kullanmanın bir dezavantajı, test edilmesinin oldukça zor olmasıdır.
Gözlemci Modeli
Gözlemci modeli, sistemimizin farklı parçaları arasındaki iletişimi optimize edilmiş bir şekilde iyileştirmemiz gereken bir senaryomuz olduğunda çok kullanışlı bir araçtır. Nesneler arasında gevşek bağlantıyı destekler.
Bu kalıbın çeşitli versiyonları vardır, ancak en temel haliyle, kalıbın iki ana parçasına sahibiz. Birincisi bir özne, ikincisi ise gözlemcilerdir.
Bir konu, gözlemcilerin abone olduğu belirli bir konuyla ilgili tüm işlemleri ele alır. Bu işlemler, bir gözlemcinin belirli bir konuya abone olmasını, belirli bir konudan bir gözlemcinin aboneliğini iptal etmesini ve bir olay yayınlandığında belirli bir konu hakkında gözlemciyi bilgilendirmesini sağlar.
Ancak, bu bölümde örnek olarak kullanacağım yayıncı/abone kalıbı adı verilen bu kalıbın bir varyasyonu var. Klasik bir gözlemci modeli ile yayıncı/abone modeli arasındaki temel fark, yayıncı/abonenin gözlemci modelinden daha fazla gevşek bağlantıyı teşvik etmesidir.
Gözlemci modelinde özne, abone olan gözlemcilere referansları tutar ve doğrudan nesnelerin kendisinden metotları çağırır, oysa yayıncı/abone modelinde abone ile yayıncı arasında bir iletişim köprüsü görevi gören kanallarımız vardır. Yayıncı bir olayı başlatır ve o olay için gönderilen geri arama işlevini yürütür.
Yayıncı/abone kalıbının kısa bir örneğini göstereceğim, ancak ilgilenenler için klasik bir gözlemci kalıbı örneği çevrimiçi olarak kolayca bulunabilir.
var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});
Bu tasarım deseni, tetiklenen tek bir olay üzerinde birden çok işlem gerçekleştirmemiz gereken durumlarda kullanışlıdır. Bir arka uç hizmetine birden fazla AJAX araması yapmamız ve ardından sonuca bağlı olarak başka AJAX aramaları gerçekleştirmemiz gereken bir senaryonuz olduğunu hayal edin. AJAX çağrılarını birbirinin içine yerleştirmeniz, muhtemelen geri arama cehennemi olarak bilinen bir duruma girmeniz gerekir. Yayıncı/abone kalıbını kullanmak çok daha şık bir çözümdür.
Bu kalıbı kullanmanın bir dezavantajı, sistemimizin çeşitli bölümlerinin zor test edilmesidir. Sistemin abone olan bölümlerinin beklendiği gibi davranıp davranmadığını bilmenin zarif bir yolu yoktur.
Aracı Modeli
Ayrıştırılmış sistemlerden bahsederken de çok faydalı olan bir örüntüyü kısaca ele alacağız. Bir sistemin birden çok parçasının iletişim kurması ve koordine etmesi gereken bir senaryomuz olduğunda, belki de bir arabulucuyu tanıtmak iyi bir çözüm olabilir.
Arabulucu, bir sistemin farklı parçaları arasındaki iletişim için merkezi bir nokta olarak kullanılan ve bunlar arasındaki iş akışını yöneten bir nesnedir. Şimdi, iş akışını idare ettiğini vurgulamak önemlidir. Bu neden önemli?
Çünkü yayıncı/abone kalıbı ile büyük bir benzerlik var. Kendinize şunu sorabilirsiniz, tamam, bu iki model de nesneler arasında daha iyi iletişim kurulmasına yardımcı olur… Fark nedir?
Aradaki fark, bir arabulucunun iş akışını yönetmesi, yayıncı/abonenin ise "ateşle ve unut" iletişim türü olarak adlandırılan bir şey kullanmasıdır. Yayıncı/abone sadece bir olay toplayıcıdır, yani olayları tetiklemek ve hangi olayların tetiklendiğini doğru abonelere bildirmekle ilgilenir. Olay toplayıcı, bir arabulucuda olduğu gibi, bir olay tetiklendikten sonra ne olacağını umursamaz.
Arabulucuya güzel bir örnek, sihirbaz tipi bir arayüzdür. Diyelim ki üzerinde çalıştığınız bir sistem için büyük bir kayıt süreciniz var. Çoğu zaman, bir kullanıcıdan çok fazla bilgi istendiğinde, bunu birden çok adıma bölmek iyi bir uygulamadır.
Bu şekilde, kod çok daha temiz (bakımı daha kolay) olacak ve kullanıcı, sadece kaydı bitirmek için istenen bilgi miktarından bunalmayacaktır. Arabulucu, her kullanıcının potansiyel olarak benzersiz bir kayıt sürecine sahip olabileceği gerçeği nedeniyle meydana gelebilecek farklı olası iş akışlarını hesaba katarak kayıt adımlarını gerçekleştirecek bir nesnedir.
Bu tasarım modelinin bariz faydası, artık tümü aracı ve daha temiz kod temeli aracılığıyla iletişim kuran bir sistemin farklı bölümleri arasındaki gelişmiş iletişimdir.
Bir dezavantajı, şimdi sistemimize tek bir arıza noktası eklemiş olmamızdır, yani arabulucumuz arızalanırsa, tüm sistem çalışmayı durdurabilir.
Prototip Modeli
Makale boyunca daha önce bahsettiğimiz gibi JavaScript, sınıfları yerel biçiminde desteklemez. Nesneler arasındaki kalıtım, prototip tabanlı programlama kullanılarak uygulanır.
Yaratılan diğer nesneler için prototip işlevi görebilecek nesneler oluşturmamızı sağlar. Prototip nesnesi, yapıcının oluşturduğu her nesne için bir plan olarak kullanılır.
Bundan önceki bölümlerde bahsettiğimiz gibi, bu kalıbın nasıl kullanılabileceğine dair basit bir örnek gösterelim.
var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();
Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.
Command Pattern
The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.
This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.
As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.
// the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));
Facade Pattern
The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.
A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:
jQuery(".parent .child div.span")
It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.
We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.
Sonraki adımlar
Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.
In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.