Apa contoh dari Prinsip Substitusi Liskov?


Jawaban:


892

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 Squareadalah a Rectangle. Memang itu adalah spesialisasi persegi panjang. "Is a" membuat Anda ingin memodelkan ini dengan warisan. Namun jika dalam kode yang Anda buat Squareberasal dari Rectangle, maka Squareharus dapat digunakan di mana saja Anda harapkan Rectangle. Ini membuat beberapa perilaku aneh.

Bayangkan Anda memiliki SetWidthdan SetHeightmetode di Rectanglekelas dasar Anda ; ini tampaknya sangat logis. Namun jika Rectanglereferensi Anda menunjuk ke Square, maka SetWidthdan SetHeighttidak masuk akal karena pengaturan yang satu akan mengubah yang lain untuk mencocokkannya. Dalam hal ini Squaregagal dengan Tes Substitusi Liskov dengan Rectangledan abstraksi memiliki Squarewarisan dari Rectangleyang buruk.

masukkan deskripsi gambar di sini

Kalian harus memeriksa Poster Motivational SOLID Principles Principles yang tak ternilai harganya .


19
@ m-tajam Bagaimana jika itu Rectangle abadi sehingga bukannya SetWidth dan SetHeight, kita memiliki metode GetWidth dan GetHeight sebagai gantinya?
Pacerier

140
Moral cerita: model kelas Anda berdasarkan perilaku bukan pada properti; modelkan data Anda berdasarkan properti dan bukan pada perilaku. Jika berperilaku seperti bebek, itu pasti burung.
Sklivvz

193
Nah, persegi jelas adalah jenis persegi panjang di dunia nyata. Apakah kita dapat memodelkan ini dalam kode kita tergantung pada spesifikasi. Yang ditunjukkan LSP adalah bahwa perilaku subtipe harus cocok dengan perilaku tipe dasar seperti yang didefinisikan dalam spesifikasi tipe dasar. Jika spec tipe dasar persegi panjang mengatakan bahwa tinggi dan lebar dapat diatur secara independen, maka LSP mengatakan bahwa persegi tidak bisa menjadi subtipe persegi panjang. Jika spesifikasi segi empat mengatakan bahwa persegi panjang tidak dapat diubah, maka persegi dapat menjadi subtipe persegi panjang. Ini semua tentang subtipe yang mempertahankan perilaku yang ditentukan untuk tipe dasar.
SteveT

63
@Pacerier tidak ada masalah jika itu tidak berubah. Masalah sebenarnya di sini adalah bahwa kita tidak memodelkan persegi panjang, melainkan "persegi panjang yang dibentuk kembali," yaitu, persegi panjang yang lebar atau tingginya dapat dimodifikasi setelah penciptaan (dan kami masih menganggapnya sebagai objek yang sama). Jika kita melihat kelas persegi panjang dengan cara ini, jelas bahwa persegi bukanlah "persegi panjang yang dibentuk kembali", karena persegi tidak dapat dibentuk kembali dan masih menjadi persegi (secara umum). Secara matematis, kita tidak melihat masalah karena ketidakstabilan bahkan tidak masuk akal dalam konteks matematika.
penanggung jawab

14
Saya punya satu pertanyaan tentang prinsip ini. Mengapa menjadi masalah jika Square.setWidth(int width)diimplementasikan seperti ini this.width = width; this.height = width;:? Dalam hal ini dijamin bahwa lebarnya sama dengan tinggi.
MC Emperor

488

