Tanrım, OCP ve LSP'nin ne olduğu konusunda bazı yanlış düşünceler var ve bazıları bazı terminolojilerin uyumsuzluğundan ve kafa karıştırıcı örneklerden kaynaklanıyor. Her iki ilke de aynı şekilde uygularsanız sadece "aynı şeydir". Desenler genellikle ilkeleri bir şekilde veya birkaç istisna dışında izler.
Farklılıklar daha da açıklanacak, ancak ilk önce kendilerinin ilkelerine bir dalış yapalım:
Açık-Kapalı İlke (OCP)
Bob Amca’ya göre :
Sınıf davranışını değiştirmeden, genişletebilmelisiniz.
Bu durumda genişlet kelimesinin mutlaka yeni davranışı gerektiren gerçek sınıfı alt almanız gerektiği anlamına gelmediğine dikkat edin. İlk terminolojinin uyuşmazlığında nasıl bahsettiğimi anladın mı? Anahtar kelime extend
yalnızca Java’da alt sınıflandırma anlamına gelir, ancak ilkeler Java’dan daha eskidir.
Orijinali 1988'de Bertrand Meyer'den geldi:
Yazılım varlıkları (sınıflar, modüller, fonksiyonlar vs.) uzatma için açık, ancak değişiklik için kapalı olmalıdır.
Burada, ilkenin yazılım varlıklarına uygulandığı çok açıktır . Kötü bir örnek, bir uzantı noktası sağlamak yerine kodu tamamen değiştirdiğiniz için yazılım varlığını geçersiz kılar. Yazılım varlığının davranışı genişletilebilir olmalıdır ve bunun iyi bir örneği, Strateji modelinin uygulanmasıdır (çünkü GoF desenleri IMHO'yu göstermek en kolay olanıdır):
// 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();
}
Yukarıdaki örnekte Context
olduğu kilitli başka değişiklikler için. Programcıların çoğu muhtemelen genişletmek için sınıfı alt sınıflamak isterdi, ancak burada yapmıyoruz çünkü davranışının arayüzü uygulayan herhangi bir şey yoluyla değiştirilebileceğini varsayıyor IBehavior
.
Yani, içerik sınıfı değişiklik için kapalı, ancak uzantı için açık . Aslında bu başka bir temel prensibi izler; çünkü davranışı miras yerine nesne kompozisyonuyla koyarız:
" Sınıf kalıtımı " yerine " nesne bileşimi " nini seçin . " (Dört 1995 Çetesi: 20)
Okuyucunun bu sorunun kapsamı dışında olduğu gibi bu prensibi okumasına izin vereceğim. Örneğe devam etmek için, IBehavior arabiriminin aşağıdaki uygulamalarına sahip olduğumuzu söyleyin:
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!");
}
}
Bu modeli kullanarak, çalışma zamanındaki bağlamın davranışını setBehavior
uzatma noktası yöntemi kullanarak değiştirebiliriz .
// 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!"
Bu nedenle, "kapalı" bağlam sınıfını genişletmek istediğinizde, "açık" işbirliği bağımlılığı alt sınıfını alarak bunu yapın. Bu açık bir şekilde bağlamın kendisini alt sınıflandırma ile aynı şey değildir, ancak OCP'dir. LSP de bundan bahsetmiyor.
Miras Yerine Mixinlerle Uzatma
OCP'yi alt sınıflandırma dışında yapmanın başka yolları da vardır. Bir yol, sınıflarınızı miks kullanımıyla açılmak için açık tutmak . Bu, örneğin sınıf tabanlı yerine prototip tabanlı dillerde faydalıdır. Buradaki düşünce, ihtiyaç duyulduğunda daha fazla yöntem veya özellik içeren dinamik bir nesneyi, diğer bir deyişle diğer nesnelerle karışan veya "karışan" nesneleri değiştirmektir.
Aşağıda, çapalar için basit bir HTML şablonu oluşturan bir karışımın bir javascript örneği verilmiştir:
// 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>
Fikir, nesneleri dinamik olarak genişletmektir ve bunun avantajı, nesnelerin tamamen farklı alanlarda olsalar bile yöntemleri paylaşabilir. Yukarıdaki durumda, ile özel uygulamanızı genişleterek kolayca diğer html çapa türlerini oluşturabilirsiniz LinkMixin
.
OCP açısından "karışımlar" uzantılardır. Yukarıdaki örnekte YoutubeLink
, modifikasyon için kapalı, ancak karışımların kullanımı yoluyla uzantılar için açık olan yazılım varlığımızdır. Nesne hiyerarşisi düzleştirilmiştir, bu da türleri kontrol etmeyi imkansız kılar. Bununla birlikte, bu gerçekten kötü bir şey değildir ve daha sonra, türleri kontrol etmenin genel olarak kötü bir fikir olduğunu ve fikri polimorfizmle bozduğunu açıklayacağım.
Çoğu extend
uygulama birden fazla nesneyi karıştırabildiğinden , bu yöntemle birden fazla kalıtım gerçekleştirmenin mümkün olduğunu unutmayın :
_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);
Akılda tutmanız gereken tek şey isimleri çarpıştırmamaktır, yani karışımlar geçersiz kılınacakları gibi bazı özelliklerin veya yöntemlerin aynı ismini tanımlamak için gerçekleşir. Alçakgönüllülük deneyimlerime göre bu bir sorun değildir ve bu gerçekleşirse, hatalı tasarımın bir göstergesidir.
Liskov'un Değişim Prensibi (LSP)
Bob Amca basitçe şöyle tanımlar:
Türetilmiş sınıflar, temel sınıfları için değiştirilmelidir.
Bu prensip eskidir, aslında Bob Amca'nın tanımı, yukarıdaki Strateji örneğinde aynı süper tipin kullanılmasıyla LSP'yi OCP ile yakından ilişkili kılan prensipleri farklılaştırmaz ( IBehavior
). Öyleyse Barbara Liskov'un orijinal tanımına bakalım ve bu ilke hakkında matematiksel bir teoriye benzeyen başka bir şey bulabilecek miyiz bakalım:
Burada aranıyor şu değiştirme özelliği gibi bir şey: Her nesne için ise o1
Çeşidi S
bir nesne yoktur o2
Çeşidi T
tüm programlar için öyle ki P
açısından tanımlanır T
, davranışı P
olduğunda değişmediği o1
için ikame edilmiştir o2
ardından S
bir tipidir T
.
Bir süre bu konuda omuz silkiyor, sınıflardan hiç bahsetmediğinden haberiniz var. JavaScript'te, açıkça sınıf tabanlı olmasa da, LSP'yi takip edebilirsiniz. Programınızda aşağıdakilerden en az birkaç JavaScript nesnesi listesi varsa:
- aynı şekilde hesaplanması gerekiyor
- aynı davranışa sahip, ve
- Aksi halde bir şekilde tamamen farklı
... sonra nesneler aynı "tip" olarak kabul edilir ve program için gerçekten önemli değildir. Bu aslında polimorfizmdir . Genel anlamda; Eğer arayüz kullanıyorsanız, gerçek alt tipi bilmeniz gerekmez. OCP bu konuda açık bir şey söylemiyor. Ayrıca, çoğu acemi programcının yaptığı gibi bir tasarım hatasını da tespit ediyor:
Ne zaman bir nesnenin alt türünü kontrol etme dürtüsünü hissettiğinizde, büyük olasılıkla YANLIŞ yapıyorsunuzdur.
Tamam, her zaman yanlış olmayabilir bu yüzden ama bazı yapmak dürtü varsa tür denetlemesi ile instanceof
veya çeteleler, sen olması gerekenden biraz daha fazla kendiniz için dolambaçlı bir program yapıyor olabilir. Ancak bu her zaman böyle değildir; işleri halletmek için hızlı ve kirli hackler, çözümün yeterince küçük olması durumunda aklımda yapmam gereken bir imtiyazdır ve acımasız bir yeniden yapılanma uyguladığınızda , değişiklikler istediğinde bir kez daha iyileşebilir .
Asıl soruna bağlı olarak bu "tasarım hatasını" düzeltmenin yolları var:
- Süper sınıf, önkoşulları çağırmaz, arayanı bunun yerine zorlar.
- Süper sınıf, arayan kişinin ihtiyaç duyduğu genel bir yöntemi kaçırıyor.
Bunların her ikisi de ortak kod tasarımı "hata" dır. Pull-up metodu gibi yapabileceğiniz birkaç farklı refactorite veya Visitor gibi bir forma refactor vardır .
Aslında, Ziyaretçi şablonunu, büyük if-ifadeli spagetti ile ilgilenebileceği için çok seviyorum ve uygulaması, mevcut kodla ilgili düşündüğünüzden daha kolaydır. Aşağıdaki içeriğe sahip olduğumuzu söyleyin:
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"
İf-ifadesinin sonuçları, her biri bir karara ve çalıştırılacak bir koda bağlı olduğu için kendi ziyaretçilerine çevrilebilir. Bunları şöyle çıkarabiliriz:
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");
}
}
Bu noktada, programcı Ziyaretçi modeli hakkında bir şey bilmiyorsa, bunun yerine belirli türde olup olmadığını kontrol etmek için Context sınıfını uygular. Ziyaretçi sınıflarının bir boolean canDo
yöntemi olduğundan, uygulayıcı, bu yöntem çağrısını, işi yapmanın doğru nesne olup olmadığını belirlemek için kullanabilir. Bağlam sınıfı, aşağıdaki gibi tüm ziyaretçileri kullanabilir (ve yenilerini ekleyebilir):
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);
}
}
Her iki model de OCP ve LSP'yi izler, ancak ikisi de onlar hakkında farklı şeyler belirtir. Peki kod ilkelerden birini ihlal ederse nasıl görünür?
Bir ilkeyi ihlal etmek, ancak diğerini takip etmek
İlkelerden birini çiğnemenin yolları var ama yine de diğeri takip ediliyor. Aşağıdaki örnekler iyi bir nedenden ötürü haklı görünüyor, ancak aslında üretim kodunda (ve hatta daha da kötüye giden) ortaya çıktıklarını gördüm:
OCP’yi izler ancak LSP’yi izlemez
Verilen kodu aldığımızı varsayalım:
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() { ... }
}
Bu kod parçası açık-kapalı prensibine uyar. Bağlamın GetPersons
yöntemini çağırırsak, hepsinin kendi uygulaması olan bir grup insan elde ederiz. Bu, IPerson'un değişiklik için kapalı, ancak uzantı için açık olduğu anlamına gelir. Ancak, kullanmamız gerektiğinde işler kararır:
// 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();
}
}
Tip kontrolü yapmalı ve dönüşüm yazmalısınız! Tip kontrolünün kötü bir şey olduğunu nasıl yukarıda belirttiğimi hatırlıyor musunuz? Oh hayır! Ancak, yukarıda da belirtildiği gibi, bazılarını yeniden çeken ya da bir Ziyaretçi düzenini uygulayan korku değil. Bu durumda genel bir yöntem ekledikten sonra basitçe yeniden başlatma işlemini yapabiliriz:
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
Artık avantaj, LSP'yi takip ederek artık tam tipini bilmenize gerek olmamasıdır:
// 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();
}
LSP’yi izler ancak OCP’yi izlemez
LSP'yi takip eden, ancak OCP'yi izleyen bazı kodlara göz atalım, bu tür bir şekilde anlaşılabilir ancak bu konuda benimle birlikte kalın, bu çok ince bir hata:
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 }
}
Kod, LSP'yi yapar çünkü bağlam, gerçek tipini bilmeden LiskovBase'i kullanabilir. Bu kodun OCP'yi de takip ettiğini düşünürsünüz ama yakından bakın, sınıf gerçekten kapalı mı? Ya doStuff
yöntem sadece bir satır basmaktan daha fazlasını yaptıysa?
OCP'yi izlerse cevap basitçe: HAYIR , çünkü bu nesne tasarımında, kodu tamamen başka bir şeyle geçersiz kılmamız gerekmiyor. Bu, işleri yürütmek için temel sınıftan kod kopyalamanız gerektiğinden kes ve yapıştır solucanlar kutusunu açar. doStuff
Yöntem emin uzatma için açık olduğunu, ancak tamamen modifikasyon için kapatılmadı.
Bunun üzerine Template metod kalıbını uygulayabiliriz . Şablon yöntemi kalıbı, çerçevelerde o kadar yaygındır ki, onu bilmeden kullanıyor olabilirsiniz (örn. Java swing bileşenleri, c # formları ve bileşenleri, vb.). İşte doStuff
değişiklik yöntemini kapatmanın ve java'nın final
anahtar kelimesiyle işaretleyerek kapalı kalmasını sağlamanın bir yolu . Bu anahtar kelime, kimsenin sınıfı daha da alt sınıflamasını engeller (C # ' sealed
da aynı şeyi yapmak için kullanabilirsiniz ).
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!";
}
}
Bu örnek OCP'yi izler ve aptalca görünür, fakat bunun işlenecek daha fazla kodla ölçeklendiğini hayal edin. Üretimde, alt sınıfların her şeyi tamamen geçersiz kıldığı ve geçersiz kılınan kodun çoğunlukla uygulamalar arasında kesildiği üretimde konuşlandırıldığını görüyorum. Çalışır, ancak tüm kod çoğaltmalarda olduğu gibi bakım kabusları için de bir kurulumdur.
Sonuç
Umarım tüm bunlar OCP ve LSP ile ilgili bazı soruları ve aralarındaki farkları / benzerlikleri temizler. Bunları aynı şekilde reddetmek kolaydır, ancak yukarıdaki örnekler olmadığını gösterir.
Not, yukarıdaki örnek koddan toplayarak:
OCP, çalışma kodunu kilitlemekle ilgili ancak yine de bir çeşit uzatma noktasıyla açık tutuyor.
Bu, Şablon Yöntemi deseni örneğinde olduğu gibi değişen kodu içine alarak kod çoğaltılmasını önlemek içindir. Ayrıca, kırılma değişiklikleri acı verici olduğu için hızlı bir şekilde başarısızlığa izin verir (örneğin bir yeri değiştirir, her yere kırır). Bakım uğruna, değişimin enkapsüle edilmesi kavramı iyi bir şeydir, çünkü değişiklikler her zaman olur.
LSP, kullanıcının gerçek tipini kontrol etmeden bir üst tip uygulayan farklı nesneleri işlemesine izin vermekle ilgilidir. Bu, doğası gereği polimorfizm hakkında.
Bu ilke, tür sayısı arttıkça elden çıkabilen ve çekmece yeniden düzenleme veya Ziyaretçi gibi desenleri uygulayarak elde edilebilecek tür kontrolü ve tür dönüştürmesi için bir alternatif sunar.