TypeScript İle SOLID

Selamlar;

2016’nın ilk makalesinde TypeScript altında SOLID yazılım prensiplerine göz atacağız. Zaten TypeScript’in en büyük özelliklerinden biri de client side tarafta Object Oriented programlama geliştirebilmektir. O zaman hadi gelin eski ile yeniyi birleştirip, Robert C. Martin‘in 2000 yıllarının başında oluşturduğu SOLID prensipleri ile 2013 yılından bu yana Microsoft tarafından Anders Hejlsberg liderliğinde geliştirilen son model Javascript Framework’ü olan TypeScript’i birlikte inceleyelim. Eğer TypeScript hakkında bilginiz yok ise önce buradaki makalemi okumanızı tavsiye ederim. Sonra burdan da derinlemesine TypeScript’i inceleyebilirsiniz.

Solid

Öncelikle SOLID klasik bir tanımlama yerine, aklınıza gelebilecek başlangıçta öngörmediğiniz birçok soruna çözüm olarak, acılar ve hatalar ile harmanlanıp büyük tecrübeler kazanılarak ortaya çıkarılmış yapılardır. Yukarıda gördüğünüz 5 ana tasarım prensibinin ezberlenmekten ziyade içinize sindirilerek bilinmesi gerekir. Bazen hiç ihtiyaç olunmadan sırf kullanmış olmak için yazılan projelerden tutun da, hiçbirşeyden haberi olunmadan binlerce satır kodlanmış projelere kadar, hergün birçoğu ile karşılaşmaktayız. Bizim de aynı hatalara düşüp dünyayı bir daha keşfetmemize gerek yok. Her bilim dalında olduğu gibi bizim de kazanılmış tecrubelerden bir ders çıkartıp SOLID olarak adlandırlan prensipleri bilmemiz; hem bize hem de bizden sonra koda bakacak developerlar için büyük önem arz etmektedir. Artık neden SOLID sorusuna cevap bulduğumuza göre gelin hangi sorunlara karşı alınan çözümler olduklarına bakalım.

Single Responsibility Principle: Öncelikle soruna deyinmek istiyorum. Sonra çözümü anlatınca zaten bu tasarım kalıbını anlamış olacaksınız. Diyelim ki bize başkasından bir proje devredildi. Biz de Join.cs sınıfının kodlarını açtık. Tek bir sınıfda binlerce satır kod var ve farklı işlerin hepsi aynı yerde yapılıyor. “XboxLive” sınıfında “User” herhangi bir “Oyun”‘a giriş yapmak istediği zaman Mobile’den, Konsol’dan ve Tablet’den bağlanan kullanıcı için User ve Oyun bilgileri alınıp gerekli koşullar platform bazında bakılarak örneğin User Oyunu almış mı? User Gold üye mi? gibi sonunda duruma göre farklı platformlar için farklı aksiyonlar alınacak. Bizden istenen sadece konsoldan “X” oyununa bağlanan kullanıcının bağlantı zamanın loglanması.

  • Sorun 1: Binlerce satır içinde ilgili kısmın bulunması çok vakit alacak ve bütün kodun belki okunması gerekecektir.
  • Sorun 2: Kodlar içindeki yapılar birbiri ile o kadar çok bağımlıdır ki örneğin birinin methodu diğer yerde de kullanılıyordur ki bir yerde yapılan değişiklik başka nereleri etkiler bilinmemektedir.
  • Sorun3: İçerisinde kullanılan yapıları başka bir yere taşımak yani başka bir projede kullanmak imkansızdır.
  • Sorun 4: İlerde Pc’den bağlanan kullanıcı içinde bir takım işlmeler yapılması istense tüm kodların tekrardan yazılması gerekmektedir.

Burdan çıkarılacak sonuç her sınıf kendine ait görevi yaparsa ve dağtık bir mimari kullanılırsa, başkasının kod üzerinde çalışması hem okunma hem de aranılan yerin kolay bulunması bakımından kolaylaşacaktır. Ayrıca ilerde proje genişletilmek yada belli sınıfların başka yerlerde kullanılması istenildiğinde bu işlemler kolaylıkla yapılabilecektir. Uzun lafın kısası  bir sınıf,fonksiyon vb.. sadece tek bir sorumluluğu yerine getirmelidir.

s

Şimdi TypeScript ile ilgili kodları yazalım:

Öncelikle User yaratan bir function yazalım: Burada amaç farklı yapılar için farklı userlar oluşturmaktır. Örneğin Windows 10 User, Azure User, TFS User gibi.

UserTypes(Enum): Gelen kullanıcı tipini tutar.

IUser(Interface): “User” sınıfının türetileceği Interface dir.

