Monolitik Bir Kod Tabanını Eklenti Mimarisine Nasıl Dönüştürdüm

Hiç devasa bir kod tabanını yeniden düzenlemek ve tüm mimariyi yeniden şekillendirmek zorunda kaldığınız bir durumla karşılaştınız mı?

***

Bu yazı, Medium.com'da yazmış olduğum ve Better Programming tarafından yayımlanmış olan orjinal hikayenin Türkçeye tercümesidir. Tercümede Deepl çeviriciden faydalanılmıştır.


Monolitik Bir Kod Tabanını Eklenti Mimarisine Nasıl Dönüştürdüm

İlk profesyonel işime başlamadan önce, geliştiricilerin bir başkasının anti-paternlerden oluşan dağınık kodları üzerinde çalışmanın nasıl bir şey olduğunu anlattıkları korku hikayelerini dinlemeye alışmıştım. Sonra, yeni bir Dotnet geliştiricisi olarak ilk profesyonel işimde ikinci görevimi aldım. Bu tam olarak korktuğum şeydi .

Figure 1. MS add-in model


Yeni işim, mühendislik kural setlerini bir mühendislik uygulamasına entegre etmekti. Uygulama zaten geliştirilmişti ve üç kural seti içeren bir kütüphane ile çalışıyordu. Bu kural setleri, alanın mühendislik analizi için teknik gereksinimleri ve kuralları tanımlayan dokümanlardı. Benim görevim kütüphaneye başka bir kural setini entegre etmekti. Beklenti, yeni dijitalleştirilmiş kural setinde bazı değişiklikler ve ayarlamalar yapmak ve projeye eklemekti. Ancak proje çözümünü açıp kod tabanını gördüğümde ciddi bir zorlukla karşı karşıya olduğumu fark ettim.

Benden mevcut kod tabanına yeni bir kural seti eklemem istenmiş olsa da sorun bundan daha fazlasıydı.

İlk olarak, sadece birkaç sınıf vardı. Bu sınıflar yüksek oranda birbirine bağlıydı ve sadece ortak alanlardan ve izolasyonsuz yöntemlerden oluşuyordu. Kural seti kütüphanesi ile ana uygulama arasındaki etkileşim açık ya da tanımlı değildi.

Kural seti sınıfları çok büyüktü; soyut harflerle adlandırılmış 200'den fazla alan ve iç içe geçmiş switch-case'ler ve if-else blokları da dahil olmak üzere binlerce satır koddan oluşan yöntemler, kod ile kural seti dokümanları arasındaki ilişkiyi takip etmeyi imkansız hale getiriyordu.

Teşhis

Sorun şuydu:

  • Kütüphaneye yeni kural setleri eklemek düzenli bir uygulama olacaktı.
  • Kural setlerinin benzer yapılara sahip olması bekleniyordu. Ancak gerçek farklıydı; sistem çeşitli kural setlerini içerecek kadar genel olmalıydı.
  • Uygulama kısmen yayınlanmıştı, bu nedenle yapılacak değişiklikler sınıflarda Açık-Kapalı ilkesine kesinlikle uymalıydı.
  • Uygulama ve kural seti kütüphanesi monolitikti (Şekil 1).
  • Sınıfların metotları herhangi bir parametre olmadan doğrudan alanları kullanıyordu.
  • Veri giriş ve çıkışları açıkça tanımlanmamıştı. Sınıfların alanları uygulama tarafından girdi ve sonuç alanları olarak doğrudan erişimle kullanılıyordu.
  • Kütüphane kullanım senaryosu, kural kümesi sınıflarıyla tamamen aynı alan ve yöntemlerden oluşan bir hub sınıfına (proxy) dayanıyordu. Uygulama hub sınıfının bir örneğini oluşturup alanlarını doğrudan ayarlıyor ve hub sınıfı da anında kural kümesinin örneklerini oluşturuyordu.
  • Ana bilgisayar uygulaması hub sınıfının Rule SetName alanını her ayarladığında ve sınıfın işlem metotlardan herhangi birini çağırdığında, hub sınıfı kendi alanlarını ilgili kural kümesi sınıfına kopyalayacak ve kural kümesinin aynı yöntem imzasına sahip yöntemini çağıracaktır.
