Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı Refactoring — #16

Refactoring Kataloğu 7: Inheritance ile Çalışmak — Pull Up/Push Down, Extract Superclass, Replace Type Code with Subclasses ve Replace Subclass with Delegate

Mert Özen 15 Kas 2026 18 dk 11 görüntülenme
Refactoring Kataloğu 7: Inheritance ile Çalışmak — Pull Up/Push Down, Extract Superclass, Replace Type Code with Subclasses ve Replace Subclass with Delegate

Inheritance refactoring'leri ortak kodu base class'a çıkarır, yanlış yere çıkanı geri indirir ve inheritance'ın sınırına çarpıldığında onu delegation ile değiştirir. Yazıda Bookstore'un kitap ve istek listesi class'ları on bir refactoring'le dönüştürülüyor; List'ten türeyen bir class'ın gizlediği Add'i atlayan çağrılar, Push Down'da derleyicinin gösterdiği yer ve TPH tablosundaki discriminator gerçek çıktılarla gösteriliyor ve seri 66 refactoring'in tamamıyla kapanıyor.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 (SDK 10.0.404) ile, testler xUnit.net v3 4.0.3 ve Verify.XunitV3 32.0.0, veritabanı örneği Entity Framework Core 10.0.15 (SQL Server provider) ile derlenip denenmiştir.

Önce Tahmin Et: İstek Listesinde Kaç Kitap Var?

Katalog bloğunun yedinci ve serinin son yazısı, inheritance ile çalışan on bir refactoring'i ele alıyor: Pull Up Method, Pull Up Field, Pull Up Constructor Body, Push Down Method, Push Down Field, Extract Superclass, Collapse Hierarchy, Replace Type Code with Subclasses, Remove Subclass, Replace Subclass with Delegate ve Replace Superclass with Delegate. Adlar refactoring.com/catalog'daki gibi; açıklama ve örnekler bizim. Sahne yine sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı): Catalog module'ündeki kitap class'ları ve Ordering'deki istek listesi. İstek listesi, aynı kitabı iki kez tutmamak için List<string>'den türetilmiş:

// Müşterinin istek listesi (ISBN'ler); aynı kitap bir kez tutulur.
public class Wishlist : List<string>
{
    public new void Add(string isbn)
    {
        if (!Contains(isbn))
            base.Add(isbn);
    }
}

Kod üç farklı yerden kitap ekliyor: sayfadaki "listeye ekle" butonu, sepetteki bütün kitapları listeye taşıyan bir işlem ve ICollection<string> alan ortak bir yardımcı method:

Wishlist wishlist = [];
wishlist.Add("9780441013593");
wishlist.Add("9780441013593");
wishlist.AddRange(["9780441013593"]);
ICollection<string> collection = wishlist;
collection.Add("9780441013593");

Console.WriteLine(wishlist.Count);

Dört kez aynı ISBN (kitapların uluslararası standart numarası) eklendi. Ekrana kaç yazılır? Liste bir API'den JSON olarak gelseydi ve içinde aynı ISBN iki kez olsaydı, deserialize edilen Wishlist'te kaç kitap olurdu?

Extract Superclass ve Pull Up'lar

Kitap tarafında başka bir eski kod var. Önce basılı kitaplar satılıyordu; e-kitap ekibi işe PrintedBook'un bir kopyasıyla başlamış ve iki class aynı alanları, aynı constructor kontrolünü ve aynı fiyat hesabını tekrar ediyor:

public class PrintedBook
{
    public PrintedBook(string isbn, string title, decimal price, int weightGrams)
    {
        if (string.IsNullOrWhiteSpace(title))
            throw new ArgumentException("A book needs a title.", nameof(title));
        Isbn = isbn;
        Title = title.Trim();
        Price = price;
        WeightGrams = weightGrams;
    }

    public string Isbn { get; }
    public string Title { get; }
    public decimal Price { get; }
    public DateOnly? ReleaseDate { get; init; }
    public int WeightGrams { get; }

    public decimal VatRate => 0.10m;

    public decimal PriceWithVat() => Math.Round(Price * (1 + VatRate), 2, MidpointRounding.AwayFromZero);

    public decimal ShippingWeightKg() => WeightGrams / 1000m;

