LSP vs OCP / Liskov Substitusi VS Buka Tutup

48

Saya mencoba untuk memahami prinsip-prinsip SOLID dari OOP dan saya sampai pada kesimpulan bahwa LSP dan OCP memiliki beberapa kesamaan (jika tidak untuk mengatakan lebih banyak).

prinsip terbuka / tertutup menyatakan "entitas perangkat lunak (kelas, modul, fungsi, dll.) harus terbuka untuk ekstensi, tetapi ditutup untuk modifikasi".

LSP dalam kata-kata sederhana menyatakan bahwa setiap instance Foodapat diganti dengan instance Barmana saja yang berasal dari Foodan program akan bekerja dengan cara yang sama.

Saya bukan programmer OOP pro, tetapi bagi saya tampaknya LSP hanya mungkin jika Bar, berasal dari Footidak mengubah apa pun di dalamnya tetapi hanya meluas. Itu berarti bahwa dalam program tertentu LSP benar hanya ketika OCP benar dan OCP benar hanya jika LSP benar. Itu berarti mereka setara.

Koreksi saya jika saya salah. Saya benar-benar ingin memahami ide-ide ini. Terima kasih banyak atas jawabannya.

Kolyunya
sumber
4
Ini adalah interpretasi yang sangat sempit dari kedua konsep. Buka / tutup dapat dipertahankan namun masih melanggar LSP. Contoh Rectangle / Square atau Ellipse / Circle adalah ilustrasi yang baik. Keduanya mematuhi OCP, namun keduanya melanggar LSP.
Joel Etherton
1
Dunia (atau setidaknya internet) bingung akan hal ini. kirkk.com/modularity/2009/12/solid-principles-of-class-design . Orang ini mengatakan pelanggaran terhadap LSP juga merupakan pelanggaran OCP. Dan kemudian dalam buku "Desain Rekayasa Perangkat Lunak: Teori dan Praktek" di halaman 156 penulis memberikan contoh sesuatu yang mematuhi OCP tetapi melanggar LSP. Saya sudah menyerah pada ini.
Manoj R
@ JoelEtherton Pasangan-pasangan itu hanya melanggar LSP jika mereka bisa berubah. Dalam kasus yang tidak dapat diubah, berasal Squaredari Rectangletidak melanggar LSP. (Tapi itu mungkin masih desain yang buruk dalam kasus abadi karena Anda dapat memiliki kuadrat Rectangleyang bukan Squareyang tidak cocok dengan matematika)
CodesInChaos
Analogi sederhana (dari sudut pandang penulis-pengguna perpustakaan). LSP seperti menjual produk (perpustakaan) yang mengklaim menerapkan 100% dari apa yang dikatakannya (pada antarmuka, atau manual), tetapi sebenarnya tidak (atau tidak cocok dengan apa yang dikatakan). OCP seperti menjual produk (perpustakaan) dengan janji dapat ditingkatkan (diperluas) saat fungsionalitas baru keluar (seperti firmware), tetapi sebenarnya tidak dapat ditingkatkan tanpa layanan pabrik.
rwong

Jawaban:

119

Astaga, ada beberapa kesalahpahaman aneh tentang apa yang OCP dan LSP dan beberapa karena ketidakcocokan beberapa terminologi dan contoh membingungkan. Kedua prinsip hanyalah "hal yang sama" jika Anda menerapkannya dengan cara yang sama. Pola biasanya mengikuti prinsip dalam satu atau lain cara dengan sedikit pengecualian.

Perbedaan akan dijelaskan lebih jauh ke bawah tetapi pertama-tama mari kita selami prinsip-prinsip itu sendiri:

Prinsip Terbuka-Tertutup (OCP)

Menurut Paman Bob :

Anda harus dapat memperluas perilaku kelas, tanpa memodifikasinya.

Perhatikan bahwa kata extended dalam kasus ini tidak selalu berarti bahwa Anda harus mensubklasifikasikan kelas aktual yang memerlukan perilaku baru. Lihat bagaimana saya sebutkan di mismatch pertama terminologi? Kata kunci extendhanya berarti subkelas di Jawa, tetapi prinsip-prinsipnya lebih tua dari Jawa.

Asli berasal dari Bertrand Meyer pada tahun 1988:

Entitas perangkat lunak (kelas, modul, fungsi, dll.) Harus terbuka untuk ekstensi, tetapi ditutup untuk modifikasi.

Di sini jauh lebih jelas bahwa prinsip tersebut diterapkan pada entitas perangkat lunak . Contoh buruk akan menimpa entitas perangkat lunak saat Anda memodifikasi kode sepenuhnya alih-alih memberikan beberapa titik ekstensi. Perilaku entitas perangkat lunak itu sendiri harus dapat diperluas dan contoh yang baik untuk hal ini adalah penerapan pola-Strategi (karena ini adalah cara termudah untuk ditunjukkan dari kumpulan pola GoF IMHO):

// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {

    // Context is however open for extension through
    // this private field
    private IBehavior behavior;

    // The context calls the behavior in this public 
    // method. If you want to change this you need
    // to implement it in the IBehavior object
    public void doStuff() {
        if (this.behavior != null)
            this.behavior.doStuff();
    }

    // You can dynamically set a new behavior at will
    public void setBehavior(IBehavior behavior) {
        this.behavior = behavior;
    }
}

// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
    public void doStuff();
}

Pada contoh di atas Contextadalah dikunci untuk modifikasi lebih lanjut. Sebagian besar programmer mungkin ingin membuat subkelas kelas untuk memperluasnya, tetapi di sini kita tidak melakukannya karena mengasumsikan perilakunya dapat diubah melalui apa pun yang mengimplementasikan IBehaviorantarmuka.

