Refactoring 101: DDD & DI

Yeni yetme öğrencilik yıllarımda kodladığım, arşivlerimin derinliklerinden şans eseri bulduğum neredeyse 10 yaşındaki IndexMaker projesini çağa uyarlamaya son hız devam ediyoruz.

Bir önceki yazıda Isolation üzerinde durmuştuk. Ne ile Nasıl ayrımını yapmak üzerine biraz ısınmıştık.

Şimdi Ne ile Nasıl’ı neredeyse tamamen birbirinden koparmak ve nihayetinde yalın, çevik ve modüler bir proje oluşturmak için domain yönelimli tasarım ve geliştirme konusunu masaya yatıracağız.

Nedir bu DDD?

Şu resim çok açıklayıcı olacaktır diye tahmin ediyorum;

Geleneksel Mimari’ye karşı DDD Soğan Mimari

Bu resmin özeti şudur, Domain’ime dokunma!

Şaka bir yana, nedir ne değildir aktarabilmek için şöyle bir kıyaslamaya başlayalım.

Geleneksel mimaride (sol), veritabanı teknolojisi, kullanılan üçüncü parti eklenti/kütüphane ve araçlar gibi projenin çevresel bileşenleri tüm katmanlardan erişilebilir durumdadır.

Bu zamana kadar hem hızlı geliştirmeye imkan sağladığı için hem de işleri başlangıçta kolaylaştırdığı için bu yaklaşım benimsenmiştir.

Ancak günümüz yazılım projeleri artık öyle küçük ve basit projeler değil ne yazık ki. Haliyle böyle bir altyapı çok ciddi sorunlara yol açabiliyor.

Örneğin en basitinden kullandığınız veritabanı teknolojisini değiştirmek isteseniz, kolları sıvayıp projenin tüm katmanlarında uzun soluklu bir gezintiye çıkmanız gerekiyor.

Ayrıca bu yaklaşımla genişlemesi, ölçeklenmesi ve bakımı zor projeler ortaya çıkıyor ki bu da yine bu devirde kabul edilemez bir problem.

Peki geleneksel mimarideki bu sorunlara DDD ile nasıl çözüm buluyoruz?

DDD, işleri gerçek hayatta olduğu gibi ele almayı savunuyor. Adında geçen domain, yapacağınız işin özünü temsil ediyor aslında.

Bir yazılım projesi hiç bir zaman bir “yazılım projesi” olsun diye geliştirilmez. Ya bir doküman yönetim sistemi, ya bir otel rezervasyon sistemi, ya da kaynak yönetim sistemi gibi süslü isimleri olur.

Daha da ötesinde, bir yazılım projesinin tam anlamıyla hangi problemi çözeceği ve kimler için bu problemi çözüyor olduğu sorularının cevapları bize Domain’i verir. Cevapta yer alan her bir eleman da aslında Domain’in elemanlarıdır.

Index Maker projesi için Domain nedir?

Index Maker, belirlediğiniz bir klasörü arayarak içindeki tüm dosya ve klasörleri size listeleyen ve içerisinde arama yapmanıza izin veren bir yazılım projesi. Bu tanımdan hareketle Index Maker’ın Domain elemanları neler olmalı?

  • Dosya
  • Klasör
  • Dizin Gezici
  • Dizin/Dosya Arama Motoru

Bu projeden beklediğim her şeyi karşılayan elemanlar bunlar.

Bunları sağda gördüğünüz o soğanın en içine gömeceğiz çünkü bu projenin işlevi tamamen değişmediği sürece yani yukarıda yazdığım tanımda ciddi bir değişiklik olmadığı sürece bu elemanlar sabit kalacak.

Şimdi gelelim bu elemanları yani Domain’i çevreleyecek diğer katmanlara. Evet bir dosya ve klasör elemanı olacağından bahsettik. Ve belirlenen bir klasör elemanı içerisinde gezip bize tüm alt klasör ve dosyaları listeleyecek bir gezici sınıf olmalı.

Bir dakika durup düşünelim.