User(Class): Aşağıda görüldüğü gibi “User” sınıfı “IUser” interface’inden “implements” keyword’ü ile türetilmiştir. Interface’deki özelliklere ek olarak “IsGoldMember” aşağıdaki gibi eklenmiştir.

CreateUser(Function): Aşağıdaki function “Generic” tipinde bir functiondır. “<u extends User>” demek “u” değişkeni “User” sınıfından “extends”(türetme) olmuştur. “new(): u” keyword’ü ile “u” türündeki “User” sınıfındaki “newUser”‘ın constructer ile yaratılaması zorunlu hale getirilmiştir. Yani (newUser = new user()). “GamerTag”, “LiveID” ve “IsGoldMember” parametre olarak alınmaktadır.  Son olarak “UserType”  => “UserTypes” enum türünde bir parametredir. Function içinde  ilgili parametreler yeni oluşturulan “user” sınıfının propertylerine atanarak geri dönülür.

Game(Class): Aşağıda Oyun adı ve Lisans numarası bilgileri tanımlanmıştır.

GameUserLicenses(Function): Kullanıcının LiveID’si ve Oyunun LicenseID’si birlikte tutularak, hangi kullanıcının hangi oyunu aldığı bilgisi saklanmaktadır.

IGameLicense(Interface): GameUserLicenses’da tutulan datanın Interface karşılığıdır.

CheckLicense(Class): Amaç “IGameLicense” tipinde(L extends IGameLicense) dönen biri dizi içindeki(Licenses:L[]) datalardan gönderilen “LiveID” ve “UserLicenseID”‘ye ait herhangi bir kayıdın olup olmadığıdır. Yani User’ın ilgili Oyunu alıp almadığna burada bakılır. Bu işlem için “filter” keyword’ü ve filter amaçlı function için “d=>” lambda expression’ı kullanılmıştır. Sonuç olarak filitrelenen “Array[]”‘in “length”‘ine bakılarak “true” veya “false” dönülmüştür.

Greet(Class): Bu kullanıcıyı Verify işleminden sonra karşılamak amacı ile kullanılmaktadır. “Greeting()” methodu ile herbir “userType”‘a göre farklı bir karşılama mesaji verilmiştir.

Verify(Class): Aşağıdaki sınıfın amacı Constructor’ında “User” ve “Game” sınıflarını alıp, ilgili user’ın istenen oyunu oynayıp oynayamayacağının belirlenmesidir. “User” ve “Game” sınıfları haricinde “Greet” adında bir sınıf ile User’ı karşılama şekli ve  “CheckLicense” sınıfı ile de ilgili User’ın Oyunu satın alıp almama durumuna göre oynama yetkisi kontrol edilir. “CanJoin()” methodunda “User” sınıfının “IsGoldMemember” özeliğinin “true” olup olmadığına ve “CheckLicense” sınıfına ait “CheckGameLicense()” methodu ile de bağlanılmak istenen oyunun alınıp alınmadığına bakılır. Sonunda olumlı ise “Greet” sınıfına ait “Greeting()” methodu çağrılır. Değilse yetkisiz olduğu mesajı verilir.

Not:Verify class’ı SRP için yapılmış bir sınıf değildir. Aslında bir Main() methodudur. Ya da javascript örneği ile init() yani initialize function’dır. Tek bir görev yapan sınıfların çağırıldığı ana sınıftır. Örnek: Projenin çalıştırıldığı nokta bu kısımdır. “var verify=new Verify(usr,game);

result

Test: Aşağıdaki testin sonucunda yukarıdaki mesaj alınır.

Görüldüğü gibi her işlem için ayrı bir sınıf yapılmış ve herbirine ayrı işler verilmiştir. Öncelikle bu sınıflardaki işler gerçekten genişleyebilecek ve detaylandırılabilecek işlerdir. Yani kolayca yeni özellikler eklenebilir. Ayrıca farklı yapılara ve platformlara destek verebilir. Şimdi gelin neden Single Responsibility kullandık sınıf sınıf irdeleyelim.

  • User: Farklı platformlardan gelen Userlar olabilir. Böylece herbir platform için User yaratan “CreateUser()”  function’ı oluşturdu. Yeni bir user tipi sisteme kolayca eklenebilmektedir.  Ayrıca user’a yeni özellikler tek bir yerden eklenebilmektedir.
  • Game: Oyunlar başka bir sınıfta tanımlanmış ve özelikleri platformdan bağımsız hale getirilmiştir. Böylece tüm platformlar için tek bir Oyun sınıfı oluşturulmuştur.
  • CheckLicense: İlgili oyunun belirtilen user tarafından alınıp alınılmadığı buradan bakılmıştır. Böylece tüm platformlar için aynı sınıf kullanılabilmektedir. Ayrıca bakılması gereken başka bir kriter olması durumunda tek bir yerden kolaylıkla değişiklik yapılabilecektir.
  • Verify: Xbox Live için tüm kontrol işlerinin yapıldığı yerdir. Yani hem CheckLicense ile oyunun user tarafında satın alınıp alınılmadığına bakınılmış hem de Xbox Live’a özel User’ın Gold özelliğine bakınılmıştır. Live için istenen yeni özellikler kolaylıkla buraya eklenebilir. Örneğin en başta yukarda istenen konsoldan “X” oyununa bağlanan kullanıcının bağlantı zamanın loglanması” tam da burada yapılabilecek bir kodlamadır.
  • Greet: İşlem başarılı ile geçildikten sonra verilecek mesaj platforma göre farklılık gösterebilir. Yani bu sınıfın genişleme ihtimali çok yüksektir.