    public string Delivery(DateOnly today) =>
        ReleaseDate > today ? $"Ships on {ReleaseDate:yyyy-MM-dd}"
        : WeightGrams > 1000 ? "Cargo (heavy), 3 days"
        : "Cargo, 2 days";
}

public class EBook
{
    // Constructor, Isbn, Title, Price, ReleaseDate, PriceWithVat ve ShippingWeightKg birebir aynı.
    public string FileFormat { get; }

    // Kargo hesabı her kitaba ağırlığını soruyor.
    public int WeightGrams => 0;

    public decimal VatRate => 0.20m;

    public string Delivery(DateOnly today) =>
        ReleaseDate > today ? $"Available on {ReleaseDate:yyyy-MM-dd}"
        : $"Download ({FileFormat})";
}

Güvenlik ağı, altı farklı kitap için fiyat, ağırlık ve teslimat metnini tutan bir Verify snapshot'ı ve boş başlığı reddeden bir test. Extract Superclass, benzer işler yapan iki class'ın ortak kısmını yeni bir base class'a çıkarır; Clean Code serisinin "Code Smell Kataloğu" yazısındaki Duplicated Code ve Alternative Classes with Different Interfaces smell'lerinin cevabı. İlk commit yalnız boş bir abstract class Book açtı ve iki class'ı ondan türetti. Sonra üç ayrı Pull Up geldi, her biri ayrı commit:

Pull Up Field: Isbn, Title, Price ve ReleaseDate base class'a çıktı. C#'ta küçük bir ara adım gerekti: subclass constructor'ı base class'taki get-only bir property'ye atama yapamaz. Property'ler bir commit boyunca { get; protected init; } oldu; init accessor'ı türetilmiş class'ın constructor'ından da çağrılabiliyor.

Pull Up Constructor Body: başlık kontrolü ve atamalar base class'ın constructor'ına taşındı, subclass'lar : base(isbn, title, price) ile onu çağırıyor ve property'ler tekrar { get; } oldu. Mekanik adımlar: base class'a constructor aç (yoksa), subclass'lardaki ortak satırları oraya taşı, subclass'lardan base'i çağır, test et. Ortak satırlar subclass'a özgü bir alanın atanmasından sonra geliyorsa önce Slide Statements ile onları başa al.

Pull Up Method: PriceWithVat iki class'ta birebir aynıydı, yalnız kullandığı VatRate farklıydı. Method base'e çıktı, VatRate abstract bir property oldu ve subclass'lar onu override ediyor. Delivery'nin gövdeleri farklı, ama imzası aynı; onu da abstract olarak base'e çıkardım ki Book tipinde bir değişken üzerinden çağrılabilsin.

public abstract class Book
{
    protected Book(string isbn, string title, decimal price)
    {
        if (string.IsNullOrWhiteSpace(title))
            throw new ArgumentException("A book needs a title.", nameof(title));
        Isbn = isbn;
        Title = title.Trim();
        Price = price;
    }

    public string Isbn { get; }
    public string Title { get; }
    public decimal Price { get; }
    public DateOnly? ReleaseDate { get; init; }

    public abstract decimal VatRate { get; }

    public decimal PriceWithVat() => Math.Round(Price * (1 + VatRate), 2, MidpointRounding.AwayFromZero);

    public abstract string Delivery(DateOnly today);
}

Push Down ve Collapse Hierarchy: Geri Almak da Bir Refactoring

Visual Studio'nun "Pull member(s) up to new base class" ya da Rider'ın Extract Superclass dialog'unda iki class'ta birebir aynı olan bir üye daha görünür: ShippingWeightKg. Aynı göründüğü için onu da base'e çektim; WeightGrams base'de abstract oldu ve e-kitap onu 0 döndürerek override etti. Testler yeşildi, commit atıldı. Ama e-kitabın ağırlığı yok; o 0, bir zamanlar kargo hesabı her kitaba ağırlık sorduğu için yazılmış bir geçici çözüm. Aynı metin, aynı bilgi demek değil. Base'deki bir üyeyi kullanmayan ya da anlamsız bir değerle dolduran subclass, Clean Code serisindeki Refused Bequest smell'i.