Bu gezici sınıfı domaine dahil edersek ne olur?

Bir dizin gezgini yazmak teknoloji bağımlı bir iş. Yani teknolojinin değişimiyle değişmesi çok muhtemel. Bu sebeple dizin gezginini domaine dahil etmek doğru değil. Ancak kendisi domain’in bir elemanı/gereksinimi. Yani domain’i tanımlayan elemanlardan biri. Haliyle domain’den çıkarmamız da uygun değil.

Ne yapacağız peki?

Keşke gereksinimleri ve fonksiyonelliği tanımlayabileceğimiz ama implementasyonu başka bir yerde yapabileceğimiz bir teknoloji olsa değil mi?

DI’a girmeye başlıyoruz, kemerleri bağlayın.

Dizin gezgininin limitlerini domainde belirlemek için dizin gezginini ifade eden bir interface oluşturacağız. Infrastructure hariç tüm yazılım dizin gezgininin yalnızca interface’ini bilecek. Böylece teknolojik detaylar işin içine girmeden dizin gezgininin tüm özelliklerini kullanabileceğiz.

Sonrasında Autofac ile bu interface ile Infrastructure katmanında yazdığımız ve teknoloji içeren Dizin Gezgini’ni register edeceğiz.

Bu sayede NE’yi ve NASIL’ı birbirinden tamamen ayırmış olduk.

Mevcut dizin gezgini windows dosya dizinlerinde arama yapmak üzere geliştirildi. Ama DDD ile geliştirme yaptığımız için, şu an desek ki bunu Linux işletim sistemlerinde de çalışacak hale getirelim (.Net Core’a geçtiğimizi varsayalım) bunu yapmak için değiştirmemiz gereken tek proje Infrastructure projesi olacak. Kod karmaşası içerisinde referans aramalar, bir değişiklik yüzünden hata veren absürt sınıflar yok!

Tamamsak koda bir göz atalım.

Önce proje yapısının yeni halini görelim;

Index Maker – DDD

1 projeden oluşan yazılımımız 3 projeye bölündü. Eskiden yalnızca bir Winforms projesinden oluşuyor iken şimdi;

  1. IndexMaker.App – WinForms projesi
  2. IndexMaker.Domain – Domain’i temsil eden Class Library
  3. IndexMakar.Infrastructure – Infra’yı temsil eden Class Library

Şimdi sırasıyla projelerin içinde neler olduğunu inceleyelim.

IndexMaker.App – WinForms Projesi

Çok temiz değil mi? Yalnızca bir form dosyası, hepsi bu.

Diğer tüm dependency’ler referans verilen projelerden geliyor.

Şimdi Domain’i inceleyelim.

Domain projesi için temelde kullandığım 4 ana klasör var.

  • Constants
    Domain’e özgü constant değişkenleri ve sınıfları tanımladığım klasör.
  • Entities
    Domain’de yer alan her bir entity’yi yani domain’e özgü varlıkları karşılayan sınıfları tanımladığım klasör.
  • Repositories
    Veriye erişim ve manipulasyon için kullanılacak repository sınıflarını tanımladığım klasör. Tıpkı dizin gezgininde olduğu gibi repository sınıflarında da yalnızca interface oluşturup implementasyonu Infrastructure katmanında gerçekleştireceğiz.
  • Services
    Uygulama içerisinde domain’e özgü servislerin (dizin gezgini dahil) interface’lerinin yer aldığı klasör. Implementasyon nerede? Infra’da…

Şimdi de infra projesine bir göz atalım.

IndexMaker.Infrastructure – Class Library

Buradaki klasör yapısı da;

  • Data
    Veriye erişim ve veriyi yönetmek ile ilgili tüm teknoloji bağımlı fonksiyonelliği tanımladığım klasör. Altındaki Entities klasörü DB tarafında yer alacak entity’leri karşılıyor. EF klasörü ise bu proje için şimdilik eklenmemiş de olsa ilerleyen süreçte ekleyeceğimiz DBContext gibi EntityFramework sınıflarını tanımladığım klasör.
  • Services
    Domain’de bahsettiğimiz Services klasöründe yer alan interface’lerin implementasyonlarının yer aldığı klasör. Görebileceğiniz gibi Dizin gezgini sınıfımız da burada.

