Author: irecep

  • Karmaşıklığı yönetmek

    Karmaşıklığı yönetmek

    Yazılım projelerinin başarısız olma sebeplerinin başında planlama, yönetim ve ister sorunları geliyor. Sanılanın aksine teknik sebepler alt sıralarda. Teknik sorunların başını ise kontrolden çıkmış karmaşıklık çekiyor. Zaman içerisinde yazılımlar büyüyor ve kimse yazılımın nasıl çalıştığını, değişiklik yapınca nerelerin etkileneceğini bilemez hale geliyor.

    Steve McConnell, Code Complete kitabında karmaşıklık yönetimini, yazılım geliştirmenin temel mecburiyeti olarak niteliyor. Bir yazılımın çalışması için gerekli altyapıyı bir düşünsenize; elektrik sinyallerinden programlama dillerimize uzanan onlarca katman. Tek bir insanın bütün bu detaya hakim olması mümkün değil. Donanım, network, işletim sistemi, derleyiciler gibi farklı bileşenler sayesinde bütün bu karmaşadan kendimizi soyutlayabiliyor ve çözmeye çalıştığımız probleme odaklanabiliyoruz.

    Peki her gün sayesinde akli dengemizi koruyabildiğimiz bu yaklaşımı kendi sistemlerimizde neden kuramıyoruz. Neden Big ball of mud hala dünyanın en çok üretilen yazılım mimarisi? Neden spagetti bizim için bir yemek değil, korkulu bir rüya? Aslında hepinizin aşina olduğuna emin olduğum pek çok kavram – OOP, SOLID, tasarım kalıpları, mikro servisler, clean code, clean architecture vb.- karmaşıklığı yönetmek için araçlar. Maalesef bu kavramları derinlemesine anlayıp kullanmak yerine taklit etmekle yetiniyoruz ve asıl amacı kaçırıyoruz.

    Yazılım geliştirirken temel hedefimiz; insan zihninin limitlerini göz önünde bulundurarak; bir tarafını anlamaya çalışırken diğer taraflarını düşünmemize gerek bırakmayan, anlaması. çalışması kolay yapılar kurgulamak olmalı. Diğer bütün ihtiyaçlar ikincil. Ancak bu sayede değişiklik ile barışabilir ve daha karmaşık problemler çözmeyi başarabiliriz.

    Dave Farley, Modern Software Engineering kitabında karmaşıklığı yönetmek için şu başlıkları önermiş:

    • Modülerlik (Modularity)
    • Kohezyon (Cohesion)
    • – (Separation of concerns)
    • Bilgiyi gizlemek ve soyutlama (Information hiding and abstraction)
    • Bağımlılık yönetimi (Managing coupling)

    Gelecek günlerde bu konulara ayrı ayrı değinmeye çalışacağım.

  • Hangi birim?

    Hangi birim?

    Birim testleri ile ne amaçladığımıza geçmiş bir yazıda bakmış ve Unit Testing Principles, Practices, and Patterns kitabından aşağıdaki tanım ile bitirmiştim:

    Bir geliştiricinin, yaptığı geliştirmenin beklediği gibi çalıştığından emin olmak amacıyla yazdığı aşağıdaki özelliklere sahip test türüdür:

    • Otomatik çalıştırılabilir
    • Kodun küçük bir parçasını (birim olarak da bilinir) doğrular
    • Hızlı çalışır
    • İzole çalışır

    Bu tanım her ne kadar açık görünse de birim kavramını iki farklı şekilde yorumlamak mümkün – klasik bakış ve taklitçi (mockist) bakış.

    Taklitçi (mockist) bakış birim kavramını izole edilmiş bir kod parçası – tek bir fonksiyon, bir sınıf, bir tip – olarak yorumlarken; klasik bakış birimi kod değil bir sistem davranışı olarak görüyor. Gelin iki yoruma biraz daha detaylı bakalım.

    Taklitçi (Mockist) okul

    Londra okulu olarak da bilinen bu grup; test esnasında birimin izole olması konusunda katıdır. Bu gruba göre bir birimin bütün bağımlılıkları test dublörleri ile değiştirilmeli ve birim bütün dış etkenlerden bağımsız şekilde test edilmelidir. Bu test tarzını Martin Fowler münzevi (solitary) testler olarak isimlendiriyor.

    Aşağıda Unit Testing Principles, Practices, and Patterns kitabından bir örnek var. İki detaya dikkatinizi çekmek istiyorum:

    • Store sınıfı yerine ikame olarak Mock<IStore> kullanılıyor
    • Doğrulama storeMock.Verify ile ikamenin üzerinde çağrı kontrolü ile yapılıyor
    public class Mockist
    {
        [Fact]
        public void Purchase_succeeds_when_enough_inventory()
        {
            // Arrange
            var storeMock = new Mock<IStore>();
            storeMock
                .Setup(x => x.HasEnoughInventory(Product.Shampoo, 5))
                .Returns(true);
            var customer = new Customer();
    
            // Act
            bool success = customer.Purchase(
                storeMock.Object, Product.Shampoo, 5);
    
            // Assert
            Assert.True(success);
            storeMock.Verify(
                x => x.RemoveInventory(Product.Shampoo, 5),
                Times.Once);
        }
    
        [Fact]
        public void Purchase_fails_when_not_enough_inventory()
        {
            // Arrange
            var storeMock = new Mock<IStore>();
            storeMock
                .Setup(x => x.HasEnoughInventory(Product.Shampoo, 5))
                .Returns(false);
            var customer = new Customer();
    
            // Act
            bool success = customer.Purchase(
                storeMock.Object, Product.Shampoo, 5);
    
            // Assert
            Assert.False(success);
            storeMock.Verify(
                x => x.RemoveInventory(Product.Shampoo, 5),
                Times.Never);
        }
    }

    Klasik okul (Detroit okulu)

    Bu bakış açısı birimi bir sistem davranışı olarak gördüğünden test yaklaşımları da sosyal (sociable) testler yazmak yönünde. Sosyal testler; kodu izole etmek yerine bir davranışı bütün dahili parçaları kullanarak sistemin sınırlarına kadar test etmeye çalışır.

    Bu bakış açısında test dublörü kullanımı sınırlıdır. Sistem sınırlarında; veritabanı, dosya sistemi gibi dış sistemler ikameleri ile değiştirilir.

    Aşağıda aynı testlerin sosyal versiyonunu bulabilirsiniz. Burada;

    • Taklitçi yaklaşımdan farklı olarak Store sınıfı olduğu gibi kullanılır
    • Doğrulama Store sınıfının üzerinde değer kontrolü ile yapılır
    public class Classical
    {
        [Fact]
        public void Purchase_succeeds_when_enough_inventory()
        {
            // Arrange
            var store = new Store();
            store.AddInventory(Product.Shampoo, 10);
            var customer = new Customer();
    
            // Act
            bool success = customer.Purchase(store, Product.Shampoo, 5);
    
            // Assert
            Assert.True(success);
            Assert.Equal(5, store.GetInventory(Product.Shampoo));
        }
    
        [Fact]
        public void Purchase_fails_when_not_enough_inventory()
        {
            // Arrange
            var store = new Store();
            store.AddInventory(Product.Shampoo, 10);
            var customer = new Customer();
    
            // Act
            bool success = customer.Purchase(store, Product.Shampoo, 15);
    
            // Assert
            Assert.False(success);
            Assert.Equal(10, store.GetInventory(Product.Shampoo));
        }
    }
    
    public enum Product
    {
        Shampoo,
        Book
    }

    Birkaç cümle ile iki yaklaşımı karşılaştıralım;

    • Taklitçi yaklaşımda testlerin başarısız olması durumunda problemi tespit etmek kolaydır çünkü kısıtlı bir kod test edilmiştir. Klasik yaklaşımda Store sınıfında yaptığımız bir hata alakalı alakasız çoğu testin başarısız olmasına sebep olacaktır.
    • Taklitçi yaklaşımın görece daha hızlı çalıştığı söylenebilir ama taklit için kullandığınız kütüphanelerin performansı belirleyici olacaktır.
    • Klasik yaklaşım disiplin ve bilgi birimi gerektirir. Taklitçi yaklaşım görece daha kolay anlatılır ve uygulanır – her sınıf için bir test sınıfı
    • Taklitçi yaklaşım koda çok bağımlıdır. Kodda yapılan değişiklikler testleri de değiştirmeye sebep olur. Klasik yaklaşım ise davranışa yani çözülen probleme bağımlı olduğundan anca problem tanımı değişirse testlerde değişiklik gerektirir.

    Ben bu son maddeyi çok kritik buluyorum. Uygulamanın kolay olmasından ötürü taklitçi yaklaşım çok yaygın ve bu yaklaşımda birim testleri faydadan çok kod ile beraber bakım gerektiren maliyetlere dönüşüyor. Büyük taklit(mock) kütüphanelerinin kullanıldığı okuması zor karmaşık yapılar haline geliyor.

    Eğer siz taklitçi gruptansanız birim testlerini ne amaçla yazdığımızı tekrar değerlendirin ve klasik yaklaşıma bir şans verin derim.

  • Göz görmeyince gönül katlanıyor mu?

    Göz görmeyince gönül katlanıyor mu?

    Mikro servis dönüşümü üzerine bir sohbette arkadaşım eski yekpare(monolith) sistemimizi eleştirirken; karmaşıklığından, her şeyin iç içe geçmiş olmasından dem vurdu. Bir değişiklik yapmak isteyen nerenin etkileneceğini bilemiyordu. Ama yeni servislerimiz öyle miydi? Satır sayıları azalmıştı. Servisler küçük olduğundan anlamak kolaydı. Ona göre yekpare yapıları yönetmek aşırı disiplin gerektiriyordu. Küçük izole servisler sayesinde yanlış yapmak daha zordu.

    Ben mikro servis mimarisine baktığımda; koordinasyon problemleri, veri tutarsızlıkları, minik bir geliştirme için bile bir çok servisi değiştirme, yeni yeni akışlar icat etme ihtiyacı görüyordum. Üstüne bir de yapılabilecek hataların ölçeği artmıştı. Disipline ihtiyaç daha da fazla idi bana göre.

    Anladım ki arkadaşım sadece kod ile ilgilendiği için sistem boyutunda oluşan maliyetlerin farkında değildi. Onun için sorumlu olduğu alan – kod – küçülmüştü. Servislerin hepsi farklı repoda olduğu için yapay bir modülerlik hissi oluşmuştu. Geliştirme yapmanın, hata ayıklamanın daha zorlu ve maliyetli hale geldiğini görmüyordu.

    Sanırım göz görmeyince gönül katlanıyor 🙂