Liskov İkame Prensibi'nin (LSP) nesne yönelimli tasarımın temel bir prensibi olduğunu duydum. Nedir ve kullanımıyla ilgili bazı örnekler nelerdir?
Liskov İkame Prensibi'nin (LSP) nesne yönelimli tasarımın temel bir prensibi olduğunu duydum. Nedir ve kullanımıyla ilgili bazı örnekler nelerdir?
Yanıtlar:
LSP'yi (yakın zamanda duyduğum bir podcast'te Bob Amca tarafından verilen) gösteren harika bir örnek, doğal dilde doğru görünen bir şeyin bazen kodda tam olarak çalışmadığıydı.
Matematikte Square
a Rectangle
. Gerçekten de bir dikdörtgenin uzmanlaşmasıdır. "İs", bunu kalıtımla modellenmek istemenizi sağlar. Ancak yaptığınız kodda Square
türetilmişse Rectangle
, o zaman a Square
, beklediğiniz her yerde kullanılabilir olmalıdır Rectangle
. Bu biraz garip davranışlar yaratır.
Temel sınıfınızda olduğunu SetWidth
ve SetHeight
yöntemlerini hayal edin Rectangle
; bu tamamen mantıklı görünüyor. Ancak Rectangle
referansınız bir a işaret ediyorsa ve bu bir anlam ifade etmiyorsa Square
, birini ayarlamak diğerini eşleştirmek için değiştirecektir. Bu durumda Liskov İkame Testi ile başarısız olur ve miras almanın soyutlanması kötüdür.SetWidth
SetHeight
Square
Rectangle
Square
Rectangle
Hepiniz diğer paha biçilmez SOLID Principles Motivational Posterlerini kontrol etmelisiniz .
Square.setWidth(int width)
böyle uygulanmış olsaydı neden sorun olurdu this.width = width; this.height = width;
? Bu durumda, genişliğin yüksekliğe eşit olduğu garanti edilir.
Liskov İkame İlkesi (LSP, LSP), Nesneye Yönelik Programlamada şunları ifade eden bir kavramdır:
İşaretçiler veya temel sınıflara başvurular kullanan işlevler, türetilmiş sınıfların nesnelerini bilmeden kullanabilmelidir.
Kalbinde LSP, arayüzler ve sözleşmeler ile bir dersi ne zaman genişleteceğinize ve hedefinize ulaşmak için kompozisyon gibi başka bir stratejiyi nasıl kullanacağınıza nasıl karar vereceğinizle ilgilidir.
Bu noktayı açıklamanın en etkili yolu Baş Önce OOA & D idi . Strateji oyunları için bir çerçeve oluşturmak için bir projede geliştirici olduğunuz bir senaryo sunarlar.
Şuna benzeyen bir tahtayı temsil eden bir sınıf sunarlar:
Tüm yöntemler, X ve Y koordinatlarını iki boyutlu dizideki döşeme konumunu bulmak için parametre olarak alır Tiles
. Bu, bir oyun geliştiricisinin oyun sırasında tahtadaki birimleri yönetmesine izin verecektir.
Kitap, oyun çerçevesi çalışmasının, uçuşu olan oyunları barındırmak için 3D oyun tahtalarını da desteklemesi gerektiğini söylemek için gereksinimleri değiştirmeye devam ediyor. Böylece genişleyen bir ThreeDBoard
sınıf tanıtıldı Board
.
İlk bakışta bu iyi bir karar gibi görünüyor. Board
hem Height
ve Width
özelliklerini hem de ThreeDBoard
Z eksenini sağlar.
Ayrıldığı yer, miras alınan diğer tüm üyelere baktığınız zamandır Board
. Yöntemleri AddUnit
, GetTile
, GetUnits
ve benzeri, tüm X ve Y parametreleri hem almak Board
sınıfta ama ThreeDBoard
sıra Z parametresini ihtiyacı var.
Bu yüzden bu yöntemleri bir Z parametresiyle tekrar uygulamalısınız. Z parametresinin Board
sınıfa bağlamı yoktur ve sınıftan devralınan yöntemler Board
anlamlarını yitirir. ThreeDBoard
Sınıfı temel sınıfı olarak kullanmaya çalışan bir kod birimi Board
çok şanssız olurdu.
Belki başka bir yaklaşım bulmalıyız. Bunun yerine uzanan Board
, ThreeDBoard
oluşan gereken Board
nesneler. Board
Z ekseninin birimi başına bir nesne.
Bu, kapsülleme ve yeniden kullanma gibi iyi nesne yönelimli ilkeleri kullanmamızı sağlar ve LSP'yi ihlal etmez.
Değiştirilebilirlik, nesne yönelimli programlamada, bir bilgisayar programında, S, T'nin bir alt tipi ise, T tipi nesnelerin S tipi nesnelerle değiştirilebileceğini belirten bir prensiptir.
Java ile basit bir örnek yapalım:
public class Bird{
public void fly(){}
}
public class Duck extends Bird{}
Ördek bir kuş olduğu için uçabilir, ama buna ne dersiniz:
public class Ostrich extends Bird{}
Devekuşu bir kuştur, ancak uçamaz, Devekuşu sınıfı Kuş sınıfının bir alt türüdür, ancak sinek yöntemini kullanamaz, bu da LSP ilkesini ihlal ettiğimiz anlamına gelir.
public class Bird{
}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
Bird bird
. Sinek kullanmak için nesneyi FlyingBirds'a atmanız gerekiyor, ki bu hoş değil mi?
Bird bird
, bu kullanılamaz demektir fly()
. Bu kadar. A'yı geçmek Duck
bu gerçeği değiştirmez. Müşteri varsa FlyingBirds bird
, o zaman geçse bile Duck
her zaman aynı şekilde çalışmalıdır.
LSP değişmezlerle ilgilidir.
Klasik örnek aşağıdaki sahte kod bildirimi ile verilmiştir (uygulamalar atlanmıştır):
class Rectangle {
int getHeight()
void setHeight(int value)
int getWidth()
void setWidth(int value)
}
class Square : Rectangle { }
Arayüz eşleşmesine rağmen şimdi bir sorunumuz var. Bunun nedeni, karelerin ve dikdörtgenlerin matematiksel tanımından kaynaklanan değişmezleri ihlal etmemizdir. Alıcıların ve ayarlayıcıların çalışma şekli, a Rectangle
aşağıdaki değişmezi karşılamalıdır:
void invariant(Rectangle r) {
r.setHeight(200)
r.setWidth(100)
assert(r.getHeight() == 200 and r.getWidth() == 100)
}
Bununla birlikte, bu değişmez , doğru bir şekilde uygulanmasıyla ihlal edilmelidirSquare
, bu nedenle geçerli bir yerine geçmez Rectangle
.
Robert Martin'in Liskov İkame Prensibi ile ilgili mükemmel bir makalesi var . İlkenin ihlal edilebileceği ince ve çok ince olmayan yolları tartışır.
Makalenin bazı ilgili bölümleri (ikinci örneğin yoğun bir şekilde yoğunlaştığına dikkat edin):
LSP İhlaline Basit Bir Örnek
Bu ilkenin en göze çarpan ihlallerinden biri, bir nesnenin türüne dayalı bir işlev seçmek için C ++ Çalışma Zamanı Türü Bilgilerinin (RTTI) kullanılmasıdır. yani:
void DrawShape(const Shape& s) { if (typeid(s) == typeid(Square)) DrawSquare(static_cast<Square&>(s)); else if (typeid(s) == typeid(Circle)) DrawCircle(static_cast<Circle&>(s)); }
Açıkçası
DrawShape
işlev kötü bir şekilde oluşturulmuştur.Shape
Sınıfın her olası türevini bilmeli ve yeni türevleriShape
her oluşturulduğunda değiştirilmelidir. Gerçekten de birçoğu bu işlevin yapısını Nesneye Dayalı Tasarıma anathema olarak görmektedir.Kare ve Dikdörtgen, Daha İnce Bir İhlal.
Bununla birlikte, LSP'yi ihlal etmenin başka, çok daha ince yolları vardır.
Rectangle
Sınıfı aşağıda açıklandığı gibi kullanan bir uygulamayı düşünün :class Rectangle { public: void SetWidth(double w) {itsWidth=w;} void SetHeight(double h) {itsHeight=w;} double GetHeight() const {return itsHeight;} double GetWidth() const {return itsWidth;} private: double itsWidth; double itsHeight; };
[...] Bir gün kullanıcıların dikdörtgenlere ek olarak kareleri manipüle etme yeteneğini talep ettiğini düşünün. [...]
Açıkçası, kare tüm normal niyet ve amaçlar için bir dikdörtgendir. ISA ilişkisi devam ettiğinden,
Square
sınıfı türetilmiş olarak modellemek mantıklıdırRectangle
. [...]
Square
SetWidth
veSetHeight
fonksiyonlarını devralır . Bu işlevler a için tamamen uygun değildirSquare
, çünkü bir karenin genişliği ve yüksekliği aynıdır. Bu, tasarımla ilgili bir sorun olduğu konusunda önemli bir ipucu olmalıdır. Ancak, sorunu ortadan kaldırmanın bir yolu var. Biz geçersiz kılabilirSetWidth
veSetHeight
[...]Ancak aşağıdaki işlevi göz önünde bulundurun:
void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }
Square
Bu işleve bir nesneye başvuruSquare
iletirsek, yükseklik değişmeyeceğinden nesne bozulur. Bu LSP'nin açık bir ihlalidir. İşlev, bağımsız değişkenlerinin türevleri için çalışmaz.[...]
Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one.
Bir çocuk sınıfı ön koşulu, ebeveyn sınıfı ön koşulundan daha güçlü ise, ön koşulu ihlal etmeden bir çocuğu ebeveyninin yerine koyamazsınız. Dolayısıyla LSP.
LSP, bazı kodların bir türün yöntemlerini çağırdığını düşündüğü durumlarda gereklidir T
ve bilmeden bir türün yöntemlerini çağırabilir ( S
burada mirastan türetir veya bir alt tiptir ).S extends T
S
T
Örneğin, bu tür bir girdi parametresine sahip bir işleve tür T
bağımsız değişken değeriyle çağrıldığında (yani çağrıldığında) oluşur S
. Veya, bir tür tanımlayıcısına bir tür T
değeri atanır S
.
val id : T = new S() // id thinks it's a T, but is a S
LSP, tip T
(örn. Rectangle
) Yöntemleri için beklentileri (yani değişmezler) gerektirir, bunun yerine tip S
(ör. Square
) Yöntemleri çağrıldığında ihlal edilmez .
val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation
Değişmez alanları olan bir türün bile değişmezleri vardır, örneğin değişmez Dikdörtgen ayarlayıcılar boyutların bağımsız olarak değiştirilmesini bekler, ancak değişmez Kare ayarlayıcılar bu beklentiyi ihlal eder.
class Rectangle( val width : Int, val height : Int )
{
def setWidth( w : Int ) = new Rectangle(w, height)
def setHeight( h : Int ) = new Rectangle(width, h)
}
class Square( val side : Int ) extends Rectangle(side, side)
{
override def setWidth( s : Int ) = new Square(s)
override def setHeight( s : Int ) = new Square(s)
}
LSP, alt tipin her yönteminin S
kontravaryant giriş parametrelerine ve bir kovaryant çıkışa sahip olmasını gerektirir.
Varyans kalıtım yönüne karşı olan kontravaryant aracı tipi, yani Si
, alt-tipinin her yöntemin her bir giriş parametresi S
, aynı veya olmalıdır süper tip Çeşidi Ti
süper tip karşılık gelen yönteminin karşılık gelen giriş parametresinin T
.
Kovaryans, varyansın kalıtımla aynı yönde olduğu anlamına gelir, yani So
alt tipin her bir yönteminin çıktısının tipi S
, üst tipin karşılık gelen yönteminin karşılık gelen çıktısının tipiyle aynı veya alt tip olmalıdır .To
T
Bunun nedeni, çağıranın bir türü T
olduğunu düşünmesi, bir yöntem çağırdığını düşünmesi durumunda, tür T
argüman (ları) Ti
sağlaması ve çıktıyı türe atamasıdır To
. Gerçekte karşılık gelen yöntemi çağırdığında, S
her Ti
girdi bağımsız değişkeni bir Si
girdi parametresine So
atanır ve çıktı türüne atanır To
. Böylece eğer Si
karşı kontravaryant wrt değildi Ti
, o zaman bir alt tipi Xi
bir alt tip olmaz -ki Si
atanacak misiniz, Ti
.
Buna ek olarak, tanım yerinde varyans tipi polimorfizm parametrelerine ek açıklamaları (örneğin jenerik), tip her tür parametresi için varyans ek açıklama ko- ya da kontra yönü dilleri (örneğin Scala veya Seylan) için T
olması gereken ters veya aynı yönde sırasıyla T
tür parametresinin türüne sahip her giriş parametresine veya çıkışına (her yöntemin ).
Ayrıca, bir fonksiyon tipine sahip her bir giriş parametresi veya çıkışı için, gereken varyans yönü tersine çevrilir. Bu kural yinelemeli olarak uygulanır.
Değişmezlerin numaralandırılabileceği yerlerde alt tipleme uygundur .
Değişmezlerin nasıl modelleneceğine dair çok sayıda araştırma var, böylece derleyici tarafından uygulanıyorlar.
Typestate (bkz. Sayfa 3), türüne dik durumdaki durum değişmezlerini bildirir ve uygular. Alternatif olarak, değişmezler iddiaları türlere dönüştürerek uygulanabilir . Örneğin, bir dosyayı kapatmadan önce açık olduğunu iddia etmek için File.open (), Dosya'da bulunmayan bir close () yöntemi içeren bir OpenFile türü döndürebilir. Bir tic tac ayak API derleme zamanında değişmezler uygulanması için yazma kullanılarak başka bir örnek olabilir. Tip sistemi Turing tamamlanmış olabilir, örneğin Scala . Bağımlı olarak yazılan diller ve teorem kanıtlayıcılar, yüksek dereceli yazım modellerini resmileştirir.
Anlambilimin uzatma üzerine soyutlanmasına duyulan ihtiyaç nedeniyle, model değişmezlere, yani birleşik yüksek dereceli anlamsal anlambilime yazmak için yazı yazmanın, Typetate'den daha üstün olmasını bekliyorum. 'Uzatma', koordine edilmemiş, modüler gelişimin sınırsız, izin verilen bileşimi anlamına gelir. Bana göre birleşme ve dolayısıyla serbestlik derecesi antitezi, genişletilebilir kompozisyon için birbiriyle birleştirilemeyen paylaşılan semantikleri ifade etmek için karşılıklı olarak bağımlı iki modele (örn. Tipler ve Tiptat) sahip olmak . Örneğin, İfade Sorunu benzeri uzantı, alt tipleme, işlev aşırı yüklenmesi ve parametrik yazım alanlarında birleştirildi.
Teorik konumum, bilginin var olması için (bkz. “Merkezileşme kör ve elverişsizdir” bölümüne), Turing-complete bilgisayar dilinde olası tüm değişmezlerin% 100 kapsamını zorlayabilecek genel bir model asla olmayacaktır. Bilginin var olması için, beklenmedik olasılıklar çoktur, yani düzensizlik ve entropi her zaman artıyor olmalıdır. Bu entropik kuvvettir. Potansiyel bir uzantının tüm olası hesaplamalarını kanıtlamak, tüm olası uzatmaların önceden hesaplanmasıdır.
Bu yüzden Durdurma Teoremi var, yani bir Turing-complete programlama dilinde her olası programın sonlanıp sonlanamayacağı kesin değil. Bazı özel programların (tüm olasılıkların tanımlandığı ve hesaplandığı) sona erdiği kanıtlanabilir. Ancak, bu programın genişletilmesi için olasılıklar Turing tamamlanmadığı sürece (örneğin, bağımlı yazarak), o programın tüm olası uzantılarının sona erdiğini kanıtlamak imkansızdır. Turing-tamlık için temel gereklilik sınırsız özyineleme olduğundan , Gödel'in eksiklik teoremlerinin ve Russell paradoksunun uzatma için nasıl uygulandığını anlamak sezgiseldir.
Bu teoremlerin bir yorumu, onları entropik gücün genel bir kavramsal anlayışına dahil eder:
Her cevapta dikdörtgenler ve kareler ve LSP'nin nasıl ihlal edileceğini görüyorum.
LSP'nin gerçek dünyadaki bir örnekle nasıl uyumlu olabileceğini göstermek istiyorum:
<?php
interface Database
{
public function selectQuery(string $sql): array;
}
class SQLiteDatabase implements Database
{
public function selectQuery(string $sql): array
{
// sqlite specific code
return $result;
}
}
class MySQLDatabase implements Database
{
public function selectQuery(string $sql): array
{
// mysql specific code
return $result;
}
}
Bu tasarım LSP'ye uygundur, çünkü kullanmayı seçtiğimiz uygulamadan bağımsız olarak davranış değişmeden kalır.
Ve evet, bu yapılandırmada LSP'yi aşağıdaki gibi basit bir değişiklik yaparak ihlal edebilirsiniz:
<?php
interface Database
{
public function selectQuery(string $sql): array;
}
class SQLiteDatabase implements Database
{
public function selectQuery(string $sql): array
{
// sqlite specific code
return $result;
}
}
class MySQLDatabase implements Database
{
public function selectQuery(string $sql): array
{
// mysql specific code
return ['result' => $result]; // This violates LSP !
}
}
Artık alt türler artık aynı sonucu vermediği için aynı şekilde kullanılamıyor.
Database::selectQuery
tarafından desteklenen SQL alt kümesini destekleme anlamını kısıtladığımız sürece LSP'yi ihlal etmez . Bu neredeyse pratik değil ... Yani, örneğin burada kullanılan diğerlerinden daha kolay kavranması daha kolay.
Liskov'u ihlal edip etmediğinizi belirlemek için bir kontrol listesi vardır.
Kontrol listesi:
Geçmiş Kısıtlaması : Bir yöntemi geçersiz kılarken, temel sınıfta değiştirilemez bir özelliği değiştirmenize izin verilmez. Bu koda bir göz atın ve Ad'ın değiştirilemez (özel küme) olarak tanımlandığını görebilirsiniz, ancak SubType, değiştirmeye izin veren yeni bir yöntem sunar (yansıma yoluyla):
public class SuperType
{
public string Name { get; private set; }
public SuperType(string name, int age)
{
Name = name;
Age = age;
}
}
public class SubType : SuperType
{
public void ChangeName(string newName)
{
var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
}
}
Başka 2 öğe daha vardır: Yöntem bağımsız değişkenlerinin çelişkisi ve dönüş türlerinin kovaryansı . Ama bu C # (ben bir C # geliştirici) mümkün değildir, bu yüzden onları umurumda değil.
Referans:
LSP sınıfların sözleşmesiyle ilgili bir kuraldır: eğer bir temel sınıf bir sözleşmeyi yerine getiriyorsa, o zaman LSP'den türetilen sınıflar da bu sözleşmeyi yerine getirmelidir.
Yalancı Piton
class Base:
def Foo(self, arg):
# *... do stuff*
class Derived(Base):
def Foo(self, arg):
# *... do stuff*
Türetilmiş bir nesnede Foo'yu her çağırdığınızda, arg aynı olduğu sürece bir Base nesnesinde Foo'yu çağırmakla aynı sonuçları verirse LSP'yi tatmin eder.
2 + "2"
). Belki de "güçlü yazılan" ı "statik olarak yazılan" ile karıştırıyorsunuz?
Uzun lafın kısası, dikdörtgenler ve kare kareler bırakalım, bir ebeveyn sınıfını genişletirken pratik bir örnek olarak, tam ana API'yi KORUYMALISINIZ veya BUNU UZATMALISINIZ.
Diyelim ki temel bir ItemsRepository'niz var.
class ItemsRepository
{
/**
* @return int Returns number of deleted rows
*/
public function delete()
{
// perform a delete query
$numberOfDeletedRows = 10;
return $numberOfDeletedRows;
}
}
Ve onu genişleten bir alt sınıf:
class BadlyExtendedItemsRepository extends ItemsRepository
{
/**
* @return void Was suppose to return an INT like parent, but did not, breaks LSP
*/
public function delete()
{
// perform a delete query
$numberOfDeletedRows = 10;
// we broke the behaviour of the parent class
return;
}
}
Ardından , Base ItemsRepository API'sı ile çalışan ve ona güvenen bir İstemciniz olabilir .
/**
* Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
*
* Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
* but if the sub-class won't abide the base class API, the client will get broken.
*/
class ItemsService
{
/**
* @var ItemsRepository
*/
private $itemsRepository;
/**
* @param ItemsRepository $itemsRepository
*/
public function __construct(ItemsRepository $itemsRepository)
{
$this->itemsRepository = $itemsRepository;
}
/**
* !!! Notice how this is suppose to return an int. My clients expect it based on the
* ItemsRepository API in the constructor !!!
*
* @return int
*/
public function delete()
{
return $this->itemsRepository->delete();
}
}
LSP zaman bozuldu ikame ebeveyn bir ile sınıf alt sınıf sonları API sözleşme .
class ItemsController
{
/**
* Valid delete action when using the base class.
*/
public function validDeleteAction()
{
$itemsService = new ItemsService(new ItemsRepository());
$numberOfDeletedItems = $itemsService->delete();
// $numberOfDeletedItems is an INT :)
}
/**
* Invalid delete action when using a subclass.
*/
public function brokenDeleteAction()
{
$itemsService = new ItemsService(new BadlyExtendedItemsRepository());
$numberOfDeletedItems = $itemsService->delete();
// $numberOfDeletedItems is a NULL :(
}
}
Kursumda sürdürülebilir yazılım yazma hakkında daha fazla bilgi edinebilirsiniz: https://www.udemy.com/enterprise-php/
İşaretçiler veya temel sınıflara başvurular kullanan işlevler, türetilmiş sınıfların nesnelerini bilmeden kullanabilmelidir.
LSP hakkında ilk okuduğumda, bunun çok sıkı bir anlamda kastedildiğini, aslında arayüzün uygulanması ve tipte güvenli dökümle eşdeğer olduğunu varsaydım. Bu, LSP'nin dil tarafından sağlanmış olup olmadığı anlamına gelir. Örneğin, bu katı anlamda, ThreeDBoard derleyici açısından kesinlikle Kurul yerine geçer.
Konsept hakkında daha fazla okuduktan sonra LSP'nin genel olarak bundan daha geniş bir şekilde yorumlandığını gördüm.
Kısacası, istemci kodunun, işaretçinin arkasındaki nesnenin işaretçi türü yerine türetilmiş bir tür olduğunu "bilmesi" ne anlama gelirse, tür güvenliği ile sınırlı değildir. LSP'ye bağlılık, nesnelerin gerçek davranışını inceleyerek de test edilebilir. Yani, bir nesnenin durumu ve yöntem bağımsız değişkenlerinin yöntem çağrılarının sonuçları veya nesneden atılan özel durum türleri üzerindeki etkisini incelemek.
Yine örneğe dönersek , teorik olarak Board yöntemleri ThreeDBoard'da iyi çalışabilir. Bununla birlikte, uygulamada, ThreeDBoard'un eklemeyi amaçladığı işlevselliği engellemeden, istemcinin düzgün işleyemeyeceği davranış farklılıklarını önlemek çok zor olacaktır.
Elimizdeki bu bilgi ile LSP uyumunun değerlendirilmesi, mirastan ziyade mevcut işlevselliği genişletmek için kompozisyonun ne zaman daha uygun bir mekanizma olduğunu belirlemede harika bir araç olabilir.
Sanırım herkes LSP'nin teknik olarak ne olduğunu kapsıyor: Temel olarak alt tip detaylardan soyutlamak ve süper tipleri güvenle kullanmak istersiniz.
Yani Liskov'un altında yatan 3 kural var:
İmza Kuralı: Alt türdeki her tür işlemin sözdizimsel olarak geçerli bir uygulaması olmalıdır. Bir derleyici sizin için kontrol edebilecek bir şey. Daha az istisna atma ve en azından süper tip yöntemler kadar erişilebilir olma konusunda küçük bir kural vardır.
Yöntemler Kural: Bu işlemlerin uygulanması anlamsal olarak doğrudur.
Özellikler Kural: Bu, bireysel işlev çağrılarının ötesine geçer.
Tüm bu özelliklerin korunması ve ekstra alt tip işlevselliğinin süper tip özelliklerini ihlal etmemesi gerekir.
Eğer bu üç şey halledilirse, altta yatan şeylerden soyutlandınız ve gevşek bir şekilde kod yazıyorsunuz.
Kaynak: Java'da Program Geliştirme - Barbara Liskov
LSP kullanımının önemli bir örneği yazılım testidir .
B'nin LSP uyumlu bir alt sınıfı olan bir A sınıfım varsa, B'yi test etmek için A test paketini yeniden kullanabilirim.
A alt sınıfını tam olarak test etmek için, muhtemelen birkaç test senaryosu daha eklemem gerekiyor, ancak en azından üst sınıf B'nin tüm test vakalarını tekrar kullanabilirim.
Bunu fark etmenin bir yolu, McGregor'un "test için paralel hiyerarşi" dediği şeyi oluşturarak: ATest
Sınıfımın miras alacaktır BTest
. Daha sonra, test durumunun B tipi yerine A tipi nesnelerle çalışmasını sağlamak için bir çeşit enjeksiyona ihtiyaç duyulur (basit bir şablon yöntemi deseni yapılır).
Süper sınama paketinin tüm alt sınıf uygulamaları için yeniden kullanılmasının aslında bu alt sınıf uygulamalarının LSP uyumlu olduğunu test etmenin bir yolu olduğunu unutmayın. Dolayısıyla, üst sınıf test takımının herhangi bir alt sınıf bağlamında çalıştırılması gerektiği de iddia edilebilir .
Ayrıca Stackoverflow sorusunun cevabına da bakınız " Bir arayüzün uygulamasını test etmek için bir dizi tekrar kullanılabilir test uygulayabilir miyim? "
Java ile açıklayalım:
class TrasportationDevice
{
String name;
String getName() { ... }
void setName(String n) { ... }
double speed;
double getSpeed() { ... }
void setSpeed(double d) { ... }
Engine engine;
Engine getEngine() { ... }
void setEngine(Engine e) { ... }
void startEngine() { ... }
}
class Car extends TransportationDevice
{
@Override
void startEngine() { ... }
}
Burada sorun yok değil mi? Bir araba kesinlikle bir ulaşım cihazıdır ve burada üst sınıfının startEngine () yöntemini geçersiz kıldığını görebiliriz.
Başka bir taşıma cihazı ekleyelim:
class Bicycle extends TransportationDevice
{
@Override
void startEngine() /*problem!*/
}
Şimdi her şey planlandığı gibi gitmiyor! Evet, bir bisiklet bir ulaşım aracıdır, ancak motoru yoktur ve bu nedenle startEngine () yöntemi uygulanamaz.
Bunlar, Liskov İkame Prensibi'nin ihlal ettiği türden problemlerdir ve çoğunlukla hiçbir şey yapmayan veya hatta uygulanamayan bir yöntemle tanınabilirler.
Bu sorunların çözümü doğru bir miras hiyerarşisidir ve bizim durumumuzda, motorlu ve motorsuz ulaşım cihazları sınıflarını farklılaştırarak sorunu çözeceğiz. Bir bisiklet bir ulaşım cihazı olsa da, motoru yoktur. Bu örnekte nakliye cihazı tanımımız yanlış. Motoru olmamalı.
TransportationDevice sınıfımızı aşağıdaki gibi yeniden düzenleyebiliriz:
class TrasportationDevice
{
String name;
String getName() { ... }
void setName(String n) { ... }
double speed;
double getSpeed() { ... }
void setSpeed(double d) { ... }
}
Artık motorsuz cihazlar için TransportationDevice'i genişletebiliriz.
class DevicesWithoutEngines extends TransportationDevice
{
void startMoving() { ... }
}
Ve motorlu cihazlar için TransportationDevice'i uzatın. Engine nesnesini eklemek daha uygundur.
class DevicesWithEngines extends TransportationDevice
{
Engine engine;
Engine getEngine() { ... }
void setEngine(Engine e) { ... }
void startEngine() { ... }
}
Böylece Liskov İkame İlkesine bağlı kalarak Araba sınıfımız daha uzmanlaşır.
class Car extends DevicesWithEngines
{
@Override
void startEngine() { ... }
}
Bisiklet sınıfımız da Liskov İkame Prensibi ile uyumludur.
class Bicycle extends DevicesWithoutEngines
{
@Override
void startMoving() { ... }
}
LSP'nin bu formülasyonu çok güçlü:
S tipi her nesne o1 için T tipi bir nesne O2 varsa, öyle ki T açısından tüm P programları tanımlanırsa, o1 o2 ile ikame edildiğinde P'nin davranışı değişmez, o zaman S T'nin bir alt tipidir.
Bu temelde S'nin T ile aynı şeyin tamamen, tamamen kapsüllenmiş bir uygulaması olduğu anlamına gelir. Ve cesur olabilirim ve performansın P'nin davranışının bir parçası olduğuna karar verebilirim ...
Yani, temel olarak, geç bağlamanın herhangi bir kullanımı LSP'yi ihlal eder. Bir tür bir nesneyi başka bir türün yerine koyduğumuzda farklı bir davranış elde etmek OO'nun bütün amacı!
Vikipedi tarafından belirtilen formülasyon daha iyidir çünkü özellik bağlama bağlıdır ve programın tüm davranışını içermez.
Çok basit bir cümleyle şunu söyleyebiliriz:
Çocuk sınıfı temel sınıf özelliklerini ihlal etmemelidir. Onunla yetenekli olmalı. Bunun alt tiple aynı olduğunu söyleyebiliriz.
Liskov'un Değiştirme Prensibi (LSP)
Her zaman bir program modülü tasarlıyoruz ve bazı sınıf hiyerarşileri yaratıyoruz. Sonra bazı sınıfları türetilmiş sınıflar yaratarak genişletiyoruz.
Yeni türetilmiş sınıfların eski sınıfların işlevlerini değiştirmeden genişlediğinden emin olmalıyız. Aksi takdirde, yeni sınıflar mevcut program modüllerinde kullanıldıklarında istenmeyen etkiler üretebilirler.
Liskov'un Değiştirme Prensibi, bir program modülü bir Base sınıfı kullanıyorsa, Base sınıfına yapılan başvurunun, program modülünün işlevselliğini etkilemeden Türetilmiş bir sınıfla değiştirilebileceğini belirtir.
Misal:
Aşağıda, Liskov'un Değiştirme Prensibi'nin ihlal edildiği klasik örnek yer almaktadır. Örnekte 2 sınıf kullanılmıştır: Dikdörtgen ve Kare. Dikdörtgen nesnesinin uygulamada bir yerde kullanıldığını varsayalım. Uygulamayı genişletip Square sınıfını ekliyoruz. Kare sınıf, bazı koşullara bağlı olarak fabrika modeliyle döndürülür ve ne tür nesnelerin döndürüleceğini tam olarak bilmiyoruz. Ama bunun bir Dikdörtgen olduğunu biliyoruz. Dikdörtgen nesnesini alıyoruz, genişliği 5'e ve yüksekliği 10'a ayarlıyoruz ve alanı alıyoruz. Genişliği 5 ve yüksekliği 10 olan bir dikdörtgen için alan 50 olmalıdır. Bunun yerine, sonuç 100 olacaktır.
// Violation of Likov's Substitution Principle
class Rectangle {
protected int m_width;
protected int m_height;
public void setWidth(int width) {
m_width = width;
}
public void setHeight(int height) {
m_height = height;
}
public int getWidth() {
return m_width;
}
public int getHeight() {
return m_height;
}
public int getArea() {
return m_width * m_height;
}
}
class Square extends Rectangle {
public void setWidth(int width) {
m_width = width;
m_height = width;
}
public void setHeight(int height) {
m_width = height;
m_height = height;
}
}
class LspTest {
private static Rectangle getNewRectangle() {
// it can be an object returned by some factory ...
return new Square();
}
public static void main(String args[]) {
Rectangle r = LspTest.getNewRectangle();
r.setWidth(5);
r.setHeight(10);
// user knows that r it's a rectangle.
// It assumes that he's able to set the width and height as for the base
// class
System.out.println(r.getArea());
// now he's surprised to see that the area is 100 instead of 50.
}
}
Sonuç:
Bu ilke sadece Açık Kapalı Prensibinin bir uzantısıdır ve yeni türetilmiş sınıfların temel sınıfları davranışlarını değiştirmeden genişlettiğinden emin olmamız gerektiği anlamına gelir.
Ayrıca bakınız: Açık Kapalı Prensibi
Daha iyi yapı için bazı benzer kavramlar: Konfigürasyon üzerine sözleşme
Bazı zeyilname:
Neden hiç kimse türetilmiş sınıflar tarafından uyulması gereken temel sınıfın Değişmez, önkoşulları ve post koşulları hakkında bir şey yazmadı. Türetilmiş bir D sınıfının Temel sınıf B tarafından tamamen sustitlabilmesi için D sınıfı belirli koşullara uymalıdır:
Bu nedenle türetilmiş olanlar, temel sınıfın empoze ettiği yukarıdaki üç koşulun farkında olmalıdır. Bu nedenle, alt tipleme kuralları önceden kararlaştırılmıştır. Yani 'IS A' ilişkisine ancak bazı kurallar alt tip uyulduğu takdirde uyulacaktır. Bu kurallar, değişmezler, önkoşullar ve sonkoşul şeklinde, resmi bir ' tasarım sözleşmesi ' ile kararlaştırılmalıdır .
Bu konuda daha fazla tartışma blogumda mevcuttur: Liskov İkame ilkesi
LSP basit terimlerle , aynı üst sınıftaki nesnelerin hiçbir şeyi bozmadan birbirleriyle değiştirilebilmesi gerektiğini belirtir .
Örneğin, Cat
bir Dog
sınıftan türetilen bir ve Animal
sınıfımız varsa, Animal sınıfını kullanan herhangi bir işlev normal şekilde kullanabilmeli Cat
veya Dog
davranabilmelidir.
Bir Kurul dizisi açısından ThreeDBoard uygulamak bu kadar faydalı olur mu?
Belki de çeşitli düzlemlerde ThreeDBoard dilimleri bir Tahta olarak tedavi etmek isteyebilirsiniz. Bu durumda, Board için birden fazla uygulamaya izin verecek bir arabirim (veya soyut sınıf) tasarlamak isteyebilirsiniz.
Harici arabirim açısından, hem TwoDBoard hem de ThreeDBoard için bir Board arabirimini hesaba katmak isteyebilirsiniz (yukarıdaki yöntemlerin hiçbiri uygun olmasa da).
Kare, genişliğin yüksekliğe eşit olduğu bir dikdörtgendir. Kare, genişlik ve yükseklik için iki farklı boyut ayarlarsa, kare değişmezini ihlal eder. Bu, yan etkiler getirilerek çözülmüştür. Ancak, dikdörtgenin önkoşulu 0 <yükseklik ve 0 <genişlik olan bir setSize (yükseklik, genişlik) varsa. Türetilmiş alt tip yöntemi height == width; daha güçlü bir önkoşul (ve bu da lsp'yi ihlal ediyor). Bu, kare bir dikdörtgen olmasına rağmen, önkoşul güçlendirildiği için geçerli bir alt tür olmadığını gösterir. Etraftaki çalışma (genel olarak kötü bir şey) bir yan etkiye neden olur ve bu da post koşulunu (lsp'yi ihlal eder) zayıflatır. Tabandaki setWidth, 0 <genişlik direk koşuluna sahiptir. Türetilmiş, height == width ile onu zayıflatır.
Bu nedenle yeniden boyutlandırılabilir bir kare yeniden boyutlandırılabilir bir dikdörtgen değildir.
Bu ilke Barbara Liskov tarafından tanıtıldı tarafından 1987 ve bir Üst Sınıf ve alt türlerinin davranışlarına odaklanarak Açık-Kapalı Prensibi'ni genişletti.
İhlal etmenin sonuçlarını düşündüğümüzde önemi belirginleşir. Aşağıdaki sınıfı kullanan bir uygulamayı düşünün.
public class Rectangle
{
private double width;
private double height;
public double Width
{
get
{
return width;
}
set
{
width = value;
}
}
public double Height
{
get
{
return height;
}
set
{
height = value;
}
}
}
Bir gün, müşterinin dikdörtgenlere ek olarak kareleri manipüle etme yeteneğini istediğini düşünün. Kare bir dikdörtgen olduğundan, kare sınıfı Rectangle sınıfından türetilmelidir.
public class Square : Rectangle
{
}
Ancak bunu yaparak iki sorunla karşılaşacağız:
Kare, dikdörtgenden devralınan hem yükseklik hem de genişlik değişkenlerine ihtiyaç duymaz ve yüz binlerce kare nesne oluşturmak zorunda kalırsak, bellekte önemli bir atık yaratabilir. Dikdörtgenden devralınan genişlik ve yükseklik ayarlayıcı özellikleri bir kare için uygun değildir, çünkü bir karenin genişliği ve yüksekliği aynıdır. Hem yüksekliği hem de genişliği aynı değere ayarlamak için aşağıdaki gibi iki yeni özellik oluşturabiliriz:
public class Square : Rectangle
{
public double SetWidth
{
set
{
base.Width = value;
base.Height = value;
}
}
public double SetHeight
{
set
{
base.Height = value;
base.Width = value;
}
}
}
Şimdi, birisi kare bir nesnenin genişliğini ayarladığında, yüksekliği buna göre değişecektir ve bunun tersi de geçerlidir.
Square s = new Square();
s.SetWidth(1); // Sets width and height to 1.
s.SetHeight(2); // sets width and height to 2.
İlerleyelim ve bu diğer işlevi düşünelim:
public void A(Rectangle r)
{
r.SetWidth(32); // calls Rectangle.SetWidth
}
Bu işleve kare bir nesneye bir başvuru iletirsek, işlev bağımsız değişkenlerinin türevleri için çalışmadığından LSP'yi ihlal ederiz. Width ve height özellikleri polimerik değildir çünkü dikdörtgen içinde sanal olarak bildirilmezler (yükseklik değiştirilmediği için kare nesne bozulur).
Ancak, ayarlayıcı özelliklerini sanal olarak ilan ederek, başka bir ihlal olan OCP ile karşılaşacağız. Aslında, türetilmiş bir sınıf karesinin oluşturulması, temel sınıf dikdörtgeninde değişikliklere neden olmaktadır.
Şimdiye kadar bulduğum LSP için en açık açıklama "Liskov İkame İlkesi, türetilmiş bir sınıfın nesnesinin, sistemde herhangi bir hata getirmeden veya temel sınıfın davranışını değiştirmeden temel sınıfın bir nesnesini değiştirebileceğini söylüyor. " buradan . Makale, LSP'yi ihlal etmek ve düzeltmek için kod örneği verir.
Diyelim ki kodumuzda bir dikdörtgen kullanıyoruz
r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);
Geometri sınıfımızda, bir karenin özel bir dikdörtgen türü olduğunu öğrendik çünkü genişliği yüksekliğiyle aynı uzunluktadır. Square
Bu bilgilere dayanarak bir sınıf da yapalım :
class Square extends Rectangle {
setDimensions(width, height){
assert(width == height);
super.setDimensions(width, height);
}
}
İlk kodumuzda Rectangle
ile değiştirirsek, Square
o zaman kırılır:
r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);
Bunun nedeni Square
biz yoktu yeni ön şartı vardır Rectangle
sınıfın: width == height
. LSP'ye göre, Rectangle
örnekler Rectangle
alt sınıf örnekleriyle değiştirilmelidir. Bunun nedeni, bu örneklerin örnekler için tür denetimini geçmesi ve Rectangle
kodunuzda beklenmedik hatalara neden olmalarıdır.
Bu, wiki makalesinde "bir alt türde önkoşullar güçlendirilemez" kısmına bir örnektir . Özetlemek gerekirse, LSP'yi ihlal etmek muhtemelen bir noktada kodunuzda hatalara neden olacaktır.
LSP, `` Nesnelerin alt türleriyle değiştirilmesi gerekir '' diyor. Öte yandan, bu ilke
Çocuk sınıfları asla ana sınıfın tür tanımlarını kırmamalıdır.
ve aşağıdaki örnek LSP'nin daha iyi anlaşılmasına yardımcı olur.
LSP olmadan:
public interface CustomerLayout{
public void render();
}
public FreeCustomer implements CustomerLayout {
...
@Override
public void render(){
//code
}
}
public PremiumCustomer implements CustomerLayout{
...
@Override
public void render(){
if(!hasSeenAd)
return; //it isn`t rendered in this case
//code
}
}
public void renderView(CustomerLayout layout){
layout.render();
}
LSP ile sabitleme:
public interface CustomerLayout{
public void render();
}
public FreeCustomer implements CustomerLayout {
...
@Override
public void render(){
//code
}
}
public PremiumCustomer implements CustomerLayout{
...
@Override
public void render(){
if(!hasSeenAd)
showAd();//it has a specific behavior based on its requirement
//code
}
}
public void renderView(CustomerLayout layout){
layout.render();
}
Makaleyi okumanızı tavsiye ederim: İhlali Liskov İkame İlkesi (LSP) .
Orada Liskov İkame İlkesi'nin ne olduğunu, zaten ihlal edip etmediğinizi tahmin etmenize yardımcı olacak genel ipuçları ve sınıf hiyerarşinizi daha güvenli hale getirmenize yardımcı olacak bir yaklaşım örneği bulabilirsiniz.
LISKOV YERLEŞTİRME İLKESİ (Mark Seemann kitabından), bir arabirimin bir uygulamasını başka bir istemciyi veya uygulamayı bozmadan değiştirebileceğimizi belirtmektedir. Bugün onları öngöremiyorum.
Bilgisayarı duvardan çıkarırsak (Uygulama), ne duvar prizi (Arayüz) ne de bilgisayar (İstemci) bozulur (aslında, bir dizüstü bilgisayarsa, bir süre pilleri üzerinde bile çalışabilir) . Bununla birlikte, yazılımla, istemci genellikle bir hizmetin kullanılabilir olmasını bekler. Hizmet kaldırıldı, bir NullReferenceException alır. Bu tür bir durumla başa çıkmak için, “hiçbir şey” yapmayan bir arayüz uygulaması oluşturabiliriz. Bu, Boş Nesne olarak bilinen bir tasarım modelidir [4] ve kabaca bilgisayarın duvardan çıkarılmasına karşılık gelir. Gevşek kuplaj kullandığımız için, gerçek bir uygulamayı sorun yaratmadan hiçbir şey yapmayan bir şeyle değiştirebiliriz.
Likov'un Değiştirme Prensibi , bir program modülü bir Base sınıfı kullanıyorsa, Base sınıfına yapılan başvurunun, program modülünün işlevselliğini etkilemeden Türetilmiş bir sınıfla değiştirilebileceğini belirtir.
Niyet - Türetilmiş türler, taban türleri için tamamen ikame edilebilir olmalıdır.
Örnek - Java'daki ko-varyant dönüş türleri.
İşte bir alıntıdır bu yazı o açıklık getirmektedir şeyler güzel:
[..] bazı ilkeleri kavramak için, ne zaman ihlal edildiğini anlamak önemlidir. Şimdi yapacağım şey bu.
Bu ilkenin ihlali ne anlama geliyor? Bir nesnenin, bir arayüzle ifade edilen bir soyutlamanın getirdiği sözleşmeyi yerine getirmediği anlamına gelir. Başka bir deyişle, soyutlamalarınızı yanlış belirlediğiniz anlamına gelir.
Aşağıdaki örneği düşünün:
interface Account
{
/**
* Withdraw $money amount from this account.
*
* @param Money $money
* @return mixed
*/
public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
private $balance;
public function withdraw(Money $money)
{
if (!$this->enoughMoney($money)) {
return;
}
$this->balance->subtract($money);
}
}
Bu bir LSP ihlali mi? Evet. Bunun nedeni, hesabın sözleşmesinin bize bir hesabın geri çekileceğini söylemesi, ancak durum her zaman böyle değildir. Düzeltmek için ne yapmalıyım? Sadece sözleşmeyi değiştiriyorum:
interface Account
{
/**
* Withdraw $money amount from this account if its balance is enough.
* Otherwise do nothing.
*
* @param Money $money
* @return mixed
*/
public function withdraw(Money $money);
}
Voilà, şimdi sözleşme yerine getirildi.
Bu ince ihlal genellikle bir müşteriye kullanılan somut nesneler arasındaki farkı söyleme yeteneği kazandırır. Örneğin, ilk Hesabın sözleşmesi göz önüne alındığında, aşağıdaki gibi görünebilir:
class Client
{
public function go(Account $account, Money $money)
{
if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
return;
}
$account->withdraw($money);
}
}
Ve bu, otomatik olarak açık-kapalı prensibini (yani para çekme gereksinimi) ihlal eder. Çünkü sözleşmeyi ihlal eden bir nesnenin yeterli parası yoksa ne olacağını asla bilemezsiniz. Muhtemelen hiçbir şey döndürmez, muhtemelen bir istisna atılır. Yani kontrol etmelisinhasEnoughMoney()
bir arayüzün parçası olmayan - . Yani bu zorla somut sınıfa bağımlı kontrol bir OCP ihlalidir].
Bu nokta ayrıca LSP ihlali hakkında sık sık karşılaştığım bir yanlış anlama da giderir. “Bir ebeveynin çocukta davranışı değiştiyse, o zaman LSP'yi ihlal eder” der. Bununla birlikte, bir çocuk ebeveyninin sözleşmesini ihlal etmediği sürece değildir.