Prinsip Pergantian Liskov (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:

Diagram Kelas

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 ThreeDBoardkelas diperkenalkan yang meluas Board.

Sepintas ini sepertinya keputusan yang bagus. Boardmemberikan baik Heightdan Widthproperti dan ThreeDBoardmenyediakan Z sumbu.

Di mana itu rusak adalah ketika Anda melihat semua anggota lain yang diwarisi Board. Metode untuk AddUnit, GetTile, GetUnitsdan sebagainya, semua mengambil baik X dan parameter Y di Boardkelas tetapi ThreeDBoardmembutuhkan parameter Z juga.

Jadi, Anda harus menerapkan metode itu lagi dengan parameter Z. Parameter Z tidak memiliki konteks ke Boardkelas dan metode yang diwarisi dari Boardkelas kehilangan artinya. Unit kode yang mencoba menggunakan ThreeDBoardkelas sebagai kelas dasarnya Boardakan sangat tidak beruntung.

Mungkin kita harus mencari pendekatan lain. Alih-alih memanjang Board, ThreeDBoardharus terdiri dari Boardobjek. Satu Boardobjek per unit sumbu Z.

Ini memungkinkan kita untuk menggunakan prinsip berorientasi objek yang baik seperti enkapsulasi dan penggunaan kembali dan tidak melanggar LSP.


10
Lihat juga Masalah Circle-Ellipse di Wikipedia untuk contoh yang serupa tetapi lebih sederhana.
Brian

Kutip kembali dari @NotMySelf: "Saya pikir contohnya adalah hanya untuk menunjukkan bahwa mewarisi dari papan tidak masuk akal dengan dalam konteks ThreeDBoard dan semua tanda tangan metode tidak ada artinya dengan sumbu Z.".
Contango

1
Jadi jika kita menambahkan metode lain ke kelas Anak tetapi semua fungsi Parent masih masuk akal di kelas Anak apakah itu melanggar LSP? Karena di satu sisi kita memodifikasi antarmuka untuk menggunakan Anak sedikit di sisi lain jika kita melemparkan Anak menjadi Orang Tua kode yang mengharapkan Orang Tua akan berfungsi dengan baik.
Nickolay Kondratyev

5
Ini adalah contoh anti-Liskov. Liskov membuat kita untuk mendapatkan Rectangle dari Square. Lebih banyak parameter-kelas dari kelas kurang-parameter. Dan Anda telah menunjukkan dengan baik bahwa itu buruk. Ini benar-benar lelucon yang bagus untuk menandai sebagai jawaban dan telah dijatuhkan 200 kali jawaban anti-liskov untuk pertanyaan liskov. Apakah prinsip Liskov benar-benar keliru?
Gangnus

3
Saya telah melihat warisan bekerja dengan cara yang salah. Berikut ini sebuah contoh. Kelas dasar harus 3DBoard dan Dewan kelas turunan. Dewan masih memiliki sumbu Z Max (Z) = Min (Z) = 1
Paulustrious

169

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:

Contoh buruk

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.

Contoh yang baik

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

3
Contoh yang bagus, tetapi apa yang akan Anda lakukan jika klien memiliki Bird bird. Anda harus melemparkan objek ke FlyingBirds untuk menggunakan lalat, yang tidak baik bukan?
Moody

17
Tidak. Jika klien memiliki Bird bird, itu berarti tidak dapat digunakan fly(). Itu dia. Lulus Ducktidak mengubah fakta ini. Jika klien memiliki FlyingBirds bird, maka bahkan jika dilewatkan, Duckitu harus selalu bekerja dengan cara yang sama.
Steve Chamaillard

9
Bukankah ini juga berfungsi sebagai contoh yang baik untuk Segregasi Antarmuka?
Saharsh

Contoh yang bagus Terima kasih Man
Abdelhadi Abdo

6
Bagaimana kalau menggunakan Antarmuka 'Terbang' (tidak bisa memikirkan nama yang lebih baik). Dengan cara ini kita tidak mengikat diri kita ke dalam hierarki yang kaku ini. Kecuali kita tahu benar-benar membutuhkannya.
Thirdy

132

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 Rectangleharus 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.


35
Dan karenanya kesulitan menggunakan "OO" untuk memodelkan apa pun yang mungkin ingin kita modelkan.
DrPizza

9
@DrPizza: Tentu saja. Namun ada dua hal. Pertama, hubungan seperti itu masih dapat dimodelkan dalam OOP, meskipun tidak lengkap atau menggunakan jalan memutar yang lebih kompleks (pilih yang sesuai dengan masalah Anda). Kedua, tidak ada alternatif yang lebih baik. Pemetaan / modelling lain memiliki masalah yang sama atau serupa. ;-)
Konrad Rudolph