Şekil 2. Uygulamanın orjinal assembly yapısı


Yol Haritası

Durumla yüzleşmeye ve sorunu kabullenmeye karar verdim. Duyduğum korku hikayelerini unuttum ve en iyi arkadaşlarım olan kağıt ve kaleme döndüm, ardından masanın üzerine eğildim ve bir yol haritası çizdim. Amaç, derlemelerin dinamik olarak yüklenmesine olanak tanıyan, genişletilebilir ve yönetilebilir bir eklenti mimarisi oluşturmaktı.


İşte attığım adımlar:

  • Uygulama ve kural seti kütüphanesi arasındaki ilişkiyi belirle,
  • Genel erişime sahip olan ve diğer sınıflar tarafından kullanılan alanları tanımla,
  • Kamunun erişimine açık olması gereken yöntemleri belirle,
  • Metotlar tarafından kullanılan alanları metotların parametreleri olarak tanımla
  • Arayüzleri tanımla ve alanları amaçlarına göre ayır
  • Arayüzleri uygulayan soyut sınıflar tanımla ve soyut sınıfları kural sınıfları tarafından miras al. Bu, alanları sorumluluklarına göre gruplandıracağı için kod tekrarını azaltacaktır
  • Paylaşılan sınıfları ayrı bir montaja aktar
  • Her kural setini tek bir god-class yerine birkaç sınıftan oluşan ayrı bir montaj haline getir
  • Ana bilgisayar uygulamasıyla etkileşimi yönetmek için bir kural seti şablonu ve arayüz oluştur
  • Okunabilir ve bakımı yapılabilir kod tabanları oluşturmak amacıyla kural seti dokümanları için alana özgü bir dil oluştur

Eklenti Mimarisi

Eklenti mimarisi, birincil kaygınız genişletilebilirlik olduğunda doğru seçimdir. Özellikle uygulama alanına yeni kural setleri eklemek düzenli bir uygulama olduğunda bu önemliydi.

Günlerce süren evrak işlerinin ardından genel arayüzler oluşturdum, gereksinimleri belirledim, yol haritasının ana hatlarını çizdim ve paylaşılan tipler için yeni montajlar ve alana özgü bir dil (DSL) oluşturmak da dahil olmak üzere sistemin yeni bir yapısını tasarladım (şekil 3).

Şekild 3. Add-in yapısı assembly diagramı

Proje geriye dönük uyumluluğu korumakla sınırlandırıldığı için .Net Framework'te bulunan eklenti modelini kullanmak mümkün değildi; bu daha fazla baş ağrısına neden olacaktı. Bunun yerine, özel bir mimari daha iyi olacaktı.

Proxy tasarım modeli kütüphanedeki baskın modeldi, çünkü hub sınıfı kural setleriyle aynı arayüzleri uygulayarak bir proxy'ye dönüştürüldü. Proxy sınıfı nihai hale getirildi.

Erişilebilir olması gerekmeyen yöntemler korumaya alındı. Bazı yöntem çağrıları ise yeni oluşturulan metotların içinde yeniden gruplandırıldı ve yeni yöntemlere erişim public yöntemler aracılığıyla sağlandı.

Alanlar özelliklere dönüştürüldü. İlgili alanları gruplamak ve özel yöntemlere aktarmak için parametre nesneleri kullanılmaya başlandı. Her parametreyi ayrı ayrı ayarlamak yerine tüm nesne, metotların parametresi olarak korundu.

Nesne Yönelimli Programlamanın Suistimali