Yakni kelas konteks ditutup untuk modifikasi tetapi terbuka untuk ekstensi . Ini sebenarnya mengikuti prinsip dasar lain karena kita menempatkan perilaku dengan komposisi objek daripada pewarisan:

"Pilih ' komposisi objek ' daripada ' kelas warisan '." (Gang Empat Four 1995: 20)

Saya akan membiarkan pembaca membaca tentang prinsip itu karena berada di luar ruang lingkup pertanyaan ini. Untuk melanjutkan dengan contoh, katakan kita memiliki implementasi antarmuka IBehavior berikut:

public class HelloWorldBehavior implements IBehavior {
    public void doStuff() {
        System.println("Hello world!");
    }
}

public class GoodByeBehavior implements IBehavior {
    public void doStuff() {
        System.out.println("Good bye cruel world!");
    }
}

Dengan menggunakan pola ini kita dapat memodifikasi perilaku konteks saat runtime, melalui setBehaviormetode sebagai titik ekstensi.

// in your main method
Context c = new Context();

c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"

c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"

Jadi, kapan pun Anda ingin memperluas kelas konteks "tertutup", lakukan dengan mensubklasifikasikan ketergantungan "terbuka" itu. Ini jelas bukan hal yang sama dengan subklasifikasi konteks itu sendiri tetapi OCP. LSP tidak menyebutkan tentang ini juga.

Memperluas dengan Mixin Alih-alih Warisan

Ada cara lain untuk melakukan OCP selain dari subklasifikasi. Salah satu caranya adalah menjaga kelas Anda tetap terbuka untuk ekstensi melalui penggunaan mixin . Ini berguna misalnya dalam bahasa yang berbasis prototipe daripada berbasis kelas. Idenya adalah untuk mengubah objek dinamis dengan lebih banyak metode atau atribut yang diperlukan, dengan kata lain objek yang menyatu atau "bercampur" dengan objek lain.

Berikut ini adalah contoh javascript dari mixin yang merender template HTML sederhana untuk jangkar:

// The mixin, provides a template for anchor HTML elements, i.e. <a>
var LinkMixin = {
    render: function() {
        return '<a href="' + this.link +'">'
            + this.content 
            + '</a>;
    }
}

// Constructor for a youtube link
var YoutubeLink = function(content, youtubeId) {
    this.content = content;
    this.setLink(this.youtubeId);
};
// Methods are added to the prototype
YoutubeLink.prototype = {
    setLink: function(youtubeid) {
        this.link = 'http://www.youtube.com/watch?v=' + youtubeid;
    }
};
// Extend YoutubeLink prototype with the LinkMixin using
// underscore/lodash extend
_.extend(YoutubeLink.protoype, LinkMixin);

// When used:
var ytLink = new YoutubeLink("Cool Movie!", "idOaZpX8lnA");

console.log(ytLink.render());
// will output: 
// <a href="http://www.youtube.com/watch?=vidOaZpX8lnA">Cool Movie!</a>

Idenya adalah untuk memperluas objek secara dinamis dan keuntungannya adalah bahwa objek dapat berbagi metode bahkan jika mereka berada dalam domain yang sama sekali berbeda. Dalam kasus di atas, Anda dapat dengan mudah membuat jangkar html jenis lain dengan memperluas implementasi spesifik Anda dengan LinkMixin.

Dalam hal OCP, "mixin" adalah ekstensi. Dalam contoh di atas YoutubeLinkadalah entitas perangkat lunak kami yang ditutup untuk modifikasi, tetapi terbuka untuk ekstensi melalui penggunaan mixin. Hirarki objek diratakan yang membuatnya tidak mungkin untuk memeriksa jenis. Namun ini bukan benar-benar hal yang buruk, dan saya akan menjelaskan lebih jauh ke bawah bahwa memeriksa jenis umumnya adalah ide yang buruk dan merusak ide dengan polimorfisme.

Perhatikan bahwa dimungkinkan untuk melakukan banyak pewarisan dengan metode ini karena sebagian besar extendimplementasi dapat menggabungkan beberapa objek:

_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);

Satu-satunya hal yang perlu Anda ingat adalah untuk tidak bertabrakan nama, yaitu mixin kebetulan mendefinisikan nama yang sama dari beberapa atribut atau metode karena mereka akan diganti. Dalam pengalaman saya yang sederhana ini adalah masalah non-dan jika itu terjadi itu merupakan indikasi desain yang cacat.

Prinsip Pergantian Liskov (LSP)

Paman Bob mendefinisikannya hanya dengan:

Kelas turunan harus dapat diganti untuk kelas dasar mereka.

Prinsip ini sudah tua, pada kenyataannya definisi Paman Bob tidak membedakan prinsip-prinsip karena membuat LSP masih terkait erat dengan OCP oleh fakta bahwa, dalam contoh Strategi di atas, supertipe yang sama digunakan ( IBehavior). Jadi mari kita lihat definisi asli dari Barbara Liskov dan lihat apakah kita dapat menemukan sesuatu yang lain tentang prinsip ini yang terlihat seperti teorema matematika:

Apa yang diinginkan di sini adalah sesuatu seperti properti substitusi berikut: Jika untuk setiap objek o1tipe Sada objek o2tipe Tsehingga untuk semua program yang Pdidefinisikan dalam hal T, perilaku Ptidak berubah ketika o1diganti untuk o2kemudian Sadalah subtipe dari T.

Mari kita angkat bahu untuk sementara waktu, perhatikan karena tidak menyebutkan kelas sama sekali. Dalam JavaScript Anda benar-benar dapat mengikuti LSP meskipun tidak berbasis kelas secara eksplisit. Jika program Anda memiliki daftar setidaknya beberapa objek JavaScript yang:

  • perlu dihitung dengan cara yang sama,
  • memiliki perilaku yang sama, dan
  • sebaliknya dalam beberapa hal sama sekali berbeda