7
@NickW Dalam beberapa kasus (tetapi tidak di atas) Anda cukup membalikkan rantai pewarisan - secara logika, titik 2D adalah-titik 3D, di mana dimensi ketiga diabaikan (atau 0 - semua titik terletak pada bidang yang sama di Ruang 3D). Tapi ini tentu saja tidak terlalu praktis. Secara umum, ini adalah salah satu kasus di mana warisan tidak benar-benar membantu, dan tidak ada hubungan alami antara entitas. Model mereka secara terpisah (setidaknya saya tidak tahu cara yang lebih baik).
Konrad Rudolph

7
OOP dimaksudkan untuk memodelkan perilaku dan bukan data. Kelas Anda melanggar enkapsulasi bahkan sebelum melanggar LSP.
Sklivvz

2
@AustinWBryan Yep; semakin lama saya bekerja di bidang ini, semakin saya cenderung menggunakan pewarisan untuk antarmuka dan kelas dasar abstrak saja, dan komposisi untuk sisanya. Kadang-kadang sedikit lebih banyak bekerja (mengetik dengan bijak) tetapi menghindari sejumlah masalah, dan secara luas dikumandangkan saran oleh programmer lain yang berpengalaman.
Konrad Rudolph

77

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 DrawShapefungsi ini terbentuk dengan buruk. Itu harus tahu tentang setiap turunan yang mungkin dari Shapekelas, 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 Rectanglekelas 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 . [...]

Squareakan mewarisi SetWidthdan SetHeightfungsi. Fungsi-fungsi ini sama sekali tidak pantas untuk a Square, 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 mengganti SetWidthdan SetHeight[...]

Tetapi pertimbangkan fungsi berikut:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

Jika kami meneruskan referensi ke Squareobjek ke fungsi ini, the Square objek akan rusak karena ketinggian tidak akan berubah. Ini jelas merupakan pelanggaran terhadap LSP. Fungsi ini tidak berfungsi untuk turunan dari argumennya.

[...]


14
Jauh terlambat, tetapi saya pikir ini adalah kutipan yang menarik dalam makalah itu: 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.
user2023861

@ user2023861 Anda benar sekali. Saya akan menulis jawaban berdasarkan ini.
inf3rno

40

