Nesne yönelimli programlama (OOP) paradigmasının temel taşlarından biri kalıtımdır. Kalıtım, kodun yeniden kullanılabilirliğini sağlar ve “is-a” (bir tür) ilişkisi kurarak kodun daha iyi organize edilmesine yardımcı olur. Ancak, kalıtımın yanlış kullanılması, birçok tasarım sorununa ve kırılgan kod yapılarına yol açabilir. İşte tam da bu noktada, SOLID prensiplerinin üçüncü bileşeni olan Liskov Yerine Geçme Prensibi (Liskov Substitution Principle – LSP) devreye girer.
Bu makalede, LSP’nin ne olduğunu, neden önemli olduğunu, nasıl uygulanacağını ve kalıtım hiyerarşilerinizi tasarlarken bu prensibi nasıl kullanabileceğinizi derinlemesine inceleyeceğiz.
Liskov Yerine Geçme Prensibi Nedir?
Liskov Yerine Geçme Prensibi, 1987 yılında bilgisayar bilimcisi Barbara Liskov tarafından tanıtılmıştır ve daha sonra Robert C. Martin tarafından SOLID prensiplerinin bir parçası olarak benimsenmiştir. Prensip şöyle ifade edilir:
“Bir programda, bir üst sınıfın nesneleri, programın doğruluğunu etkilemeden alt sınıflarının nesneleriyle değiştirilebilir olmalıdır.”
Daha teknik bir ifadeyle, bir alt sınıf S, bir üst sınıf T’nin yerine geçebiliyorsa, herhangi bir T türündeki nesne kullanılan program, S türündeki bir nesne kullanıldığında da aynı şekilde çalışmalıdır.
Bu tanım biraz soyut görünebilir, bu yüzden daha basit bir şekilde ifade edelim: Bir alt sınıf, üst sınıfın davranışını değiştirmemeli, sadece genişletmelidir.
Neden Liskov Yerine Geçme Prensibi Önemlidir?
LSP’nin uygulanması, yazılım sistemlerinizde çeşitli faydalar sağlar:
1. Kod Güvenilirliği
LSP’ye uyan kod, alt sınıfların üst sınıfların yerini alabilmesini garanti eder, bu da kodun daha öngörülebilir ve güvenilir olmasını sağlar.
2. Daha İyi Polimorfizm
Polimorfizm, OOP’nin temel ilkelerinden biridir ve LSP, polimorfizmin sağlıklı bir şekilde kullanılmasını destekler. LSP sayesinde, kodunuz farklı nesnelerle tutarlı bir şekilde çalışır.
3. Daha Az Koşullu Mantık
LSP’ye uygun tasarlanmış sistemlerde, nesne türüne bağlı koşullu ifadelere daha az ihtiyaç duyulur. Bu, kodun daha temiz ve bakımı daha kolay olmasını sağlar.
4. Test Edilebilirlik
LSP’ye uygun kod, daha kolay test edilebilir çünkü testler üst sınıf arayüzünü kullanabilir ve bu testler tüm alt sınıflar için de geçerli olmalıdır.
5. Daha Modüler Kod
LSP, daha modüler ve bağımsız kod bileşenleri oluşturmanıza yardımcı olur, bu da kodun bakımını ve genişletilmesini kolaylaştırır.
Liskov Yerine Geçme Prensibinin İhlali Nasıl Olur?
LSP ihlalleri, genellikle alt sınıfların üst sınıflarının davranışını değiştirdiği durumlarda ortaya çıkar. İşte bazı yaygın LSP ihlalleri:
1. Metot Önkoşullarını Güçlendirme
Bir alt sınıf, üst sınıfın metodunun önkoşullarını daha katı hale getirdiğinde LSP ihlal edilmiş olur.
Örnek – LSP İhlali:
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
// Karede genişlik ve yükseklik aynı olmalıdır
this.width = width;
this.height = width;
}
@Override
public void setHeight(int height) {
// Karede genişlik ve yükseklik aynı olmalıdır
this.width = height;
this.height = height;
}
}
Bu örnekte, Square
sınıfı Rectangle
sınıfından türetilmiştir, ancak bir karenin genişliği ve yüksekliği aynı olmalıdır. Bu nedenle, setWidth
ve setHeight
metodları her iki boyutu da aynı değere ayarlar. Bu, LSP’yi ihlal eder çünkü aşağıdaki kod Rectangle
için çalışırken, Square
için çalışmaz:
void testRectangle(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
// Rectangle için bu assert geçerli olmalıdır
assert(r.getArea() == 20);
}
Eğer testRectangle
metoduna bir Square
nesnesi geçirilirse, alanın 20 yerine 16 olacağı görülür çünkü setHeight(4)
çağrısı genişliği de 4 yapar.
2. Metot Sonkoşullarını Zayıflatma
Bir alt sınıf, üst sınıfın metodunun sonkoşullarını daha zayıf hale getirdiğinde LSP ihlal edilmiş olur.
Örnek – LSP İhlali:
class Bird {
public void fly() {
System.out.println("Kuş uçuyor...");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguenler uçamaz!");
}
}
Bu örnekte, Penguin
sınıfı Bird
sınıfından türetilmiştir, ancak penguenler uçamaz. Bu nedenle, fly
metodu bir istisna fırlatır. Bu, LSP’yi ihlal eder çünkü Bird
türünde bir nesne bekleyen kod, bir Penguin
nesnesi alırsa çalışmaz.
3. İstisnaları Değiştirme
Bir alt sınıf, üst sınıfın metodunun fırlattığı istisnaları değiştirdiğinde LSP ihlal edilmiş olur.
Örnek – LSP İhlali:
class FileProcessor {
public void processFile(String path) throws IOException {
// Dosya işleme mantığı
}
}
class NetworkFileProcessor extends FileProcessor {
@Override
public void processFile(String path) throws IOException, NetworkException {
// Ağ dosyası işleme mantığı
}
}
Bu örnekte, NetworkFileProcessor
sınıfı, FileProcessor
sınıfının processFile
metoduna yeni bir istisna (NetworkException
) ekler. Bu, LSP’yi ihlal eder çünkü FileProcessor
bekleyen kod bu ilave istisnayı beklemiyor olabilir.
4. Değişmezleri (Invariants) Korumama
Bir alt sınıf, üst sınıfın değişmezlerini korumazsa LSP ihlal edilmiş olur.
Örnek – LSP İhlali:
class Account {
protected double balance;
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public void withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
}
}
}
class OverdraftAccount extends Account {
private double overdraftLimit;
public OverdraftAccount(double overdraftLimit) {
this.overdraftLimit = overdraftLimit;
}
@Override
public void withdraw(double amount) {
if (amount > 0 && balance + overdraftLimit >= amount) {
balance -= amount;
}
}
}
Bu örnekte, Account
sınıfı, bakiyenin negatif olmaması gerektiği değişmezini korur. Ancak, OverdraftAccount
sınıfı bu değişmezi ihlal eder ve bakiyenin negatif olmasına izin verir (overdraftLimit dahilinde). Bu, LSP’yi ihlal eder çünkü Account
türünde bir nesne bekleyen kod, bakiyenin her zaman pozitif olacağını varsayabilir.
Liskov Yerine Geçme Prensibinin Doğru Uygulanması
LSP’yi doğru bir şekilde uygulamak için, kalıtım hiyerarşilerinizi tasarlarken belirli kurallara ve pratiklere uymanız gerekir. İşte bazı temel yaklaşımlar:
1. Doğru Soyutlama Seviyesi Kullanma
Sınıfları doğru soyutlama seviyesinde tasarlayın. Eğer bir davranış tüm alt sınıflar için uygun değilse, o davranışı üst sınıfa koymak yerine, daha spesifik bir alt grup için bir ara soyut sınıf veya arayüz oluşturun.
Örnek – LSP’ye Uygun Tasarım:
interface Bird {
void eat();
void sleep();
}
interface FlyingBird extends Bird {
void fly();
}
class Sparrow implements FlyingBird {
@Override
public void eat() {
System.out.println("Serçe yem yiyor...");
}
@Override
public void sleep() {
System.out.println("Serçe uyuyor...");
}
@Override
public void fly() {
System.out.println("Serçe uçuyor...");
}
}
class Penguin implements Bird {
@Override
public void eat() {
System.out.println("Penguen balık yiyor...");
}
@Override
public void sleep() {
System.out.println("Penguen uyuyor...");
}
}
Bu tasarımda, tüm kuşlar eat
ve sleep
metodlarına sahiptir, ancak sadece uçabilen kuşlar fly
metoduna sahiptir. Bu, LSP’ye uygun bir tasarımdır çünkü herhangi bir Bird
nesnesi eat
ve sleep
metodlarını çağırabilir, ve herhangi bir FlyingBird
nesnesi de ek olarak fly
metodunu çağırabilir.
2. Kompozisyon Kullanımı
Kalıtımın yanlış kullanımını önlemek için, bazen kompozisyon (composition) daha iyi bir alternatif olabilir. Kompozisyon, bir nesnenin başka bir nesneyi içermesi ve onun davranışlarını kullanmasıdır.
Örnek – Kompozisyon Kullanarak LSP’ye Uygun Tasarım:
interface Shape {
double area();
}
class Rectangle implements Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
public void setSide(int side) {
this.side = side;
}
@Override
public double area() {
return side * side;
}
}
Bu tasarımda, Square
sınıfı Rectangle
sınıfından türetilmek yerine, her ikisi de Shape
arayüzünü uygular. Bu, LSP’ye uygun bir tasarımdır çünkü her iki sınıf da kendi içinde tutarlı davranır ve Shape
türünde bir nesne bekleyen kod her iki sınıf için de çalışır.
3. Savunmacı Programlama
Metodlarınızı, alt sınıfların davranışlarını değiştirmeyeceği şekilde tasarlayın. Bu, önkoşulları, sonkoşulları ve değişmezleri belgelemek ve uygulamak anlamına gelir.
Örnek – Savunmacı Programlama:
abstract class Vehicle {
// Değişmez: speed >= 0
protected int speed = 0;
// Önkoşul: amount > 0
// Sonkoşul: yeni speed = eski speed + amount
public void accelerate(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Hızlanma miktarı pozitif olmalıdır.");
}
speed += amount;
}
// Önkoşul: amount > 0 ve speed >= amount
// Sonkoşul: yeni speed = eski speed - amount
public void decelerate(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Yavaşlama miktarı pozitif olmalıdır.");
}
if (speed < amount) {
throw new IllegalStateException("Araç o kadar yavaşlayamaz.");
}
speed -= amount;
}
public int getSpeed() {
return speed;
}
}
class Car extends Vehicle {
@Override
public void accelerate(int amount) {
// Car sınıfı, Vehicle sınıfının önkoşullarını ve sonkoşullarını korur
super.accelerate(amount);
}
@Override
public void decelerate(int amount) {
// Car sınıfı, Vehicle sınıfının önkoşullarını ve sonkoşullarını korur
super.decelerate(amount);
}
}
Bu tasarımda, Vehicle
sınıfı, hızın negatif olmaması gerektiği değişmezini korur ve metodların önkoşullarını ve sonkoşullarını belirtir. Car
sınıfı, bu kuralları korur ve üst sınıfın davranışını değiştirmez.
4. Kontrat Tasarımı (Design by Contract)
Kontrat tasarımı, bir metodun davranışını tanımlayan formal bir spesifikasyon yaklaşımıdır. Metodun önkoşulları, sonkoşulları ve değişmezleri belirlenir. Alt sınıflar, üst sınıfın kontratına uymak zorundadır.
Örnek – Kontrat Tasarımı:
// Kontrat: Bu sınıf, banka hesaplarını temsil eder.
// Değişmez: balance >= 0 (hesap bakiyesi her zaman pozitif veya sıfır olmalıdır).
class BankAccount {
protected double balance = 0;
// Kontrat: Para yatırma işlemi.
// Önkoşul: amount > 0 (yatırılan miktar pozitif olmalıdır).
// Sonkoşul: yeni balance = eski balance + amount (bakiye yatırılan miktar kadar artmalıdır).
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Yatırılan miktar pozitif olmalıdır.");
}
balance += amount;
}
// Kontrat: Para çekme işlemi.
// Önkoşul: amount > 0 ve balance >= amount (çekilen miktar pozitif ve yeterli bakiye olmalıdır).
// Sonkoşul: yeni balance = eski balance - amount (bakiye çekilen miktar kadar azalmalıdır).
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Çekilen miktar pozitif olmalıdır.");
}
if (balance < amount) {
throw new IllegalStateException("Yetersiz bakiye.");
}
balance -= amount;
}
public double getBalance() {
return balance;
}
}
class SavingsAccount extends BankAccount {
private double interestRate;
public SavingsAccount(double interestRate) {
this.interestRate = interestRate;
}
// Kontrat: Faiz ekleyen metod.
// Önkoşul: Yok.
// Sonkoşul: yeni balance = eski balance * (1 + interestRate) (bakiye faiz oranı kadar artmalıdır).
public void addInterest() {
balance = balance * (1 + interestRate);
}
}
Bu tasarımda, BankAccount
sınıfı, hesap bakiyesinin negatif olmaması gerektiği değişmezini korur ve metodların önkoşullarını ve sonkoşullarını belirtir. SavingsAccount
sınıfı, bu kuralları korur ve üst sınıfın davranışını değiştirmez.
LSP’nin Gerçek Dünya Uygulamaları
LSP’nin gerçek dünya uygulamaları arasında şunlar bulunur:
1. Framework ve API Tasarımı
LSP, framework ve API tasarımında özellikle önemlidir. Kullanıcılar, bir framework veya API’nin davranışına güvenirler ve alt sınıfların bu davranışı değiştirmemesi beklenir.
Örnek – Java Collections Framework:
List<String> list = new ArrayList<>(); // ArrayList, List arayüzünü uygular
list.add("Hello");
list.add("World");
// LinkedList de List arayüzünü uygular ve aynı davranışı gösterir
List<String> linkedList = new LinkedList<>();
linkedList.add("Hello");
linkedList.add("World");
// Her iki liste türü de aynı şekilde kullanılabilir
for (String s : list) {
System.out.println(s);
}
for (String s : linkedList) {
System.out.println(s);
}
Bu örnekte, ArrayList
ve LinkedList
sınıfları, List
arayüzünü uygular ve aynı davranışı gösterir. Bu, LSP’ye uygun bir tasarımdır çünkü her iki sınıf da List
türünde bir nesne bekleyen kod için çalışır.
2. Çok Katmanlı Mimariler
Çok katmanlı mimarilerde, her katmanın arayüzü, bu arayüzü uygulayan tüm sınıflar için tutarlı bir davranış sağlamalıdır. Bu, LSP’nin bir uygulamasıdır.
Örnek – Veri Erişim Katmanı:
interface UserRepository {
User findById(long id);
List<User> findAll();
void save(User user);
void delete(User user);
}
class DatabaseUserRepository implements UserRepository {
// Veritabanı kullanarak UserRepository arayüzünü uygular
// ...
}
class CachedUserRepository implements UserRepository {
// Önbellek kullanarak UserRepository arayüzünü uygular
// ...
}
// Her iki repository türü de aynı şekilde kullanılabilir
class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(long id) {
return userRepository.findById(id);
}
// Diğer metodlar...
}
Bu tasarımda, DatabaseUserRepository
ve CachedUserRepository
sınıfları, UserRepository
arayüzünü uygular ve aynı davranışı gösterir. Bu, LSP’ye uygun bir tasarımdır çünkü her iki sınıf da UserRepository
türünde bir nesne bekleyen kod için çalışır.
3. Test Dublörleri (Test Doubles)
Test dublörleri, test edilecek birimin bağımlılıklarının yerini alan nesnelerdir. LSP, test dublörlerinin gerçek nesnelerin yerini alabilmesini garanti eder.
Örnek – Test Dublörleri:
interface PaymentGateway {
boolean processPayment(double amount);
}
class RealPaymentGateway implements PaymentGateway {
@Override
public boolean processPayment(double amount) {
// Gerçek ödeme işleme mantığı
return true;
}
}
class MockPaymentGateway implements PaymentGateway {
@Override
public boolean processPayment(double amount) {
// Test için ödeme simülasyonu
return amount > 0;
}
}
class PaymentService {
private PaymentGateway paymentGateway;
public PaymentService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public boolean makePayment(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Ödeme miktarı pozitif olmalıdır.");
}
return paymentGateway.processPayment(amount);
}
}
// Test
public void testMakePayment() {
PaymentGateway mockGateway = new MockPaymentGateway();
PaymentService service = new PaymentService(mockGateway);
assertTrue(service.makePayment(100));
// Negatif miktarla ödeme yapmaya çalışıldığında bir istisna fırlatılmalıdır
assertThrows(IllegalArgumentException.class, () -> service.makePayment(-100));
}
Bu örnekte, MockPaymentGateway
sınıfı, RealPaymentGateway
sınıfının yerine kullanılabilir ve PaymentService
sınıfı için aynı davranışı gösterir. Bu, LSP’ye uygun bir tasarımdır.
LSP’nin Uygulanmasında Karşılaşılan Zorluklar ve Çözümleri
LSP’yi uygulamak her zaman kolay değildir ve bazı zorluklar beraberinde gelebilir:
1. Doğru Soyutlama Seviyesini Bulma
Zorluk: Doğru soyutlama seviyesini belirlemek zor olabilir. Çok genel bir soyutlama, alt sınıfların LSP’yi ihlal etmesine neden olabilir; çok spesifik bir soyutlama ise kod tekrarına yol açabilir.
Çözüm:
- Domain analizine zaman ayırın
- İş gereksinimlerini ve kullanım senaryolarını dikkatlice incelemeli
- Gereksinimlere göre soyutlamaları iteratif olarak güncellenebilir
- Arayüz Ayrımı Prensibi’ni (Interface Segregation Principle) kullanarak arayüzleri daha küçük ve spesifik hale getirebilirsin.
2. Önceden Var Olan Koddaki LSP İhlallerini Düzeltme
Zorluk: Önceden var olan kodda LSP ihlalleri olabilir ve bunları düzeltmek, mevcut kodu kullanan diğer kodları etkileyebilir.
Çözüm:
- Aşamalı bir yaklaşım benimseni
- Önce testler yaz ve güveni sağla
- Kalıtımı kompozisyonla değiştir ya da yeniden soyutlama yapdış
- “Adapter” veya “Decorator” gibi tasarım desenlerini kullan
3. Pratikte Katı Bir Kontrat Tanımlama
Zorluk: Metodların önkoşullarını, sonkoşullarını ve değişmezlerini katı bir şekilde tanımlamak, pratikteki tüm kullanım senaryolarını öngörmek zor olabilir.
Çözüm:
- Kontratları mümkün olduğunca açık ve belgelenmiş yap
- Birim testlerini kontratları doğrulamak için kullan
- “Defensive programming” tekniklerini kullan
- Design by Contract (DbC) araçları ve kütüphaneleri kullan
LSP’nin Diğer SOLID Prensipleriyle İlişkisi
LSP, diğer SOLID prensipleriyle yakından ilişkilidir ve genellikle bunlarla birlikte uygulanır:
Tek Sorumluluk Prensibi (SRP) ile İlişkisi
SRP, bir sınıfın sadece bir değişim nedeni olması gerektiğini söyler. LSP ile birlikte uygulandığında, her sınıf hem tek bir sorumluluk üstlenir hem de üst sınıfların davranışını korur.
Açık/Kapalı Prensibi (OCP) ile İlişkisi
OCP, yazılım varlıklarının genişletmeye açık, değiştirmeye kapalı olması gerektiğini belirtir. LSP, OCP’yi destekler çünkü alt sınıflar, üst sınıfların davranışını değiştirmeden genişletirse, OCP daha etkili bir şekilde uygulanabilir.
Arayüz Ayrımı Prensibi (ISP) ile İlişkisi
ISP, büyük arayüzlerin daha küçük ve spesifik olanlar lehine parçalanmasını önerir. LSP ile birlikte uygulandığında, daha küçük arayüzler, her alt sınıfın tüm arayüzü doğru bir şekilde uygulamasını kolaylaştırır.
Bağımlılığın Tersine Çevrilmesi Prensibi (DIP) ile İlişkisi
DIP, üst düzey modüllerin alt düzey modüllere bağımlı olmaması gerektiğini belirtir. LSP ile birlikte uygulandığında, üst düzey modüller, alt sınıfların üst sınıfların yerini alabilmesine güvenebilir.
LSP ve Tasarım Desenleri
LSP, birçok tasarım deseni ile doğal olarak uyumludur:
1. Strateji Deseni (Strategy Pattern)
Strateji deseni, algoritmaları (stratejileri) kapsüller ve bunları değiştirilebilir hale getirir. Bu desen, LSP’yi destekler çünkü her strateji, bir strateji arayüzünü uygular ve aynı davranışı gösterir.
interface SortStrategy {
void sort(int[] array);
}
class QuickSortStrategy implements SortStrategy {
@Override
public void sort(int[] array) {
// QuickSort algoritması
}
}
class MergeSortStrategy implements SortStrategy {
@Override
public void sort(int[] array) {
// MergeSort algoritması
}
}
class Sorter {
private SortStrategy strategy;
public Sorter(SortStrategy strategy) {
this.strategy = strategy;
}
public void sort(int[] array) {
strategy.sort(array);
}
}
2. Fabrika Deseni (Factory Pattern)
Fabrika deseni, nesne oluşturma mantığını istemci kodundan ayırmanızı sağlar. Bu desen, LSP’yi destekler çünkü fabrika, belirli bir arayüzü uygulayan nesneler döndürür ve istemci kodu, bu arayüzün davranışına güvenir.
interface Product {
void operation();
}
class ConcreteProductA implements Product {
@Override
public void operation() {
System.out.println("ConcreteProductA işlemi gerçekleştiriyor...");
}
}
class ConcreteProductB implements Product {
@Override
public void operation() {
System.out.println("ConcreteProductB işlemi gerçekleştiriyor...");
}
}
interface Factory {
Product createProduct();
}
class ConcreteFactoryA implements Factory {
@Override
public Product createProduct() {
return new ConcreteProductA();
}
}
class ConcreteFactoryB implements Factory {
@Override
public Product createProduct() {
return new ConcreteProductB();
}
}
// İstemci kodu
class Client {
private Factory factory;
public Client(Factory factory) {
this.factory = factory;
}
public void doSomething() {
Product product = factory.createProduct();
product.operation();
}
}
Bu örnekte, istemci kodu sadece Product
arayüzüne bağımlıdır ve herhangi bir Product
implementasyonu ile çalışabilir. Bu, LSP’ye uygun bir tasarımdır.
3. Şablon Metod Deseni (Template Method Pattern)
Şablon metod deseni, bir algoritmanın iskeletini tanımlar ve bazı adımları alt sınıflara bırakır. Bu desen, LSP’yi destekler çünkü alt sınıflar, üst sınıfın davranışını değiştirmeden algoritmanın belirli adımlarını uygular.
abstract class AbstractClass {
// Şablon metod
public final void templateMethod() {
// Değişmez adımlar
step1();
step2();
// Alt sınıflar tarafından değiştirilebilir adım
step3();
// Değişmez adımlar
step4();
}
// Değişmez adımlar
private void step1() {
System.out.println("AbstractClass: Adım 1");
}
private void step2() {
System.out.println("AbstractClass: Adım 2");
}
// Alt sınıflar tarafından uygulanacak adım
protected abstract void step3();
private void step4() {
System.out.println("AbstractClass: Adım 4");
}
}
class ConcreteClass1 extends AbstractClass {
@Override
protected void step3() {
System.out.println("ConcreteClass1: Adım 3");
}
}
class ConcreteClass2 extends AbstractClass {
@Override
protected void step3() {
System.out.println("ConcreteClass2: Adım 3");
}
}
Bu örnekte, ConcreteClass1
ve ConcreteClass2
sınıfları, AbstractClass
sınıfının şablon metodunu değiştirmeden step3
metodunu uygular. Bu, LSP’ye uygun bir tasarımdır.
4. Komut Deseni (Command Pattern)
Komut deseni, bir isteği bir nesne olarak kapsüller, bu da farklı istekleri parametrize etmenizi, istekleri sıraya koymanızı ve istekleri geri almanızı sağlar. Bu desen, LSP’yi destekler çünkü her komut, bir komut arayüzünü uygular ve aynı davranışı gösterir.
interface Command {
void execute();
}
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}
class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
}
class Light {
public void turnOn() {
System.out.println("Işık açıldı");
}
public void turnOff() {
System.out.println("Işık kapandı");
}
}
// İstemci kodu
class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
Bu örnekte, istemci kodu sadece Command
arayüzüne bağımlıdır ve herhangi bir Command
implementasyonu ile çalışabilir. Bu, LSP’ye uygun bir tasarımdır.
LSP’nin Programlama Dillerindeki Uygulanması
Farklı programlama dilleri, LSP’yi desteklemek için farklı mekanizmalar sunar:
Java’da LSP
Java, LSP’yi desteklemek için güçlü bir tür sistemi ve arayüz mekanizması sunar. Java’da, alt sınıflar üst sınıfların metodlarını override edebilir, ancak üst sınıfın davranışını değiştirmemelidir.
class Bird {
public void eat() {
System.out.println("Kuş yem yiyor...");
}
}
class Sparrow extends Bird {
@Override
public void eat() {
// Üst sınıfın davranışını koruyor
System.out.println("Serçe yem yiyor...");
}
public void fly() {
System.out.println("Serçe uçuyor...");
}
}
// İstemci kodu
void feedBird(Bird bird) {
bird.eat();
}
// Bu kod her iki sınıf için de çalışır
Bird bird = new Bird();
feedBird(bird); // "Kuş yem yiyor..." yazdırır
Bird sparrow = new Sparrow();
feedBird(sparrow); // "Serçe yem yiyor..." yazdırır
C#’da LSP
C#, Java’ya benzer şekilde LSP’yi destekler ve ek olarak covariant ve contravariant jenerik tür parametreleri sunar.
class Bird {
public virtual void Eat() {
Console.WriteLine("Kuş yem yiyor...");
}
}
class Sparrow : Bird {
public override void Eat() {
// Üst sınıfın davranışını koruyor
Console.WriteLine("Serçe yem yiyor...");
}
public void Fly() {
Console.WriteLine("Serçe uçuyor...");
}
}
// İstemci kodu
void FeedBird(Bird bird) {
bird.Eat();
}
// Bu kod her iki sınıf için de çalışır
Bird bird = new Bird();
FeedBird(bird); // "Kuş yem yiyor..." yazdırır
Bird sparrow = new Sparrow();
FeedBird(sparrow); // "Serçe yem yiyor..." yazdırır
Python’da LSP
Python, dinamik tipli bir dil olduğu için LSP’yi desteklemek için tür kontrolleri yapmaz. Ancak, “duck typing” prensibi sayesinde LSP benzeri bir davranış elde edilebilir: “Eğer bir kuş gibi ses çıkarıyorsa ve bir kuş gibi uçuyorsa, o bir kuştur.”
class Bird:
def eat(self):
print("Kuş yem yiyor...")
class Sparrow(Bird):
def eat(self):
# Üst sınıfın davranışını koruyor
print("Serçe yem yiyor...")
def fly(self):
print("Serçe uçuyor...")
# İstemci kodu
def feed_bird(bird):
bird.eat()
# Bu kod her iki sınıf için de çalışır
bird = Bird()
feed_bird(bird) # "Kuş yem yiyor..." yazdırır
sparrow = Sparrow()
feed_bird(sparrow) # "Serçe yem yiyor..." yazdırır
Python’da, bir nesnenin belirli bir arayüzü uygulaması beklenmez; sadece beklenen metodlara sahip olması yeterlidir. Bu, LSP’nin bir tür gevşek uygulamasıdır.
LSP’nin Karmaşık Dünya Senaryoları
Bazen, gerçek dünya durumları LSP’ye uymakta zorlanabilir. Bu durumlar için bazı stratejiler şunlar olabilir:
1. İş Kuralları ve Teknik Tasarım Arasındaki Çelişkiler
İş dünyasında, “is-a” ilişkisi teknik tasarımın “is-a” ilişkisine tam olarak uymayabilir. Örneğin, iş kurallarına göre bir kare, bir dikdörtgendir, ancak OOP’de bunları modellerken LSP’yi ihlal edebiliriz.
Çözüm:
- İş modelini ve teknik modeli ayırın
- Kompozisyonu kullanın
- İş modelini daha soyut bir seviyede tanımlayın
// İş modeli
interface Shape {
double area();
}
class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
class Square implements Shape {
private double side;
public Square(double side) {
this.side = side;
}
@Override
public double area() {
return side * side;
}
}
2. Miras Alınan Sistemlerdeki LSP İhlalleri
Önceden var olan sistemlerde LSP ihlalleri olabilir ve bunları düzeltmek, sistem değişikliği olmadan mümkün olmayabilir.
Çözüm:
- Adaptör deseni kullanın
- Facade deseni kullanın
- Dekoratör deseni kullanın
// LSP'yi ihlal eden miras alınan sınıf
class LegacyRectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
// LSP'yi ihlal eden miras alınan sınıf
class LegacySquare extends LegacyRectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width;
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
// LSP'ye uygun adaptör
interface Shape {
double area();
}
class RectangleAdapter implements Shape {
private LegacyRectangle rectangle;
public RectangleAdapter(LegacyRectangle rectangle) {
this.rectangle = rectangle;
}
@Override
public double area() {
return rectangle.getArea();
}
}
// İstemci kodu
void printArea(Shape shape) {
System.out.println("Alan: " + shape.area());
}
// Bu kod her iki adaptör için de çalışır
LegacyRectangle rectangle = new LegacyRectangle();
rectangle.setWidth(5);
rectangle.setHeight(4);
Shape rectangleAdapter = new RectangleAdapter(rectangle);
printArea(rectangleAdapter); // "Alan: 20" yazdırır
LegacySquare square = new LegacySquare();
square.setWidth(5);
Shape squareAdapter = new RectangleAdapter(square);
printArea(squareAdapter); // "Alan: 25" yazdırır
3. Çoklu Kalıtım ve LSP
Bazı programlama dilleri çoklu kalıtımı destekler (C++ gibi) ve bu, LSP uyumluluğunu daha karmaşık hale getirebilir.
Çözüm:
- Arayüz kalıtımını tercih edin
- Kompozisyonu kullanın
- Mixin’leri kullanın
// C++ çoklu kalıtım örneği
class Swimmer {
public:
virtual void swim() {
std::cout << "Yüzüyor..." << std::endl;
}
};
class Flyer {
public:
virtual void fly() {
std::cout << "Uçuyor..." << std::endl;
}
};
class Duck : public Swimmer, public Flyer {
public:
void swim() override {
std::cout << "Ördek yüzüyor..." << std::endl;
}
void fly() override {
std::cout << "Ördek uçuyor..." << std::endl;
}
};
// İstemci kodu
void letSwim(Swimmer* swimmer) {
swimmer->swim();
}
void letFly(Flyer* flyer) {
flyer->fly();
}
// Bu kod Duck nesnesi için çalışır
Duck duck;
letSwim(&duck); // "Ördek yüzüyor..." yazdırır
letFly(&duck); // "Ördek uçuyor..." yazdırır
LSP ve Kod Kalitesi Metrikleri
LSP uyumluluğunu değerlendirmek için çeşitli kod kalitesi metrikleri kullanılabilir:
1. Kalıtım Derinliği (Depth of Inheritance Tree – DIT)
Kalıtım hiyerarşisinde çok fazla seviye olması, genellikle LSP ihlallerine yol açabilir. Sığ kalıtım hiyerarşileri genellikle daha yönetilebilir ve LSP’ye daha uyumludur.
2. Alt Sınıf Sayısı (Number of Children – NOC)
Bir sınıfın çok sayıda alt sınıfı olması, o sınıfın davranışının tüm alt sınıflar için uygun olduğundan emin olmayı zorlaştırabilir.
3. Metod Geçersiz Kılma Oranı (Method Override Ratio)
Alt sınıfların üst sınıfların metodlarını ne kadar sık geçersiz kıldığını ölçer. Yüksek bir oran, alt sınıfların üst sınıfların davranışını değiştiriyor olabileceğini gösterebilir.
4. LCOM (Lack of Cohesion of Methods)
Bir sınıfın içindeki metodların ne kadar ilişkili olduğunu ölçer. Yüksek bir LCOM, sınıfın birden fazla sorumluluğa sahip olabileceğini ve potansiyel olarak LSP’yi ihlal edebileceğini gösterebilir.
5. Coupling Metrics
Sınıflar arasındaki bağımlılıkları ölçer. Yüksek coupling, LSP ihlallerine yol açabilecek karmaşık bağımlılıklar olduğunu gösterebilir.
Sonuç
Liskov Yerine Geçme Prensibi, nesne yönelimli programlamadaki kalıtım hiyerarşilerinin sağlam ve tutarlı olmasını sağlayan temel bir prensiptir. Bu prensip, alt sınıfların üst sınıfların yerini alabilmesini ve üst sınıfların davranışını değiştirmemesini garanti eder. LSP’yi doğru bir şekilde uygulamak, daha modüler, bakımı kolay ve genişletilebilir kod elde etmenizi sağlar.
LSP’yi uygulamak için, doğru soyutlama seviyesi kullanma, kompozisyon, savunmacı programlama ve kontrat tasarımı gibi teknikler kullanabilirsiniz. Ancak, her tasarım prensibinde olduğu gibi, LSP’yi uygularken de pragmatik olmak, projenizin bağlamını ve ihtiyaçlarını göz önünde bulundurmak önemlidir.
SOLID prensiplerini bir bütün olarak uygulamak, daha modüler, test edilebilir, bakımı kolay ve genişletilebilir kod oluşturmanıza yardımcı olur. LSP, bu prensipler içinde özel bir yere sahiptir çünkü kalıtım hiyerarşilerinin doğru tasarlanmasını sağlar.
Unutmayın, yazılım geliştirme bir zanaat ve sanat karışımıdır. Prensipleri öğrenmek önemlidir, ancak ne zaman ve nasıl uygulanacaklarını bilmek tecrübe gerektirir. LSP’yi projenizde uygularken, dengeli bir yaklaşım benimseyin ve sürekli olarak kodunuzu iyileştirmeye çalışın.