... maka objek dianggap memiliki "tipe" yang sama dan tidak terlalu penting untuk program. Ini pada dasarnya adalah polimorfisme . Dalam arti umum; Anda tidak perlu tahu subtipe yang sebenarnya jika Anda menggunakan antarmuka itu. OCP tidak mengatakan sesuatu yang eksplisit tentang ini. Ini juga menunjukkan kesalahan desain yang dilakukan oleh kebanyakan programmer pemula:

Setiap kali Anda merasakan keinginan untuk memeriksa subtipe suatu objek, kemungkinan besar Anda melakukannya SALAH.

Oke, jadi itu mungkin tidak salah sepanjang waktu, tetapi jika Anda memiliki keinginan untuk melakukan beberapa jenis pengecekan dengan instanceofatau enum, Anda mungkin melakukan program sedikit lebih berbelit-belit untuk diri sendiri daripada yang seharusnya. Tetapi ini tidak selalu terjadi; peretasan yang cepat dan kotor untuk membuat semuanya berfungsi adalah konsesi yang baik untuk dibuat di benak saya jika solusinya cukup kecil, dan jika Anda mempraktikkan refactoring tanpa ampun , itu dapat ditingkatkan setelah perubahan menuntutnya.

Ada beberapa cara untuk mengatasi "kesalahan desain" ini, tergantung pada masalah sebenarnya:

  • Kelas super tidak memanggil prasyarat, memaksa penelepon untuk melakukannya.
  • Kelas super tidak memiliki metode generik yang dibutuhkan pemanggil.

Kedua hal ini adalah "kesalahan" desain kode yang umum. Ada beberapa refactoring berbeda yang dapat Anda lakukan, seperti metode pull-up , atau refactor untuk pola seperti pola Pengunjung .

Saya sebenarnya sangat menyukai pola Pengunjung karena dapat menangani spageti if-statement yang besar dan lebih mudah diterapkan daripada apa yang Anda pikirkan pada kode yang ada. Katakanlah kita memiliki konteks berikut:

public class Context {

    public void doStuff(string query) {

        // outcome no. 1
        if (query.Equals("Hello")) {
            System.out.println("Hello world!");
        } 

        // outcome no. 2
        else if (query.Equals("Bye")) {
            System.out.println("Good bye cruel world!");
        }

        // a change request may require another outcome...

    }

}

// usage:
Context c = new Context();

c.doStuff("Hello");
// prints "Hello world"

c.doStuff("Bye");
// prints "Bye"

Hasil dari pernyataan jika dapat diterjemahkan ke pengunjung mereka sendiri karena masing-masing tergantung pada beberapa keputusan dan beberapa kode untuk dijalankan. Kita dapat mengekstrak ini seperti ini:

public interface IVisitor {
    public bool canDo(string query);
    public void doStuff();
}

// outcome 1
public class HelloVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Hello");
    }
    public void doStuff() {
         System.out.println("Hello World");
    }
}

// outcome 2
public class ByeVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Bye");
    }
    public void doStuff() {
        System.out.println("Good bye cruel world");
    }
}

Pada titik ini, jika pemrogram tidak tahu tentang pola Pengunjung, ia malah menerapkan kelas Konteks untuk memeriksa apakah itu dari jenis tertentu. Karena kelas Pengunjung memiliki canDometode boolean , implementor dapat menggunakan pemanggilan metode itu untuk menentukan apakah itu adalah objek yang tepat untuk melakukan pekerjaan itu. Kelas konteks dapat menggunakan semua pengunjung (dan menambahkan yang baru) seperti ini:

public class Context {
    private ArrayList<IVisitor> visitors = new ArrayList<IVisitor>();

    public Context() {
        visitors.add(new HelloVisitor());
        visitors.add(new ByeVisitor());
    }

    // instead of if-statements, go through all visitors
    // and use the canDo method to determine if the 
    // visitor object is the right one to "visit"
    public void doStuff(string query) {
        for(IVisitor visitor : visitors) {
            if (visitor.canDo(query)) {
                visitor.doStuff();
                break;
                // or return... it depends if you have logic 
                // after this foreach loop
            }
        }
    }

    // dynamically adds new visitors
    public void addVisitor(IVisitor visitor) {
        if (visitor != null)
            visitors.add(visitor);
    }
}

Kedua pola mengikuti OCP dan LSP, namun keduanya menunjukkan berbagai hal tentang mereka. Jadi bagaimana kode terlihat jika melanggar salah satu prinsip?

Melanggar satu prinsip tetapi mengikuti yang lain

Ada cara untuk melanggar salah satu prinsip tetapi masih ada yang lain yang harus diikuti. Contoh di bawah ini sepertinya dibuat-buat, untuk alasan yang baik, tetapi saya benar-benar melihat ini muncul dalam kode produksi (dan bahkan lebih buruk):

Mengikuti OCP tetapi tidak LSP

Katakanlah kita memiliki kode yang diberikan:

public interface IPerson {}

public class Boss implements IPerson {
    public void doBossStuff() { ... }
}

public class Peon implements IPerson {
    public void doPeonStuff() { ... }
}

public class Context {
    public Collection<IPerson> getPersons() { ... }
}

Sepotong kode ini mengikuti prinsip buka-tutup. Jika kita memanggil metode konteks GetPersons, kita akan mendapatkan banyak orang dengan implementasi mereka sendiri. Itu berarti bahwa IPerson ditutup untuk modifikasi, tetapi terbuka untuk perpanjangan. Namun segala sesuatunya berubah menjadi gelap ketika kita harus menggunakannya:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // now we have to check the type... :-P
    if (person instanceof Boss) {
        ((Boss) person).doBossStuff();
    }
    else if (person instanceof Peon) {
        ((Peon) person).doPeonStuff();
    }
}