LSP diperlukan ketika beberapa kode menganggapnya memanggil metode tipe T, dan mungkin tanpa sadar menyebut metode tipe S, di mana S extends T(yaitu Smewarisi, 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 Sharus 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 Tiparameter 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 Tooutput 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 Tidan memberikan output ke tipe To. Ketika sebenarnya memanggil metode yang sesuai S, maka setiap Tiargumen input ditugaskan ke Siparameter input, dan Sooutput ditugaskan untuk tipe tersebut To. Jadi, jika Sibukan 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 Tharus 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:

  • Teorema ketidaklengkapan Gödel : teori formal apa pun, di mana semua kebenaran aritmatika dapat dibuktikan, tidak konsisten.
  • Paradoks Russell : setiap aturan keanggotaan untuk himpunan yang dapat berisi himpunan, baik menyebutkan jenis spesifik dari setiap anggota atau berisi itu sendiri. Dengan demikian set tidak dapat diperpanjang atau rekursi tidak terbatas. Sebagai contoh, himpunan segala sesuatu yang bukan teko, termasuk dirinya sendiri, yang mencakup dirinya sendiri, yang mencakup dirinya sendiri, dll…. Dengan demikian aturan tidak konsisten jika (mungkin berisi satu set dan) tidak menyebutkan jenis tertentu (yaitu memungkinkan semua jenis yang tidak ditentukan) dan tidak mengizinkan ekstensi tanpa batas. Ini adalah himpunan set yang bukan anggota dari diri mereka sendiri. Ketidakmampuan untuk menjadi konsisten dan benar-benar disebutkan dalam semua kemungkinan ekstensi, adalah teorema ketidaklengkapan Gödel.
  • Prinsip Substisi Liskov : umumnya merupakan masalah yang tidak dapat diputuskan apakah himpunan bagian merupakan himpunan bagian yang lain, yaitu pewarisan umumnya tidak dapat diputuskan.
  • Referensi Linsky : tidak dapat dipungkiri apakah perhitungan sesuatu itu, ketika dideskripsikan atau dirasakan, yaitu persepsi (kenyataan) tidak memiliki titik referensi absolut.
  • Teorema Coase : tidak ada titik referensi eksternal, sehingga setiap penghalang untuk kemungkinan eksternal yang tidak terbatas akan gagal.
  • Hukum kedua termodinamika : seluruh alam semesta (sistem tertutup, yaitu segalanya) cenderung mengalami gangguan maksimum, yaitu kemungkinan independen maksimum.

17
@ Shelyby: Kamu terlalu banyak campur. Hal-hal yang tidak membingungkan seperti yang Anda nyatakan. Sebagian besar pernyataan teoretis Anda berdiri di atas dasar yang lemah, seperti 'Agar pengetahuan ada, kemungkinan tak terduga banyak ada, .........' DAN 'umumnya merupakan masalah yang tidak dapat diputuskan apakah set adalah subset dari yang lain, yaitu warisan pada umumnya tidak dapat dipastikan '. Anda dapat memulai blog terpisah untuk masing-masing poin ini. Bagaimanapun, pernyataan dan asumsi Anda sangat dipertanyakan. Seseorang tidak boleh menggunakan hal-hal yang tidak disadarinya!
aknon

1
@ aknon Saya punya blog yang menjelaskan hal ini secara lebih mendalam. Model TOE saya tentang ruangwaktu tak terbatas adalah frekuensi tanpa batas. Tidak membingungkan saya bahwa fungsi induktif rekursif memiliki nilai awal yang diketahui dengan batas ujung yang tak terbatas, atau fungsi koinduktif memiliki nilai akhir yang tidak diketahui dan batas awal yang diketahui. Relativitas adalah masalah begitu rekursi diperkenalkan. Inilah sebabnya mengapa Turing selesai setara dengan rekursi tak terbatas .
Shelby Moore III

4
@ShelbyMooreIII Anda terlalu banyak arah. Ini bukan jawaban.
Soldalma

1
@Soldalma itu adalah jawaban. Apakah Anda tidak melihatnya di bagian Jawab. Milik Anda adalah komentar karena ada di bagian komentar.
Shelby Moore III

1
Sukai pencampuran Anda dengan dunia scala!
Ehsan M. Kermani

24

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.


6
Contoh ini tidak melanggar LSP hanya selama kami membatasi semantik Database::selectQueryuntuk 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.
Palec

5
Saya menemukan jawaban ini yang paling mudah untuk dipahami.
Malcolm Salvador

23

Ada daftar periksa untuk menentukan apakah Anda melanggar Liskov atau tidak.

  • Jika Anda melanggar salah satu item berikut -> Anda melanggar Liskov.
  • Jika Anda tidak melanggar -> tidak bisa menyimpulkan apa pun.

Daftar periksa:

  • Tidak ada pengecualian baru yang harus dilemparkan ke kelas turunan : Jika kelas dasar Anda melempar ArgumentNullException maka sub kelas Anda hanya diizinkan untuk melempar pengecualian tipe ArgumentNullException atau pengecualian apa pun yang berasal dari ArgumentNullException. Melempar IndexOutOfRangeException adalah pelanggaran terhadap Liskov.
  • Pra-kondisi tidak dapat diperkuat : Asumsikan kelas dasar Anda berfungsi dengan int anggota. Sekarang subtipe Anda mengharuskan int untuk menjadi positif. Ini diperkuat pra-kondisi, dan sekarang semua kode yang berfungsi dengan baik sebelum dengan int negatif rusak.
  • Pasca kondisi tidak bisa dilemahkan : Asumsikan kelas dasar Anda mengharuskan semua koneksi ke database harus ditutup sebelum metode kembali. Di sub-kelas Anda, Anda mengesampingkan metode itu dan membiarkan koneksi terbuka untuk digunakan kembali. Anda telah melemahkan kondisi setelah metode itu.
  • Invarian harus dilestarikan : Kendala yang paling sulit dan menyakitkan untuk dipenuhi. Invarian beberapa waktu tersembunyi di kelas dasar dan satu-satunya cara untuk mengungkapkannya adalah dengan membaca kode kelas dasar. Pada dasarnya Anda harus yakin ketika Anda mengganti metode apa pun yang tidak dapat diubah harus tetap tidak berubah setelah metode yang diganti dijalankan. Hal terbaik yang dapat saya pikirkan adalah untuk menegakkan batasan invarian ini di kelas dasar tetapi itu tidak akan mudah.
  • 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:


Saya seorang pengembang C # juga dan saya akan mengatakan pernyataan terakhir Anda tidak benar pada Visual Studio 2010, dengan kerangka .Net 4.0. Kovarian jenis pengembalian memungkinkan untuk jenis kembali lebih diturunkan daripada apa yang didefinisikan oleh antarmuka. Contoh: Contoh: IEnumerable <T> (T is covariant) IEnumerable <T> (T is covariant) IQueryable <T> (T is covariant) IGrouping <TKey, TElement> (TKey dan TElement adalah kovarian) IComparer <T> (T adalah contravariant) IEqualityComparer <T> (T contravariant) IComparable <T> (T contravariant) msdn.microsoft.com/en-us/library/dd233059(v=vs.100).aspx
LCarter

1
Jawaban yang bagus dan terfokus (meskipun pertanyaan awal tentang contoh lebih dari aturan).
Mike

22

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.


9
Tetapi ... jika Anda selalu mendapatkan perilaku yang sama, lalu apa gunanya memiliki kelas turunan?
Leonid

2
Anda melewatkan satu poin: itu adalah perilaku yang diamati sama . Anda mungkin, misalnya mengganti sesuatu dengan kinerja O (n) dengan sesuatu yang setara secara fungsional, tetapi dengan kinerja O (lg n). Atau Anda dapat mengganti sesuatu yang mengakses data yang diimplementasikan dengan MySQL dan menggantinya dengan database di memori.
Charlie Martin

@ Chris Martin, pengkodean ke antarmuka daripada implementasi - Saya menggali itu. Ini tidak unik untuk OOP; bahasa fungsional seperti Clojure juga mempromosikannya. Bahkan dalam hal Java atau C #, saya pikir bahwa menggunakan antarmuka daripada menggunakan kelas abstrak plus hirarki kelas akan alami untuk contoh yang Anda berikan. Python tidak diketik dengan kuat dan tidak benar-benar memiliki antarmuka, setidaknya tidak secara eksplisit. Kesulitan saya adalah bahwa saya telah melakukan OOP selama beberapa tahun tanpa mematuhi SOLID. Sekarang saya menemukan itu, tampaknya membatasi dan hampir saling bertentangan.
Hamish Grubijan

Nah, Anda harus kembali dan memeriksa kertas asli Barbara. report-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps Itu tidak benar-benar dinyatakan dalam hal antarmuka, dan itu adalah hubungan logis yang memegang (atau tidak) dalam bahasa pemrograman yang memiliki beberapa bentuk warisan.
Charlie Martin

1
@HamishGrubijan Saya tidak tahu siapa yang memberi tahu Anda bahwa Python tidak diketik dengan kuat, tetapi mereka berbohong kepada Anda (dan jika Anda tidak percaya, jalankan juru bahasa Python dan coba 2 + "2"). Mungkin Anda bingung "sangat diketik" dengan "diketik secara statis"?
penanggung jawab

21

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/


20

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.


19

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:

  1. 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.

  2. Metode Aturan: Implementasi dari operasi-operasi tersebut secara semantik sehat.

    • Prasyarat yang lebih lemah: Fungsi subtipe harus mengambil setidaknya apa yang diambil supertipe sebagai input, jika tidak lebih.
    • Kondisi Postingan yang Lebih Kuat: Mereka harus menghasilkan subset dari output metode supertype yang dihasilkan.
  3. Properti Aturan: Ini melampaui panggilan fungsi individu.

    • Invarian: Hal-hal yang selalu benar harus tetap benar. Misalnya. Ukuran suatu set tidak pernah negatif.
    • Properti Evolusi: Biasanya ada hubungannya dengan kekekalan atau jenis keadaan objek bisa masuk. Atau mungkin objek hanya tumbuh dan tidak pernah menyusut sehingga metode subtipe seharusnya tidak membuatnya.

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


18

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": ATestKelas 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? "


14

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() { ... }
}