Form uygulamasındaki Program.cs dosyasına da bir göz atalım.

IndexMaker.App Program.cs

Burada daha önce bahsettiğim tüm Registration’ları gerçekleştireceğiz. Bunun için Autofac kütüphanesini kullanacağız. Nuget paketi olarak NPM ile edinebilirsiniz.

Buradaki mantık da kabaca şöyle. Dikkat ederseniz MainForm.cs form sınıfımız constructor’ında bir interface alıyor. Ancak bu sınıfı ilkleyen bir başka sınıf mevcut değil. Peki nasıl oluyor da bu sınıf ilkleniyor?

ConfigureDependencies() metodunu incelediğimizde builder üzerinden çağırılmış bir RegisterType() komutunun builder’a “IDirectoryManagementService interface’ini gördüğünde DirectoryManagerService sınıfından bir instance yarat ve onu kullan” dediğini görebiliyoruz.

Ayrıca MainForm sınıfının constructor’ında bir parametre alacak şekilde değişiklik yaptığımız için artık eski usül form oluşturamıyoruz. Oluşturabiliriz, ancak bu durumda DirectoryManagerService sınıfından bir instance yaratıp bunu MainForm()’a geçirmemiz gerekecek ki bu hareket ile de Dependency Inject etmiş olmayacak, dependency kullanmış olacağız.

Bu sebeple MainForm sınıfını da builder’ımıza register ediyoruz. (Şu cümledeki kelimelerin yarısı İngilizce olmasına rağmen cümlenin Türkçe olması sinir bozuyor biliyorum, ama tümünü Türkçe yazınca da hiç bir şey anlaşılmıyor)

Bu işlem sayesinde artık MainForm’dan bir instance yaratma işi de builder’a aktarılmış durumda. Ve builder IDirectoryManagerService interface’ini gördüğünde DirectoryManagerService sınıfından bir instance yaratıp onu kullanması gerektiğini bildiği için sorunsuz bir şekilde formu kullanabiliyoruz.

Tüm kaynak kodlar github’da DDD klasörü altında mevcut.

Linki de buraya bırakayım: https://github.com/ycansener/IndexMaker

Sorular için Twitter’dan ya da comment ile bana ulaşabilirsiniz.

Ortasından başlayan ve merak edenler için bir önceki yazıda Isolation nedir? Neden yapılır? Nasıl yapılır? demiştik.

Buradan erişilebilir: http://blog.ycansener.com/?p=21

Bir sonraki yazıda işin içine bir de EF ve Code First yaklaşım ile bir veri tabanı ekleyelim diyorum.

Kısmet.

Refactoring 101: Isolation

İlk adım, izolasyon.

Neleri izole etmeliyiz?

Bu soruya cevap verirken dikkat edeceğimiz birkaç nokta var.

  1. Her bir metodun/sınıfın/projenin tek bir çalışma amacı olmalı, ve tek bir amaç için değiştirilmeli/güncellenmeli/genişletilmeli. (SOLID -> S -> Single Responsibility Principle)
  2. Front-end ile back-end’i birbirinden olabildiğince ayırmalıyız. Bu sebeple formlar içerisindeki metotlar ile işin kendisini yapan back-end kodlarını izole etmeliyiz.
    Neye göre karar vereceğiz derseniz, bu adım için kolay bir yöntem var. Front-end ile back-end’i birbirinden ayırırken her zaman kendinize “minimum efor ve back-end’de minimum değişiklik ile yeni bir front-end projesi oluşturabiliyor muyum?” diye sorabilirsiniz. (Örneğin bir web client projeniz var ise onun yanına bir mobil client projesi eklemek gibi düşünebilirsiniz.) Cevap evet olana kadar da ara kütüphaneler ve sınıflar ile izolasyona devam etmelisiniz.
  3. Ne ve Nasıl sorularının cevapları birbirinden izole olmalı. Domain Driven Design konusunda belki de en çok başvurmanız gereken sorudur Ne? sorusu. Bir örnekle açıklayayım.