Push Down Method ve Push Down Field, base class'taki bir üyeyi yalnız onu gerçekten kullanan subclass'a indirir; Pull Up'ların tersi. Mekanik adımlar: üyeyi ihtiyacı olan her subclass'a kopyala, base'den sil, test et, kullanmayan subclass'lardan sil. Silince derleyici, e-kitaba ağırlık soran tek yeri gösterdi; o da characterization testinin kendisiydi:

tests/Bookstore.Catalog.Tests/BookTests.cs(31,70): error CS1061: 'EBook' does not contain a definition for 'ShippingWeightKg' and no accessible extension method 'ShippingWeightKg' accepting a first argument of type 'EBook' could be found (are you missing a using directive or an assembly reference?)

Test e-kitap için ağırlığı artık sormuyor. Verify varsayılan değerleri (0, null) yazmadığı için snapshot'ta e-kitapların ağırlık satırı zaten yoktu ve dosya değişmedi. Üretim kodunda e-kitaba ağırlık soran bir yer olsaydı, aynı hata onu da gösterecekti ve orada vereceğin karar bir tasarım kararı olurdu: o kod gerçekten her kitabın ağırlığını mı istiyor, yoksa yalnız kargoya gidenlerin mi?

Eski kodda bir subclass daha vardı: SignedPrintedBook. PrintedBook'tan türüyordu ve tek farkı imzalayan kişinin adını tutan bir property'ydi; hiçbir davranışı override etmiyordu. Collapse Hierarchy, birbirinden yeterince farklı olmayan bir base class ve subclass'ı tek class'ta birleştirir. Mekanik adımlar: hangisinin kalacağına karar ver, diğerinin üyelerini Pull Up ya da Push Down ile ona taşı, referansları kalan class'a çevir, boş class'ı sil. Kalan PrintedBook oldu ve imza bir property:

// Önce
public class SignedPrintedBook : PrintedBook
{
    public SignedPrintedBook(string isbn, string title, decimal price, int weightGrams, string signedBy)
        : base(isbn, title, price, weightGrams)
    {
        SignedBy = signedBy;
    }

    public string SignedBy { get; }
}

// Sonra (PrintedBook içinde)
public string? SignedBy { get; init; }

// Kullanım
new PrintedBook("9780261103344", "The Hobbit", 349.90m, 700) { SignedBy = "Grace" };

Collapse Hierarchy, Extract Superclass'ın tersi. Clean Code serisindeki Lazy Element ve Speculative Generality smell'lerine cevap: "ileride imzalı kitapların farklı bir teslimatı olur" diye açılmış ama hiç dolmamış bir subclass.

Replace Subclass with Delegate ve Remove Subclass

Hiyerarşi artık temiz: ortak kısım base'de, farklılık subclass'larda. Peki neden bir adım daha? Inheritance'ın üç sınırı Bookstore'da gerçek. Birincisi, bir nesnenin class'ı değişemez. Basılı baskısı tükenen bir kitabı aynı katalog kaydıyla e-kitap ya da baskıya göre basım (print-on-demand) olarak satmaya devam etmek, mevcut nesneyi atıp yeni bir subclass nesnesi oluşturmak demek. İkincisi, inheritance tek bir eksende çalışır. Format yanında ikinci bir eksen gelirse (örneğin okullara satılan kurumsal baskı) her kombinasyon için bir subclass gerekir. Üçüncüsü, subclass'lar base class'ın iç detaylarına sıkı bağlıdır; SOLID (beş temel tasarım ilkesinin baş harflerinden oluşan kısaltma) ve Design Principles serisinin "Composition over Inheritance: Fragile Base Class, Delegation ve Davranışı Takmak" yazısı bu bağın bedelini ayrıntılı anlatıyor.

Replace Subclass with Delegate, subclass'ların farklı davranışını ayrı bir delegate nesnesine taşır ve base class o nesneyi tutar. Mekanik adımlar: subclass'a özgü verilerle oluşturulan boş bir delegate tipi aç; base class'a delegate'i tutan bir alan ekle ve subclass'lar oluşurken bu alanı doldursun; subclass'taki method'ları birer birer delegate'e taşı (Move Function) ve base class'ta delegate'e yönlendir; subclass'lar boşalınca onları sil. Subclass'ları çok yerde new ile oluşturuyorsan önce bir önceki yazıdaki Replace Constructor with Factory Function'ı uygulamak işi kolaylaştırır. Ben VatRate ve Delivery'yi tek commit'te taşıdım; her method için ayrı commit de olurdu.

