Saya telah mendengar bahwa Prinsip Pergantian Liskov (LSP) adalah prinsip dasar desain berorientasi objek. Apa itu dan apa saja contoh penggunaannya?
Saya telah mendengar bahwa Prinsip Pergantian Liskov (LSP) adalah prinsip dasar desain berorientasi objek. Apa itu dan apa saja contoh penggunaannya?
Jawaban:
Sebuah contoh yang bagus menggambarkan LSP (diberikan oleh Paman Bob dalam podcast yang saya dengar baru-baru ini) adalah bagaimana kadang-kadang sesuatu yang terdengar benar dalam bahasa alami tidak cukup berfungsi dalam kode.
Dalam matematika, a Square
adalah a Rectangle
. Memang itu adalah spesialisasi persegi panjang. "Is a" membuat Anda ingin memodelkan ini dengan warisan. Namun jika dalam kode yang Anda buat Square
berasal dari Rectangle
, maka Square
harus dapat digunakan di mana saja Anda harapkan Rectangle
. Ini membuat beberapa perilaku aneh.
Bayangkan Anda memiliki SetWidth
dan SetHeight
metode di Rectangle
kelas dasar Anda ; ini tampaknya sangat logis. Namun jika Rectangle
referensi Anda menunjuk ke Square
, maka SetWidth
dan SetHeight
tidak masuk akal karena pengaturan yang satu akan mengubah yang lain untuk mencocokkannya. Dalam hal ini Square
gagal dengan Tes Substitusi Liskov dengan Rectangle
dan abstraksi memiliki Square
warisan dari Rectangle
yang buruk.
Kalian harus memeriksa Poster Motivational SOLID Principles Principles yang tak ternilai harganya .
Square.setWidth(int width)
diimplementasikan seperti ini this.width = width; this.height = width;
:? Dalam hal ini dijamin bahwa lebarnya sama dengan tinggi.
Prinsip Pergantian Liskov (LSP, lsp) adalah konsep dalam Pemrograman Berorientasi Objek yang menyatakan:
Fungsi yang menggunakan pointer atau referensi ke kelas dasar harus dapat menggunakan objek dari kelas turunan tanpa menyadarinya.
Pada intinya LSP adalah tentang antarmuka dan kontrak serta bagaimana memutuskan kapan harus memperluas kelas vs menggunakan strategi lain seperti komposisi untuk mencapai tujuan Anda.
Cara yang efektif yang paling saya telah melihat untuk menggambarkan hal ini adalah di Head First OOA & D . Mereka menyajikan skenario di mana Anda adalah pengembang pada proyek untuk membangun kerangka kerja untuk permainan strategi.
Mereka menyajikan kelas yang mewakili papan yang terlihat seperti ini:
Semua metode mengambil koordinat X dan Y sebagai parameter untuk menemukan posisi ubin dalam array dua dimensi Tiles
. Ini akan memungkinkan pengembang game untuk mengelola unit di papan selama permainan.
Buku ini selanjutnya mengubah persyaratan untuk mengatakan bahwa kerangka kerja permainan juga harus mendukung papan permainan 3D untuk mengakomodasi permainan yang memiliki penerbangan. Jadi ThreeDBoard
kelas diperkenalkan yang meluas Board
.
Sepintas ini sepertinya keputusan yang bagus. Board
memberikan baik Height
dan Width
properti dan ThreeDBoard
menyediakan Z sumbu.
Di mana itu rusak adalah ketika Anda melihat semua anggota lain yang diwarisi Board
. Metode untuk AddUnit
, GetTile
, GetUnits
dan sebagainya, semua mengambil baik X dan parameter Y di Board
kelas tetapi ThreeDBoard
membutuhkan parameter Z juga.
Jadi, Anda harus menerapkan metode itu lagi dengan parameter Z. Parameter Z tidak memiliki konteks ke Board
kelas dan metode yang diwarisi dari Board
kelas kehilangan artinya. Unit kode yang mencoba menggunakan ThreeDBoard
kelas sebagai kelas dasarnya Board
akan sangat tidak beruntung.
Mungkin kita harus mencari pendekatan lain. Alih-alih memanjang Board
, ThreeDBoard
harus terdiri dari Board
objek. Satu Board
objek per unit sumbu Z.
Ini memungkinkan kita untuk menggunakan prinsip berorientasi objek yang baik seperti enkapsulasi dan penggunaan kembali dan tidak melanggar LSP.
Substitutability adalah prinsip dalam pemrograman berorientasi objek yang menyatakan bahwa, dalam program komputer, jika S adalah subtipe dari T, maka objek tipe T dapat diganti dengan objek tipe S
mari kita lakukan contoh sederhana di Jawa:
public class Bird{
public void fly(){}
}
public class Duck extends Bird{}
Bebek bisa terbang karena itu adalah burung, tetapi bagaimana dengan ini:
public class Ostrich extends Bird{}
Burung unta adalah burung, Tapi itu tidak bisa terbang, kelas burung unta adalah subtipe burung kelas, tetapi tidak bisa menggunakan metode terbang, itu berarti bahwa kita melanggar prinsip LSP.
public class Bird{
}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
Bird bird
. Anda harus melemparkan objek ke FlyingBirds untuk menggunakan lalat, yang tidak baik bukan?
Bird bird
, itu berarti tidak dapat digunakan fly()
. Itu dia. Lulus Duck
tidak mengubah fakta ini. Jika klien memiliki FlyingBirds bird
, maka bahkan jika dilewatkan, Duck
itu harus selalu bekerja dengan cara yang sama.
LSP menyangkut invarian.
Contoh klasik diberikan oleh deklarasi kode semu berikut (implementasi dihilangkan):
class Rectangle {
int getHeight()
void setHeight(int value)
int getWidth()
void setWidth(int value)
}
class Square : Rectangle { }
Sekarang kami memiliki masalah meskipun antarmuka cocok. Alasannya adalah bahwa kita telah melanggar invarian yang berasal dari definisi matematika kuadrat dan persegi panjang. Cara getter dan setters bekerja, a Rectangle
harus memenuhi invarian berikut:
void invariant(Rectangle r) {
r.setHeight(200)
r.setWidth(100)
assert(r.getHeight() == 200 and r.getWidth() == 100)
}
Namun, invarian ini harus dilanggar oleh implementasi yang benar Square
, oleh karena itu itu bukan pengganti yang valid Rectangle
.
Robert Martin memiliki makalah yang bagus tentang Prinsip Substitusi Liskov . Ini membahas cara-cara yang halus dan tidak begitu halus di mana prinsip tersebut dilanggar.
Beberapa bagian kertas yang relevan (perhatikan bahwa contoh kedua sangat padat):
Contoh Sederhana dari Pelanggaran LSP
Salah satu pelanggaran paling mencolok dari prinsip ini adalah penggunaan C ++ Run-Time Type Information (RTTI) untuk memilih fungsi berdasarkan jenis objek. yaitu:
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)); }
Jelas
DrawShape
fungsi ini terbentuk dengan buruk. Itu harus tahu tentang setiap turunan yang mungkin dariShape
kelas, dan itu harus diubah setiap kali turunan baru dariShape
dibuat. Memang, banyak yang melihat struktur fungsi ini sebagai laknat bagi Desain Berorientasi Objek.Persegi dan Persegi Panjang, Pelanggaran Yang Lebih Halus.
Namun, ada cara lain yang jauh lebih halus untuk melanggar LSP. Pertimbangkan aplikasi yang menggunakan
Rectangle
kelas seperti yang dijelaskan di bawah ini: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; };
[...] Bayangkan bahwa suatu hari pengguna menuntut kemampuan untuk memanipulasi kotak selain persegi panjang. [...]
Jelas, persegi adalah persegi panjang untuk semua maksud dan tujuan normal. Karena hubungan ISA berlaku, masuk akal untuk memodelkan
Square
kelas yang berasalRectangle
. [...]
Square
akan mewarisiSetWidth
danSetHeight
fungsi. Fungsi-fungsi ini sama sekali tidak pantas untuk aSquare
, karena lebar dan tinggi persegi adalah identik. Ini harus menjadi petunjuk penting bahwa ada masalah dengan desain. Namun, ada cara untuk menghindari masalah. Kita bisa menggantiSetWidth
danSetHeight
[...]Tetapi pertimbangkan fungsi berikut:
void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }
Jika kami meneruskan referensi ke
Square
objek ke fungsi ini, theSquare
objek akan rusak karena ketinggian tidak akan berubah. Ini jelas merupakan pelanggaran terhadap LSP. Fungsi ini tidak berfungsi untuk turunan dari argumennya.[...]
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.
Jika pra-kondisi kelas anak lebih kuat daripada pra-kondisi kelas orang tua, Anda tidak bisa mengganti anak dengan orang tua tanpa melanggar pra-kondisi. Oleh karena itu LSP.
LSP diperlukan ketika beberapa kode menganggapnya memanggil metode tipe T
, dan mungkin tanpa sadar menyebut metode tipe S
, di mana S extends T
(yaitu S
mewarisi, berasal dari, atau merupakan subtipe dari, supertipe T
).
Sebagai contoh, ini terjadi di mana fungsi dengan parameter input tipe T
, disebut (yaitu dipanggil) dengan nilai argumen tipe S
. Atau, tempat pengenal tipe T
, diberi nilai tipe S
.
val id : T = new S() // id thinks it's a T, but is a S
LSP membutuhkan ekspektasi (yaitu invarian) untuk metode tipe T
(misalnya Rectangle
), tidak dilanggar ketika metode tipe S
(misalnya Square
) dipanggil sebagai gantinya.
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
Bahkan tipe dengan bidang yang tidak dapat diubah masih memiliki invarian, mis. Penyetel Rectangle yang tidak dapat diubah mengharapkan dimensi untuk dimodifikasi secara independen, tetapi setters Square yang tidak dapat diubah melanggar ekspektasi ini.
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 mensyaratkan bahwa setiap metode subtipe S
harus memiliki parameter input kontravarian dan output kovarian.
Contravarian berarti varians yang bertentangan dengan arah pewarisan, yaitu tipe Si
, dari setiap parameter input dari setiap metode subtipe S
, harus sama atau supertipe dari jenis Ti
parameter input yang sesuai dari metode yang sesuai dari supertype T
.
Kovarian berarti varians dalam arah yang sama dari warisan, yaitu jenis So
, dari output masing-masing metode subtipe S
, harus sama atau subtipe dari jenis To
output yang sesuai dari metode yang sesuai dari supertype T
.
Ini karena jika penelepon mengira itu memiliki tipe T
, mengira itu memanggil metode T
, maka ia memasok argumen tipe Ti
dan memberikan output ke tipe To
. Ketika sebenarnya memanggil metode yang sesuai S
, maka setiap Ti
argumen input ditugaskan ke Si
parameter input, dan So
output ditugaskan untuk tipe tersebut To
. Jadi, jika Si
bukan contravariant wrt Ti
, maka subtipe Xi
— yang tidak akan menjadi subtipe Si
— dapat ditugaskan Ti
.
Selain itu, untuk bahasa (misalnya Scala atau Ceylon) yang memiliki anotasi varian-situs definisi pada parameter polimorfisme tipe (yaitu generik), ko-atau kontra-anotasi anotasi varians untuk setiap parameter tipe dari tipe T
harus berlawanan atau arah yang sama masing-masing untuk setiap parameter input atau output (dari setiap metode T
) yang memiliki tipe tipe parameter.
Selain itu, untuk setiap parameter input atau output yang memiliki tipe fungsi, arah varians yang diperlukan dibalik. Aturan ini diterapkan secara rekursif.
Subtyping sesuai jika invarian dapat disebutkan.
Ada banyak penelitian yang sedang berlangsung tentang cara memodelkan invarian, sehingga mereka ditegakkan oleh kompiler.
Ketik (lihat halaman 3) menyatakan dan memberlakukan invarian negara ortogonal untuk mengetik. Atau, invarian dapat ditegakkan dengan mengubah pernyataan menjadi tipe . Misalnya, untuk menyatakan bahwa file terbuka sebelum menutupnya, maka File.open () dapat mengembalikan tipe OpenFile, yang berisi metode close () yang tidak tersedia di File. Sebuah tic-tac-toe API dapat menjadi contoh lain dari mempekerjakan mengetik untuk menegakkan invariants pada saat kompilasi. Sistem tipe mungkin bahkan Turing-complete, misalnya Scala . Bahasa yang diketik secara tergantung dan teorema provers memformalkan model pengetikan tingkat tinggi.
Karena perlunya semantik untuk abstrak lebih dari ekstensi , saya berharap bahwa menggunakan pengetikan untuk memodelkan invarian, yaitu semantik denotasional tingkat tinggi yang disatukan, lebih unggul daripada Typestate. 'Extension' berarti komposisi tak terbatas yang diijinkan dari pengembangan modular, tidak terkoordinasi. Karena bagi saya tampaknya merupakan kebalikan dari penyatuan dan dengan demikian derajat kebebasan, memiliki dua model yang saling bergantung (misalnya tipe dan Typestate) untuk mengekspresikan semantik bersama, yang tidak dapat disatukan satu sama lain untuk komposisi yang dapat diperluas . Misalnya, ekstensi seperti Masalah Ekspresi disatukan dalam subtipe, fungsi yang berlebihan, dan domain pengetikan parametrik.
Posisi teoritis saya adalah agar pengetahuan ada (lihat bagian “Sentralisasi buta dan tidak layak”), tidak akan pernah ada model umum yang dapat memberlakukan cakupan 100% dari semua invarian yang mungkin dalam bahasa komputer Turing-lengkap. Agar pengetahuan ada, banyak kemungkinan yang tidak terduga, yaitu gangguan dan entropi harus selalu meningkat. Ini adalah kekuatan entropik. Untuk membuktikan semua perhitungan yang mungkin dari ekstensi potensial, adalah untuk menghitung apriori semua ekstensi yang mungkin.
Inilah sebabnya Teorema Halting ada, yaitu tidak dapat diputuskan apakah setiap program yang mungkin dalam bahasa pemrograman Turing-complete berakhir. Dapat dibuktikan bahwa beberapa program tertentu berakhir (yang semua kemungkinan telah didefinisikan dan dihitung). Tetapi tidak mungkin untuk membuktikan bahwa semua kemungkinan perpanjangan dari program itu berakhir, kecuali kemungkinan untuk perpanjangan dari program itu tidak lengkap Turing (mis. Melalui ketergantungan-mengetik). Karena persyaratan mendasar untuk kelengkapan Turing adalah rekursi yang tidak terbatas , intuitif untuk memahami bagaimana teorema ketidaklengkapan Gödel dan paradoks Russell berlaku untuk ekstensi.
Penafsiran teorema ini menggabungkan mereka dalam pemahaman konseptual umum dari kekuatan entropis:
Saya melihat persegi panjang dan bujur sangkar di setiap jawaban, dan bagaimana cara melanggar LSP.
Saya ingin menunjukkan bagaimana LSP dapat disesuaikan dengan contoh dunia nyata:
<?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;
}
}
Desain ini sesuai dengan LSP karena perilaku tetap tidak berubah terlepas dari implementasi yang kami pilih untuk digunakan.
Dan ya, Anda dapat melanggar LSP dalam konfigurasi ini dengan melakukan satu perubahan sederhana seperti:
<?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 !
}
}
Sekarang subtipe tidak dapat digunakan dengan cara yang sama karena mereka tidak menghasilkan hasil yang sama lagi.
Database::selectQuery
untuk mendukung hanya subset SQL yang didukung oleh semua mesin DB. Itu hampir tidak praktis ... Konon, contohnya masih lebih mudah dipahami daripada kebanyakan yang digunakan di sini.
Ada daftar periksa untuk menentukan apakah Anda melanggar Liskov atau tidak.
Daftar periksa:
Batasan Riwayat : Saat mengganti metode Anda tidak diizinkan untuk memodifikasi properti yang tidak dapat dimodifikasi di kelas dasar. Lihatlah kode ini dan Anda dapat melihat Name didefinisikan sebagai tidak dapat dimodifikasi (private set) tetapi SubType memperkenalkan metode baru yang memungkinkan memodifikasinya (melalui refleksi):
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);
}
}
Ada 2 item lain: Contravariance dari argumen metode dan Covariance dari tipe yang dikembalikan . Tapi itu tidak mungkin di C # (saya seorang pengembang C #) jadi saya tidak peduli tentang mereka.
Referensi:
LSP adalah aturan tentang kontrak klausa: jika kelas dasar memenuhi kontrak, maka oleh LSP kelas turunan juga harus memenuhi kontrak itu.
Dalam Pseudo-python
class Base:
def Foo(self, arg):
# *... do stuff*
class Derived(Base):
def Foo(self, arg):
# *... do stuff*
memenuhi LSP jika setiap kali Anda memanggil Foo pada objek yang diturunkan, ia memberikan hasil yang sama persis dengan memanggil Foo pada objek Basis, selama arg adalah sama.
2 + "2"
). Mungkin Anda bingung "sangat diketik" dengan "diketik secara statis"?
Panjang cerita pendek, mari kita tinggalkan persegi panjang persegi panjang dan kotak kotak, contoh praktis ketika memperpanjang kelas induk, Anda harus baik MEMELIHARA tepat orangtua API atau MEMPERPANJANG IT.
Katakanlah Anda memiliki basis ItemsRepository dasar .
class ItemsRepository
{
/**
* @return int Returns number of deleted rows
*/
public function delete()
{
// perform a delete query
$numberOfDeletedRows = 10;
return $numberOfDeletedRows;
}
}
Dan sub kelas memperluasnya:
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;
}
}
Maka Anda bisa meminta Klien bekerja dengan Base ItemsRepository API dan mengandalkannya.
/**
* 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();
}
}
The LSP rusak ketika mengganti orangtua kelas dengan sub istirahat kelas kontrak API .
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 :(
}
}
Anda dapat mempelajari lebih lanjut tentang cara menulis perangkat lunak yang dapat dipelihara dalam kursus saya: https://www.udemy.com/enterprise-php/
Fungsi yang menggunakan pointer atau referensi ke kelas dasar harus dapat menggunakan objek dari kelas turunan tanpa menyadarinya.
Ketika saya pertama kali membaca tentang LSP, saya berasumsi bahwa ini dimaksudkan dalam arti yang sangat ketat, pada dasarnya menyamakannya dengan implementasi antarmuka dan casting tipe-aman. Yang berarti bahwa LSP dipastikan atau tidak oleh bahasa itu sendiri. Sebagai contoh, dalam pengertian ketat ini, ThreeDBoard tentu saja dapat diganti untuk Dewan, sejauh menyangkut kompiler.
Setelah membaca lebih lanjut tentang konsepnya meskipun saya menemukan bahwa LSP umumnya ditafsirkan lebih luas dari itu.
Singkatnya, apa artinya kode klien untuk "tahu" bahwa objek di belakang pointer adalah tipe turunan daripada tipe pointer tidak terbatas pada tipe-safety. Ketaatan pada LSP juga dapat diuji melalui penyelidikan perilaku objek aktual. Yaitu, memeriksa dampak keadaan objek dan argumen metode pada hasil pemanggilan metode, atau jenis pengecualian yang dilemparkan dari objek.
Kembali ke contoh lagi, secara teori metode Dewan dapat dibuat berfungsi dengan baik di ThreeDBoard. Namun dalam praktiknya, akan sangat sulit untuk mencegah perbedaan perilaku yang mungkin tidak ditangani klien dengan benar, tanpa mengganggu fungsi yang ingin ditambahkan oleh ThreeDBoard.
Dengan pengetahuan ini, mengevaluasi kepatuhan LSP dapat menjadi alat yang hebat dalam menentukan kapan komposisi adalah mekanisme yang lebih tepat untuk memperluas fungsi yang ada, daripada pewarisan.
Saya kira semua orang membahas apa yang LSP secara teknis: Anda pada dasarnya ingin dapat abstrak jauh dari rincian subtipe dan menggunakan supertipe dengan aman.
Jadi Liskov memiliki 3 aturan mendasar:
Aturan Tanda Tangan: Seharusnya ada implementasi yang valid dari setiap operasi supertype dalam subtipe secara sintaksis. Sesuatu yang bisa diperiksa oleh kompiler. Ada sedikit aturan tentang melemparkan lebih sedikit pengecualian dan setidaknya dapat diakses seperti metode supertype.
Metode Aturan: Implementasi dari operasi-operasi tersebut secara semantik sehat.
Properti Aturan: Ini melampaui panggilan fungsi individu.
Semua properti ini harus dipertahankan dan fungsionalitas subtipe tambahan tidak boleh melanggar sifat supertipe.
Jika ketiga hal ini diatasi, Anda telah mengambil abstrak dari hal-hal yang mendasarinya dan Anda menulis kode yang digabungkan secara longgar.
Sumber: Pengembangan Program di Jawa - Barbara Liskov
Contoh penting dari penggunaan LSP adalah dalam pengujian perangkat lunak .
Jika saya memiliki kelas A yang merupakan subkelas B yang sesuai dengan LSP, maka saya dapat menggunakan kembali suite uji B untuk menguji A.
Untuk sepenuhnya menguji subclass A, saya mungkin perlu menambahkan beberapa test case lagi, tetapi setidaknya saya dapat menggunakan kembali semua test case superclass B.
Cara untuk mewujudkannya adalah dengan membangun apa yang oleh McGregor disebut sebagai "Hirarki paralel untuk pengujian": ATest
Kelas saya akan mewarisi dari BTest
. Beberapa bentuk injeksi kemudian diperlukan untuk memastikan test case bekerja dengan objek tipe A daripada tipe B (pola metode templat sederhana akan dilakukan).
Perhatikan bahwa menggunakan kembali paket uji super untuk semua implementasi subclass sebenarnya adalah cara untuk menguji bahwa implementasi subclass ini sesuai dengan LSP. Dengan demikian, orang juga dapat berpendapat bahwa seseorang harus menjalankan suite uji superclass dalam konteks setiap subkelas.
Lihat juga jawaban untuk pertanyaan Stackoverflow " Bisakah saya menerapkan serangkaian tes yang dapat digunakan kembali untuk menguji implementasi antarmuka? "
Mari kita ilustrasikan di Jawa:
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() { ... }
}
Tidak ada masalah di sini, kan? Mobil jelas merupakan alat transportasi, dan di sini kita dapat melihat bahwa itu menimpa metode startEngine () dari superclassnya.
Mari tambahkan perangkat transportasi lain:
class Bicycle extends TransportationDevice
{
@Override
void startEngine() /*problem!*/
}
Semuanya tidak berjalan seperti yang direncanakan sekarang! Ya, sepeda adalah alat transportasi, bagaimanapun, ia tidak memiliki mesin dan karenanya, metode startEngine () tidak dapat diimplementasikan.
Ini adalah jenis masalah yang dilanggar oleh Prinsip Pergantian Liskov, dan biasanya dapat dikenali dengan metode yang tidak melakukan apa-apa, atau bahkan tidak dapat diimplementasikan.
Solusi untuk masalah ini adalah hierarki warisan yang benar, dan dalam kasus kami, kami akan memecahkan masalah dengan membedakan kelas perangkat transportasi dengan dan tanpa mesin. Meskipun sepeda adalah alat transportasi, ia tidak memiliki mesin. Dalam contoh ini, definisi alat transportasi kita salah. Seharusnya tidak memiliki mesin.
Kami dapat memperbaiki kelas TransportDevice kami sebagai berikut:
class TrasportationDevice
{
String name;
String getName() { ... }
void setName(String n) { ... }
double speed;
double getSpeed() { ... }
void setSpeed(double d) { ... }
}
Sekarang kita dapat memperluas Alat Transportasi untuk perangkat tidak bermotor.
class DevicesWithoutEngines extends TransportationDevice
{
void startMoving() { ... }
}
Dan memperluas Alat Transportasi untuk perangkat bermotor. Di sini lebih tepat untuk menambahkan objek Engine.
class DevicesWithEngines extends TransportationDevice
{
Engine engine;
Engine getEngine() { ... }
void setEngine(Engine e) { ... }
void startEngine() { ... }
}
Dengan demikian, kelas Mobil kami menjadi lebih terspesialisasi, dengan tetap berpegang pada Prinsip Pergantian Liskov.
class Car extends DevicesWithEngines
{
@Override
void startEngine() { ... }
}
Dan kelas Sepeda kami juga mematuhi Prinsip Substitusi Liskov.
class Bicycle extends DevicesWithoutEngines
{
@Override
void startMoving() { ... }
}
Formulasi LSP ini terlalu kuat:
Jika untuk setiap objek o1 dari tipe S ada objek o2 dari tipe T sedemikian rupa sehingga untuk semua program P didefinisikan dalam istilah T, perilaku P tidak berubah ketika o1 diganti dengan o2, maka S adalah subtipe dari T.
Yang pada dasarnya berarti bahwa S adalah implementasi lain yang sepenuhnya dienkapsulasi dari hal yang sama persis seperti T. Dan saya dapat menjadi berani dan memutuskan bahwa kinerja adalah bagian dari perilaku ...
Jadi, pada dasarnya, penggunaan apa pun yang mengikat lambat melanggar LSP. Inti dari OO adalah untuk mendapatkan perilaku yang berbeda ketika kita mengganti objek dari satu jenis dengan yang lainnya!
Formulasi yang dikutip oleh wikipedia lebih baik karena properti tergantung pada konteksnya dan tidak harus mencakup seluruh perilaku program.
Dalam kalimat yang sangat sederhana, kita dapat mengatakan:
Kelas anak tidak boleh melanggar karakteristik kelas dasarnya. Itu harus mampu dengan itu. Kita dapat mengatakan itu sama dengan subtyping.
Prinsip Pergantian Liskov (LSP)
Setiap saat kami merancang modul program dan kami membuat beberapa hierarki kelas. Kemudian kami memperluas beberapa kelas membuat beberapa kelas turunan.
Kita harus memastikan bahwa kelas turunan baru hanya memperpanjang tanpa mengganti fungsionalitas kelas lama. Jika tidak, kelas-kelas baru dapat menghasilkan efek yang tidak diinginkan ketika mereka digunakan dalam modul program yang ada.
Prinsip Substitusi Liskov menyatakan bahwa jika modul program menggunakan kelas Base, maka referensi ke kelas Base dapat diganti dengan kelas Derived tanpa memengaruhi fungsionalitas modul program.
Contoh:
Di bawah ini adalah contoh klasik yang melanggar Prinsip Pergantian Liskov. Dalam contoh ini, 2 kelas digunakan: Rectangle dan Square. Mari kita asumsikan bahwa objek Rectangle digunakan di suatu tempat dalam aplikasi. Kami memperluas aplikasi dan menambahkan kelas Square. Kelas kuadrat dikembalikan oleh pola pabrik, berdasarkan pada beberapa kondisi dan kami tidak tahu persis jenis objek yang akan dikembalikan. Tapi kita tahu itu adalah Rectangle. Kami mendapatkan objek persegi panjang, atur lebar ke 5 dan tinggi ke 10 dan dapatkan luasnya. Untuk persegi panjang dengan lebar 5 dan tinggi 10, area harus 50. Sebagai gantinya, hasilnya adalah 100
// 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.
}
}
Kesimpulan:
Prinsip ini hanyalah perpanjangan dari Prinsip Buka Tutup dan itu berarti bahwa kita harus memastikan bahwa kelas turunan baru memperluas kelas dasar tanpa mengubah perilaku mereka.
Lihat juga: Prinsip Buka Tutup
Beberapa konsep serupa untuk struktur yang lebih baik: Konvensi alih konfigurasi
Beberapa tambahan:
Saya heran mengapa tidak ada yang menulis tentang Invarian, prasyarat dan memposting kondisi kelas dasar yang harus dipatuhi oleh kelas turunan. Agar kelas D yang diturunkan sepenuhnya berkelanjutan oleh kelas B, kelas D harus mematuhi ketentuan tertentu:
Jadi yang diturunkan harus menyadari ketiga kondisi di atas yang diberlakukan oleh kelas dasar. Oleh karena itu, aturan subtyping sudah diputuskan sebelumnya. Yang berarti, hubungan 'IS A' akan dipatuhi hanya ketika aturan tertentu dipatuhi oleh subtipe. Aturan-aturan ini, dalam bentuk invarian, prekursor dan pascakondisi, harus diputuskan oleh ' kontrak desain ' formal .
Diskusi lebih lanjut tentang ini tersedia di blog saya: Prinsip Pergantian Liskov
LSP secara sederhana menyatakan bahwa objek dari superclass yang sama harus dapat saling bertukar tanpa melanggar apa pun.
Sebagai contoh, jika kita memiliki Cat
dan Dog
kelas turunan dari Animal
kelas, setiap fungsi menggunakan kelas Hewan harus dapat menggunakan Cat
atau Dog
dan bersikap normal.
Apakah menerapkan ThreeDBoard dalam hal susunan Dewan akan bermanfaat?
Mungkin Anda mungkin ingin memperlakukan irisan ThreeDBoard di berbagai pesawat sebagai Dewan. Dalam hal ini, Anda mungkin ingin abstrak antarmuka (atau kelas abstrak) untuk Dewan untuk memungkinkan beberapa implementasi.
Dalam hal antarmuka eksternal, Anda mungkin ingin mempertimbangkan antarmuka Dewan untuk TwoDBoard dan ThreeDBoard (meskipun tidak ada metode di atas yang cocok).
Kotak adalah kotak di mana lebar sama dengan tinggi. Jika kuadrat menetapkan dua ukuran berbeda untuk lebar dan tinggi, itu melanggar invarian kuadrat. Ini dikerjakan dengan memperkenalkan efek samping. Tetapi jika persegi panjang memiliki setSize (tinggi, lebar) dengan prasyarat 0 <tinggi dan 0 <lebar. Metode subtipe turunan membutuhkan tinggi == lebar; prasyarat yang lebih kuat (dan itu melanggar lsp). Ini menunjukkan bahwa meskipun persegi adalah persegi panjang, itu bukan subtipe yang valid karena prasyarat diperkuat. Bekerja di sekitar (secara umum hal yang buruk) menyebabkan efek samping dan ini melemahkan kondisi pos (yang melanggar lsp). setWidth di pangkalan memiliki kondisi posting 0 <lebar. Turunan melemahkannya dengan tinggi == lebar.
Oleh karena itu persegi resizable bukan persegi panjang resizable.
Prinsip ini diperkenalkan oleh Barbara Liskov pada tahun 1987 dan memperluas Prinsip Terbuka-Tertutup dengan berfokus pada perilaku superclass dan subtipe-nya.
Pentingnya menjadi jelas ketika kita mempertimbangkan konsekuensi dari melanggarnya. Pertimbangkan aplikasi yang menggunakan kelas berikut.
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;
}
}
}
Bayangkan suatu hari, klien menuntut kemampuan untuk memanipulasi kotak selain persegi panjang. Karena persegi adalah persegi panjang, kelas persegi harus diturunkan dari kelas Persegi Panjang.
public class Square : Rectangle
{
}
Namun, dengan melakukan itu kita akan menghadapi dua masalah:
Kuadrat tidak memerlukan variabel tinggi dan lebar yang diwarisi dari persegi panjang dan ini bisa membuat pemborosan yang signifikan dalam memori jika kita harus membuat ratusan ribu objek persegi. Properti setter lebar dan tinggi yang diwarisi dari persegi panjang tidak sesuai untuk sebuah persegi karena lebar dan tinggi persegi adalah identik. Untuk mengatur tinggi dan lebar ke nilai yang sama, kita dapat membuat dua properti baru sebagai berikut:
public class Square : Rectangle
{
public double SetWidth
{
set
{
base.Width = value;
base.Height = value;
}
}
public double SetHeight
{
set
{
base.Height = value;
base.Width = value;
}
}
}
Sekarang, ketika seseorang akan mengatur lebar objek persegi, tingginya akan berubah sesuai dan sebaliknya.
Square s = new Square();
s.SetWidth(1); // Sets width and height to 1.
s.SetHeight(2); // sets width and height to 2.
Mari kita maju dan mempertimbangkan fungsi lain ini:
public void A(Rectangle r)
{
r.SetWidth(32); // calls Rectangle.SetWidth
}
Jika kami meneruskan referensi ke objek kuadrat ke dalam fungsi ini, kami akan melanggar LSP karena fungsi tersebut tidak berfungsi untuk turunan dari argumennya. Properti lebar dan tinggi bukan polimorfik karena tidak dinyatakan virtual dalam persegi panjang (objek kuadrat akan rusak karena ketinggian tidak akan berubah).
Namun, dengan mendeklarasikan properti setter sebagai virtual, kami akan menghadapi pelanggaran lain, OCP. Bahkan, penciptaan kuadrat kelas turunan menyebabkan perubahan pada persegi panjang kelas dasar.
Penjelasan paling jelas untuk LSP yang saya temukan sejauh ini adalah "Prinsip Substitusi Liskov mengatakan bahwa objek kelas turunan harus dapat menggantikan objek kelas dasar tanpa membawa kesalahan dalam sistem atau memodifikasi perilaku kelas dasar. "dari sini . Artikel ini memberikan contoh kode untuk melanggar LSP dan memperbaikinya.
Katakanlah kita menggunakan kotak di dalam kode kita
r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);
Di kelas geometri kami, kami belajar bahwa persegi adalah jenis persegi panjang khusus karena lebarnya sama dengan tingginya. Mari kita buat Square
kelas juga berdasarkan info ini:
class Square extends Rectangle {
setDimensions(width, height){
assert(width == height);
super.setDimensions(width, height);
}
}
Jika kita mengganti Rectangle
dengan Square
dalam kode pertama kita, maka itu akan rusak:
r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);
Hal ini karena Square
memiliki prasyarat baru kami tidak memiliki dalam Rectangle
kelas: width == height
. Menurut LSP Rectangle
instance harus diganti dengan Rectangle
instance subclass. Ini karena instance ini melewatkan pemeriksaan tipe untuk Rectangle
instance dan karenanya mereka akan menyebabkan kesalahan tak terduga dalam kode Anda.
Ini adalah contoh untuk bagian "prasyarat tidak dapat diperkuat dalam subtipe" di artikel wiki . Singkatnya, melanggar LSP mungkin akan menyebabkan kesalahan dalam kode Anda di beberapa titik.
LSP mengatakan bahwa '' Objek harus diganti oleh subtipe mereka ''. Di sisi lain, prinsip ini menunjuk ke
Kelas anak tidak boleh melanggar definisi tipe kelas induk.
dan contoh berikut membantu untuk memiliki pemahaman yang lebih baik tentang LSP.
Tanpa LSP:
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();
}
Memperbaiki dengan LSP:
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();
}
Saya mendorong Anda untuk membaca artikel: Melanggar Prinsip Substitusi Liskov (LSP) .
Anda dapat menemukan di sana penjelasan apa itu Prinsip Pergantian Liskov, petunjuk umum yang membantu Anda menebak jika Anda telah melanggarnya dan contoh pendekatan yang akan membantu Anda membuat hierarki kelas Anda menjadi lebih aman.
PRINSIP PENGGANTI LISKOV (Dari buku Mark Seemann) menyatakan bahwa kita harus dapat mengganti satu implementasi antarmuka dengan yang lain tanpa melanggar salah satu klien atau implementasi. Ini adalah prinsip ini yang memungkinkan untuk mengatasi persyaratan yang terjadi di masa depan, bahkan jika kita dapat ' t meramalkan mereka hari ini.
Jika kita mencabut komputer dari dinding (Implementasi), outlet dinding (Antarmuka) atau komputer (Klien) tidak rusak (pada kenyataannya, jika itu adalah komputer laptop, ia bahkan dapat menggunakan baterai untuk jangka waktu tertentu) . Namun, dengan perangkat lunak, klien sering mengharapkan layanan akan tersedia. Jika layanan itu dihapus, kami mendapatkan NullReferenceException. Untuk menghadapi situasi seperti ini, kita dapat membuat implementasi antarmuka yang tidak melakukan apa-apa. Ini adalah pola desain yang dikenal sebagai Null Object, [4] dan sesuai dengan mencabut komputer dari dinding. Karena kami menggunakan kopling longgar, kami dapat mengganti implementasi nyata dengan sesuatu yang tidak melakukan apa-apa tanpa menyebabkan masalah.
Prinsip Substitusi Likov menyatakan bahwa jika modul program menggunakan kelas Base, maka referensi ke kelas Base dapat diganti dengan kelas Derived tanpa memengaruhi fungsionalitas modul program.
Intent - Derived types harus sepenuhnya dapat menggantikan tipe dasar mereka.
Contoh - Jenis pengembalian varian bersama di java.
Berikut adalah kutipan dari posting ini yang mengklarifikasi hal-hal dengan baik:
[..] untuk memahami beberapa prinsip, penting untuk disadari ketika telah dilanggar. Inilah yang akan saya lakukan sekarang.
Apa arti pelanggaran prinsip ini? Ini menyiratkan bahwa suatu objek tidak memenuhi kontrak yang dipaksakan oleh abstraksi yang dinyatakan dengan antarmuka. Dengan kata lain, itu berarti Anda salah mengidentifikasi abstraksi Anda.
Perhatikan contoh berikut:
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);
}
}
Apakah ini pelanggaran terhadap LSP? Iya. Ini karena kontrak akun memberi tahu kita bahwa sebuah akun akan ditarik, tetapi tidak selalu demikian. Jadi, apa yang harus saya lakukan untuk memperbaikinya? Saya hanya memodifikasi kontrak:
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);
}
Voa, sekarang kontrak puas.
Pelanggaran halus ini seringkali memaksakan klien dengan kemampuan untuk memberi tahu perbedaan antara objek konkret yang digunakan. Misalnya, mengingat kontrak Akun pertama, itu bisa terlihat seperti berikut:
class Client
{
public function go(Account $account, Money $money)
{
if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
return;
}
$account->withdraw($money);
}
}
Dan, ini secara otomatis melanggar prinsip buka-tutup [yaitu, untuk persyaratan penarikan uang. Karena Anda tidak pernah tahu apa yang terjadi jika suatu benda yang melanggar kontrak tidak memiliki cukup uang. Mungkin itu tidak mengembalikan apa-apa, mungkin pengecualian akan dilemparkan. Jadi, Anda harus memeriksa apakah itu hasEnoughMoney()
- yang bukan bagian dari antarmuka. Jadi pemeriksaan paksa yang bergantung pada kelas beton ini merupakan pelanggaran OCP].
Poin ini juga membahas kesalahpahaman yang sering saya temui tentang pelanggaran LSP. Dikatakan "jika perilaku orang tua berubah pada anak, maka, itu melanggar LSP." Namun, tidak - selama seorang anak tidak melanggar kontrak orang tuanya.