Eğer tüm kod tek bir sınıf altında yazılsa idi güncel hayatta bolca duyulan “Çalışan Koda Dokunmayalım!” yada “Burayı Değiştirirsem Neresi Patlar Bilemiyorum!” gibi sözlere hazır olunması gerekirdi. Ayrıca birazdan yapılacak ufacık yeni bir eklenti bile çok zaman alabilecekti.

Tüm Kod(Single Responsibility Principle):

Open/Closed Principle:  Sorun yukarıda istenen duruma göre konsoldan “X” oyununa bağlanan kullanıcının bağlantı zamanın loglanması gerekmektedir. Diyelim ki logları MsSql bir db’ye yazılıyor.  Daha sonra MongoDB’ ye yazılması istendi. Ya da Xml olarak tutulması istendi. Tüm kodların baştan yazılması gerekmektedir. İşte tam bu durumda genişlemeye açık ama değişime kapalı olma ilkesi devreye girmektedir.

Open

Yukarıda görüldüğü gibi öncelikle log tutacak ILogger interface’ine “WriteLog()” methodu yazılmıştır. Daha sonra oluşturulan tüm sınıflar bu interface’den türetilerek ortak bir “WriteLog()” methoduna ihtiyaç duyulması sağlanmıştır.

Tüm Kod(Open/Closed Principle):  Aşağıdaki kodda da görüldüğü gibi ILoger interface’i ve bundan türetilmiş 3 sınıf  “XmlLog, MsSqlLog ve MongoDbLog”‘larıdır. Hepsi aynı Interfaceden türetildikleri için ortak method “WriteLog()”‘dur. “LogProcess” sınıfı Constructer’ında “ILogger” beklemektedir. Böylece yeni bir Log yapısı oluşturulsa dahi aynı interfaceden türetüleceği için ve gene aynı method ismi olan “WriteLog()” kullanılacağı için herhangi bir kod değişikliğine gidilmeyecektir. Ekran çıktısı aşağıdaki gibidir.

Iloger

Şimdi gelelim User konsoldan oyuna girince ilgili log’un tutulmasına: XboxLive için “XboxLiveLogger” sınıfı “ILogger” Interface’inden türetilmiştir. Böylece herhangi bir kod değişikliğine gidilmeden yeni bir loglama sınıfı yazılmış ve genişleme işlemi kolaylıkla yapılabilmiştir. Burada ilgili User’ın “GamerTag”‘ı Constructor’da alınmış ve giriş yapılan zaman ile birlikte loglanmıştır.

class Verify sınıfının CanJoin() methoduna aşağıdaki Log eklenir: Bu yeni yazılan “XboxLiveLogger” sınıfına ait “LogProcess()” methodunun istenen durum sağlandığında çağrılması ve giren user’ın GamerTag’ı, girdiği zaman ile birlikte loglanmasını sağlamıştır.

Log

Verify(Class) Son Hali: Aşağıda görüldüğü gibi değiştirilmiştir. Eğer Join olmaya çalışan user  Console user ise “if (this.user.UserType == UserTypes.Console)“, “XboxLiveLog” sınıfı ile ilgili user’ın “GamerTag”‘ı ve join olduğu zaman loglanır. İlgili ekran görüntüsü yukarıdadır. Böylece “Open/Closed Principle” kullanılarak ilgili loglama sınıflarına bir yenisi eklenmiş ve genişlemeye açık bir yapı oluşturulmuştur. Artık farklı yapılardan istendiği kadar loglama operasyonu gelmesi durumunda, hiç kod değişikliği olmadan kolaylıkla eklenebilecektir.