Anda harus melakukan pengecekan tipe dan konversi tipe! Ingat bagaimana saya sebutkan di atas bagaimana pengecekan tipe adalah hal yang buruk ? Oh tidak! Tapi jangan takut, seperti juga disebutkan di atas baik melakukan pull-up refactoring atau menerapkan pola Pengunjung. Dalam hal ini kita cukup melakukan pull up refactoring setelah menambahkan metode umum:

public class Boss implements IPerson {
    // we're adding this general method
    public void doStuff() {
        // that does the call instead
        this.doBossStuff();
    }
    public void doBossStuff() { ... }
}


public interface IPerson {
    // pulled up method from Boss
    public void doStuff();
}

// do the same for Peon

Keuntungannya sekarang adalah Anda tidak perlu lagi mengetahui jenis pastinya, mengikuti LSP:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // yay, no type checking!
    person.doStuff();
}

Mengikuti LSP tetapi tidak OCP

Mari kita lihat beberapa kode yang mengikuti LSP tetapi tidak OCP, itu agak dibuat-buat tapi tetap saya yang ini kesalahan yang sangat halus:

public class LiskovBase {
    public void doStuff() {
        System.out.println("My name is Liskov");
    }
}

public class LiskovSub extends LiskovBase {
    public void doStuff() {
        System.out.println("I'm a sub Liskov!");
    }
}

public class Context {
    private LiskovBase base;

    // the good stuff
    public void doLiskovyStuff() {
        base.doStuff();
    }

    public void setBase(LiskovBase base) { this.base = base }
}

Kode tidak LSP karena konteksnya dapat menggunakan LiskovBase tanpa mengetahui tipe yang sebenarnya. Anda akan berpikir kode ini mengikuti OCP juga tetapi perhatikan dengan teliti, apakah kelasnya benar-benar tertutup ? Bagaimana jika doStuffmetode ini lebih dari sekadar mencetak satu baris?

Jawabannya jika mengikuti OCP adalah sederhana: TIDAK , itu bukan karena dalam desain objek ini kita diharuskan untuk menimpa kode sepenuhnya dengan sesuatu yang lain. Ini membuka cut-and-paste kaleng cacing karena Anda harus menyalin kode dari kelas dasar untuk membuat semuanya berfungsi. The doStuffMetode yakin ini terbuka untuk ekstensi, tapi itu tidak benar-benar tertutup untuk modifikasi.