Kod satırlarının her yerinde if-else bloğu katmanları ile derinlemesine iç içe geçmiş switch-case ifadeleri vardı. Teknik bir kural setinin doğası çok sayıda koşul ve seçenek gerektirmektedir. Yaklaşım, tüm kural setini tek bir işlem olarak kodlamak ve kurallarda bir koşul ortaya çıktığında switch case'leri devreye sokarak kural seti dokümanını satır satır temsil etmekti. Ancak, kodun arkasındaki mantık dalları karmaşıktı ve dosyanın her tarafına dağılmıştı (tek bir dosya veya on bin satırdan fazla kod içeren bir sınıf).

Durumlar ve isteğe bağlı değerler olarak dizeler kullanılıyordu, bu da çalışma zamanından önce tür denetimini devre dışı bırakıyor ve dizelerin birbirleriyle bozulmadan tutulmasını zorlaştırıyordu. Dize değerinin değişmesi gerektiğinde, aynı değişikliği birkaç yerde de yapmanız gerekirdi.

Enum'ların Tanımlanması

Arayüzleri ve projenin genel yapısını uyguladıktan sonra, kural kümelerinin iş mantığı içindeki kodun kalitesini artırmak için mücadele ettim. Kuralları temsil etmek, istenen sorguları çalıştırmak ve kuralları analiz etmek için daha iyi bir yol aramaya başladım.

Yaptığım ilk şey enumları tanımlamak ve dizeleri enumlarla değiştirmek oldu. Dizeleri UI'dan enumlara çevirmek ve enum adlarını Host uygulamasının UI'ı tarafından tüketilecek dize listelerine çevirmek için genel yöntemler içeren EnumHelpers adlı statik bir sınıf oluşturdum.

<Code>
internal static class EnumHelpers
{
//...
internal static T GetEnum<T>(string type)
{
type = type.Replace(' ', '_');
return (T)Enum.Parse(typeof(T), type);
}

internal static string[] GetEnumList<T>(this T enumType) where T : Type
{
string[] list = Enum.GetNames(enumType);
for(int i = 0; i < list.Length; i++)
{
list[i] = list[i].Replace('_', ' ');
}
return list;
}
//...
}
</Code>

Alana Özgü Dil

Kural setlerinin kurallarını kod tabanına eklerken, zaman zaman kalıplar fark ettim. Örneğin, bazı kurallar kapsamlarıyla ilgiliydi, bazıları katsayıları döndüren saf fonksiyonlardı ve diğerleri çeşitli analiz türlerinin ana süreciyle ilgiliydi. Bu yüzden, alana özgü bir dil oluşturmaya karar verdim.

Alana özgü bir dil (DSL), programlamayı daha kolay ve anlaşılır hale getirmek için belirli bir alanda kullanılmak üzere tasarlanmıştır. Örneğin, SQL bir DSL türüdür. İki tür DSL vardır: harici ve dahili. SQL kendi sözdizimine sahip harici bir DSL iken, LINQ dahili bir DSL örneğidir. DSL, ana diliyle aynı sözdizimine sahipse dahili olarak adlandırılır. Başka bir deyişle, dahili bir DSL, bir etki alanı için özel olarak oluşturulan türlerin ve yöntemlerin bir listesidir.

Bunun gibi bir nesnenin örneğini oluşturabilirsiniz:

<Code>
Rules.Add(new RuleStatement
(
articleCode: "A.1.2",
articleName: "Modulus of section",
memberType: Enums::MemberTypes.Type1 | Enums::MemberTypes.Type3,
ruleType: RuleTypes.Requirement,
scope: new Scope((Reader reader) =>
{
bool res = _localContext.Region != Enums::ReinforcementRegions.None;
return res;
}),
ruleMethod: (RuleReader reader) =>
{
// do stuff
}
));
</Code>

Bu kod geleneksel C# sözdizimiyle yazılmış olmasına rağmen, ilk bakışta kodun anlamını kavramak kolay değildir. Aynı ifadeyi yukarıdan yeniden düzenlersek nasıl görünürdü:

<Code>