IEnumerable<string> GetUserNames();

Bu metot NE yapıyor? diye sorduğunuzda vereceğiniz cevap “Tüm kullanıcıların isimlerini getiriyor” olmalı. Ve içeriğindeki kodu incelediğinizde eğer vereceğiniz cevap “EntityFramework ile MyTestDB veritabanına bağlanıp Users tablosu üzerinden User DBSet’ini çekip, bir döngü içerisinde yeni bir string listesine kullanıcı adlarını yazmak ve bunu return etmek.” ise, vay halinize…

Ne sorusuna vereceğiniz cevap hiçbir zaman teknoloji ya da yöntem içermemeli. Benzer şekilde nasıl sorusu sorduğumuz bir metot da Ne sorusunun cevabını verememeli.

Bu bilgiler ışığında projenin ilk versiyonu üzerinde izolasyon çalışmalarına başlayalım.

Arayüz üzerinde herhangi bir değişiklik yapmamaya çalışacağım gerekmedikçe.

Mevcutta arayüzün geldiği nokta şu şekilde:

Mühendislerin dostu, ajansların korkulu rüyası “Çirkin ama işlevsel arayüz”

Formun code-behind tarafında ne gibi değişiklikler olduğu da proje yapısındaki değişimden görülebilir.

Yeni proje yapısı

Helpers ve Models isminde iki yeni klasör eklendi. Yalnızca bu ekran görüntüsünden artık Windows’un kendi File ve Folder sınıflarını kullanmadığımızı, kendimize ait yeni birer FileModel ve FolderModel sınıfı oluşturduğumuzu görebiliriz. Bu iki model sınıfı da ortak bir IDirectoryItem interface’inden türüyor. Gerçek dünyada olduğu gibi nasıl ki File ve Folder bir dosya sisteminin iki elemanı ise, kodda da bu yapıyı kurmalıyız. Domain Driven Design’a yaklaştıkça bu eğilimi daha net görebileceksiniz.

Bir diğer yenilik, DirectoryManager sınıfı. Buradan da anlıyoruz ki dizinler ile ilgili işlemleri yönetmek üzere bir yardımcı sınıf oluşturulup tüm fonksiyonellik buraya taşınmış.

Şimdi forma ait code-behind’a bir göz atalım ve nelerin izole olduğunu görelim.

Eski ve yeni kodları arka arkaya bölüm bölüm ekleyerek nelerin neden değiştiğini aktaracağım.

Eski Form1.cs

FolderBrowserDialog fbd;
        string path;
        string[] folders;
        string[] files;
        public Form1()
        {
            InitializeComponent();
            toolTip1.SetToolTip(listBox1, "Bulunduğu dizini açmak için çift tıklayınız. İşlem menüsü için sağ tıklayınız.");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            fbd = new FolderBrowserDialog();
            fbd.ShowDialog();
            if (fbd.SelectedPath.ToString() != "")
            {
                path = fbd.SelectedPath;
                textBox1.Text = path;
                folders = Directory.GetDirectories(path);
                files = Directory.GetFiles(path);
            }
        }

Yeni Form1.cs

FolderModel _folderModel;
        public Form1()
        {
            InitializeComponent();
            toolTip1.SetToolTip(treeViewResults, "Bulunduğu dizini açmak için çift tıklayınız. İşlem menüsü için sağ tıklayınız.");
        }

        private void buttonBrowse_Click(object sender, EventArgs e)
        {
            using (FolderBrowserDialog fbd = new FolderBrowserDialog())
            {
                DialogResult dr = fbd.ShowDialog();
                if (dr.Equals(DialogResult.OK))
                {
                    string path = fbd.SelectedPath;
                    string name = DirectoryManager.GetName(path);
                    textBoxPath.Text = path;
                    _folderModel = new FolderModel(name, path, null);
                }
            }
        }