9

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.


2
Erm, formulasi itu milik Barbara Liskov. Barbara Liskov, "Abstraksi Data dan Hirarki," SIGPLAN Notices, 23,5 (Mei, 1988). Ini bukan "terlalu kuat", "tepat", dan tidak memiliki implikasi seperti yang Anda pikirkan. Itu kuat, tetapi memiliki jumlah kekuatan yang tepat.
DrPizza

Lalu, ada sedikit subtipe dalam kehidupan nyata :)
Damien Pollet

3
"Perilaku tidak berubah" tidak berarti bahwa subtipe akan memberi Anda nilai hasil konkret yang sama persis. Ini berarti bahwa perilaku subtipe cocok dengan apa yang diharapkan dalam tipe dasar. Contoh: tipe dasar Bentuk dapat memiliki metode draw () dan menetapkan bahwa metode ini harus membuat bentuk. Dua subtipe Shape (mis. Square dan Circle) akan mengimplementasikan metode draw () dan hasilnya akan terlihat berbeda. Tetapi selama perilaku (rendering bentuk) cocok dengan perilaku Shape yang ditentukan, maka Square dan Circle akan menjadi subtipe Shape sesuai dengan LSP.
SteveT

9

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.


9

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


8

Prinsip Substitusi Liskov

  • Metode yang diganti tidak boleh tetap kosong
  • Metode yang diganti tidak boleh membuat kesalahan
  • Kelas dasar atau perilaku antarmuka tidak boleh dilakukan untuk modifikasi (pengerjaan ulang) karena perilaku kelas yang diturunkan.