AddNewRule(new RuleStatement())
.WithArticleCode("A.1.2")
.WithArticleName("Modulus of section")
.ForMembers(Enums::MemberTypes.Type1 | Enums::MemberTypes.Type3)
.OfType(RuleTypes.Requirement)
.When(new Scope((Reader reader) => {
return _localContext.Region != Enums::ReinforcementRegions.None;
})
)
.And(new Scope((Reader reader) => {
return (Location == Locations.FirstLocation);
})
)
.WillDo((Reader reader) =>
{
// do stuff
});

</Code>

Bu şekilde kod kendini daha iyi açıklar ve tespit edilmesi daha kolay olur.

Ayrıca kural setleri için kullanılmak üzere bu ifadeler gibi bazı şeyler geliştirdim:

<Code>

double value1 = value.AtLeast(100); // value1 = value < 100 ? 100 : value
double value2 = value.Utmost(100); // value1 = value > 100 ? 100 : value

</Code>

Bu şekilde, kod tabanında yeni kural kümeleri geliştirmek daha hızlı olur ve bakımı ve hata ayıklaması daha kolay hale gelir.

Delegeleri Kullanma

Kuralların davranışları için bir DSL oluşturmak üzere delegeleri kullandım. Bu sayede aynı çağrı imzalarına sahip yöntemler için çeşitli davranışlar tanımlamak mümkün oldu.

Kural Okuyucusu Oluşturma

Bir DSL oluşturduktan ve bu dil aracılığıyla kuralları tanımladıktan sonra, bir kural okuyucu da geliştirmek mümkün hale geldi. Bu, dağınık switch-case'leri ve if-el'leri ortadan kaldıracaktı. Ve benim yaptığım da buydu. Bir sorgu bağlamına göre tüm kural kümesi üzerinde yinelenen bir okuyucu nesnesi oluşturdum; kapsamında olduğu kuralların yöntemlerini çağıracaktı.

Tip Yönlendirme

Bu tür bir mimari dönüşüm sırasında karşılaşabileceğiniz benzersiz sorunlardan biri döngüsel bağımlılık sorunudur. Eklenti mimarisini uyguladığınızda, eklenti derlemesi ve ana bilgisayar uygulaması aynı türleri kullanmalıdır. Ana bilgisayar derlemesi eklenti türlerini referans alırken, eklenti türlerinin ana bilgisayar derlemesindeki türleri referans alması .Net çerçevesi tarafından izin verilmeyen döngüsel bir bağımlılık yaratacaktır.

Tek çözüm, paylaşılan nesneleri bağımsız bir derlemeye taşımaktır. Bu aynı zamanda geriye dönük uyumluluğun bozulması gibi ciddi bir sorunu da beraberinde getirir. Henüz endişelenmeyin. Dotnet bu soruna düzgün bir çözüm sunar: tip yönlendirme. Tipleri bir assembly'nin dışına taşıyabilir ve çağırana "tip artık burada değil; lütfen tipi bu assembly'den çağırın" diyebilirsiniz.

<Code>

<Assembly: TypeForwardedTo(GetType([TypeName]))>

</Code>

Tip yönlendirme, orijinal derlemeyi kullanan uygulamaları yeniden derlemek zorunda kalmadan bir tipi başka bir derlemeye taşımanıza olanak tanır. (Microsoft)

Sonuç

Bu deneyimden çok şey öğrendim. Ancak öğrendiğim en önemli ders, problem ne kadar karmaşık veya zor görünürse görünsün, yaklaşımınıza odaklanmanız, problemi tanımlamanız, yönetilebilir adımlara ayırmanız ve anlamanız gerektiğidir. Ardından kodlama yapmadan önce kağıt üzerinde elinizden gelenin en iyisini yapın. Yeni bir .Net geliştiricisi olarak, bu zorluğun korkunç bir kabusa yakın bile olmadığını, aksine benim için büyük bir fırsat olduğunu artık biliyorum.

Yorumlar

Bu blogdaki popüler yayınlar

Deniz Kurdu / tasarım ve çizim aşaması

Doğaya Bakış

Deniz Kurdu - Yelken Direği