Web uygulamalarında frontend, uygulamanızın en kolay “çığrından çıkacağı” kısım. Uygulamanın veritabanı ve sunucu tarafını klasik yazılım geliştirme yöntemleri ile tamamlayabiliriz. Markup kalitesini yüksek tutmak, taşınabilirliği ve performansı yüksek uygulamalar yazmak farklı çözümler gerektiriyor.
Yakın zamanda tamamladığımız bir projenin önyüzü Symfony 1.4 üzerinde templateler ile oluşturulmuştu. Fakat geliştirmeler o kadar javascript bağımlı ki tarayıcı kısa sürede tıkanıyor, bir hatayı gidermek bazen işkenceye dönüşüyor, çoğunda da uzun zaman alıyordu. İş geliştirme’den gelen isteklerden biri “Facebook Wall”‘a bir diğeri de “Linkedin Profil” sayfasına benzer istekler. Tasarımlar ve işlevler farklı aslında fakat daha populer sitelerden daha benzer örnekler bulamadım. Özetle proje yüksek oranda javascript bağımlı sayfalardan oluşuyor.
Projenin sonuna yakın dahil oldum, ne kadar “yanlış yapılmış, düzeltmeliyiz” desem de anlatamadım derdimi, uzun bir süre “jquery eklentileri” ile çabaladıktan sonra “tamam, frontend mimarisini değiştirecez” kararını aldırdım.
İki parça “demo” yaptıktan sonra ekip üyelerinin ortak fikirleri: “Backend’deki bu böyle olmaz dediğimiz herşeyi sildik, frontend yazmıyoruz sanki backend’in kötü kısmını siliyoruz”.
Yanlış açıkca ortadaydı, “projenin JavaScript bağımlılık oranı çok yüksek” markup’ı javascript ile çizersek hem backend’i temizlemiş, hem de taşınabilir bir uygulama elde etmiş olacaz. Frontend’de oluşacak memory leakleri de büyük oranda engellemiş olacağız. Arama motoru derdimiz yok, içerik sadece üyelere açık olacak.
Yol Haritası ve Karar : Object Orient JavaScript (Pure)
Projeye başlamadan önce disiplini sağlayabilmemiz ve geliştiriciler arasında kod standartını sağlayabilmek için bazı kurallar koyduk. Projeye başladığımızda izleyeceğimiz yol:
– Kayıt, giriş gibi statik sayfalar geleneksel yolla oluşturulacak, markup, data ile birlikte sunucudan gelecek.
– DOM ile ilişiği olan her bir “obje”nin “.element”i olacak
– DOM elementleri (mümkün ise) tekrar kullanılabilir olacak.
– Her class/modul ayrı bir dosyada tutulacak.
– ‘new’ ile çağırılacak her bir class ‘B’üyük harf ile başlar
– Private methodlar ve değişkenler ‘_’ ile başlar
– Bize bir dependency manager lazım! (app.provide ve app.depends methodları)
– Basit bir araç kutusu lazım. DOM, ajax, extend, interface gibi.
– Uygulamanın her bir parçası ayrı derlenecek (Uygulamanın boyutu 5MB’ye ulaşırsa?).
– Her bir sayfaya tekil bir kimlik verilecek (userRegister, userWall, userProfile, userInbox gibi).
– Javascript kısımını 3 ayrı katmanda çözeceğiz. (Yazıda bu katmanları detaylandırmayacağım Core (çekirdek), Application (uygulama), Implementation (entegrasyon)).
– Tarayıcıya sunucudan HTML olarak sadece temel layout gelecek.
– Tüm sayfaların entegrasyon kodu sayfaya özel oluşturduğumuz isim ile aynı isimde olan ayrı bir dosyada olacak.
– Uygulamanın parçaları manalı namespace’ler altında toplanacak.
– Uygulamanın hiç bir parçası global scope’a çıkmayacak.
Şu an hatırladıklarım bunlar, bir kaç madde daha vardı sanırım. Yazıyı mümkün olduğunca kısa tutabilmek için oluşturduğum 2 demo’dan sadece birinden bahsedeceğim.
Diyagramlar (Wireframe)
İlk yaptığım demo aşağıdaki gibi bir menü.
Request, Notification ve bir tane daha menümüz var (üçüncüyü çizime gerek duymadım). İleride dördüncü/beşinciyi eklemeyeceğimizin, veya olanlardan birini çıkartmayacağımızın garantisi yok.
Menü’nün temel özellikleri:
– Kullanıcı menüye tıkladığında kırmızı daireler rakamlarıyla birlikte kaybolsun, yeni bir uyarı/istek var ise tekrar görünsün.
– “Notification” menüsünde uyarı yeni ise farklı arkaplan rengi ile belirtilsin
– Kullanıcı “action’lardan” birine tıkladığında farklı mesaj gösterebilelim.
– Zaman objeleri kendini güncelleyebilsin. (Bu yazıda bu modulden bahsetmeyeceğim)
UML’e geçmeden önce menüyü mantıksal parçalara ayırıp namespace’ler ile birlikte isimlendirelim.
Mavi renk app.menu namespace‘ini gösterir.
Sarı renk app.navigation namespace‘ini gösterir.
Gördüğünüz gibi tasarımımız iki farklı namespace altında. Bunun amacı, ileride “notifiable” fakat farklı görünen (facebook’da sol tarafda bulunan, groups, favorites, apps gibi bir menü düşünün) bir menü istediğimizde aynı yapıyı kullanabilmek istiyoruz. JavaScript “class less” olduğu için bundan sonra “class” yerine “constructor” olarak isimlendireceğim.
Constructor Diyagramı
Evet, henüz kod yazmıyoruz, biraz daha sabır. Şimdi constructor’ları UML diyagramı ile belirleyelim. Tabii ki abstract class veya parent class görevini üstlenen constructor’larımız da olacak. Interface implementasyonlarını, testleri burada pas geçiyorum. Yazının sonunda neleri atladığımızı bulabilirsiniz. Önce diyagramı görelim, sonra hangi pattern’leri nerelerde kullandığımızı açıklayalım.
notifiable.Container constructor’ın tam path’i app.menu.notifiable.Container, notifiable.Item da aynı şekilde app.menu.notifiable.Item olarak okuyun. Sadeleştirmek adına tam yol’u yazmadım. Diğer tüm constructor’lar app.navigation.* namespace’i altında bulunuyor. (UML’i hatırladığım kadarıyla çiziyorum, hata varsa bildirebilirsiniz)
Kullandığımız Tasarım Desenleri (Design Patterns)
Notifiable Menu
Composite ve Virtual Proxy
notifiable.Container ve alt objeleri olan notifiable.Item composite pattern ile oluşturduk. notifiable.Item yapılandırılırken ‘this.menuEL’e gerekli ‘click’ event’ini bağlıyor. Oluşturduğumuz objeyi Container’a argüment olarak geçiyoruz. Burada aynı zamanda virtual proxy kullandık. Kullanıcı tıklayıncaya kadar richMenu.Container çalıştırılmıyor. Aynı zamanda ‘this.notify’ (şu okunmamış mesaj numarası)’i güncellemek için sunucuyu dinliyoruz. Bu dinlemeyi de notifiable.Container’da yapıyoruz, her bir element için ayrı ayrı dinleme yapmıyoruz.
Rich Menu
Genel yapı için Composite ve Bridge, butonlar için Command
richMenu.Container diğer constructor’lar gibi sadece html elementden oluşmuyor, aslında manager bir constructor. Menü daha açık mı kapalı mı, daha önceden çalıştırılmış mı yoksa ilk defa mı çalıştırılıyor bu objede tutuyoruz. Bu class’ın open/close/update/destroy gibi method’ları da var. app.menu.richMenu.types.Notification menümüz bir “tür”, aynı constructor’dan kalıtım alan bir diğer constructor’da aynı namespace altındaki Request constructor’ı. İki menünün ortak özellikleri de var, farklı özellikleri de. Her ikisinin de “leaf” (yaprak, composite patterndeki en küçük birim)larını tutan “children” değerleri var. Bunun yanında RequestHeader class’ı sadece “Request” menüde çalıştırılıyor.
Command pattern’i butonlarda uyguladık. Her bir butonun “gotAction” method’u var. Karmaşık kısmı burada yapıp, click event’e bu methodu bağlıyoruz. Bu sayede yeni bir tür “action” oluşturduğumuzda “gotAction” yok ise Interface (diyagramda yok) aracılığıyla hata fırlatıp developer’i “gotAction” methodu oluşturması için uyarabiliyoruz.
Bridge pattern’i neredeyse bu tasarımın heryerinde kullandık. “this.init” methodları ile constructor’dan sonra oluşturulması gereken objelerin constructor’larını burada çağırdık.
Constructor’lar
Ana konumuz mimari olduğu için anlaşılabilir olması açısından detaylara girmeden sadece UML diyagramında göründüğü kadar kod yazacağız.
app.navigation.notifiable.Container
app.provide('app.navigation.notifiable.Container');
app.navigation.notifiable.Container = function(menus, parent) {
this.element = document.createElement('ul');
this.menus = menus;
this.parent = parent;
// burada daha fazla yapılandırıcı kod var
};
app.navigation.notifiable.Container.prototype = {
updateNotify: function(num){
// "notify" rakamı güncelliyoruz
// composite patten'e örnek görebilmeniz için sadece bu methodu implement ettim.
var i;
for (i in this.menus) {
if (this.menus[i].hasOwnProperty(i)) {
this.menus[i].updateNotify(num);
}
}
},
clearNotify: function(){
// "notify" deki rakamı kaldırıyoruz
},
setNotify: function(){
// notify alan ekliyoruz
}
// burada daha fazla prototype methodu var.
};
app.navigation.notifiable.Item
app.provide('app.navigation.notifiable.Item');
app.navigation.notifiable.Item = function(classNames) {
this.element = document.createElement('li');
this.element.className = classNames;
this.notifyNum = 0;
// burada daha fazla yapılandırıcı kod var
};
app.navigation.notifiable.Item.prototype = {
updateNotify: function(num){
this.notifyNum = num;
this.notify.innerHTML = num;
},
clearNotify: function(){
this.notify.style.display = 'none';
this.notifyNum = 0;
},
setNotify: function(){
this.notify = document.createElement('span');
this.element.appendChild(this.notify);
}
// burada daha fazla prototype methodu var.
};
app.menu.richMenu.* namespace’i altındaki yapılandırıcılara (class veya constructor) geçmeden önce yukarıdaki kodun entegrasyon kodunu verelim. Bu constructor entegrasyon katmanında olduğu için UML diyagramına koymadım.
Örneğin sayfamız kullanıcı profil sayfası olsun, sayfamızdaki body elementinin id’si “userProfile” olarak ele alalım. Bu durumda implementasyon kodumuz şu şekilde olacaktır:
app.pages.userProfile
app.provide('app.pages.userProfile');
app.depends('app.navigation.notifiable.Item', 'app.navigation.notifiable.Container');
app.pages.userProfile = function() {
var parent = $('header')[0];
new app.navigation.notifiable.Container([
new app.navigation.notifiable.Item('Request'),
new app.navigation.notifiable.Item('Notification')
], parent);
// burada daha fazla entegrasyon kodu var
}
Yukarıdaki gibi sayfaya özel kodları app.pages namespace’i altında tutuyoruz. Sayfa yüklendiğinde bu namespace ile body id’yi map eden bir objemiz var. O da bu yazıda yer almıyor maalesef. Ayrıca yukarıdaki örnek kodlarda gördüğünüz app.depends ve app.provide gibi oluşturduğumuz methodları da bu yazıda vermiyorum. app.provide verilen parametredeki namespace’i oluşturuyor. app.depends de verilen parametrelerdeki constructor’ları aynı namespace yapısı altındaki dosyaları sunucudan çekmekle yükümlü. Dosya organizasyonumuz da aslında bu namespace’ler ile aynı dizin yapısı içerisinde tutuluyor. Detaylara takılmadan “mimariye” odaklanmaya çalışın.
app.menu.richMenu.* namespace’i altında yazacağımız constructor’larımız da şu şekilde:
app.menu.richMenu.Container
app.provide('app.menu.richMenu.Container');
app.menu.richMenu.Container = function(parent) {
this.element = document.createElement('div');
this.parent = parent;
this.initialized = {}; // oluşturulmuş menüleri burada tutuyoruz
this.status = null; // burada da açık menü'nün tekil kimlik ID'sini tutuyoruz.
// burada çok daha fazla constructor kod var
};
app.menu.richMenu.Container.prototype = {
init: function(){
// oluşturulacak menü ye switch/case ile burada karar veriyoruz
},
open: function(){
// "rich menü" yü açacak kodlar burada
},
close: function(){
// daha iyi anlaşılabilmesi için bu methodu implement ettim
// "rich menü" yü kapatacak kodlar şöyle:
this.status = 'closed';
this.element.style.display = 'none';
return this;
},
destroy: function(){
// oluşmuş bir menü'nün tüm event'lerini ve elementlerini kaldır
}
};
app.menu.richMenu.types.Abstract
Bu constructor’ımız “parent” olduğu için this.element tanımlamıyoruz. Bu constructor’ı extend eden “child” de this.element tanımlayacağız. Burada parent olarak UL tanımladık. Bu constructor zaten küçük, tamamını veriyorum:
app.provide('app.menu.richMenu.types.Abstract');
app.menu.richMenu.types.Abstract = function(container) {
this.parent = document.createElement('ul');
this.parent.className = 'menuContainer';
this.menuItems = [];
container.appendChild(this.parent);
};
app.menu.richMenu.types.Abstract.prototype = {
init: function(){
// bu methodu her bir alt constructor'da tekrar tanımlamak gerekli,
// developer'i bir hata fırlatarak uyarıyoruz:
app.error('Parent class does not support this operation: Called: ',
arguments.callee, ' Caller: ', this.constructor);
}
};
Sıra geldi menü türlerimizi tanımlamaya. Şimdilik iki tür menümüz var, biri Request diğeri Notification.
Daha sonra RequestHeader yapılandırıcısını görüp menü elementlerini oluşturacak constructor’lara geçeceğiz.
app.menu.richMenu.types.Request
//namespace'i olutşturuyoruz
app.provide('app.menu.richMenu.types.Request');
// bağımlılıkları belirliyoruz
app.depends(
'app.menu.richMenu.items.Request',
'app.menu.richMenu.types.Abstract',
'app.menu.richMenu.types.RequestHeader'
);
app.menu.richMenu.types.Request = function(){
// RequestHeader'i henüz oluşturmadık Notification'dan sonra oluşturacağız.
this.header = new app.menu.richMenu.types.RequestHeader(this.element);
}
// extend methodu da kendi yazdığımız bir method, bu yazıya dahil değil.
app.extend('app.menu.richMenu.types.Request', 'app.menu.richMenu.types.Abstract');
app.menu.richMenu.types.Request.prototype.init = function(){
//ajax request yap ve dönen data için app.menu.richMenu.items.Request'i oluştur.
// bu method'u developer unutursa "parent"den hata mesajı gösteriyoruz.
};
app.menu.richMenu.types.Notification
Bu constructor’ımız da neredeyse Request ile aynı, şöyle ki:
app.provide('app.menu.richMenu.types.Notification');
// bağımlılıkları belirliyoruz
app.depends(
'app.menu.richMenu.items.Notification',
'app.menu.richMenu.types.Abstract'
);
app.menu.richMenu.types.Notification = function(){};
app.extend(
'app.menu.richMenu.types.Notification',
'app.menu.richMenu.types.Abstract'
);
app.menu.richMenu.types.Notification.prototype.init = function(){
//ajax request ve diğer işlemler burada
};
Notification menümüzde başlık yoktu fakat Request menümüzde bir başlık var. Şimdi bu constructor’ı yazalım:
app.menu.richMenu.types.RequestHeader
app.provide('app.menu.richMenu.types.RequestHeader');
app.menu.richMenu.types.RequestHeader = function(){
this.element = document.createElement('div');
this.element.className = 'requestMenuHeader';
// burada daha başka constructor işlemleri yapılıyor
};
app.menu.richMenu.types.Notification.prototype = {
// burada bazı prototype methodları var
};
Evet, iki farklı menümüzün farklı yapıları olduğu için farklı dosyalar da farklı constructor ile menümüzü neredeyse tamamladık. Sonraki adım olarak *.items.Abstract, *.items.Request, *.items.Notification constructor’larını oluşturuyoruz:
app.menu.richMenu.items.Abstract
app.provide('app.menu.richMenu.items.Abstract');
app.menu.richMenu.items.Abstract = function(){
// daha fazla detay kod ile kafa karışıklığına sebep olmayalım.
// buralar hep constructor kod
};
app.menu.richMenu.items.Abstract.prototype = {
// burada bazı prototype methodları var, setTime, setUserName, setActions gibi.
// bir tane örnek yazalım;
setAvatar: function(){
this.avatar = new Image();
this.avatar.src = this.data.user.avatar;
this.avatar.className = 'richUserMenuAvatar';
this.element.appendChild(this.avatar);
return this;
}
};
app.menu.richMenu.items.Request
app.provide('app.menu.richMenu.items.Request');
app.menu.richMenu.items.Request = function(){
this.setAvatar().setTime().setUsername();
// gibi parent constructor'dan gerekli methodları çağırıyoruz
// diğer constructor methodlarımız da burada.
};
app.extend(
'app.menu.richMenu.items.Request',
'app.menu.richMenu.items.Abstract'
);
// burada prototype'a ihtiyacımız yok.
app.menu.richMenu.items.Notification
app.provide('app.menu.richMenu.items.Notification');
app.menu.richMenu.items.Notification = function(){
// Bu menümüzde sadece tarih ve text objemiz var.
// Request constructor'da olduğu gibi burada sadece onları çağırıyoruz.
};
app.extend(
'app.menu.richMenu.items.Notification',
'app.menu.richMenu.items.Abstract'
);
// burada da ekstra prototype methoduna ihtiyacımız yok
Son kısıma geldik, action’lar. Hani şu “connection” isteği geldiği zaman kabul veya red ile cevap verebileceğiniz butonlar. Parent constructor’ımız şöyle:
app.menu.richMenu.actions.Abstract
app.provide('app.menu.richMenu.actions.Abstract');
app.menu.richMenu.actions.Abstract = function(){
this.element = document.createElement('a');
// burada event binding işlemlerini felan yapıyoruz ki,
// developer yeni buton oluşturduğunda onClick
// methodunu eklemesi gerektiğini söyleyelim.
};
app.menu.richMenu.actions.Abstract.prototype = {
onClick: function(){
app.error('Parent class does not support this operation: Called: ',
arguments.callee, ' Caller: ', this.constructor);
},
onSuccess: function(){
// göstermek istediğimiz ön tanımlı uyarı mesajını burada implement ediyoruz.
}
}
app.menu.richMenu.actions.Accept
app.provide('app.menu.richMenu.actions.Accept');
app.menu.richMenu.actions.Accept = function(){
this.element = document.createElement('a');
// burada event binding işlemlerini felan yapıyoruz ki,
// developer yeni buton oluşturduğunda onClick
// methodunu eklemesi gerektiğini söyleyelim.
}
app.extend(
'app.menu.richMenu.actions.Accept',
'app.menu.richMenu.actions.Abstract'
);
app.menu.richMenu.actions.Accept.prototype.onClick = function(){
// burada methodun implementasyonu var
}
app.menu.richMenu.actions.Accept.prototype.onSuccess = function(){
// Eğer bağlantı isteğini kabul etmişseniz, objemizin görünüşü,
// state'i değişiyordu. 'X artık arkadaşınız' gibi bir mesaj
// gösteriyoruz. Bu entegrasyonu burada yapıyoruz.
}
app.menu.richMenu.actions.Ignore
app.provide('app.menu.richMenu.actions.Ignore');
app.menu.richMenu.actions.Ignore = function(){
this.element = document.createElement('a');
// app.menu.richMenu.actions.Accept'deki aynı işlemleri Ignore için yapıyoruz
};
// Parent constructor'dan kalıtım alalım
app.extend(
'app.menu.richMenu.actions.Ignore',
'app.menu.richMenu.actions.Abstract'
);
app.menu.richMenu.actions.Ignore.prototype.onClick = function(){
// onClick de yapılacak işlemler
}
app.menu.richMenu.actions.Ignore.prototype.onSuccess = function(){
// ajax request sonrası neler yapacağız?
}
Bir gün yeni özellik gerekirse?
Yeni bir menü eklemek
Kullanıcı güvenlik ayarlarını değiştireceği yeni bir menümüz istendiğini varsayalım. Yazının başındaki şemalara tekrar göz atın, şimdi yanına bir menü daha eklemek istiyoruz. UML diyagramımızı şu şekilde değiştirdik:
Kırmızı renk ile gösterilmiş constructor’ları ekledik, şimdi bu iki constructor’ı yazalım:
app.menu.richMenu.types.Settings
app.provide('app.menu.richMenu.types.Settings');
// bağımlılıkları belirliyoruz
app.depends(
'app.menu.richMenu.items.Settings',
'app.menu.richMenu.types.Abstract'
);
app.menu.richMenu.types.Settings = function(){};
app.extend(
'app.menu.richMenu.types.Settings',
'app.menu.richMenu.types.Abstract'
);
app.menu.richMenu.types.Settings.prototype.init = function(){
//ajax request ve diğer işlemler burada
};
app.menu.richMenu.items.Settings
app.provide('app.menu.richMenu.items.Settings');
app.menu.richMenu.items.Settings = function(){
// constructor kodlar burada
};
app.extend(
'app.menu.richMenu.items.Settings',
'app.menu.richMenu.items.Abstract'
);
Bu yeni “settings” menüsünü kullanıma sunmak için implementasyon kodunda
new app.navigation.notifiable.Container([
new app.navigation.notifiable.Item('Request'),
new app.navigation.notifiable.Item('Notification')
], parent);
yerine
new app.navigation.notifiable.Container([
new app.navigation.notifiable.Item('Request'),
new app.navigation.notifiable.Item('Notification'),
new app.navigation.notifiable.Item('Settings')
], parent);
yazıyoruz. Evet sadece bir satır.
Yeni bir action eklemek
Bir süre sonra menü ile ilgili yeni bir geliştirme isteniyor. Kullanıcı isteklerinin gönderildiği “Request” menümüzdeki Accept, Ignore butonlarının yanına ek olarak Block seçeneği eklememiz gerekiyor. UML’i aşağıdaki şekilde güncelliyoruz:
Mavi renk ile gösterilen constructor’ımızı yazmamız yeterli olacaktır.
app.menu.richMenu.actions.Block
app.provide('app.menu.richMenu.actions.Block');
app.menu.richMenu.actions.Block = function(){
this.element = document.createElement('a');
// burada yine event binding işlemlerini felan yapıyoruz
}
app.extend(
'app.menu.richMenu.actions.Block',
'app.menu.richMenu.actions.Abstract'
);
app.menu.richMenu.actions.Block.prototype.onClick = function(){
// click methodu
}
app.menu.richMenu.actions.Block.prototype.onSuccess = function(){
// success de ne yapıyoruz?
}
Sonuç
Projede javascript bağımlılığı ajax/göster/gizle/slider gibi basit olsaydı bu tür bir mimariye hiç gerek kalmayacaktı. AngularJS veya EmberJS ile de bu kadar büyük bir projeyi tamamlayabilirdik. Fakat yine backend’de bir sürü markup oluşacak, ayrıca bu kadar “temiz” bir kod elde edemeyecektik. Performans ve taşınabilirlik konusunda da bu kadar yarar sağlayamayacaktık.
Ne elde ettik
Bakım: Bakımı yapılabilir, tekrar kullanılabilirliği yüksek, dökümantasyonu tam, sürdürülebilir bir ürün elde ettik. Yeni bir menü eklemek için bir constructor oluşturup, bağımlılığını singleton factory’e eklemek yeterli oldu. Yeni bir “rich menu” için ise aynı interface’leri kullanarak, diğerlerini bozma endişesi olmadan yine bir constructor ile çözebiliyoruz.
Taşınabilirlik: Bir süre sonra Symfony 1.4 den 2.0 a sadece 1 gün, evet şaka değil, sadece 1 gün de tüm frontend’i taşıdık. İleride bir gün çok bilen bir arkadaş “PHP kötü yeaaa hadi backend’i Java yapek!!1!” derse frontend de yine 1-2 günlük işimiz olacak.
Düşük library bağımlılığı: Kendi oluşturduğumuz “araç kutusu”nda bazı eksik yerleri gördük, eksikleri tamamlamak zaman alacaktı, tüm sistemi üç dört günde google closure a geçirdik. Biz sistemi oluştururken bilmiyorduk fakat closure’da bizim ile aynı yolları izliyormuş, bazı yerlerde oluşturduğumuz değişken, method isimleri, temel classların bile aynı olduğunu gördüğümüzde sadece bizim değil, google’ın da aynı metodolojiler ile çalıştığını farkettik, mutlu olduk.
Performans: Yaptığımız testlerde V8 ile (ajax request’ler hariç) hiç bir işlem 1 ms (yazıyla bir) den fazla sürmüyor. SpiderMonkey ile de 2 ms (yazıyla iki)’nin altında. (300-400 ms’e kadar kabul edelibelir olmasına rağmen).
Hata ayıklama: “Frontend çalışmıyoeaea” diyen testçimize, bir cookie set edip testleri tetiklemesini istedik. Eğer bizim maketlediğimiz json dosyaları ile tüm testlerden “pass” aıyorsak “backend’ci naber, hade iş başına” dedik. Hata giderme süreçlerimiz çok daha hızlandı.
Hızlı iş teslimi: “Facebook Wall”ı (sol menü ve header değil, ortadaki stream kısım) sadece 3 günde hemen hemen tüm özellikleriyle 1 developer yazdı (Aslında daha gelişmiş bir wall yazdık). Toplamda Wall’ın testi ve tamamlanması 1 developer ile sadece 5 gün sürdü. Post, comment, share, post ve comment için like buton, like edenleri göster, resim, video veya link önizleme, parent-child yorum sistemi vs. vs.
Çoklu dil desteği: Sunucudan sadece gerekli “json data objeleri” çekiyoruz, çoklu dil desteğini de frontend de tamamladık. (başka bir yazının konusu)
Nelerden bahsetmedik
Konu zaten uzun, daha da uzatmamak için hiç bahsetmediğim, eksik bıraktığım yerler var, izlediğimiz yol yazıda bahsettiğimden biraz daha detaylı;
– Buraya kadar FAZ1’di, FAZ2’den bahsetmedik.
– FAZ2’de facade pattern arkasına yerleştirdik ve addMenuItem(), bindMenuItem(‘ThirdMenu’) şeklinde menüleri oluşturduk.
– ajax, dom, extend ve interface için “light” bir library oluşturduk,
– app namespace’i altındaki “depends” methodu ile bağımlılıkları çözdük,
– Her bir constructor’un bir interface’i var aslında, interface’i implement etmiyor ise hata fırlatıyoruz.
– UML diyagramından hemen sonra testleri oluşturduk,
– Her bir ajax request için bir .json dosyası bıraktık sunucuya (mocked up!!),
– Backend’ci arkadaşlar sadece bu json’larda beklenen verileri sağladılar,
– Backend’e bıraktığımız .json dosyaları tüm “case”leri test edebilecek kadar detaylı,
– UML diyagramları çok daha detaylı, methodlar, değişkenler, bağlanacak ve dinlenecek event’leri belirledik,
– Deployment ve sürdürebilirlik için bir strateji geliştirdik,
– Dökümantasyonu JSDoc ile oluşturduk,
– Kaynak kodu web’den ulaşılamayacak bir yere koyduk, sadece derlenmiş javascript dosyalarına ulaşılabiliyor.
– Cache invalidate için sürüm numaralarını kullandık.
– JS yazarken constructor ve implementation katmanlarından değişik class’lar atadık, css hiç yazmadık.
– HTML ve JS tamamlandıktan, testlerden “pass” aldıktan sonra css uyguladık, js yazarken sitenin tasarımını görmemiştik bile.
– Sürümleme için <major>.<minor>.<bugfix> gibi bir yapı kullandık (1.0.0 gibi),
– Boss‘u özelleştirdik, projeye özel scriptler ekledik, boss ile deploy ettik.
Yazımıza son verirken esenlikler (şaka şaka, trt-1 miyiz biz esenlikler felan) “happy coding” diyoruz.
Not: Yazı çok uzun olduğu için çok iyi toparlayamadım, arada bug çıkabilir, kodlarda syntax hatası olabilir. UML çizimleri sıkış tıkış olmasının sebebi yazıya tam boyut ile sığdırmaya çalıştığım için, kusura bakmayın. Diğer çizimlerin çirkin olmasının sebebi ilkokuldaki resim öğretmenimin yüzünden.
Hala FTP ile live server’a dosya gönderenlerin oranı web üzerindeki domainlerin çoğunluğunu oluşturuyor. Revision Control kullananların bir kısmı da doğru sürümleme yapmıyor. Oyunu kurallarına göre oynamak, ürünün sürdürülebilirliği açısından önemlidir.
İşlerinizi otomatikleştirmek, en zayıf halka olan insan hata oranını olabildiğince aza düşürür, size zaman kazandırır. Yazılımınızın deployment stratejisi olması bu noktada önemli. Büyük bir ekip de olsanız, reklam ajansında küçük web siteleri yapıyor da olsanız düzeninizi kurduktan sonra sorunların çok daha hızlı çözüldüğünü göreceksiniz.
Boss Parametreleri
help Yardım dosyasını gösterir
test Local, stage veya live sunucu üzerinde verilen projenin testini tetikler.
deploy Verilen server üzerine verilen projenin verilen sürümünü deploy eder.
rollback Local, stage veya live sunucu üzerinde son yapılan deployment’i geri alır.
project Yeni proje eklemek, listelemek ve silmek için kullanılır.
Deployment
Web dünyasında deployment dediğimiz zaman akla gelen işlemler;
- Code base update
- Get pass frontend unit/integration tests
- Pass backend tests
- File permission updates
- Database update (sql/no-sql)
- Cache invalidation
Bu işlemleri önce kullandığınız bilgisayarda, sonra stage sunucuda son olarak da live sunucuda yaparsınız. Boss‘u ben bu işlemler için kullanıyorum. Farklı deployment yaklaşımları olabilir, size kalmış. Örneğin birden fazla application server’iniz var ise boss size yetersiz gelecektir, henüz çoklu live sunucu desteklenmiyor.
Bir kurulum ile birden fazla proje deploy edebilirsiniz. Sürüm için branch değil tag kullanır, kurulum çok basit;
$ git clone git@github.com/irfan/boss.git boss
$ cd boss
$ sh install.sh
Proje ismi, git repo, projenin bilgisayarınızda ki yolu, stage ve live sunucu bilgilerini girdikten sonra deployment için ilk proje config dosyanız ~/.boss/etc/<projeadi>.conf altında oluşturulacak.
NOT: Bu bilgileri eksiksiz girmelisiniz. sunucudaki proje dizininiz örneğin /var/www altında ise normal user ile burada değişiklik yapamayacağınız için root ssh login’in sunucuda açık olması ve root’u kullanmanız kolaylık sağlar. Eğer normal kullanıcı girerseniz, proje dizininiz /home/irfan altında olsa bile web sunucunuz www-data gibi bir kullanıcı ile çalıştığı için yine sıkıntı olacaktır. Fakat web sunucunuzu ssh ile aynı kullanıcıyla çalıştırıyorsanız sorun yaşamazsınız.
Versioning
Verisyon numaralarının anlamlı olması önemli. Bu konuda düşünceleriniz net değilse Wikipedia daki Software versioning başlığı sürümleme nedir ve nasıl olmalıdır konusunda size ön bilgili verebilir.
Boss sadece tag ile deployment yapabilir. Zaten branch ile yapılan sürümleme işlemi yanlıştır. Bu konu da ayrı bir yazının konusu.
Birinci sürüm deployment
Local, live ve stage sunucunuz da git repository’nizin olduğunu ve git pull yaptığınız zaman git sunucunuzdan güncellemeleri aldığınız bir sisteminizin olduğunu varsayarak devam ediyoruz. HEAD’inizi 1.0.0 sürümü oluşturarark sırasıyla local, stage ve live a deploy edelim.
NOT: Deployment a başlamadan önce .gitignore dosyanıza .boss/* satırını eklemelisiniz. .boss dizini altında deployment ve sürüm logları tutulmaktadır.
$ git status
# On branch master
nothing to commit (working directory clean)
$ git tag
$ git fetch --tags
$ git tag -a 1.0.0 -m "Version 1.0.0 releasing"
$ git push --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 172 bytes, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@happycoding.net:blog
* [new tag] 1.0.0 -> 1.0.0
$
Bulunduğumuz commit’i yani HEAD’ı 1.0.0 version olarak oluşturduk ve git server’a gönderdik. Şimdi bunu satge a deploy edip testlerimizi çalıştıralım.
$ boss deploy stage blog 1.0.0
==========================================
Deploying to: stage
Project: blog
Version: 1.0.0
==========================================
Pseudo-terminal will not be allocated because stdin is not a terminal.
--> Directory changing
--> creating log directory
fatal: No names found, cannot describe anything.
fatal: No names found, cannot describe anything.
--> Current version is
--> Checking uncommitted changes
--> Fetching tags
--> Pulling with flag
--> Getting new version
--> Setting permissions
Deployment succeed.
Current version is 1.0.0
$
İki tane fatal görünüyor, ilk kez deploy ettiğiniz için bunlar görünüyor. Bir sonraki sürümde görünmeyecek, bunları görmezden gelip devam edebiliriz. Şimdi sıra testlerimizi çalıştırmada.
NOT: Projenizi yapılandırırken testleri trigger edecek scriptin bulunduğu path i ve parametreleri girmiştiniz. Proje dizininizin altında bu scriptin bulunduğunu ve gerekli testleri tetikleyip sonucu verecek yeteneğe sahip olduğunu varsayıyoruz.
$ boss test stage blog
==========================================
Testing on: stage
Project: blog
Version: 1.0.0
==========================================
--> Running 74 frontend tests, please wait
--> 73 of 74 passed
--> 1 test failed:
- Test ID : 12
- Error : User ID could not found in user object
--> Running 138 backend tests, please wait
--> 138 of 138 test passed
Connection to irfandurmus.com closed.
$
Bir testimiz başarısız oldu. Gerekli değişikliği yapıp master branch’a commit edip, sunucuya push’luyoruz. Şimdi bir bugfix yapmış olduk ve bu ayrı bir sürüm olmalı. Sürümümüzün adı 1.0.0-1 olsun.
$ git tag -a 1.0.0-1 -m "12th failed test fixed"
$ git push --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 173 bytes, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@happycoding.net:blog
* [new tag] 1.0.0-1 -> 1.0.0-1
$
Şimdi tekrar stage e deploy edip testleri çalıştıralım.
$ boss deploy stage blog 1.0.0-1
==========================================
Deploying to: stage
Project: blog
Version: 1.0.0-1
==========================================
Pseudo-terminal will not be allocated because stdin is not a terminal.
--> Directory changing
--> Current version is 1.0.0
--> Checking uncommitted changes
--> Fetching tags
--> Pulling with flag
--> Getting new version
--> Setting permissions
Deployment succeed.
Current version is 1.0.0-1
$
Gördüğünüz gibi stage sunucu üzerindeki sürüm 1.0.0 dan 1.0.0-1 e yükselmiş oldu. Şimdi testleri çalıştıralım.
$ boss test stage blog
==========================================
Testing on: stage
Project: blog
Version: 1.0.0-1
==========================================
--> Running 74 frontend tests, please wait
--> 74 of 74 passed
--> Running 138 backend tests, please wait
--> 138 of 138 test passed
Connection to irfandurmus.com closed.
$
Tüm testlerimiz başarılı olduğuna göre artık live sunucuya bu sürümümüzü deploy edebiliriz.
$ boss deploy live blog 1.0.0-1
==========================================
Deploying to: live
Project: blog
Version: 1.0.0-1
==========================================
Pseudo-terminal will not be allocated because stdin is not a terminal.
--> Directory changing
--> Current version is 1.0.0-1
--> Checking uncommitted changes
--> Fetching tags
--> Pulling with flag
--> Getting new version
--> Setting permissions
Deployment succeed.
Current version is 1.0.0-1
İstersek testlerimizi live sunucu üzerinde de çalıştırıp bir hata varsa aynı şekilde fix edebiliriz fakat stage ile live aynı sonucu vermiyorsa sorun sistemler arasındaki farktadır.
Son olarak belirtmeliyim ki boss kesinlikle bir Jenkins veya Hudson alternatifi değildir, olamazda. Sadece işlerin nasıl olması gerektiğini gösteren basit bir script’dir.