Kita dapat menerapkan pola metode Templat pada ini. Pola metode templat sangat umum dalam kerangka kerja yang Anda mungkin telah menggunakannya tanpa menyadarinya (misalnya komponen java swing, formulir dan komponen c #, dll.). Inilah salah satu cara untuk menutup doStuffmetode modifikasi dan memastikannya tetap ditutup dengan menandainya dengan finalkata kunci java . Kata kunci itu mencegah siapa pun dari subklas kelas lebih lanjut (dalam C # Anda dapat menggunakan sealeduntuk melakukan hal yang sama).

public class LiskovBase {
    // this is now a template method
    // the code that was duplicated
    public final void doStuff() {
        System.out.println(getStuffString());
    }

    // extension point, the code that "varies"
    // in LiskovBase and it's subclasses
    // called by the template method above
    // we expect it to be virtual and overridden
    public string getStuffString() {
        return "My name is Liskov";
    }
}

public class LiskovSub extends LiskovBase {
    // the extension overridden
    // the actual code that varied
    public string getStuffString() {
        return "I'm sub Liskov!";
    }
}

Contoh ini mengikuti OCP dan tampak konyol, tetapi bayangkan ini ditingkatkan dengan lebih banyak kode untuk ditangani. Saya terus melihat kode yang digunakan dalam produksi di mana subclass sepenuhnya menimpa segalanya dan kode yang ditimpa sebagian besar dipotong-n-disisipkan antara implementasi. Ini bekerja, tetapi karena dengan semua duplikasi kode juga merupakan set-up untuk mimpi buruk pemeliharaan.

Kesimpulan

Saya harap ini semua membersihkan beberapa pertanyaan tentang OCP dan LSP dan perbedaan / kesamaan di antara mereka. Mudah untuk mengabaikannya sebagai hal yang sama tetapi contoh di atas harus menunjukkan bahwa mereka tidak sama.

Perhatikan bahwa, kumpulkan dari kode contoh di atas:

  • OCP adalah tentang mengunci kode kerja tetapi tetap tetap membukanya dengan beberapa titik ekstensi.

    Ini untuk menghindari duplikasi kode dengan merangkum kode yang berubah seperti contoh pola Metode Templat. Ini juga memungkinkan untuk gagal puasa karena melanggar perubahan menyakitkan (yaitu mengubah satu tempat, memecahnya di tempat lain). Demi pemeliharaan, konsep enkapsulasi perubahan adalah hal yang baik, karena perubahan selalu terjadi.

  • LSP adalah tentang membiarkan pengguna menangani objek berbeda yang mengimplementasikan supertype tanpa memeriksa apa tipe sebenarnya mereka. Inilah yang menjadi sifat polimorfisme .

    Prinsip ini memberikan alternatif untuk melakukan pengecekan tipe dan konversi tipe, yang tidak dapat dilakukan ketika jumlah tipe bertambah, dan dapat dicapai melalui pull-up refactoring atau menerapkan pola seperti Visitor.

Spoike
sumber
7
Ini adalah penjelasan yang baik, karena tidak terlalu menyederhanakan OCP dengan menyiratkannya selalu berarti implementasi oleh warisan. Penyederhanaan berlebihan itulah yang menyatukan OCP dan SRP dalam benak sebagian orang, padahal sebenarnya mereka bisa menjadi dua konsep yang sepenuhnya terpisah.
Eric King
5
Ini adalah salah satu jawaban pertukaran stack terbaik yang pernah saya lihat. Saya berharap saya bisa membesarkannya 10 kali. Bagus, dan terima kasih atas penjelasannya.
Bob Horn
Di sana, saya menambahkan uraian tentang Javascript yang bukan bahasa pemrograman berbasis kelas tetapi masih dapat mengikuti LSP dan mengedit teks sehingga mudah-mudahan membaca lebih lancar. Fiuh!
Spoike
Meskipun kutipan Anda dari Paman Bob dari LSP benar (sama seperti situs webnya), bukankah seharusnya sebaliknya? Bukankah seharusnya menyatakan bahwa "Kelas dasar harus dapat diganti untuk kelas turunannya"? Pada LSP tes "kompatibilitas" dilakukan terhadap kelas turunan dan bukan basis. Namun, saya bukan penutur asli bahasa Inggris dan saya pikir mungkin ada beberapa detail tentang frasa yang mungkin saya lewatkan.
Alpha
@Alpha: Itu pertanyaan yang bagus. Kelas dasar selalu dapat diganti dengan kelas turunannya atau warisan tidak akan berfungsi. Kompiler (setidaknya dalam Java dan C #) akan mengeluh jika Anda meninggalkan anggota (metode atau atribut / bidang) dari kelas tambahan yang perlu diimplementasikan. LSP dimaksudkan agar Anda tidak menambahkan metode yang hanya tersedia secara lokal pada kelas turunan, karena itu mengharuskan pengguna kelas turunan untuk mengetahuinya. Ketika kode bertambah, metode seperti itu akan sulit dipertahankan.
Spoike
15

Ini adalah sesuatu yang menyebabkan banyak kebingungan. Saya lebih suka mempertimbangkan prinsip-prinsip ini secara filosofis, karena ada banyak contoh berbeda untuk mereka, dan kadang-kadang contoh nyata tidak benar-benar menangkap seluruh esensi mereka.

Apa yang OCP coba perbaiki

Katakanlah kita perlu menambahkan fungsionalitas ke program yang diberikan. Cara termudah untuk melakukannya, terutama bagi orang-orang yang dilatih untuk berpikir secara prosedural, adalah menambahkan klausa if di mana pun dibutuhkan, atau semacamnya.

Masalah dengan itu adalah

  1. Ini mengubah aliran kode kerja yang ada.
  2. Ini memaksa percabangan bersyarat baru pada setiap kasus. Misalnya, Anda memiliki daftar buku, dan beberapa di antaranya sedang dijual, dan Anda ingin mengulanginya semua dan mencetak harganya, sehingga jika sedang dijual, harga yang dicetak akan termasuk string " (DIJUAL)".

Anda dapat melakukan ini dengan menambahkan bidang tambahan ke semua buku bernama "is_on_sale", dan kemudian Anda dapat memeriksa bidang itu saat mencetak harga buku apa pun, atau sebagai alternatif , Anda dapat membuat instance buku penjualan dari database menggunakan jenis yang berbeda, yang dicetak "(DIJUAL)" dalam string harga (bukan desain yang sempurna tetapi memberikan titik pulang).

Masalah dengan solusi prosedural yang pertama, adalah bidang ekstra untuk setiap buku, dan kompleksitas berlebihan yang berlebihan dalam banyak kasus. Solusi kedua hanya memaksa logika di mana sebenarnya diperlukan.

Sekarang pertimbangkan fakta bahwa mungkin ada banyak kasus di mana data dan logika yang berbeda diperlukan, dan Anda akan melihat mengapa mengingat OCP saat merancang kelas Anda, atau bereaksi terhadap perubahan persyaratan, adalah ide yang bagus.

Sekarang Anda harus mendapatkan ide utama: Cobalah untuk menempatkan diri Anda dalam situasi di mana kode baru dapat diimplementasikan sebagai ekstensi polimorfik, bukan modifikasi prosedural.

Tetapi jangan pernah takut untuk menganalisis konteksnya, dan lihat apakah kekurangannya melebihi manfaatnya, karena bahkan prinsip seperti OCP dapat membuat kekacauan 20-kelas dari program 20-line, jika tidak ditangani dengan hati-hati .

Apa yang LSP coba perbaiki

Kita semua suka menggunakan kembali kode. Suatu penyakit yang mengikuti adalah bahwa banyak program tidak memahaminya sepenuhnya, ke titik di mana mereka secara membabi buta memfaktorkan garis-garis kode yang umum hanya untuk menciptakan kompleksitas yang tidak dapat dibaca dan penggabungan erat yang berlebihan antar modul yang, selain beberapa baris kode, tidak memiliki kesamaan sejauh pekerjaan konseptual yang harus dilakukan berjalan.

Contoh terbesar dari hal ini adalah antarmuka digunakan kembali . Anda mungkin menyaksikannya sendiri; kelas mengimplementasikan antarmuka, bukan karena itu implementasi logis dari itu (atau ekstensi dalam kasus kelas dasar beton), tetapi karena metode itu menyatakan pada saat itu memiliki tanda tangan yang tepat sejauh yang bersangkutan.

Tapi kemudian Anda menemui masalah. Jika kelas mengimplementasikan antarmuka hanya dengan mempertimbangkan tanda tangan dari metode yang mereka nyatakan, maka Anda mendapati diri Anda dapat meneruskan instance kelas dari satu fungsi konseptual ke tempat-tempat yang menuntut fungsionalitas yang sama sekali berbeda, yang hanya bergantung pada tanda tangan yang serupa.

Itu tidak terlalu mengerikan, tetapi menyebabkan banyak kebingungan, dan kami memiliki teknologi untuk mencegah diri dari membuat kesalahan seperti ini. Yang perlu kita lakukan adalah memperlakukan antarmuka sebagai API + Protokol . API jelas dalam deklarasi, dan protokolnya jelas dalam penggunaan antarmuka yang ada. Jika kita memiliki 2 protokol konseptual yang berbagi API yang sama, protokol tersebut harus direpresentasikan sebagai 2 antarmuka yang berbeda. Kalau tidak, kita terjebak dalam dogmatisme KERING dan, ironisnya, hanya membuat lebih sulit untuk mempertahankan kode.

Sekarang Anda harus dapat memahami definisi dengan sempurna. LSP mengatakan: Jangan mewarisi dari kelas dasar dan mengimplementasikan fungsionalitas dalam sub-kelas yang, tempat lain, yang bergantung pada kelas dasar, tidak akan cocok.

Yam Marcovic
sumber
1
Saya mendaftar hanya untuk dapat memilih ini dan jawaban Spoike - pekerjaan bagus.
David Culp
7

Dari pengertian saya:

OCP mengatakan: "Jika Anda akan menambahkan fungsionalitas baru, buat kelas baru dengan memperluas yang sudah ada, daripada mengubahnya."

LSP mengatakan: "Jika Anda membuat kelas baru yang memperluas kelas yang sudah ada, pastikan itu benar-benar dapat dipertukarkan dengan basisnya."

Jadi saya pikir mereka saling melengkapi tetapi mereka tidak setara.

henginy
sumber
4

Meskipun benar bahwa OCP dan LSP keduanya berkaitan dengan modifikasi, jenis modifikasi yang dibicarakan oleh OCP bukanlah yang dibicarakan oleh LSP.

Memodifikasi berkaitan dengan OCP adalah tindakan fisik dari pengembang menulis kode di kelas yang ada.

LSP berurusan dengan modifikasi perilaku yang diturunkan oleh kelas turunan dibandingkan dengan kelas dasarnya, dan perubahan runtime dari eksekusi program yang dapat disebabkan oleh penggunaan subclass bukan superclass.

Jadi meskipun mereka mungkin terlihat serupa dari jarak OCP! = LSP. Sebenarnya saya pikir mereka mungkin satu-satunya 2 prinsip SOLID yang tidak dapat dipahami dalam hal satu sama lain.

guillaume31
sumber
2

LSP dalam kata-kata sederhana menyatakan bahwa instance Foo dapat diganti dengan instance apa pun dari Bar yang berasal dari Foo tanpa kehilangan fungsionalitas program.

Ini salah. LSP menyatakan bahwa kelas Bar tidak boleh memperkenalkan perilaku, yang tidak diharapkan ketika kode menggunakan Foo, ketika Bar berasal dari Foo. Ini tidak ada hubungannya dengan hilangnya fungsionalitas. Anda dapat menghapus fungsionalitas, tetapi hanya ketika kode menggunakan Foo tidak tergantung pada fungsi ini.

Tetapi pada akhirnya, ini biasanya sulit dicapai, karena sebagian besar waktu, kode menggunakan Foo tergantung pada semua perilaku itu. Jadi menghapus itu melanggar LSP. Tetapi menyederhanakannya seperti ini hanya bagian dari LSP.

Euforia
sumber
Kasus yang sangat umum adalah ketika objek yang diganti menghilangkan efek samping : mis. dummy logger yang tidak menghasilkan apa-apa, atau objek tiruan yang digunakan dalam pengujian.
berguna
0

Tentang benda yang mungkin dilanggar

Untuk memahami perbedaannya, Anda harus memahami subjek dari kedua prinsip. Bukan bagian abstrak dari kode atau situasi yang mungkin melanggar atau bukan prinsip. Selalu ada komponen tertentu - fungsi, kelas atau modul - yang dapat melanggar OCP atau LSP.

Siapa yang mungkin melanggar LSP

Satu dapat memeriksa apakah LSP rusak hanya ketika ada antarmuka dengan beberapa kontrak dan implementasi dari antarmuka itu. Jika implementasi tidak sesuai dengan antarmuka atau, secara umum, dengan kontrak, maka LSP rusak.

Contoh paling sederhana:

class Container {
    // Should add the object to the container.
    void addObject(object) {
        internalArray.append(object);
    }

    int size() {
        return internalArray.size();
    }
}

class CustomContainer extends Container {
    @Override void addObject(object) {
        System.console.print("Skipping object! Ha-ha!");
    }
}

void fillWithRandomNumbers(Container container) {
    while (container.size() < 42) {
        container.addObject(Randomizer.getNumber())
    }
}

Kontrak dengan jelas menyatakan bahwa addObjectharus menambahkan argumennya ke wadah. Dan CustomContainerjelas melanggar kontrak itu. Dengan demikian CustomContainer.addObjectfungsinya melanggar LSP. Dengan demikian CustomContainerkelas melanggar LSP. Konsekuensi yang paling penting adalah bahwa CustomContainertidak dapat diteruskan ke fillWithRandomNumbers(). Containertidak dapat diganti dengan CustomContainer.

Ingatlah hal yang sangat penting. Bukan seluruh kode ini yang memecah LSP, melainkan secara khusus CustomContainer.addObjectdan umum CustomContaineryang memecah LSP. Ketika Anda menyatakan bahwa LSP dilanggar, Anda harus selalu menentukan dua hal:

  • Entitas yang melanggar LSP.
  • Kontrak yang dilanggar oleh entitas.

Itu dia. Hanya kontrak dan implementasinya. Downcast dalam kode tidak mengatakan apa-apa tentang pelanggaran LSP.

Siapa yang mungkin melanggar OCP

Seseorang dapat memeriksa apakah OCP dilanggar hanya ketika ada set data yang terbatas dan komponen yang menangani nilai dari set data itu. Jika batas-batas kumpulan data dapat berubah dari waktu ke waktu dan itu membutuhkan perubahan kode sumber komponen, maka komponen tersebut melanggar OCP.

Kedengarannya rumit. Mari kita coba contoh sederhana:

enum Platform {
    iOS,
    Android
}

class PlatformDescriber {
    String describe(Platform platform) {
        switch (platform) {
            case iOS: return "iPhone OS, v10.0.1";
            case Android: return "Android, v7.1";
        }
    }
}

Set data adalah set platform yang didukung. PlatformDescriberadalah komponen yang menangani nilai dari kumpulan data itu. Menambahkan platform baru memerlukan memperbarui kode sumber PlatformDescriber. Dengan demikian PlatformDescriberkelas melanggar OCP.

Contoh lain:

class Shop {
    void sellItemToCustomer(item, customer) {
        // some buisiness logic here
        ...
        logger.logItemSold()
    }
}

class Logger {
    void logItemSold() {
        logger.logToStdErr("an item was sold")
        logger.logToRemote("an item was sold")
        logger.logToDatabase("an item was sold")
    }
}

"Kumpulan data" adalah kumpulan saluran tempat entri log harus ditambahkan. Loggeradalah komponen yang bertanggung jawab untuk menambahkan entri ke semua saluran. Menambahkan dukungan untuk cara logging lain memerlukan pembaruan kode sumber Logger. Dengan demikian Loggerkelas melanggar OCP.

Perhatikan bahwa dalam kedua contoh, kumpulan data bukanlah sesuatu yang diperbaiki secara semantik. Mungkin berubah seiring waktu. Platform baru mungkin muncul. Saluran logging baru mungkin muncul. Jika komponen Anda harus diperbarui ketika itu terjadi, itu melanggar OCP.

Mendorong batas

Sekarang bagian yang sulit. Bandingkan contoh di atas dengan yang berikut:

enum GregorianWeekDay {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

String translateToRussian(GregorianWeekDay weekDay) {
    switch (weekDay) {
        case Monday: return "Понедельник";
        case Tuesday: return "Вторник";
        case Wednesday: return "Среда";
        case Thursday: return "Четверг";
        case Friday: return "Пятница";
        case Saturday: return "Суббота";
        case Sunday: return "Воскресенье";
    }
}

Anda mungkin berpikir translateToRussianmelanggar OCP. Tapi sebenarnya tidak. GregorianWeekDaymemiliki batas spesifik 7 hari kerja dengan nama yang tepat. Dan yang penting adalah bahwa batasan ini secara semantik tidak dapat berubah seiring waktu. Akan selalu ada 7 hari di minggu keemasan. Akan selalu ada hari Senin, Selasa, dll. Kumpulan data ini secara semantik diperbaiki. Tidak mungkin bahwa translateToRussiankode sumber akan memerlukan modifikasi. Dengan demikian OCP tidak dilanggar.

Sekarang harus jelas bahwa switchpernyataan yang melelahkan tidak selalu merupakan indikasi OCP yang rusak.

Perbedaan

Sekarang rasakan perbedaannya:

  • Subjek LSP adalah "implementasi antarmuka / kontrak". Jika implementasi tidak sesuai dengan kontrak maka itu melanggar LSP. Tidaklah penting apakah implementasi itu dapat berubah dari waktu ke waktu atau tidak, apakah bisa diperpanjang atau tidak.
  • Subjek OCP adalah "cara menanggapi perubahan persyaratan". Jika dukungan untuk tipe data baru memerlukan perubahan kode sumber komponen yang menangani data itu, maka komponen itu akan memutus OCP. Tidak penting apakah komponen itu melanggar kontraknya atau tidak.

Kondisi ini sepenuhnya ortogonal.

Contohnya

Dalam jawaban @ Spoike ini yang Melanggar satu prinsip tetapi setelah lain bagian benar-benar salah.

Dalam contoh pertama bagian for-loop jelas melanggar OCP karena tidak dapat diperpanjang tanpa modifikasi. Tetapi tidak ada indikasi pelanggaran LSP. Dan bahkan tidak jelas apakah Contextkontrak tersebut memungkinkan getPersons untuk mengembalikan apa pun kecuali Bossatau Peon. Bahkan dengan asumsi kontrak yang memungkinkan setiap IPersonsubclass untuk dikembalikan, tidak ada kelas yang menimpa pasca-kondisi ini dan melanggarnya. Terlebih lagi, jika getPersons akan mengembalikan instance dari beberapa kelas ketiga, for-loop akan melakukan tugasnya tanpa kegagalan. Tapi fakta itu tidak ada hubungannya dengan LSP.

Lanjut. Dalam contoh kedua baik LSP, maupun OCP dilanggar. Sekali lagi, Contextbagian itu tidak ada hubungannya dengan LSP - tidak ada kontrak yang ditentukan, tidak ada subclassing, tidak ada melanggar menimpa. Bukan Contextsiapa yang harus mematuhi LSP, tidak LiskovSubboleh melanggar kontrak dari pangkalannya. Mengenai OCP, apakah kelasnya benar-benar tertutup? - ya itu. Tidak diperlukan modifikasi untuk memperpanjangnya. Jelas nama titik ekstensi menyatakan Lakukan apa pun yang Anda inginkan, tanpa batas . Contohnya tidak terlalu berguna dalam kehidupan nyata, tetapi jelas tidak melanggar OCP.

Mari kita coba membuat beberapa contoh yang benar dengan pelanggaran OCP atau LSP.

Ikuti OCP tetapi bukan LSP

interface Platform {
    String name();
    String version();
}

class iOS implements Platform {
    @Override String name() { return "iOS"; }
    @Override String version() { return "10.0.1"; }
}

interface PlatformSerializer {
    String toJson(Platform platform);
}

class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return platform.name() + ", v" + platform.version();
    }
}

Di sini, HumanReadablePlatformSerializertidak memerlukan modifikasi apa pun ketika platform baru ditambahkan. Jadi itu mengikuti OCP.

Tetapi kontrak mengharuskan bahwa toJsonharus mengembalikan JSON diformat dengan benar. Kelas tidak melakukannya. Karena itu tidak dapat diteruskan ke komponen yang digunakan PlatformSerializeruntuk memformat tubuh permintaan jaringan. Dengan demikian HumanReadablePlatformSerializermelanggar LSP.

Ikuti LSP tetapi tidak OCP

Beberapa modifikasi pada contoh sebelumnya:

class Android implements Platform {
    @Override String name() { return "Android"; }
    @Override String version() { return "7.1"; }
}
class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return "{ "
                + "\"name\": \"" + platform.name() + "\","
                + "\"version\": \"" + platform.version() + "\","
                + "\"most-popular\": " + isMostPopular(platform) + ","
                + "}"
    }

    boolean isMostPopular(Platform platform) {
        return (platform instanceof Android)
    }
}