7

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:

  • Varian kelas dasar harus dipertahankan oleh kelas turunan
  • Pra-kondisi kelas dasar tidak harus diperkuat oleh kelas turunan
  • Pasca kondisi kelas dasar tidak boleh dilemahkan oleh kelas turunan.

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


6

LSP secara sederhana menyatakan bahwa objek dari superclass yang sama harus dapat saling bertukar tanpa melanggar apa pun.

Sebagai contoh, jika kita memiliki Catdan Dogkelas turunan dari Animalkelas, setiap fungsi menggunakan kelas Hewan harus dapat menggunakan Catatau Dogdan bersikap normal.


4

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).


1
Saya pikir contohnya adalah hanya untuk menunjukkan bahwa mewarisi dari papan tidak masuk akal dengan dalam konteks ThreeDBoard dan semua tanda tangan metode tidak ada artinya dengan sumbu Z.
NotMyself

4

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.


4

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.


3

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.


1
Berikan contoh kode pada stackoverflow.
sebenalern

3

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 Squarekelas juga berdasarkan info ini:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

Jika kita mengganti Rectangledengan Squaredalam 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 Squarememiliki prasyarat baru kami tidak memiliki dalam Rectanglekelas: width == height. Menurut LSP Rectangleinstance harus diganti dengan Rectangleinstance subclass. Ini karena instance ini melewatkan pemeriksaan tipe untuk Rectangleinstance 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.


3

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();
}

2

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.


2

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.


2

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.


1

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.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.