İlk göze çarpan global değişkenler olmalı. Eski versiyonda dosyalar, klasörler, FolderBrowserDialog’a kadar pek çok global değişken vardı.

Tüm bunlar yerine yalnızca seçilen klasöre ait bilgileri tutan FolderModel sınıfından bir değişken yeterli.

Bir diğer iyileştirme, isimlendirmeler. button1 gibi bir isimlendirme yerine butonun yaptığı iş ile butonu ve butona ait metotları isimlendirmek kodun okunabilirliğini ve haliyle bakımını kolaylaştıracaktır.

Bir diğer yenilik, Disposable bir sınıf olan FolderBrowserDialog sınıfının kullanımı. Daha önce uygulamanın kapanışında manuel olarak dispose edilen bu sınıf artık browse butonunun click eventinde using ile initialize edilerek kullanılıyor ve görevini tamamladıktan sonra, yani bize seçilen folder’ın yolunu verdikten sonra otomatik olarak dispose ediliyor.

Bir diğer güzellik de, using içerisinde tanımladığımız tüm değişkenler görevini yerine getirdikten sonra dışarıya yalnızca _folderModel sınıfı çıkabiliyor. Ve bizim için gerekli olan tek sınıf bu. Diğer tüm referans ve değer değişkenlerinin ömrü sonlanıyor.

Gelelim klasörleri/dosyaları arama ve listeleme işlevselliğine.

Eski Kod

private void button2_Click(object sender, EventArgs e)
        {
            if (folders != null || files != null)
            {
                if (checkBox1.Checked)
                {
                    yaz(folders, 0);
                    yaz(files, 0);
                }
                else
                {
                    duzYaz(folders);
                    duzYaz(files);
                }
            }
        }

        private void yaz(string[] paths,int degree)
        {
            string newPath;
            string ayirac = "";
            for (int i = 0; i < paths.Length; i++)
            {
                for (int j = 0; j < degree; j++)
                    ayirac += "     ";
                listBox1.Items.Add(ayirac +"-"+ paths[i].ToString());
                newPath = paths[i].ToString();
                if (Directory.Exists(newPath))
                {
                    yaz(Directory.GetDirectories(newPath),degree+1);
                    yaz(Directory.GetFiles(newPath),degree+1);
                }
                ayirac = "";
            }
        }

        private void duzYaz(string[] paths)
        {
            string newPath;
            for (int i = 0; i < paths.Length; i++)
            {
                listBox1.Items.Add(paths[i].ToString());
                newPath = paths[i].ToString();
                if (Directory.Exists(newPath))
                {
                    duzYaz(Directory.GetDirectories(newPath));
                    duzYaz(Directory.GetFiles(newPath));
                }
            }
        }

Yeni Kod

private void buttonStartListing_Click(object sender, EventArgs e)
        {
            Clear();
            bool showCompletePath = checkBoxShowCompletePath.Checked;

            DirectoryManager dm = new DirectoryManager(_folderModel);
            dm.Investigate();

            FillTree(_folderModel, showCompletePath);
        }

        private void FillTree(FolderModel rootFolder, bool showCompletePath)
        {
            TreeNode rootNode = AddNode(null, rootFolder, showCompletePath);
            treeViewResults.Nodes.Add(rootNode);

            FillTree(rootNode, rootFolder, showCompletePath);
        }
        private void FillTree(TreeNode parentNode, FolderModel selectedFolder, bool showCompletePath)
        {
            TreeNode currentNode = AddNode(parentNode, selectedFolder, showCompletePath);

            foreach (var subFolder in selectedFolder.GetSubFolders())
            {
                FillTree(currentNode, subFolder, showCompletePath);
            }

            foreach (var file in selectedFolder.GetFiles())
            {
                AddNode(parentNode, file, showCompletePath);
            }
        }

        private TreeNode AddNode(TreeNode parentNode, IDirectoryItem itemToAdd, bool showCompletePath)
        {
            string nodeText = itemToAdd.Name;
            if (showCompletePath)
            {
                nodeText = itemToAdd.CompletePath;
            }

            if (parentNode != null)
                return parentNode.Nodes.Add(nodeText);

            return new TreeNode(nodeText);
        }