Serializer mengembalikan string JSON yang diformat dengan benar. Jadi, tidak ada pelanggaran LSP di sini.

Tetapi ada persyaratan bahwa jika platform paling banyak digunakan maka harus ada indikasi yang sesuai di JSON. Dalam contoh ini OCP dilanggar oleh HumanReadablePlatformSerializer.isMostPopularfungsi karena suatu hari iOS menjadi platform yang paling populer. Secara formal itu berarti bahwa set platform yang paling banyak digunakan didefinisikan sebagai "Android" untuk saat ini, dan isMostPopulartidak memadai menangani set data itu. Kumpulan data tidak secara semantik diperbaiki dan dapat dengan bebas berubah seiring waktu. HumanReadablePlatformSerializerKode sumber harus diperbarui jika terjadi perubahan.

Anda juga dapat melihat pelanggaran Tanggung Jawab Tunggal dalam contoh ini. Saya sengaja membuatnya untuk dapat menunjukkan kedua prinsip pada entitas subjek yang sama. Untuk memperbaiki SRP Anda dapat mengekstrak isMostPopularfungsi ke beberapa eksternal Helperdan menambahkan parameter PlatformSerializer.toJson. Tapi itu cerita lain.

mekarthedev
sumber
0

LSP dan OCP tidak sama.

