Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Yazılım geliştirme sürecinde, kod kalitesini artırmak, sürdürülebilirliği sağlamak ve uzun vadede yazılımın yönetimini kolaylaştırmak için belirli prensiplere uyulması kritik öneme sahiptir. Bu prensipler, yazılım süreçlerindeki davranışlarımızı ve kararlarımızı yönlendiren evrensel ilkeler olarak düşünülebilir. Yazılım prensiplerinin anlaşılması, sadece daha iyi kod yazmayı değil, aynı zamanda kodun uzun vadede nasıl yönetileceğini de anlamayı sağlar.
Prensipler, genel anlamda bir bütünün, bir olgunun ahlaki sınırlarını çizen evrensel ilkelerdir. Prensipler, fiziki kurallara dayalı değildir; zaman, mekan ve koşullardan bağımsızdırlar. Prensipler, kuralların üzerinde yer alır ve daha soyut, daha evrensel niteliktedirler. Kurallar, belirli koşullara göre değişebilirken, prensipler zamandan ve mekandan bağımsız olarak sabit kalır.
Örneğin, “Bir insanın kalbini kırmamak” bir prensiptir ve bu ilkeye uyan bir kişi, koşullardan bağımsız olarak bu prensibi takip eder. Benzer şekilde, yazılım dünyasında da prensipler, kod yazarken belirli ahlaki sınırları belirler ve bu sınırlar içinde kalınarak kod yazılması, kodun sürdürülebilir ve yönetilebilir olmasını sağlar.
Yazılım prensipleri, yazılım geliştirme sürecinde en ideal kodlama ahlakını sağlayan ilkelerdir. Bu prensipler, yazılım süreçlerinde kodlama sırasında hangi durumlarda nasıl davranışlar sergilenmesi gerektiğini belirler. Yazılım prensiplerine uyularak inşa edilen kodlar, daha yönetilebilir, daha esnek ve gelecekteki ihtiyaçlara daha hızlı yanıt verebilir hale gelir.
Yazılım prensipleri, genel anlamda bir yazılım sisteminin sürdürülebilirliğini, esnekliğini ve bakımını kolaylaştırmak amacıyla geliştirilmiştir. Bu prensiplere uyulmaması durumunda, kodun karmaşıklaşması ve yönetiminin zorlaşması kaçınılmaz olur. Prensipler, kodun daha modüler, yeniden kullanılabilir ve okunabilir olmasını sağlar.
Aşağıda, yazılım geliştirmede sıkça kullanılan bazı temel prensipler listelenmiştir:
Yazılım prensipleri, kodun uzun vadede sürdürülebilir ve yönetilebilir olmasını sağlar. Bu prensiplere uyulmadığında, kod karmaşıklaşır, hataya açık hale gelir ve bakım maliyetleri artar. Prensipler, yazılım geliştiricilerin kod yazarken daha bilinçli ve sorumlu hareket etmelerini sağlar.
Örneğin, bir yazılım geliştirme projesinde, prensiplere uyulmadan yazılan kodlar zamanla karmaşıklaşabilir ve projeye yeni geliştiricilerin katılması durumunda, kodun anlaşılması zorlaşabilir. Bu da projenin sürdürülebilirliğini tehlikeye atar. Ancak prensipli bir şekilde yazılan kodlar, modüler, esnek ve yeniden kullanılabilir olduğundan, yeni özelliklerin eklenmesi veya mevcut kodun değiştirilmesi daha kolay ve daha güvenli olur.
Loose Coupling (gevşek bağlılık), yazılım geliştirme süreçlerinde kullanılan temel prensiplerden biridir. Bu prensip, özellikle nesneye yönelik tasarım ilkelerinde sıkça vurgulanan, yazılımın daha esnek, sürdürülebilir ve bakımı kolay olmasını sağlayan önemli bir yaklaşımdır. Gevşek bağlılık prensibi, nesneler arasında bir bağımlılığın kaçınılmaz olduğunu kabul eder, ancak bu bağımlılığın mümkün olduğunca esnek ve yönetilebilir olmasını önerir.
Bağımlılık, bir sınıfın ya da nesnenin başka bir sınıfın ya da nesnenin işlevselliğine ihtiyaç duyduğu durumlarda ortaya çıkar. Yazılım geliştirirken nesnelerin birbirleriyle iş birliği yapması gerekebilir. Ancak bu iş birliği sonucunda nesneler arasında kaçınılmaz olarak bir bağımlılık oluşur. Bu bağımlılığın seviyesi iki kategoriye ayrılabilir:
Loose coupling, nesneler arasında esnek bir bağımlılık kurmayı amaçlar. Bağımlılık kaçınılmazdır, ancak bu bağımlılığın sıkı olmaması gerekir. Bir nesnenin diğer bir nesne hakkında çok fazla bilgiye sahip olması, yazılımın esnekliğini azaltır ve bakımını zorlaştırır. Gevşek bağlılıkta amaç, nesnelerin sadece ihtiyaç duydukları minimum bilgilere sahip olmasını sağlamaktır.
Loose coupling prensibini açıklamak için verilen bir örnek, telefon ve SIM kart ilişkisiyle ilgilidir. Bir telefonun çalışması için bir SIM karta ihtiyacı vardır, bu da bir bağımlılık oluşturur. Ancak, telefon herhangi bir SIM kartla çalışabilir. Telefon, belirli bir SIM karta sıkı sıkıya bağlı değildir; farklı operatörlerden SIM kartlar kullanabilir ve bu SIM kartları değiştirmek için telefonda büyük bir değişiklik yapmaya gerek yoktur. Bu durum, gevşek bir bağımlılık örneğidir. Telefonun bir SIM karta ihtiyacı vardır (bağımlılık), ancak bu bağımlılık esnektir.
Eğer telefon, sadece tek bir operatörün SIM kartıyla çalışacak şekilde tasarlanmış olsaydı, bu durumda sıkı bağlılık oluşurdu. Bu tür bir durumda, operatör değiştiğinde telefonu değiştirmek gerekebilirdi. Loose coupling sayesinde, telefon birden fazla SIM kartı kabul edecek şekilde tasarlanmıştır.
Yazılımda loose coupling uygulamak için nesneler arasındaki bağımlılığı minimumda tutacak yapılar kullanılır. Bu yapılardan en önemlisi arayüzler (interfaces) ve soyut sınıflardır (abstract classes). Arayüzler ve soyut sınıflar, bir nesnenin başka bir nesneyle nasıl etkileşime gireceğini tanımlar, ancak bu nesnelerin birbirlerine sıkı sıkıya bağlı olmasını engeller.
Bir mail gönderme sistemini ele alalım. Aşağıda, bu senaryoda gevşek bağlılığın nasıl uygulanabileceğini gösteren bir örnek bulunmaktadır:
Tight Coupling Durumu:
public class MailSender
{
private GmailService _gmailService = new GmailService();
public void SendMail()
{
_gmailService.Send("example@gmail.com", "Hello World");
}
}
public class GmailService
{
public void Send(string recipient, string message)
{
// Gmail API ile mail gönderme işlemi
}
}
Bu örnekte, MailSender
sınıfı doğrudan GmailService
sınıfına bağlıdır. Eğer gelecekte MailSender
sınıfı, Gmail yerine başka bir mail servisini kullanmak isterse, kodun içinde büyük değişiklikler yapmak zorunda kalacağız. Bu sıkı bağlılık örneğidir ve yazılımın esnekliğini kısıtlar.
Loose Coupling Durumu:
public interface IMailService
{
void Send(string recipient, string message);
}
public class MailSender
{
private IMailService _mailService;
public MailSender(IMailService mailService)
{
_mailService = mailService;
}
public void SendMail()
{
_mailService.Send("example@gmail.com", "Hello World");
}
}
public class GmailService : IMailService
{
public void Send(string recipient, string message)
{
// Gmail API ile mail gönderme işlemi
}
}
public class HotmailService : IMailService
{
public void Send(string recipient, string message)
{
// Hotmail API ile mail gönderme işlemi
}
}
Bu örnekte, MailSender
sınıfı artık bir IMailService
arayüzüne bağlıdır. Bu sayede, GmailService
, HotmailService
gibi farklı mail servisleri bu arayüzü implemente edebilir ve MailSender
sınıfı herhangi bir mail servisinden bağımsız olarak çalışabilir. Artık sadece IMailService
ile çalıştığı için, mail servisinin değişmesi durumunda MailSender
üzerinde değişiklik yapmamıza gerek yoktur. Bu gevşek bağlılık örneğidir.
Single Responsibility Principle (SRP), SOLID prensiplerinin ilki olan ve yazılım geliştirme sürecinde kritik bir öneme sahip olan bir prensiptir. Adından da anlaşılacağı gibi, SRP, her sınıfın veya modülün yalnızca bir tek sorumluluğu olması gerektiğini savunur. Bu prensip, kodun daha kolay yönetilebilir, esnek ve sürdürülebilir olmasını sağlar.
SRP, sadece yazılım geliştirme sürecinde değil, aynı zamanda günlük hayatımızda da geçerli olan bir ilkedir. Gerçek hayatta bir kişi aynı anda birden fazla işi yapmaya çalıştığında, genellikle verim düşer ve hata yapma olasılığı artar. Örneğin, araba kullanırken aynı anda telefonla konuşmak, dikkati dağıtır ve kazaya neden olabilir. Aynı durum yazılım geliştirme için de geçerlidir: Bir sınıfın birden fazla sorumluluğa sahip olması, bu sınıfın yönetilmesini zorlaştırır ve hata yapma olasılığını artırır.
SRP, bir sınıfın yalnızca tek bir sorumluluğa odaklanması gerektiğini belirtir. Eğer bir sınıfın değiştirilmesi için birden fazla neden varsa, bu sınıfın birden fazla sorumluluğu olduğu anlamına gelir ve bu, SRP’ye aykırıdır. SRP’ye göre, bir sınıfın yalnızca tek bir sorumluluğu olmalıdır ve bu sorumluluk, sınıfın varlık sebebini belirlemelidir.
SRP’yi uygulamak, bir sınıfın sadece tek bir işi yapmasını sağlamak anlamına gelir. Bu, sınıfın sorumluluklarının net bir şekilde tanımlanması gerektiği anlamına gelir. Eğer bir sınıf, birden fazla sorumluluğa sahip olacak şekilde tasarlanmışsa, bu sınıfın bölünmesi ve her bir sorumluluğun ayrı bir sınıfa taşınması gerekir.
public class Employee
{
public string Name { get; set; }
public void CalculateSalary()
{
// Maaş hesaplama işlemleri
}
public void GenerateReport()
{
// Rapor oluşturma işlemleri
}
}
Bu örnekte, Employee
sınıfı hem maaş hesaplama hem de rapor oluşturma işlemlerini yapmaktadır. Bu, SRP’ye aykırıdır çünkü Employee
sınıfı birden fazla sorumluluğa sahiptir. Bu durum, sınıfın karmaşıklaşmasına ve yönetiminin zorlaşmasına neden olur.
public class Employee
{
public string Name { get; set; }
}
public class SalaryCalculator
{
public void CalculateSalary(Employee employee)
{
// Maaş hesaplama işlemleri
}
}
public class ReportGenerator
{
public void GenerateReport(Employee employee)
{
// Rapor oluşturma işlemleri
}
}
Bu doğru uygulamada, maaş hesaplama ve rapor oluşturma sorumlulukları farklı sınıflara ayrılmıştır. Employee
sınıfı yalnızca bir çalışanın özelliklerini barındırırken, maaş hesaplama işlemleri SalaryCalculator
sınıfına, rapor oluşturma işlemleri ise ReportGenerator
sınıfına taşınmıştır. Böylece her sınıfın tek bir sorumluluğu vardır ve SRP prensibine uyulmuştur.
Bir sınıfın tek bir sorumluluğa sahip olması, kodun daha kolay anlaşılmasını, test edilmesini ve bakımını sağlar. SRP’ye uyulmadığında, bir sınıfta yapılan bir değişiklik, sınıfın diğer sorumluluklarını da etkileyebilir ve bu da yazılımın sürdürülebilirliğini tehlikeye atar. Bu yüzden, yazılım geliştirme sürecinde SRP’ye dikkat etmek, daha sağlam ve yönetilebilir bir yazılım elde etmek için kritik öneme sahiptir.
Open/Closed Principle (OCP), SOLID prensiplerinin ikinci ilkesidir ve yazılım geliştirme sürecinde kodun genişletilebilir olmasını, ancak değiştirilebilir olmamasını savunur. Bu prensip, yazılımın sürdürülebilirliğini ve esnekliğini artırarak, gereksinim değişikliklerine daha dayanıklı hale getirilmesini sağlar.
OCP, iki temel kavram üzerine kuruludur:
Bu prensip, yazılımın gelişimi sırasında yeni gereksinimler ortaya çıktığında mevcut kodun değiştirilmeden genişletilmesine olanak tanır. Böylece, yazılımın bakım maliyetleri azalır ve hata yapma olasılığı en aza indirilir.
Kodun Değiştirilmesi: Bir yazılım bileşeninde yeni gereksinimler ortaya çıktığında mevcut kodun değiştirilmesi anlamına gelir. Bu, genellikle mevcut kodun yeniden düzenlenmesi, değiştirilmesi veya tamamen yeniden yazılması anlamına gelir.
Kodun Genişletilmesi: Yeni gereksinimler ortaya çıktığında mevcut kodun değiştirilmesine gerek kalmadan yeni davranışların yazılıma eklenmesi anlamına gelir. Bu, genellikle mevcut kodun üzerine yeni kod ekleyerek gerçekleştirilir.
OCP, kodun genişletilebilir olmasını, ancak değiştirilmemesini savunur. Bu sayede, yazılımın mevcut işlevselliği korunurken yeni özellikler eklenebilir.
OCP’yi uygulamak için yazılım bileşenlerinin genişletilebilir bir şekilde tasarlanması gerekir. Bu, genellikle arayüzler (interfaces) ve soyut sınıflar (abstract classes) kullanılarak yapılır. Arayüzler ve soyut sınıflar, yeni davranışların mevcut kodu değiştirmeden eklenmesine olanak tanır.
Bir yazılım sistemi geliştirdiğimizi varsayalım ve bu sistemde farklı bankalar aracılığıyla para transferi işlemleri gerçekleştiriyoruz.
public class PaymentProcessor
{
public void ProcessPayment(string bankType, double amount)
{
if (bankType == "Garanti")
{
// Garanti Bankası için ödeme işlemleri
}
else if (bankType == "Halkbank")
{
// Halkbank için ödeme işlemleri
}
// Yeni bankalar eklendiğinde bu metoda yeni if blokları eklenmesi gerekecek.
}
}
Bu örnekte, yeni bir banka eklemek istediğimizde ProcessPayment
metodunu değiştirmemiz gerekecektir. Bu durum, OCP’ye aykırıdır çünkü mevcut kodda değişiklik yapılması gerekir.
public interface IBank
{
void ProcessPayment(double amount);
}
public class GarantiBank : IBank
{
public void ProcessPayment(double amount)
{
// Garanti Bankası için ödeme işlemleri
}
}
public class Halkbank : IBank
{
public void ProcessPayment(double amount)
{
// Halkbank için ödeme işlemleri
}
}
public class PaymentProcessor
{
public void ProcessPayment(IBank bank, double amount)
{
bank.ProcessPayment(amount);
}
}
Bu doğru uygulamada, yeni bir banka eklemek istediğimizde sadece yeni bir sınıf oluşturup IBank
arayüzünü implemente etmemiz yeterli olacaktır. PaymentProcessor
sınıfında herhangi bir değişiklik yapmamıza gerek kalmaz. Bu sayede, OCP prensibine uygun bir yapı elde edilmiş olur.
OCP, yazılımın daha sürdürülebilir, genişletilebilir ve esnek olmasını sağlar. Bu prensip, yazılım bileşenlerinin mevcut işlevselliği koruyarak yeni gereksinimlere kolayca uyum sağlamasına olanak tanır. Kodun değiştirilmesi, genellikle yeni hatalara yol açabileceğinden, OCP’nin uygulanması bu riskleri en aza indirir.
Liskov Substitution Principle (LSP), SOLID prensiplerinin üçüncü ilkesidir ve nesne yönelimli programlama (OOP) süreçlerinde oldukça kritik bir rol oynar. LSP, bir sınıf hiyerarşisinde türetilen sınıfların (subclasses) temel sınıfın (base class) yerine geçebileceğini ve temel sınıfın yerini alırken programın işleyişinde herhangi bir bozulma meydana gelmemesi gerektiğini savunur. Bu, türetilen sınıfların temel sınıfın sunduğu davranışları bozmadan genişletebilmesi gerektiği anlamına gelir.
LSP’nin özünde şu iki ana kavram yatar:
LSP’yi doğru bir şekilde uygulayabilmek için, temel sınıfların ve alt sınıfların doğru bir şekilde tasarlanması gerekir. Alt sınıflar, temel sınıfın davranışlarını genişletebilmeli, ancak bu davranışları bozacak şekilde değiştirmemelidir.
Bir bulut hizmetleri sistemi geliştirdiğimizi varsayalım. Bu sistemde, farklı bulut sağlayıcıları (Amazon, Azure, Google Cloud) ile entegre olan bir yapı bulunuyor ve bu sağlayıcılar farklı hizmetler sunuyor.
public abstract class CloudService
{
public abstract void Translate(string text);
public abstract void MachineLearning(string data);
}
public class AmazonCloudService : CloudService
{
public override void Translate(string text)
{
// Amazon Translate API çağrısı
}
public override void MachineLearning(string data)
{
// Amazon Machine Learning API çağrısı
}
}
public class AzureCloudService : CloudService
{
public override void Translate(string text)
{
// Azure Translate API yok, bu metot boş bırakılabilir ya da bir hata fırlatılabilir
throw new NotImplementedException();
}
public override void MachineLearning(string data)
{
// Azure Machine Learning API çağrısı
}
}
Bu örnekte, AzureCloudService
sınıfı Translate
metodunu implemente etmek zorunda bırakılmıştır. Ancak, Azure Translate hizmeti sunmadığı için bu metodun içi boş bırakılmış veya bir hata fırlatılmıştır. Bu, LSP’ye aykırı bir tasarımdır çünkü AzureCloudService
, CloudService
sınıfının tüm davranışlarını anlamlı bir şekilde devralmamaktadır.
public interface ICloudService
{
void MachineLearning(string data);
}
public interface ITranslationService
{
void Translate(string text);
}
public class AmazonCloudService : ICloudService, ITranslationService
{
public void Translate(string text)
{
// Amazon Translate API çağrısı
}
public void MachineLearning(string data)
{
// Amazon Machine Learning API çağrısı
}
}
public class AzureCloudService : ICloudService
{
public void MachineLearning(string data)
{
// Azure Machine Learning API çağrısı
}
}
Bu doğru uygulamada, Translate
işlemini gerçekleştiren sınıflar sadece bu hizmeti sunan sınıflar tarafından implemente edilir. AzureCloudService
, Translate
metodunu implemente etmek zorunda kalmaz, çünkü Azure bu hizmeti sunmaz. Bu sayede, AzureCloudService
sınıfı yalnızca gerçek davranışları barındırır ve LSP’ye uygun hale gelir.
LSP, yazılımın sürdürülebilirliğini ve genişletilebilirliğini artırır. Bu prensip, temel sınıflardan türeyen sınıfların, temel sınıfın davranışlarını değiştirmeden genişletmesine olanak tanır. Bu da kodun yeniden kullanılabilirliğini ve esnekliğini artırır.
LSP’yi uygularken yapılan yaygın hatalar arasında, alt sınıfların temel sınıftan devralınan metotları anlamsız bir şekilde implemente etmesi veya bu metotları boş bırakması yer alır. Bu, alt sınıfların temel sınıfın sunduğu sözleşmeye uymadığını gösterir ve yazılımın işleyişinde hatalara yol açabilir.
Interface Segregation Principle (ISP), SOLID prensiplerinin dördüncü ilkesidir ve yazılım geliştirme sürecinde arayüzlerin (interface) nasıl tasarlanması gerektiğini ele alır. Bu prensip, bir arayüzün (interface) tek bir amaca hizmet etmesi gerektiğini savunur ve bir sınıfın sadece ihtiyaç duyduğu metotları içeren arayüzleri implemente etmesini önerir.
ISP, yazılımda arayüzlerin daha spesifik ve odaklı olmasını savunur. Prensibin temel amacı, gereksiz metotları içeren büyük arayüzlerin (fat interfaces) kullanılmasından kaçınmaktır. ISP’ye göre, bir arayüz bir sınıfa yalnızca ihtiyaç duyduğu metotları sağlamalıdır; bu sayede sınıflar gereksiz metotları implemente etmek zorunda kalmaz.
ISP’yi doğru bir şekilde uygulamak için, büyük ve çok amaçlı arayüzleri daha küçük, spesifik arayüzlere bölmek gerekir. Bu, her bir arayüzün yalnızca belirli bir davranışı temsil etmesini sağlar ve bir sınıfın yalnızca ihtiyaç duyduğu arayüzleri implemente etmesine olanak tanır.
Bir yazıcı sistemi geliştirdiğimizi varsayalım. Bu sistemde, farklı yazıcı türleri (HP, Samsung, Lexmark) ile çalışan bir yapı bulunuyor ve bu yazıcılar farklı işlevlere sahip.
public interface IPrinter
{
void Print();
void Scan();
void PrintDuplex();
}
public class HPPrinter : IPrinter
{
public void Print()
{
// HP Yazıcı ile yazdırma işlemi
}
public void Scan()
{
// HP Yazıcı ile tarama işlemi
}
public void PrintDuplex()
{
// HP Yazıcı ile çift taraflı yazdırma işlemi
}
}
public class SamsungPrinter : IPrinter
{
public void Print()
{
// Samsung Yazıcı ile yazdırma işlemi
}
public void Scan()
{
// Samsung Yazıcı tarama işlemi
}
public void PrintDuplex()
{
throw new NotImplementedException(); // Samsung çift taraflı yazdırmayı desteklemiyor
}
}
public class LexmarkPrinter : IPrinter
{
public void Print()
{
// Lexmark Yazıcı ile yazdırma işlemi
}
public void Scan()
{
throw new NotImplementedException(); // Lexmark taramayı desteklemiyor
}
public void PrintDuplex()
{
throw new NotImplementedException(); // Lexmark çift taraflı yazdırmayı desteklemiyor
}
}
Bu örnekte, IPrinter
arayüzü her yazıcı için geçerli olmayabilecek metotları içerir. Örneğin, SamsungPrinter
çift taraflı yazdırmayı desteklemez, ancak bu metodu implemente etmek zorunda kalır. Bu durum, ISP’ye aykırı bir tasarımdır çünkü SamsungPrinter
gereksiz bir metodu implemente etmek zorunda bırakılır.
public interface IPrint
{
void Print();
}
public interface IScan
{
void Scan();
}
public interface IPrintDuplex
{
void PrintDuplex();
}
public class HPPrinter : IPrint, IScan, IPrintDuplex
{
public void Print()
{
// HP Yazıcı ile yazdırma işlemi
}
public void Scan()
{
// HP Yazıcı ile tarama işlemi
}
public void PrintDuplex()
{
// HP Yazıcı ile çift taraflı yazdırma işlemi
}
}
public class SamsungPrinter : IPrint, IScan
{
public void Print()
{
// Samsung Yazıcı ile yazdırma işlemi
}
public void Scan()
{
// Samsung Yazıcı ile tarama işlemi
}
}
public class LexmarkPrinter : IPrint
{
public void Print()
{
// Lexmark Yazıcı ile yazdırma işlemi
}
}
Bu doğru uygulamada, her yazıcı yalnızca ihtiyaç duyduğu arayüzleri implemente eder. Örneğin, SamsungPrinter
ve LexmarkPrinter
, çift taraflı yazdırmayı desteklemedikleri için IPrintDuplex
arayüzünü implemente etmek zorunda kalmaz. Bu sayede, her sınıf yalnızca ilgili olduğu işlevleri yerine getirir ve gereksiz metotları implemente etmez.
ISP, yazılımın daha esnek ve sürdürülebilir olmasını sağlar. Gereksiz metotların implemente edilmesi zorunluluğunu ortadan kaldırarak, sınıfların yalnızca ihtiyaç duydukları işlevleri yerine getirmesini sağlar. Bu da yazılımın daha modüler, bakımının daha kolay ve genişletilebilir olmasına katkıda bulunur.
ISP’yi uygularken yapılan yaygın hatalar arasında, büyük ve çok amaçlı arayüzler oluşturmak yer alır. Bu tür arayüzler, bir sınıfın implemente etmemesi gereken metotları implemente etmesini zorunlu kılar, bu da kodun karmaşıklığını artırır ve bakımını zorlaştırır.
Dependency Inversion Principle (DIP), SOLID prensiplerinin beşincisidir ve yazılım geliştirmede bağımlılıkların nasıl ele alınması gerektiğini açıklar. Bu prensip, yüksek seviye modüllerin düşük seviye modüllere bağımlı olmaması gerektiğini, aksine her iki seviyenin de soyutlamalara (abstractions) bağımlı olması gerektiğini savunur. DIP, yazılım sistemlerinin daha esnek, sürdürülebilir ve ölçeklenebilir olmasını sağlayan bir prensiptir.
DIP, yazılım geliştirme süreçlerinde bağımlılıkları yönetme şeklini yeniden yapılandırır. Yüksek seviyeli modüllerin, belirli düşük seviyeli modüllere doğrudan bağımlı olması yerine, bu modüllerin soyutlamalar üzerinden birbirleriyle iletişim kurmalarını önerir. Bu, bağımlılıkların tersine çevrilmesi olarak adlandırılır.
DIP’in uygulanması, yazılım geliştirme sürecinde sınıfların birbirine bağımlı olmasını engellemek için soyutlamalar kullanmayı içerir. Bu soyutlamalar, genellikle arayüzler (interfaces) veya soyut sınıflar (abstract classes) olarak karşımıza çıkar. Yüksek seviyeli bir modül, düşük seviyeli bir modülle doğrudan etkileşime girmek yerine, aralarındaki iletişimi bir arayüz veya soyut sınıf üzerinden gerçekleştirir.
Bir mail servisi uygulaması geliştirdiğimizi düşünelim. Bu uygulama, çeşitli e-posta servis sağlayıcılarını (Gmail, Yahoo, Outlook) desteklemelidir. DIP’e uygun olmayan bir yapı, mail servisini doğrudan Gmail gibi belirli bir servise bağımlı hale getirir. Bu yapı, mail servisinin esnekliğini kısıtlar ve değişiklik yapmayı zorlaştırır.
public class MailService
{
private GmailService _gmailService;
public MailService()
{
_gmailService = new GmailService();
}
public void SendEmail(string message)
{
_gmailService.Send(message);
}
}
public class GmailService
{
public void Send(string message)
{
// Gmail üzerinden e-posta gönderme işlemi
}
}
Bu yanlış uygulamada, MailService
sınıfı doğrudan GmailService
sınıfına bağımlıdır. Bu yapı, başka bir e-posta servis sağlayıcısını eklemek istediğimizde MailService
sınıfında değişiklik yapmamızı gerektirir ve bu durum DIP’e aykırıdır.
public interface IEmailService
{
void SendEmail(string message);
}
public class GmailService : IEmailService
{
public void SendEmail(string message)
{
// Gmail üzerinden e-posta gönderme işlemi
}
}
public class YahooService : IEmailService
{
public void SendEmail(string message)
{
// Yahoo üzerinden e-posta gönderme işlemi
}
}
public class MailService
{
private IEmailService _emailService;
public MailService(IEmailService emailService)
{
_emailService = emailService;
}
public void SendEmail(string message)
{
_emailService.SendEmail(message);
}
}
Bu doğru uygulamada, MailService
sınıfı herhangi bir spesifik e-posta servisine bağımlı değildir. Bunun yerine, IEmailService
adlı bir arayüz üzerinden tüm e-posta servis sağlayıcıları ile iletişim kurar. Bu, MailService
sınıfının esnekliğini artırır ve gelecekte farklı e-posta servis sağlayıcıları eklenmesi gerektiğinde sınıfta herhangi bir değişiklik yapılmasını gerektirmez.
DIP, yazılımın esnekliğini, sürdürülebilirliğini ve ölçeklenebilirliğini artırır. Yüksek seviyeli modüllerin düşük seviyeli modüllere doğrudan bağımlı olmaması, sistemde yapılacak değişikliklerin etkilerini minimize eder ve kodun bakımını kolaylaştırır.
DIP’i uygularken yapılan yaygın hatalar arasında, sınıfların doğrudan başka sınıflara bağımlı hale getirilmesi yer alır. Bu, sistemin esnekliğini azaltır ve bakımını zorlaştırır. Soyutlamaların doğru kullanılması, DIP’in başarılı bir şekilde uygulanmasını sağlar.
Bir sonraki yazıda görüşmek dileğiyle!”