SOLID kısaltması Robert C. Martin (Uncle Bob - Bob Amca olarak da bilinir) tarafından ortaya atılan bir dizi Nesne Yönelimli Programlama (OOP - Object Oriented Programming) prensiplerinden 5 tanesinin baş harflerinden oluşturulmuş ve ilk defa Michael Feathers tarafından ortaya atılmış bir kısaltmadır. Hem Bob Amca hem de Michael Feathers OOP yaklaşımının yaygınlaşmasında ve doğru bir şekilde kavranmasında önemli rolleri olan kişilerdir. Bu yazımda iyi bir yazılım geliştiricinin mutlaka bilmesi gerektiğini düşündüğüm SOLID prensiplerini en yalın hali ile ve kısa C# örnekleri ile size aktarmayı hedefliyorum.
İstanbul Bilgi Üniversitesi Yazılım Geliştirme Ekibi olarak Cuma sabahları 10:00 - 12:00 arasında programlama dilleri, frameworkler (Türkçesini tam oturtamadım) ve genel olarak iyi yazılım geliştirme pratikleri üzerine az sunumlu bol kodlu oturumlar yapıyoruz.
Bu oturumlardan sonuncusunda SOLID prensiplerini ele aldık. SOLID prensipleri, anlaşılabilir, esnek ve yönetilebilir OOP kod yazmak için takip edilmesi önerilen pratikleri ifade eder. Bana sorarsanız SOLID sadece OOP kapsamında değil diğer programlama yaklaşım ve disiplinlerinde de oldukça kullanışlı bir dizi prensibi ifade ediyor. SOLID prensipleri, C# ve Java gibi OOP diller ile çalışırken ne kadar faydalı ise çok uç bir örnek olarak PL-SQL ve T-SQL gibi SQL dillerinden birisi ile çalışırken de faydalı bir düşünsel çerçeve sunabilir. Lafı çok uzatmadan SOLID kısaltmasını oluşturan prensipleri hızlıca açıklayalım.
- Single Responsibility : Sınıflarımızın iyi tanımlanmış tek bir sorumluluğu olmalı.
- Open/Closed : Sınıflarımız değişikliğe kapalı ancak yeni davranışların eklenmesine açık olmalı.
- Liskov Substitution : Kodumuzda herhangi bir değişiklik yapmaya gerek kalmadan türetilmiş sınıfları (sub class) türedikleri ata sınıfın (base class) yerine kullanabilmeliyiz.
- Interface Segregation : Genel kullanım amaçlı tek bir kontrat yerine daha özelleşmiş birden çok kontrat oluşturmayı tercih etmeliyiz.
- Dependency Inversion : Katmanlı mimarilerde üst seviye modüller alt seviyedeki modüllere doğruda bağımlı olmamalıdır.
Gelin şimdi bu prensipleri adım adım oluşturacağımız basit bir örnek ile ele alalım.
Amacımız
Freformatter.com‘un sunduğu Json, HTML, JavaScript formatlama/güzelleştirme işlevine benzer işlevi sunan bir modül tasarlamak. Örneğimizde, formatlama işlemini yapmak yerine sınıf tasarımlarımızın SOLID prensiplerine uygun olarak nasıl tasarlayabileceğimize odaklanacağız.
Formatlama/güzelleştirme, herhangi bir formatlama kuralına uyulmadan rastgele oluşturulmuş ilk örnektekine benzer Json (Html, Xml, JavaScript, C#, PL-SQL de olabilir) metnin formatlanarak ikinci örnekteki hale getirilmesidir.
Formatlanmamış metin
{ "id":1,"ad" :"Ali", "soyad": "Özgür"}
Formatlanmış metin
{
"id": 1,
"ad": "Ali",
"soyad": "Özgür"
}
Kod örneklerini takip edebilmek için C# veya Java, C++ benzeri OOP dillerden birisini okuyabilmeniz yeterli olacaktır.
Single Responsibility (Tek Sorumluluk)
Sınıflarımızın iyi tanımlanmış tek bir sorumluluğu olmalı.
Sınıflarımızın sorumluluklarını tasarlarken en zorlandığımız aşama bu sorumlulukların neyi kapsayıp neleri kapsamayacağına karar vermektir. Aşağıdaki JsonFormatter
sınıfını olabildiğince basit bir şekilde ve tek sorumluluğu verilen metni Format methodu ile formatlayacak şekilde oluşturuyoruz.
public class JsonFormatter
{
public string Format(string input)
{
// Formatlama işlemini yap
return "formatlanmış metin!";
}
}
JsonFormatter
sınıfının işlevi üzerinde düşündükçe şunu farkediyoruz; Json metinler belirli kurallara uygun olarak oluşturulması gereken metinlerdir. Bu nedele formatlama işleminin ön aşaması olarak verilen metnin geçerli bir Json metin olup olmadığının da denetlenmesi (validation) gerekir. JsonFormatter
sınıfını IsInputValid metodunu da ekleyerek aşağıdaki gibi düzenliyoruz.
public class ValidationException:ApplicationException{}
public class JsonFormatter
{
public string Format(string input)
{
// Kural denetimi talep et
if (!IsInputValid(input))
throw new ValidationException();
// Formatlama işlemini yap
return "formatlanmış metin!";
}
private bool IsInputValid(string input)
{
// Kural denetimini yap
return true;
}
}
İşler tam da bu noktada biraz sıkıntılı bir hal almaya başlıyor, çünkü IsInputValid metodunun eklenmesi ile birlikte JsonFormatter
sınıfında Single Responsibility prensibinden uzaklaşmaya başlıyoruz. Sınfımıza Json formatlama sorumluluğuna ilave olarak bir de Json kural denetimi sorumluluğunu ekledik.
JsonFormatter
sınıfını fazladan kural denetleme sorumluluğundan kurtarmak için yapmamız gereken şey aslında çok basit; yeni bir kural denetim sınıfı oluşturup bu sorumluluğu o sınıfa vermek. Aşağıdakine benzer bir JsonValidator
sınıfı ile kural denetimlerini yapabiliriz.
public class JsonValidator
{
public bool IsValid(string input)
{
// Kural denetimini yap
return true;
}
}
JsonFormatter
sınıfını kural denetimi sorumluluğundan kurtardığımıza göre aşağıdaki gibi yeniden düzenleyerek tekrar Single Responsibility prensibine uygun hale getirebiliriz.
public class JsonFormatter
{
private JsonValidator _validator = new JsonValidator();
public string Format(string input)
{
// Kural denetimini _validator nesnesine yaptır
if (!_validator.IsValid(input))
throw new ValidationException();
// Formatlama işlemini yap
return "formatlanmış metin!";
}
}
Open/Closed (Açık/Kapalı)
Sınıflarımız değişikliğe kapalı ancak yeni davranışların eklenmesine açık olmalı.
Bu prensip daha çok alt seviyede işlevlere erişimi sağlayan birleştirici sınıflar için geçerlidir. Formatlama modülümüzün amaçlarından bir tanesi de birden fazla metin tipi (Json, Html, Xml vb) için formatlama işlevi sunmaktır. Bu amacı gerçekleştirmek için PrettyFormatter
isimli birleştirici bir sınıf tanımlayalım. Bu sınıfın sorumluluğu seçilen metin tipi için formatlama işlevi sağlamak olsun.
public class PrettyFormatter
{
// Format tipleri
public enum FormatTypes
{
Json,
Html
}
// Formatlama işlemini yapacak olan nesneler
private JsonFormatter _jsonFormatter = new JsonFormatter();
private HtmlFormatter _htmlFormatter = new HtmlFormatter();
// Formatlama işlemini yapan metod
public string Format(FormatTypes inputType, string input)
{
switch (inputType)
{
case FormatTypes.Json:
return _jsonFormatter.Format(input);
case FormatTypes.Html:
return _htmlFormatter.Format(input);
default:
throw new Exception("Desteklenmeyen format tipi!");
}
}
}
Yukarıdaki hali ile PrettyFormatter
sınıfı Json ve Html tipinden metinler için formatlama işlevi sunar. PrettyFormatter
sınıfına Xml için de formatlama desteği eklemek istersek XmlFormatter
ve XmlValidator
sınıflarını tanımladıktan sonra PrettyFormatter
sınıfında aşağıda yorum satırlarında numaralandırılmış 3 değişikliğin yapılması gerekir.
public class PrettyFormatter
{
// Format tipleri
public enum FormatTypes
{
Json,
Html,
Xml // (1) <-- !!!
}
// Formatlama işlemini yapacak olan nesneler
private JsonFormatter _jsonFormatter = new JsonFormatter();
private HtmlFormatter _htmlFormatter = new HtmlFormatter();
private XmlFormatter _xmlFormatter = new XmlFormatter(); // (2) <-- !!!
// Formatlama işlemini yapan metod
public string Format(FormatTypes inputType, string input)
{
switch (inputType)
{
case FormatTypes.Json:
return _jsonFormatter.Format(input);
case FormatTypes.Html:
return _htmlFormatter.Format(input);
case FormatTypes.Xml: // (3) <-- !!!
return _xmlFormatter.Format(input);
default:
throw new Exception("Desteklenmeyen format tipi!");
}
}
}
Bu düzenlemeler ile PrettyFormatter
sınıfına Xml formatlama işlevini de ekledik ancak Open/Closed (Açık/Kapalı) prensibine aykırı olarak tam üç yerde değişiklik yapmak zorunda kaldık. Bu sorunu gidermek için takip edilebilecek yöntemlerden birisi formatlama işlemini genel hatları ile tanımlayan ata bir sınıf oluşturup (PrettyFormatProvider
) asıl işlevleri de türetilmiş sınıfların (JsonFormatter
vb) sorumluluğu olarak tanımlayabiliriz.
// Soyut kontrat sınıfı (Ata Sınıf)
public abstract class PrettyFormatProvider
{
public abstract string Format(string input);
}
// Somut özelleşmiş sınıf (Türetilmiş Sınıf)
public class JsonFormatter : PrettyFormatProvider
{
private JsonValidator _validator = new JsonValidator();
public override string Format(string input)
{
if (!_validator.IsValid(input))
throw new ValidationException();
// Formatlama işlemini yap
return "formatlanmış metin!";
}
}
// Somut özelleşmiş sınıf (Türetilmiş Sınıf)
public class HtmlFormatter : PrettyFormatProvider
{
private HtmlValidator _validator = new HtmlValidator();
public override string Format(string input)
{
if (!_validator.IsValid(input))
throw new ValidationException();
// Formatlama işlemini yap
return "formatlanmış metin!";
}
}
Bu düzenlemelerden sonra PrettyFormatter
sınıfının Format metodu aşağıdaki gibi yazılabilir. PrettyFormatter
sınıfı ile PrettyFormatProvider
kontratına uygun olarak tasarlanmış herhangi bir özelleşmiş formatlama sınıfı kullanılarak ve kod değişikliğine gerek kalmadan işlem yapabilir.
public class PrettyFormatter
{
// Formatlama işlemini yapan metod
public string Format(PrettyFormatProvider provider, string input)
{
return provider.Format(input);
}
}
Liskov Substitution (Yerine Geçebilme)
Kodumuzda herhangi bir değişiklik yapmaya gerek kalmadan türetilmiş sınıfları (sub class) türedikleri ata sınıfın (base class) yerine kullanabilmeliyiz.
Yerine geçebilme prensibi OOP tasarımlarımızda ata sınıflar kullanılarak kontratlar tanımlanması ve bu kontratların taviz verilmeden takip edilmesi gerektiğini ifade eder. Yerine geçebilme prensibine uygun ve kontratlara dayalı sınıf tasarımları bir anlamda açık/kapalı prensibine uygunluğu da tetikler.
Gelin şimdi validator sınıflarımıza odaklanarak yerine geçebilme prensibini ele alalım. XML dokümanlarını basit ve genel XML söz dizimi kurallarına göre doğrulayabildiğimiz gibi verilen bir şemaya (XSD) göre de doğrulayabiliriz. Örnek XmlFormatter
sınıfının XSD şemaları ile kural denetimi yapmasını sağlayabilmek için aşağıdaki gibi basit bir XmlSchemaValidator
sınıfı tanımlayalım.
public class XmlSchemaValidator
{
private string _xsdSchema;
public XmlSchemaValidator(string xsd)
{
_xsdSchema = xsd;
}
public bool IsValid(string input)
{
// XSD şemaya göre kural denetimini yap
return true;
}
}
XmlFormatter
sınıfını XmlSchemaValidator
ile de çalışabilecek hale getirmek için aşağıdaki değişiklikleri yapmamız gerekiyor. Bunlar sırası ile;
XmlSchemaValidator
tipinden değişken tanımı- XSD şemanın dışarıdan verilmesi ve buna göre doğru denetleme sınıfının oluşturulması için yapıcı metod (constructor) tanımı
- Format metodunun yapıcı metod ile verilen tanıma uygun çalışır hale getirilmesi için kontrollerin kodlanması
public class XmlFormatter : PrettyFormatProvider
{
private XmlValidator _validator;
private XmlSchemaValidator _schemaValidator; // (1) <-- !!!
public XmlFormatter(string xsd) // (2) <-- !!!
{
if (String.IsNullOrWhiteSpace(xsd))
_validator = new XmlValidator();
else
_schemaValidator = new XmlSchemaValidator(xsd);
}
public override string Format(string input) // (3) <-- !!!
{
if (_validator != null && _validator.IsValid(input))
throw new ValidationException();
if (_schemaValidator != null && _schemaValidator.IsValid(input))
throw new ValidationException();
// Formatlama işlemini yap
return "formatlanmış metin!";
}
}
Bu hali ile XmlFormatter
sınıfı benzer işlev sunan iki ayrı denetleme sınıfını kullanmak için oldukça fazla kontrol içerir. Bu kontrollerden kurtulmak ve XmlFormatter
sınıfını hafifletmek için denteleme işlevini bir kontrat ile genelleştirmeliyiz. Bu amaçla aşağıdaki PrettyFormatValidator
soyut kontrat sınıfını oluşturalım.
public abstract class PrettyFormatValidator
{
public abstract bool IsValid(string input);
}
PrettyFormatValidator
‘ı artık XmlValidator
ve XmlSchemaValidator
sınıflarının atası olarak kullanabiliriz.
public class XmlValidator:PrettyFormatValidator
{
public override bool IsValid(string input)
{
// Kural denetimini yap
return true;
}
}
public class XmlSchemaValidator:PrettyFormatValidator
{
private string _xsdSchema;
public XmlSchemaValidator(string xsd)
{
_xsdSchema = xsd;
}
public override bool IsValid(string input)
{
// XSD şemaya göre kural denetimini yap
return true;
}
}
Bu düzenlemelerden sonra XmlFormatter
sınıfına parametre olarak PrettyFormatValidator
tipinden nesne alan yapıcı fonksiyon (constructor) ekleyip Format metodunu da PrettyFormatValidator
sınıfının IsValid metodunu kullanacak şekilde düzenleyebiliriz.
public class XmlFormatter : PrettyFormatProvider
{
private PrettyFormatValidator _validator;
public XmlFormatter(PrettyFormatValidator validator)
{
_validator = validator;
}
public override string Format(string input)
{
if (_validator.IsValid(input))
throw new ValidationException();
// Formatlama işlemini yap
return "formatlanmış metin!";
}
}
Yukarıdaki düzenlemeler ile XmlFormatter
sınıfını, çalışma anında ata sınıfın yerine türetilmiş sınıflardan nesneleri kullanarak ilgili işlevlere erişebilir hale getirdik.
Interface Segregation (Özelleşmiş Kontrat/Arayüz Ayrımı)
Genel kullanım amaçlı tek bir kontrat yerine daha özelleşmiş birden çok kontrat oluşturmayı tercih etmeliyiz.
Kural denetimi yapmak için kullandığımız XmlValidator
, XmlSchemaValidator
, JsonValidator
ve HtmlValidator
sınıflarının herhangi birinde XmlSchemaValidator
sınıfındaki gibi şema kullanımına izin vermek için PrettyFormatValidator
soyut ata sınıfı yerine aşağıdaki gibi bir IPrettyFormatValidator
arayüzü (interface) tanımlayalım.
public interface IPrettyFormatValidator
{
string Schema { get; set; }
bool IsValid(string input);
}
Arayüz tanımını yaptıktan sonra denetim sınıflarımızı,
- Hepsi
IPrettyFormatValidator
arayüzünden türetelim ve - Hepsine Schema özelliğini ekleyelim
public class JsonValidator:IPrettyFormatValidator
{
public string Schema { get; set; }
public bool IsValid(string input)
{
// Kural denetimini yap
return true;
}
}
public class HtmlValidator:IPrettyFormatValidator
{
public string Schema { get; set; }
public bool IsValid(string input)
{
// Kural denetimini yap
return true;
}
}
public class XmlValidator:IPrettyFormatValidator
{
public string Schema { get; set; }
public bool IsValid(string input)
{
// Kural denetimini yap
return true;
}
}
public class XmlSchemaValidator:IPrettyFormatValidator
{
public string Schema { get; set; }
public bool IsValid(string input)
{
// XSD şemaya göre kural denetimini yap
return true;
}
}
Bu düzenlemedeki temel sorun, şema denetimi yapmayacakları halde JsonValidator
, HtmlValidator
ve XmlValidator
sınıflarının da Schema özelliğine sahip olma zorunluluğudur. Denetleme sınıflarımızı kontrat tasarımından kaynaklanan bu zorunluluktan kurtarmak için IPrettyFormatValidator
arayüzündeki Schema özelliğini daha özelleşmiş bir arayüz olan IPrettyFormatSchemaValidator
arayüzüne taşıyabiliriz.
public interface IPrettyFormatValidator
{
bool IsValid(string input);
}
public interface IPrettyFormatSchemaValidator:IPrettyFormatValidator
{
string Schema { get; set; }
}
Denetleme sınıflarımızı ihtiyaçlarına göre bu iki arayüzden birini kullanacak şekilde aşağıdaki gibi yeniden düzenleyelim.
public class XmlValidator:IPrettyFormatValidator
{
public bool IsValid(string input)
{
// Kural denetimini yap
return true;
}
}
public class XmlSchemaValidator:IPrettyFormatSchemaValidator
{
public string Schema { get; set; }
public bool IsValid(string input)
{
// XSD şemaya göre kural denetimini yap
return true;
}
}
XmlValidator
sınıfı IPrettyFormatValidator
arayüzünden türetiliyor ve ihtiyacı olmayan Schema özelliğini artık barındırmıyor. Ancak, XmlSchemaValidator
sınıfı daha özelleşmiş bir arayüz olan IPrettyFormatSchemaValidator
arayüzünden türetiliyor ve ihtiyaç duyduğu Schema özelliğini kontrata uygun olarak barındırıyor.
Dependency Inversion (Bağımlılıkların Tersyüz Edilmesi)
Katmanlı mimarilerde üst seviye modüller alt seviyedeki modüllere doğruda bağımlı olmamalıdır.
Open/closed prensibini açıklarken kullandığımız aşağıdaki örnek kod parçasında yer alan PrettyFormatter
sınıfının web uygulama katmanında yer alan bir sınıf olduğunu JsonFormatter
ve HtmlFormatter
sınıflarının ise kütüphane olarak kullanılan daha alt katmanlardan birinde yer aldığını varsayalım.
public class PrettyFormatter
{
// Format tipleri
public enum FormatTypes
{
Json,
Html
}
// Formatlama işlemini yapacak olan nesneler
private JsonFormatter _jsonFormatter = new JsonFormatter();
private HtmlFormatter _htmlFormatter = new HtmlFormatter();
// Formatlama işlemini yapan metod
public string Format(FormatTypes inputType, string input)
{
switch (inputType)
{
case FormatTypes.Json:
return _jsonFormatter.Format(input);
case FormatTypes.Html:
return _htmlFormatter.Format(input);
default:
throw new Exception("Desteklenmeyen format tipi!");
}
}
}
Yukarıdaki hali ile üst katmandaki PrettyFormatter
sınıfı alt katmanda yer alan JsonFormatter
ve HtmlFormatter
sınıflarından nesneleri kendisi oluşturmalıdır yani PrettyFormatter
sınıfı sorumluluğunu yerine getirmek için alt katmanlardaki sınıflara bağımlıdır. Bu bağımlılığı ortadan kaldırmak için JsonFormatter
ve HtmlFormatter
sınıflarını PrettyFormatProvider
sınıfından türeterek PrettyFormatter
sınıfına kurucu fonksiyonu (constructor) vasıtası ile dışarıdan verilmesini (constructor injection) sağlayabiliriz.
public class PrettyFormatter
{
private PrettyFormatProvider _provider;
public PrettyFormatter(PrettyFormatProvider provider)
{
_provider = provider;
}
// Formatlama işlemini yapan metod
public string Format(string input)
{
return _provider.Format(input);
}
}
NOT: PrettyFormatter sınıfının kodunun sizde olmadığını ve bu sınıfta dependency inversion prensibine uyulmadığını varsayın. Bu durumda PrettyFormatter sınıfını kendi geliştireceğiniz JavaScriptFormatter benzeri yeni bir formatlayıcıyı kullanmasını sağlamanız mümkün olmazdı.
Son Söz
İyi bir yazılım geliştirici olmak için SOLID prensiplerini mutlaka ama mutlaka öğrenmelisiniz. SOLID prensipleri sadece OOP (Nesne Yönelimli Programlama) alanında değil diğer bir çok yaklaşım ve alanda da dolaylı olarak kullanılabilir veya farklı bakış açılarını harekete geçirebilir. Tüm OOP tasarımlarınız ve kodunuz SOLID prensiplerine her an yüzde yüz uygun olmayabilir. Ancak, özellikle yeni işlevler eklerken veya hata düzeltmeleri yaparken mutlaka ama mutlaka refactoring (işlevi bozmadan yeniden yazma) pratikleri ve SOLID bilgisi ile kodunuzun SOLID prensiplerine uygunluğunu arttırabilirsiniz.
Bu yazıyı beğendiyseniz Twitter’da takipçilerinizle paylaşabilir veya beni Twitter’da takip edebilirsiniz.
Örnek Kod
using System;
namespace Solid
{
class Program
{
static void Main(string[] args)
{
var jsonFormatter = new JsonFormatter();
PrettyFormatter formatter = new PrettyFormatter(jsonFormatter);
var formattedText = formatter
.Format(@"{""id"":1,""ad"":""Ali"",""soyad"":""Özgür""}");
}
}
public class PrettyFormatter
{
private PrettyFormatProvider _provider;
public PrettyFormatter(PrettyFormatProvider provider)
{
_provider = provider;
}
// Formatlama işlemini yapan metod
public string Format(string input)
{
return _provider.Format(input);
}
}
#region Providers
public abstract class PrettyFormatProvider
{
public abstract string Format(string input);
}
public class JsonFormatter : PrettyFormatProvider
{
private IPrettyFormatValidator _validator;
public JsonFormatter(IPrettyFormatValidator validator)
{
_validator = validator;
}
public override string Format(string input)
{
if (!_validator.IsValid(input))
throw new ValidationException();
// Formatlama işlemini yap
return "formatlanmış metin!";
}
}
public class HtmlFormatter : PrettyFormatProvider
{
private IPrettyFormatValidator _validator;
public HtmlFormatter(IPrettyFormatValidator validator)
{
_validator = validator;
}
public override string Format(string input)
{
if (!_validator.IsValid(input))
throw new ValidationException();
// Formatlama işlemini yap
return "formatlanmış metin!";
}
}
public class XmlFormatter : PrettyFormatProvider
{
private IPrettyFormatValidator _validator;
public XmlFormatter(IPrettyFormatValidator validator)
{
_validator = validator;
}
public override string Format(string input)
{
if (_validator.IsValid(input))
throw new ValidationException();
// Formatlama işlemini yap
return "formatlanmış metin!";
}
}
#endregion // Providers
#region Validators
public class ValidationException : ApplicationException { }
public interface IPrettyFormatValidator
{
bool IsValid(string input);
}
public interface IPrettyFormatSchemaValidator:IPrettyFormatValidator
{
string Schema { get; set; }
}
public class JsonValidator:IPrettyFormatValidator
{
public bool IsValid(string input)
{
// Kural denetimini yap
return true;
}
}
public class HtmlValidator:IPrettyFormatValidator
{
public bool IsValid(string input)
{
// Kural denetimini yap
return true;
}
}
public class XmlValidator:IPrettyFormatValidator
{
public bool IsValid(string input)
{
// Kural denetimini yap
return true;
}
}
public class XmlSchemaValidator:IPrettyFormatSchemaValidator
{
public string Schema { get; set; }
public bool IsValid(string input)
{
// XSD şemaya göre kural denetimini yap
return true;
}
}
#endregion //Validators
}