LSP berbicara tentang kebenaran program yang ada . Jika contoh subtipe akan merusak kebenaran program ketika diganti menjadi kode untuk tipe leluhur, maka Anda telah menunjukkan pelanggaran LSP. Anda mungkin harus membuat tes untuk memperlihatkan ini, tetapi Anda tidak perlu mengubah basis kode yang mendasarinya. Anda memvalidasi program itu sendiri untuk melihat apakah memenuhi LSP.

OCP berbicara tentang kebenaran perubahan dalam kode program, delta dari satu versi sumber ke yang lain. Perilaku tidak boleh dimodifikasi. Seharusnya hanya diperpanjang. Contoh klasik adalah penambahan bidang. Semua bidang yang ada terus beroperasi seperti sebelumnya. Bidang baru hanya menambah fungsionalitas. Namun menghapus bidang, biasanya merupakan pelanggaran OCP. Di sini Anda memvalidasi delta versi program untuk melihat apakah itu memenuhi OCP.

Jadi itulah perbedaan utama antara LSP dan OCP. Yang pertama memvalidasi hanya basis kode yang ada , yang terakhir memvalidasi hanya basis kode dari satu versi ke yang berikutnya . Dengan demikian mereka tidak dapat menjadi hal yang sama, mereka didefinisikan sebagai memvalidasi hal-hal yang berbeda.