16-replace-subclass-with-delegate-tr.png

public interface IBookFormat
{
    decimal VatRate { get; }
    string Delivery(DateOnly? releaseDate, DateOnly today);
}

public sealed record PrintedFormat(int WeightGrams) : IBookFormat
{
    public string? SignedBy { get; init; }

    public decimal VatRate => 0.10m;

    public decimal ShippingWeightKg() => WeightGrams / 1000m;

    public string Delivery(DateOnly? releaseDate, DateOnly today) =>
        releaseDate > today ? $"Ships on {releaseDate:yyyy-MM-dd}"
        : WeightGrams > 1000 ? "Cargo (heavy), 3 days"
        : "Cargo, 2 days";
}

public sealed record DigitalFormat(string FileFormat) : IBookFormat
{
    public decimal VatRate => 0.20m;

    public string Delivery(DateOnly? releaseDate, DateOnly today) =>
        releaseDate > today ? $"Available on {releaseDate:yyyy-MM-dd}"
        : $"Download ({FileFormat})";
}

public sealed class Book
{
    public Book(string isbn, string title, decimal price, IBookFormat format)
    {
        if (string.IsNullOrWhiteSpace(title))
            throw new ArgumentException("A book needs a title.", nameof(title));
        Isbn = isbn;
        Title = title.Trim();
        Price = price;
        Format = format;
    }

    public string Isbn { get; }
    public string Title { get; }
    public decimal Price { get; }
    public DateOnly? ReleaseDate { get; init; }
    public IBookFormat Format { get; }

    public decimal VatRate => Format.VatRate;

    public decimal PriceWithVat() => Math.Round(Price * (1 + VatRate), 2, MidpointRounding.AwayFromZero);

    public string Delivery(DateOnly today) => Format.Delivery(ReleaseDate, today);
}

Son adım ayrı bir refactoring: Remove Subclass. Delegate'e taşındıktan sonra PrintedBook ve EBook'ta yalnız constructor ve delegate'e yönlenen birkaç property kalmıştı. Remove Subclass, yeterince iş yapmayan subclass'ları base class'taki bir alanla değiştirir; burada o alan Format. Mekanik adımlar: subclass'ları oluşturan yerleri base class'ı oluşturacak şekilde değiştir (subclass'ı bir alanın değeri ayırt ediyorsa onu da ekle), tip kontrollerini (is PrintedBook) alana bakan kontrollere çevir, subclass'ları sil. Kullanım new Book("9780441013593", "Dune", 120m, new PrintedFormat(450)) oldu ve snapshot'ın tek satırı değişmedi.

Format'lar record ve immutable. Format property'si şimdilik get-only; baskıya göre basım gibi bir ihtiyaç gelince onu değiştirilebilir yapmak bir method'luk iş, bugün yazmak ise YAGNI (You Aren't Gonna Need It; henüz gerekmeyeni yazma) ihlali olur. Delegate hiyerarşisini ayrıca bir interface ile kurdum, çünkü ortak davranış yok, yalnız ortak soru var; Fowler'ın tarifinde delegate'ler de kendi aralarında inheritance kullanabilir.

Bu hiyerarşi bir veritabanına map edilmiş olsaydı iş kodla bitmezdi. Entity Framework Core'un (EF Core; .NET'in veritabanı erişim kütüphanesi) varsayılan inheritance stratejisi TPH (table-per-hierarchy; bütün hiyerarşi tek tabloda, satırın tipi bir discriminator kolonunda). Eski hiyerarşinin sadeleştirilmiş bir modeli için EF Core'un ürettiği tabloya veritabanına bağlanmadan baktım (samples/tph.cs, GenerateCreateScript()):

CREATE TABLE [Books] (
    [Id] int NOT NULL IDENTITY,
    [Isbn] nvarchar(13) NOT NULL,
    [Title] nvarchar(max) NOT NULL,
    [Price] decimal(10,2) NOT NULL,
    [Discriminator] nvarchar(13) NOT NULL,
    [FileFormat] nvarchar(max) NULL,
    [WeightGrams] int NULL,
    CONSTRAINT [PK_Books] PRIMARY KEY ([Id])
);