İlk değişiklik listView yerine TreeView kontrolünün kullanılması. Böylelikle eski kodda yer alan indent verme karmaşıklığı tamamen ortadan kalktı. Bir diğer önemli değişiklik tüm directory kodları ve fonksiyonelliğin DirectoryManager sınıfına taşınması. Bu aşamadaki en büyük izolasyon bu sınıf ile gerçekleşiyor.

Son olarak eski kodlarda iterative şekilde kodlanmış ağaç oluşturma fonksiyonları, yeni kodlarda recursive kodlanmış.

Bu aşamada hala tek bir form application üzerinden ilerliyoruz. Domain – Infrastructure – App üçgenini henüz kurmaya başlamadık. Ancak bu adımda yaptığımız izolasyon sayesinde bu ayrımı daha rahat görebiliyoruz artık.

Yeni eklenen modelleri ve DirectoryManager sınıfını github üzerinden inceleyebilirsiniz. Ayrıca bu aşamaya kadarki tüm kodlara yine github üzerindeki aynı repository’den, yazının başlığı ile aynı isimli klasör altında ulaşabilirsiniz.

Bir sonraki adımda Domain ve Infra ayrımını yapıp Inversion of Control ve Dependency Inversion’ı odağımıza alacağız. Ne ve Nasıl‘ı birbirinden ayırmaya devam edeceğiz.

Sıradaki yazı; Refactoring 101: DDD, IoC ve DI

Refactoring 101: IndexMaker

Lisans döneminden kalma ödev, not, proje dokümanlarını kurcalıyorum son birkaç gündür. Arşivimi temizlemek ve taşımak vesilesiyle giriştiğim bu yolda o kadar güzel içerikler edindim ki, hala hayret ediyorum.

Tahminimce 2010 yılında yazdığım, baktıkça bu güne dek ne kadar yol katettiğimi gördüğüm bir proje IndexMaker.

O zamandan bu zamana neler öğrenmişim, neleri yanlış yapmışım hepsini sizlerle paylaşabileceğim çok güzel bir case-study olacak.

Refactoring 101, bir yazı dizisi olsun isterim. Adım adım yaptığım iyileştirmeleri tek tek açıklayarak paylaşacağım.

Dizinin özeti, kabaca aşağıdaki gibi.

Use-case: Windows işletim sisteminin kullandığı dosya sistemi için bir gezgin yazmak istiyoruz. Bu uygulamayı yazdığım dönemde içerik arşivciliği çok yaygındı. Filmler, diziler, müzikler harddisklerde saklanır ve paylaşılırdı. Haliyle kendi içeriğini ve kimde hangi içeriklerin olduğunu listelemek ciddi bir problemdi. Bu probleme çözüm amaçlı böyle bir uygulama geliştirmişim zamanında.

O zamanlardan kalan blog postu da burada.

Neler göreceğiz:

  • SOLID Principles
  • Domain Driven Design
  • Clean Architecture
  • .Net Core Web API projesi geliştirme
  • .Net Core MVC projesi geliştirme
  • Aklıma gelmeyen ama refactor ederken değineceğim daha nice şirinlikler!

Bir yazılım geliştiricinin bilmesi gereken pek çok konuyu bu uygulama üzerinde paylaşmış olmayı umuyorum.

Sorular ve öneriler geldikçe, daha da gelişeceğinden şüphem yok.

2010 yılında yazdığım kodu da içeren Github repository’sine ait linke de buradan erişebilirsiniz.

Her release çıkardığımda yaptığım iyileştirmeleri detaylı olarak aktaran bir ReadMe dosyası ile birlikte yeni bir klasör altında solution’ı bu repodan paylaşıyor olacağım.

Güzel ve yararlı bir dizi olmasını dilerim.

Sıradaki yazı, Refactoring 101: Isolation