Saya akan memberi Anda bukti yang lebih formal: Untuk mengatakan "LSP menyiratkan OCP" akan menyiratkan delta (karena OCP memerlukan satu selain dari kasus sepele), namun LSP tidak memerlukan satu. Jadi itu jelas salah. Sebaliknya, kita dapat menyangkal "OCP menyiratkan LSP" hanya dengan mengatakan OCP adalah pernyataan tentang delta karena itu ia tidak mengatakan apa-apa tentang pernyataan tentang program di tempat. Itu mengikuti dari fakta bahwa Anda dapat membuat delta APAPUN dimulai dengan program APA SAJA di tempat. Mereka sepenuhnya independen.

Brad Thomas
sumber
-1

Saya akan melihatnya dari sudut pandang klien. jika Klien menggunakan fitur antarmuka, dan secara internal fitur tersebut telah diterapkan oleh Kelas A. Misalkan ada kelas B yang memperluas kelas A, maka besok jika saya menghapus kelas A dari antarmuka itu dan meletakkan kelas B, maka kelas B harus juga menyediakan fitur yang sama kepada klien. Contoh standar adalah kelas Bebek yang berenang, dan jika ToyDuck memperpanjang Bebek maka ia juga harus berenang dan tidak mengeluh bahwa ia tidak dapat berenang, jika tidak ToyDuck tidak boleh memperpanjang kelas Bebek.

AKS
sumber
Akan sangat konstruktif jika orang-orang memberikan komentar juga saat memilih apa pun. Lagipula kita semua di sini untuk berbagi pengetahuan, dan hanya menghakimi tanpa alasan yang tepat tidak akan melayani tujuan apa pun.
AKS
ini tampaknya tidak menawarkan sesuatu yang substansial atas poin yang dibuat dan dijelaskan dalam 6 jawaban sebelumnya
nyamuk
1
Sepertinya Anda baru saja menjelaskan salah satu prinsip, L yang saya pikir. Untuk apa Tidak apa-apa tapi pertanyaannya menanyakan perbandingan / kontras dari dua prinsip yang berbeda. Mungkin itulah sebabnya seseorang menurunkannya.
StarWeaver