Discriminator kolonunda class'ın adı (PrintedBook, EBook) duruyor. Subclass'ları kaldırınca bu kolon anlamını kaybeder: delegate'in hangi format olduğunu saklamak için yeni bir kolon açmak, mevcut değerleri ona çevirmek ve eski kolonu ancak bütün okuyucular yeni kolona geçtikten sonra silmek gerekir. Bu, yedinci yazıdaki expand-contract adımlarının aynısı. TPT'de (table-per-type; her subclass'ın kendi tablosu) iş daha da büyük, çünkü tabloları birleştirmek gerekir. İki stratejinin farkı EF Core serisinin "EF Core'da Inheritance Mapping: TPH, TPT ve Hangisini Ne Zaman Seçmeli" yazısında. Delegate'li yeni modelin mapping'ini bu yazı için yazmadım.

Replace Superclass with Delegate: İstek Listesi

Wishlist : List<string>'in sorunu Refused Bequest'in tersi: subclass mirasın hepsini kabul etmek zorunda. .NET 10'da List<string>'in (object'ten gelenler ve property accessor'ları hariç) 32 farklı adda public instance method'u var; reflection ile saydım. Insert, AddRange, Sort, RemoveAll ve diğerlerinin hepsi artık istek listesinin de API'si ve hiçbiri tekrar kontrolünü bilmiyor. Replace Superclass with Delegate, base class'ı private bir alana çevirir ve yalnız gerçekten gereken işlemleri ona yönlendirir. Mekanik adımlar: subclass'ta base class tipinde bir alan aç, çağıranların kullandığı her base method için o alana yönlenen bir method yaz, inheritance'ı kaldır, test et.

public class Wishlist : IReadOnlyCollection<string>
{
    private readonly List<string> _isbns = [];

    public int Count => _isbns.Count;

    public void Add(string isbn)
    {
        if (!_isbns.Contains(isbn))
            _isbns.Add(isbn);
    }

    public void AddRange(IEnumerable<string> isbns) => _isbns.AddRange(isbns);

    public bool Contains(string isbn) => _isbns.Contains(isbn);

    public IEnumerator<string> GetEnumerator() => _isbns.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

AddRange ilk commit'te eskisi gibi doğrudan listeye ekliyor; refactoring davranışı değiştirmez. Add ve IEnumerable sayesinde collection expression (Wishlist wishlist = [];) hâlâ çalışıyor. Derleyici ise kapanan kapıyı gösterdi:

tests/Bookstore.Ordering.Tests/WishlistTests.cs(27,42): error CS0266: Cannot implicitly convert type 'Bookstore.Ordering.Wishlist' to 'System.Collections.Generic.ICollection<string>'. An explicit conversion exists (are you missing a cast?)

Cevap: Üç Kitap, JSON'dan İki

Açılıştaki kod 3 yazdırıyordu. İki Add çağrısı tek kitap ekledi, ama AddRange ve ICollection<string>.Add gizlenen method'u hiç görmedi. new anahtar kelimesi override değil, method hiding: hangi method'un çalışacağına derleme zamanında değişkenin tipine bakılarak karar verilir. List<string>'in kendi method'ları, interface üzerinden yapılan çağrılar ve bir serializer, hepsi base class'ın Add'ini çağırır. System.Text.Json ile ["9780441013593","9780441013593"]'i küçük bir denemede Wishlist'e deserialize ettim; sonuçta iki kitap vardı. Characterization testi bu şaşırtıcı davranışı bilerek Assert.Equal(3, ...) ile sabitliyordu.

Delegate'e geçince ICollection kapısı derleme hatasıyla kapandı ve test o satırdan arındı. AddRange'in tekrar kontrolü yapmaması ise artık açıkça görünen bir hata; onu ayrı bir fix: commit'inde Add'i çağıracak şekilde düzelttim. Bu ayrım ilk yazıdaki iki şapka kuralının kendisi: önce davranışı koruyarak yapıyı değiştir, sonra kolaylaşan düzeltmeyi ayrı yap. JSON tarafı için de not: Wishlist artık bir collection değil; bir API'den okunacaksa ya bir DTO'dan (yalnız veri taşıyan basit tip) oluşturulmalı ya da kendi converter'ı yazılmalı. Bunu bu yazıda yapmadım.

Replace Type Code with Subclasses: Ödeme Yöntemi

Koşullar yazısında ödeme yöntemine göre karar veren switch'leri IPaymentOption class'larına taşımış, ama Cart'ta PaymentMethod enum'unu bırakmıştık. Her çağrı önce PaymentOptions.For(cart.Payment) ile enum'dan nesneye geçiyordu ve CheckoutPolicy'de hâlâ cart.Payment == PaymentMethod.BankTransfer gibi karşılaştırmalar vardı. Replace Type Code with Subclasses, davranışı belirleyen bir type code'u (enum, string, int) subclass'larla değiştirir; Repeated Switches ve Primitive Obsession smell'lerine cevap. Mekanik adımlar: type code'u bir property'nin arkasına al, her değer için bir subclass aç ve doğru subclass'ı döndüren bir factory yaz, type code'u kullanan kontrolleri subclass'lara taşı ya da tip kontrolüne çevir, type code alanını kaldır.

Subclass'lar ve factory zaten vardı. Cart'taki alanın tipini IPaymentOption yapınca derleyici kalan dört yeri listeledi:

src/Bookstore.Ordering/CheckoutPolicy.cs(44,85): error CS0029: Cannot implicitly convert type 'Bookstore.Ordering.PaymentMethod' to 'Bookstore.Ordering.IPaymentOption'
src/Bookstore.Ordering/CheckoutPolicy.cs(62,38): error CS0019: Operator '==' cannot be applied to operands of type 'IPaymentOption' and 'PaymentMethod'
src/Bookstore.Ordering/CheckoutPolicy.cs(70,64): error CS1503: Argument 1: cannot convert from 'Bookstore.Ordering.IPaymentOption' to 'Bookstore.Ordering.PaymentMethod'
src/Bookstore.Ordering/CheckoutPolicy.cs(74,69): error CS1503: Argument 1: cannot convert from 'Bookstore.Ordering.IPaymentOption' to 'Bookstore.Ordering.PaymentMethod'
public sealed record Cart(Customer Customer, IReadOnlyList<CartLine> Lines, IPaymentOption Payment)
{
    public decimal Subtotal => Lines.Sum(l => l.UnitPrice * l.Quantity);
}

private static bool IsEligibleForDiscount(Cart cart) =>
    cart is not ({ Customer.IsGuest: true } or { Lines.Count: 0 } or { Payment: CashOnDeliveryPayment });

// CheckoutError
if (cart.Customer.IsGuest && cart.Payment is BankTransferPayment)
    return "Guests cannot pay by bank transfer.";

public decimal PaymentFee(Cart cart) => cart.Payment.Fee(cart.Subtotal);

C#'ın type pattern'leri (is BankTransferPayment, { Payment: CashOnDeliveryPayment }) enum karşılaştırmalarının yerini doğrudan aldı. Enum ise kaybolmadı: veritabanında ve API'de saklanan değer o ve PaymentOptions.For sistemin sınırında enum'dan nesneye çeviren tek yer. Fowler'ın adı "subclass" diyor; burada ortak tip bir interface, çünkü ödeme yöntemlerinin paylaştığı bir davranış yok. Ödeme yöntemi bir sepet için değişebiliyorsa (müşteri ödeme ekranında kartı bırakıp havaleyi seçer) subclass'lar Cart'ın kendisinden değil, onun taşıdığı nesneden türemeli; burada yaptığımız da bu.

IDE Desteği ve Bütün Adımlar

IDE sütunu Microsoft Learn ve JetBrains belgelerine dayanıyor; IDE'leri bu yazı için çalıştırmadım. Derleyici satırları yukarıdaki gerçek çıktılardan.

RefactoringSmellIDE / analyzerTersi
Pull Up MethodDuplicated CodePull members up to base type (Visual Studio), Pull Members Up (Rider)Push Down Method
Pull Up FieldDuplicated CodePull members up to base type (Visual Studio), Pull Members Up (Rider)Push Down Field
Pull Up Constructor BodyDuplicated Codeelleyok
Push Down MethodRefused BequestPush Members Down (Rider)Pull Up Method
Push Down FieldRefused BequestPush Members Down (Rider)Pull Up Field
Extract SuperclassDuplicated CodePull member(s) up to new base class (Visual Studio), Extract Superclass (Rider)Collapse Hierarchy
Collapse HierarchyLazy Element, Speculative GeneralityelleExtract Superclass
Replace Type Code with SubclassesRepeated Switches, Primitive Obsessionelle; tip değişikliğinde derleyici hataları yerleri listelerRemove Subclass
Remove SubclassLazy ElementelleReplace Type Code with Subclasses
Replace Subclass with Delegateyok (inheritance'ın sınırları)elleyok
Replace Superclass with DelegateRefused Bequestelleyok
$ git log --oneline
225c4c6 chore: samples/tph.cs (how EF Core stores the old hierarchy)
c1f508a refactor: replace type code Cart.Payment (PaymentMethod) with IPaymentOption classes
1c3339f fix: Wishlist.AddRange keeps a book once
74f0f26 refactor: replace superclass List<string> with a private list in Wishlist
dac1460 refactor: remove subclasses PrintedBook and EBook (Book holds its format)
3c6985e refactor: replace subclass behavior with delegate IBookFormat (PrintedFormat, DigitalFormat)
74b4c48 refactor: collapse hierarchy (SignedPrintedBook into PrintedBook.SignedBy)
20b35ca refactor: push down WeightGrams and ShippingWeightKg to PrintedBook
8e0f742 refactor: pull up ShippingWeightKg (identical in both classes)
6a49bf3 refactor: pull up method PriceWithVat (VatRate and Delivery abstract)
e502546 refactor: pull up constructor body (title check and assignments)
8144118 refactor: pull up fields Isbn, Title, Price, ReleaseDate
e233dca refactor: extract superclass Book (empty)
1318a32 test: characterize books and wishlist (legacy)

Pull up ve push down commit'leri yan yana duruyor ve bilerek öyle bıraktım: geçmişi temizlemek yerine bir adımın neden geri alındığını göstermek, kodu sonradan okuyan için daha faydalı.

66 Refactoring, Yedi Aile: Katalog Nerede?

Bu yazıyla refactoring.com/catalog'daki 66 adın hepsi bu seride en az bir kez Bookstore kodu üzerinde, testleri önce ve sonra çalıştırılarak geçti. Ailelere göre:

  • Temel refactoring'ler (10. yazı, 9): Extract/Inline Function, Extract/Inline Variable, Change Function Declaration, Rename Variable, Rename Field, Replace Magic Literal, Replace Inline Code with Function Call.
  • Encapsulation ve veri (11. yazı, 9): Encapsulate Variable/Record/Collection, Replace Primitive with Object, Replace Temp with Query, Replace Derived Variable with Query, Split Variable, Change Reference to Value, Change Value to Reference.
  • Sorumlulukları gruplamak (12. yazı, 10): Introduce Parameter Object, Preserve Whole Object, Combine Functions into Class/Transform, Split Phase, Extract/Inline Class, Hide Delegate, Remove Middle Man, Substitute Algorithm.
  • Taşımak (13. yazı, 8): Move Function, Move Field, Move Statements into Function, Move Statements to Callers, Slide Statements, Split Loop, Replace Loop with Pipeline, Remove Dead Code.
  • Koşullar (14. yazı, 7): Decompose Conditional, Consolidate Conditional Expression, Replace Nested Conditional with Guard Clauses, Replace Control Flag with Break, Replace Conditional with Polymorphism, Introduce Special Case, Introduce Assertion.
  • API (15. yazı, 12): Separate Query from Modifier, Parameterize Function, Remove Flag Argument, Replace Parameter with Query, Replace Query with Parameter, Remove Setting Method, Replace Constructor with Factory Function, Replace Function with Command, Replace Command with Function, Return Modified Value, Replace Error Code with Exception, Replace Exception with Precheck.
  • Inheritance (bu yazı, 11): Pull Up Method/Field/Constructor Body, Push Down Method/Field, Extract Superclass, Collapse Hierarchy, Replace Type Code with Subclasses, Remove Subclass, Replace Subclass with Delegate, Replace Superclass with Delegate.

Kataloğu ezberlemek gerekmiyor. Gerekli olan, bir smell'i gördüğünde aklına gelecek iki üç adayın olması ve her birinin tersinin de var olduğunu bilmek: Pull Up'ın Push Down'ı, Extract'ın Inline'ı var. Yanlış yöne atılan bir adım geri alınabilir, yeter ki testler her adımda çalışsın.

Sık Düşülen Tuzaklar

Extract Superclass ile "BaseService" çöplüğü üretmek. Birkaç class'ta ortak görünen her şeyi bir base class'a toplamak, zamanla hiçbir subclass'ın tamamını kullanmadığı, herkesin değiştirmekten korktuğu bir class üretir. Design Patterns serisinin "Anti-Pattern'ler: God Object, Service Locator, Singleton Abuse, Anemic Domain Model ve Pattern Hastalığı" yazısı bu yolun sonunu anlatıyor. Ortak kod gerçekten aynı bilgiyi mi ifade ediyor, yoksa bugün tesadüfen mi aynı? İkincisiyse Extract Class ya da küçük bir yardımcı fonksiyon daha doğru.

Aynı metni aynı bilgi sanmak. ShippingWeightKg iki class'ta karakteri karakterine aynıydı, ama e-kitapta bir geçici çözümün parçasıydı. Pull Up yapmadan önce her iki subclass için "bu üye burada ne anlama geliyor?" diye sor.

Collection class'larından türetmek ve new ile method gizlemek. List<T>, Dictionary<TKey, TValue> gibi class'lar inheritance için tasarlanmadı; method'ları virtual değil. new ile gizlenen method, interface ya da base tip üzerinden yapılan her çağrıda atlanır. Davranış eklemek istiyorsan delegate kullan ya da bunun için tasarlanmış Collection<T>'den türet.

Veritabanına map edilmiş bir hiyerarşiyi yalnız kodda değiştirmek. TPH'de discriminator değerleri, TPT'de tablolar verinin parçası. Subclass eklemek, silmek ya da delegate'e geçmek bir migration ve veri dönüşümü demek; expand-contract ile adım adım yürü.

Replace Subclass with Delegate'i her hiyerarşiye uygulamak. Tek eksenli, nesnenin tipinin değişmediği ve base class'ın senin kontrolünde olduğu bir hiyerarşi için inheritance hâlâ en kısa yol. Delegate'e, inheritance'ın sınırlarından birine gerçekten çarptığında geç.

Küçük Bir Deneme

★☆☆ Codebase'te List<T>, Dictionary<TKey, TValue> ya da HashSet<T>'ten türetilmiş class'ları ara. Hangileri bir method'u new ile gizliyor? Her biri için gizlenen method'u atlayan bir çağrı yazmayı dene.

★★☆ Bir hiyerarşide base class'taki üyelerin her biri için hangi subclass'ların onu gerçekten kullandığını bir tabloya yaz. Refused Bequest gördüğün yerde Push Down uygula ve derleyicinin gösterdiği çağıranlara bak.

★★★ EF Core ile TPH olarak map edilmiş bir hiyerarşi seç. Subclass'lardan birini kaldırmanın ya da bir delegate'e geçmenin migration'ını yaz: yeni kolon, veri dönüşümü, okuyucuların taşınması ve eski kolonun silinmesi kaç ayrı deploy'a yayılıyor?

Seri burada bitiyor. İlk dokuz yazıda refactoring'i bir disiplin olarak ele aldık: davranışı korumak, güvenlik ağı kurmak, küçük adımlarla ilerlemek, dependency kırmak, büyük değişikliği Mikado Method ve branch by abstraction ile planlamak, contract'ları kırmadan değiştirmek, Angular kodunu modernleştirmek ve teknik borcu ölçüp savunmak. Son yedi yazıda ise o disiplinin her gün kullanılan araçlarını, kataloğun 66 refactoring'ini tek tek Bookstore üzerinde denedik. İkisi birlikte işe yarıyor: katalog ne yapılacağını, disiplin ne zaman ve hangi güvenceyle yapılacağını söylüyor. Bir sonraki adım kendi codebase'inde, bir sonraki küçük ve test edilmiş commit'te.