Bu makalede SOLID prensiplerinden  “Single Responsibility” ve “Open/Closed”  prensiplerini TypeScript üzerinde incelemeye çalıştık. Diğer makalede SOLID prensiplerinin devamına deyineceğiz. Yeni bir makalede görüşmek üzere hoşçakalın.

Source Code: https://github.com/borakasmer/TypeScriptSOLID

Source:

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

9 Cevaplar

  1. Bertan İlhan dedi ki:

    Elinize sağlık çok güzel bir konuya değinmişsiniz. Aklıma takılan bir kaç konu var. Single Responsibility, bir classın sadece kendi işini yapması gerektiğini söylemekte, peki örneğin sadece x işini yapan bir classı loglamak istediğimizde hem x + loglama yapacağı için teorik olarak Single Responsibility bozulmuş oluyor. Bunu AOP teknikleri ile çözümleri var. (https://aspect.codeplex.com/)

    Cross Cutting sınıfları için Single Responsibility görmezden mi gelmeliyiz yoksa kod biraz uzasa dahi AOP teknikleri ile mi çözmeliyiz ?

    Teşekkürler.

    • borsoft dedi ki:

      Selamlar Bertan;
      Öncelikle yorumların için teşekkür ederim. Aslında makalede loglama mantığına baktığınızda “class LogProcess” sınıfı Open/Closed Principle için örnek verilmiştir. Ama mantıken yine sadece bir işi yapmaktadır. Yani tek bir sorumluluğu vardır log tutmak. Ve sadece tek tip log tutmaktadır. Biz hangisini tercih ederi isek o şekilde bir loglama vardır. Yani makalede “+” kavarımı yoktur. Tek bir işlem şekilinde Log işlemi yapılmaktadır. Sadece yapıcağı log işi kendisine verilen sınıfa göre farklılık gösterebilmektedir. Cross Cutting sınıflarda amaç TypeScript için method çağrılmadan önce , sonra ve hata zamanında Handlerlar yazmaktır. Bu durumlarda Single Responsibilty bence bozulmaz. Çünkü o sınıfın görevi yine tektir. Aslında bu biraz işin felsefesi oluyor. Ama bu yaklaşımınızı çok beyendim:)

      İyi çalışmalar.

  2. Fatih Tolga Ata dedi ki:

    Güzel, faydalı bir makale olmuş, elinize sağlık. Devamını bekleriz.

  3. Mustafa dedi ki:

    Doya doya okudum, örnek yapıp diğer makaleye de geçeceğim :)

  4. Mustafa dedi ki:

    2 adet sorum vardı size;
    1) CreateUser fonksiyonunu User class ı içerisinde neden bir metot olarak tanımlamadık?
    2) CreateUser fonksiyonu içerisinde u değişkenini user() ile instance aldık, User() olarak almayacak mıydık? Muhtemelen parametre tarafında user() olarak tanımladık diyeceksiniz ama User() olarak instance alamaz mıydık hocam? User() olarak kendim yazdığımda kabul etmiyor.

    • borsoft dedi ki:

      Selam Mustafa,

      1-)Yumurtamı tavuktan çıkar, tavuk mu yumurtadan:) Diyerek söze gireyim. Zaten farklı tipte userlar olabilir. Mobile, Tablet, XboxOne gibi amaç yeni bir User yaratmak. Doğal olarak içinde tanımlamak pek doğru değil. Yani yeni bir user’ı yine bir user içinde oluşturmak:) Ayrıca amaç ne Single Responsibility :)

      2-)Hayır almıyacaktık. Çünkü “user” bir parametredir. Ve ilgili sınıf ile aynı adı almamalıdır. Yani “user” aslında Instance alınması mecburi bırakılan “u” tipinde bir parametredir. “u” tipi de “User” sınıfından türemesi zorunludur.Buna göre “user”‘da “User” sınıfında türemisi zorunlu bir parametredir.

      Güzel gidiyorsun. İyi çalışmalar..

      • Mustafa dedi ki:

        1) Evet dediğiniz gibi farklı tipte User lar olabilir, fakat sonuçta hepsi bir User, ve bizim de User sınıfımız var. User ile ilgili direk olan işlemleri User sınıfı içinde yapmak gerekmez mi? Zaten parametre olarak UserType alıyoruz, farklı User tipleri olması buna engel teşkil etmiyor?

        2) Dediğim gibi yani parametre kısmında zaten User tipinde olması zorunlu koştuk bu sebeple user() olarak instance aldık. Peki direk User tipinde almamızın sakıncası nedir? Single Responsibility kuralına ters düştüğü için mi böyle kullandık?

        İçses: Umarım sorularımdan dolayı bunaltmamışımdır. :)

borsoft için bir cevap yazın Cevabı iptal et

E-posta hesabınız yayımlanmayacak.