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 extend
hanya 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 Context
adalah 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 IBehavior
antarmuka.
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 setBehavior
metode 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 YoutubeLink
adalah 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 extend
implementasi 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 o1
tipe S
ada objek o2
tipe T
sehingga untuk semua program yang P
didefinisikan dalam hal T
, perilaku P
tidak berubah ketika o1
diganti untuk o2
kemudian S
adalah 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 instanceof
atau 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 canDo
metode 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 doStuff
metode 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 doStuff
Metode 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 doStuff
metode modifikasi dan memastikannya tetap ditutup dengan menandainya dengan final
kata kunci java . Kata kunci itu mencegah siapa pun dari subklas kelas lebih lanjut (dalam C # Anda dapat menggunakan sealed
untuk 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.