LSP vs OCP / Liskov Değişimi VS Aç Kapat


48

OOP'un SOLID ilkelerini anlamaya çalışıyorum ve LSP ile OCP'nin bazı benzerlikleri olduğu sonucuna vardım (daha fazla söylemediyseniz).

açık / kapalı prensipte "yazılım varlıkları (sınıflar, modüller, fonksiyonlar vb.) uzatma için açık, ancak değişiklik için kapalı olmalıdır" yazmaktadır.

Basit bir ifadeyle LSP, herhangi bir örneğinin türetilmiş Fooherhangi bir örneğiyle değiştirilebileceğini ve programın aynı şekilde çalışacağını Barbelirtir Foo.

Ben profesyonel bir OOP programcısı değilim, ama bana öyle geliyor ki, LSP'nin içinde Bar, türetilmiş bir Fooşey değişmediği sürece, sadece onu genişlettiği takdirde mümkün olduğu anlaşılıyor. Bu, özellikle, LSP programında yalnızca OCP doğruyken, OCP ise yalnızca LSP doğruysa doğrudur. Bu, eşit oldukları anlamına gelir.

Yanlışsam düzelt. Bu fikirleri gerçekten anlamak istiyorum. Cevap için çok teşekkürler.


4
Bu, her iki kavramın da çok dar bir yorumudur. Açık / kapalı tutulabilir, ancak yine de LSP'yi ihlal eder. Dikdörtgen / Kare veya Elips / Daire örnekleri iyi örneklerdir. Her ikisi de OCP'ye bağlı, ancak her ikisi de LSP'yi ihlal ediyor.
Joel Etherton

1
Dünya (ya da en azından internet) bu konuda karışık. kirkk.com/modularity/2009/12/solid-principles-of-class-design . Bu adam, LSP'nin ihlalinin de OCP'yi ihlal ettiğini söylüyor. Daha sonra sayfa 156'daki "Yazılım Mühendisliği Tasarımı: Teorisi ve Uygulaması" kitabında, yazar OCP'ye yapışan, ancak LSP'yi ihlal eden bir örnek verir. Bundan vazgeçtim.
Manoj R

@JoelEtherton Bu çiftler, yalnızca değişken olmaları durumunda LSP'yi ihlal eder. Değişmez durumda, türetmek Squaredan RectangleLSP'yi ihlal etmez. (Köşeli olabilir çünkü Fakat kötü tasarım değişmez durumda muhtemelen hala Rectanglebir olmayan s Squarematematik uymuyor)
CodesInChaos

Basit benzetme (bir kütüphane yazarından kullanıcının bakış açısına göre). LSP, söylediklerinin% 100'ünü (arayüzde veya kullanım kılavuzunda) uyguladığını iddia eden bir ürün (kütüphane) satmak gibidir, ancak gerçekte söylenmeyen (veya söylenenlerle eşleşmeyen). OCP, yeni işlevsellik ortaya çıktığında (ürün yazılımı gibi) yükseltilebileceği (uzatılabileceği) bir ürün (kütüphane) satmak gibidir, ancak fabrika servisi olmadan yükseltilemez.
rwong

Yanıtlar:


119

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 extendyalnı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 Contextolduğ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ı setBehavioruzatma 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 extenduygulama 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 Sbir nesne yoktur o2Çeşidi Ttüm programlar için öyle ki Paçısından tanımlanır T, davranışı Polduğunda değişmediği o1için ikame edilmiştir o2ardından Sbir 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 instanceofveya ç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 canDoyö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 GetPersonsyö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 doStuffyö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. doStuffYö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 doStuffdeğişiklik yöntemini kapatmanın ve java'nın finalanahtar 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 # ' sealedda 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.


7
Bu iyi bir açıklama, çünkü her zaman miras yoluyla uygulama anlamına geldiğini ima ederek OCP'yi fazla basitleştirmiyor. Bazı insanların kafasında OCP ve SRP'ye katılan aşırı basitleştirme, gerçekte iki ayrı kavram olabilir.
Eric King

5
Bu şimdiye kadar gördüğüm en iyi bordro cevaplarından biri. Keşke 10 kez oy kullanabilseydim. Aferin ve mükemmel açıklama için teşekkür ederim.
Bob Horn,

Orada, Javascript’te sınıf tabanlı bir programlama dili olmayan ama yine de LSP’yi izleyebilen bir metin bulanıklığı ekledim ve metni düzenledim; Uf!
Spoike

LSP’den Bob Amca’dan aldığınız alıntı doğru olsa da (web sitesinde olduğu gibi), tam tersi olmaz mıydı? "Temel sınıfların türetilmiş sınıfları ile değiştirilebilir olması gerektiğini" belirtmemeli mi? LSP'de "uyumluluk" testi, temel sınıftan değil türetilmiş sınıfa karşı yapılır. Yine de, anadili İngilizce değil ve eksik olabileceğim tabiriyle ilgili bazı ayrıntılar olabileceğini düşünüyorum.
Alpha

@ Alfa: Bu iyi bir soru. Temel sınıf her zaman türetilmiş sınıfları ile ikame edilebilir veya başka bir miras işe yaramaz. Derleyici (en azından Java ve C # dilinde), uygulanması gereken genişletilmiş sınıftan bir üye (yöntem veya öznitelik / alan) dışlıyorsanız şikayet edecektir. LSP, yalnızca türetilmiş sınıflar üzerinde yerel olarak kullanılabilen yöntemler eklemekten alıkoymak içindir, çünkü bu türetilmiş sınıfların kullanıcısını, onlar hakkında bilgi sahibi olmasını gerektirir. Kod büyüdükçe, bu tür yöntemlerin bakımı zor olacaktır.
Spoike

15

Bu, çok fazla kafa karışıklığına neden olan bir şeydir. Bu ilkeleri biraz felsefi olarak düşünmeyi tercih ederim, çünkü onlar için çok farklı örnekler var ve bazen somut örnekler onların özünü tam olarak yakalayamıyor.

OCP düzeltmeye çalıştığı şey

Belirli bir programa işlevsellik eklememiz gerektiğini söyleyin. Bunu yapmanın en kolay yolu, özellikle usule göre düşünmek için eğitilmiş insanlar için, gerektiğinde bir if cümlesi ya da benzeri bir şey eklemektir.

Bununla ilgili sorunlar

  1. Var olan çalışma kodunun akışını değiştirir.
  2. Her durumda yeni koşullu bir dallanmaya zorlar. Örneğin, bir kitap listeniz olduğunu ve bir kısmının satışta olduğunu ve bunların tümünü yinelemek ve fiyatlarını yazdırmak istediğinizi varsayalım, öyle ki eğer satıştalarsa, yazdırılan fiyat dizeyi içerecektir " (SATILIK)".

Bunu, "is_on_sale" adlı tüm kitaplara ek bir alan ekleyerek yapabilirsiniz ve sonra herhangi bir kitabın fiyatını yazdırırken bu alanı kontrol edebilir veya alternatif olarak , satış kitaplarını, yazdırılan farklı bir tür kullanarak veritabanından başlatabilirsiniz. Fiyat dizisindeki "(ON SATILIK)" (mükemmel bir tasarım değil, ana fikri sağlar).

İlk, prosedürel çözümle ilgili sorun, her kitap için fazladan bir alan ve birçok durumda fazladan karmaşıklıktır. İkinci çözüm yalnızca gerçekten gerekli olduğu yerde mantığı zorlar.

Şimdi, farklı verilerin ve mantığın gerekli olduğu birçok durum olabileceğini ve sınıflarınızı tasarlarken veya gereksinimlerdeki değişikliklere tepki verirken OCP'yi göz önünde bulundurmanın neden iyi bir fikir olduğunu göreceksiniz.

Şimdiye dek asıl fikre sahip olmalısınız: Kendinizi prosedürel değişiklikler değil, polimorfik uzantılar olarak yeni kodun uygulanabileceği bir duruma sokmaya çalışın.

Ancak, bağlamı analiz etmekten asla korkmayın ve sakıncaların faydalardan ağır basıp basmadığına bakın, çünkü OCP gibi bir ilke bile 20 satırlık bir programdan dikkatli bir şekilde muamele edilmemesi halinde 20 sınıfı bir karışıklık yaratabilir .

Ne LSP düzeltmeye çalışır

Hepimiz aşk kodunu yeniden kullanırız. Bunu izleyen bir hastalık, birçok programın, tamamen okunamayan karmaşıklıklar yaratmak için ortak kod satırlarını kör bir şekilde çarpanlara ayırdıkları noktaya ve birkaç kod satırından başka, modüller arasında fazla sıkı bir bağlantıya geçmediği noktaya geldiğinde, Kavramsal işler yapılacak sürece hiçbir ortak yanı yok.

Bunun en büyük örneği arayüz yeniden kullanımıdır . Muhtemelen kendinize tanık oldunuz; Bir sınıf, bunun mantıklı bir şekilde uygulanması (ya da somut temel sınıflar halinde uzatılması) için bir arayüz uygular, ancak bu noktada ilan ettiği yöntemler söz konusu olduğunda doğru imzalara sahip oldukları için.

Ama sonra bir problemle karşılaşırsın. Sınıflar, arayüzleri yalnızca beyan ettikleri yöntemlerin imzalarını göz önünde bulundurarak uygularsa, kendinizi sınıf örneklerini bir kavramsal işlevsellikten tamamen farklı işlevsellik gerektiren yerlere, sadece benzer imzalara bağlı olarak geçirebileceksiniz.

Bu o kadar da korkunç değil, ama çok fazla kafa karışıklığına neden oluyor ve kendimizi böyle hatalar yapmasını engelleyen teknolojiye sahibiz. Yapmamız gereken, arayüzleri API + Protokolü olarak ele almak . API bildirimlerde açıkça görülür ve protokol, arayüzün mevcut kullanımlarında görünür. Aynı API'yi paylaşan 2 kavramsal protokolümüz varsa, bunlar 2 farklı arayüz olarak gösterilmelidir. Aksi taktirde DRY dogmatizmine yakalanırız ve ironik olarak, sadece kodu korumak için daha zor oluştururlar.

Şimdi tanımı tam olarak anlayabilmelisin. LSP diyor ki: Bir temel sınıftan miras almayın ve bu alt sınıflarda, temel sınıfa bağlı diğer yerlerin geçinmeyeceği işlevselliği uygulayın.


1
Bu oyu ve Spoike'in cevaplarını oylayabilmek için kayıt oldum - harika bir iş.
David Culp

7

Anladığım kadarıyla:

OCP şöyle diyor: “Yeni işlevler ekleyecekseniz, değiştirmek yerine mevcut olanı genişleten yeni bir sınıf oluşturun.”

LSP şunları söylüyor: “Mevcut bir sınıfı genişleten yeni bir sınıf oluşturursanız, tabanla tamamen değiştirildiğinden emin olun.”

Bu yüzden birbirlerini tamamladıklarını ancak eşit olmadıklarını düşünüyorum.


4

OCP ve LSP'nin her ikisinin de değişiklik yapmak zorunda kaldığı doğru olsa da, OCP'nin konuştuğu değişiklik türü, LSP'nin konuştuğu şey değildir.

OCP'ye göre değişiklik yapmak, bir geliştiricinin mevcut bir sınıfta kod yazma fiziksel eylemidir .

LSP, türetilmiş bir sınıfın temel sınıfına kıyasla getirdiği davranış değişikliği ve üst sınıf yerine alt sınıf kullanılarak neden olabilecek programın çalışma zamanındaki değişikliği ile ilgilidir.

Bu nedenle, OCP! Aslında, birbirleriyle anlaşılamayan tek 2 KATI ilke olabileceğini düşünüyorum.


2

Basit bir deyişle LSP, herhangi bir Foo örneğinin, program işlevselliği kaybı olmadan Foo'dan türetilen herhangi bir Bar örneği ile değiştirilebileceğini belirtir.

Bu yanlış. LSP, Bar sınıfı Foo'dan türetildiğinde, Bar Foo kullandığında beklenmeyen bir Bar sınıfı davranışı tanıtmaması gerektiğini belirtir. İşlevsellik kaybıyla ilgisi yok. İşlevselliği kaldırabilirsiniz, ancak yalnızca Foo kullanan kod bu işlevselliğe bağlı olmadığında.

Ancak sonuçta, bunu başarmak genellikle zordur, çünkü çoğu zaman Foo kullanan kod tüm davranışlarına bağlıdır. Bu yüzden kaldırılması LSP'yi ihlal ediyor. Ancak bu şekilde basitleştirmek, LSP'nin sadece bir kısmı.


Çok yaygın bir durum, ikame edilmiş nesnenin yan etkileri ortadan kaldırdığı durumdur : örn. hiçbir şey çıkaran kukla bir kayıt cihazı veya testlerde kullanılan sahte bir nesne.
İşe yaramaz

0

İhlal edebilecek nesneler hakkında

Farkı anlamak için her iki ilkenin konusunu da anlamalısınız. Bazı prensipleri ihlal edip etmeyecek bir kod veya durum soyut kısmı değildir. Her zaman OCP veya LSP'yi ihlal edebilecek belirli bir bileşendir - fonksiyon, sınıf veya modül -.

LSP'yi kim ihlal edebilir

Bir kişi, LSP'nin yalnızca bir sözleşme ile bir arayüz ve bu arayüzün bir uygulaması olduğunda kırılıp kırılmadığını kontrol edebilir. Uygulama, ara yüze veya genel olarak sözleşmeye uymuyorsa, LSP bozulur.

En basit örnek:

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

Sözleşme açıkça addObjectargümanını konteynere eklemesi gerektiğini belirtir . Ve CustomContaineraçıkça bu sözleşmeyi kırar. Böylece CustomContainer.addObjectişlev LSP'yi ihlal ediyor. Böylece CustomContainersınıf LSP'yi ihlal ediyor. En önemli sonuç CustomContainerbunun geçilememesidir fillWithRandomNumbers(). Containerile değiştirilemez CustomContainer.

Akılda tutulması gereken çok önemli bir nokta. LSP'yi kıran kodun tamamı değil, spesifik olarak CustomContainer.addObjectve genellikle LSP'yi kıran koddur CustomContainer. LSP'nin ihlal edildiğini belirttiğinizde her zaman iki şey belirtmelisiniz:

  • LSP'yi ihlal eden varlık.
  • İşletme tarafından yapılan sözleşme.

Bu kadar. Sadece bir sözleşme ve uygulaması. Koddaki bir downcast LSP ihlali hakkında hiçbir şey söylemez.

OCP'yi kim ihlal edebilir

OCP'nin yalnızca sınırlı veri kümesi ve bu veri kümesinden değerleri işleyen bir bileşen olduğunda ihlal edilip edilmediği kontrol edilebilir. Veri setinin sınırları zaman içinde değişebilir ve bu, bileşenin kaynak kodunun değiştirilmesini gerektiriyorsa, bileşen OCP'yi ihlal eder.

Kulağa karmaşık geliyor. Basit bir örnek deneyelim:

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";
        }
    }
}

Veri seti, desteklenen platformlar kümesidir. PlatformDescriberbu veri kümesindeki değerleri ele alan bileşendir. Yeni bir platform eklemek, kaynak kodunun güncellenmesini gerektirir PlatformDescriber. Bu yüzden PlatformDescribersınıf OCP'yi ihlal ediyor.

Başka bir örnek:

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")
    }
}

"Veri seti", bir günlük girişinin eklenmesi gereken kanallardır. LoggerTüm kanallara girişler eklemekten sorumlu olan bileşendir. Başka bir kayıt yöntemi için destek eklemek, kaynak kodunun güncellenmesini gerektirir Logger. Bu yüzden Loggersınıf OCP'yi ihlal ediyor.

Her iki örnekte de veri kümesinin semantik olarak sabit bir şey olmadığını unutmayın. Zamanla değişebilir. Yeni bir platform ortaya çıkabilir. Yeni bir kayıt kanalı ortaya çıkabilir. Bu olduğunda bileşeninizin güncellenmesi gerekiyorsa, OCP'yi ihlal eder.

Limitleri zorlamak

Şimdi zor kısmı. Yukarıdaki örnekleri aşağıdakilerle karşılaştırın:

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 "Воскресенье";
    }
}

translateToRussianOCP'yi ihlal ettiğini düşünebilirsiniz . Ama aslında değil. GregorianWeekDayTam adlarıyla 7 haftanın belirli bir sınırı vardır. Önemli olan, bu sınırların anlamsal olarak zaman içinde değişemeyeceğidir. Gregorian haftasında her zaman 7 gün olacak. Her zaman Pazartesi, Salı, vb. Olacak. Bu veri seti anlamsal olarak sabittir. translateToRussianKaynak kodunun değişiklik gerektirmesi mümkün değildir . Böylece OCP ihlal edilmez.

Şimdi yorucu switchifadenin her zaman bozuk OCP'nin bir göstergesi olmadığı açık olmalıdır .

Fark

Şimdi farkı hissedin:

  • LSP'nin konusu “arayüz / sözleşmenin uygulanması” dır. Uygulama sözleşmeye uymuyorsa, LSP'yi ihlal eder. Bu uygulamanın zaman içinde değişip değişmemesi, genişletilebilir veya genişletilmemesi önemli değildir.
  • OCP'nin konusu “bir gereksinim değişikliğine cevap vermenin bir yolu”. Yeni bir veri türü desteği, bu verileri işleyen bileşenin kaynak kodunu değiştirmeyi gerektiriyorsa, o bileşen OCP'yi keser. Bileşenin sözleşmesini ihlal edip etmemesi önemli değildir.

Bu koşullar tamamen diktir.

Örnekler

In Spoike cevabı @ bir ilke ihlal ancak diğer aşağıdaki kısmı tamamen yanlıştır.

İlk örnekte for-loop kısmı OCP'yi açıkça ihlal ediyor çünkü değişiklik yapmadan genişletilemez. Ancak LSP ihlalinin belirtisi yoktur. Ve Contextsözleşmenin GetPonsons'un Bossveya dışında bir şey iade etmesine izin verip vermediği bile net değil Peon. Herhangi bir IPersonalt sınıfın iade edilmesine izin veren bir sözleşme kabul etmekle birlikte, bu post-koşulu geçersiz kılan ve ihlal eden bir sınıf yoktur. Dahası, eğer getPersons bazı üçüncü sınıfların bir örneğini döndürürse, for-loop işini hatasız olarak gerçekleştirir. Ancak bu gerçeğin LSP ile ilgisi yok.

Sonraki. İkinci örnekte ne LSP, ne de OCP ihlal edilmiştir. Yine, Contextparçanın sadece LSP ile ilgisi yok - tanımlanmış sözleşme yok, alt sınıf yok, kırılma yok. O değil Contextöyle, LSP itaat kimin LiskovSubonun tabanının sözleşme kırmak olmamalıdır. OCP ile ilgili olarak, sınıf gerçekten kapalı mı? - Evet öyle. Uzatmak için değişiklik yapılması gerekmez. Belli ki uzatma noktasının adı, istediğin her şeyi yap, sınırları yok . Örnek gerçek hayatta pek kullanışlı değil, ancak açıkça OCP'yi ihlal etmiyor.

Doğru OCP veya LSP ihlali ile bazı doğru örnekler yapmaya çalışalım.

OCP'yi izleyin, ancak LSP'yi takip edin

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

Burada, HumanReadablePlatformSerializeryeni bir platform eklendiğinde herhangi bir değişiklik yapılması gerekmez. Böylece OCP'yi takip eder.

Ancak sözleşme, toJsonuygun şekilde biçimlendirilmiş bir JSON'u iade etmesini gerektirir . Sınıf bunu yapmaz. Bu nedenle, PlatformSerializerbir ağ isteğinin gövdesini biçimlendirmek için kullanılan bir bileşene geçirilemez . Böylece HumanReadablePlatformSerializerLSP'yi ihlal ediyor.

LSP'yi izleyin, ancak OCP'yi takip edin

Önceki örnekte yapılan bazı değişiklikler:

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

Serileştirici doğru biçimlendirilmiş JSON dizesini döndürür. Yani, burada hiçbir LSP ihlali yok.

Ancak, eğer platform en çok kullanılıyorsa, JSON'da ilgili endikasyonların bulunması şartı aranmaktadır. Bu örnekte OCP, bir HumanReadablePlatformSerializer.isMostPopulargün iOS en popüler platform haline geldiğinden , işlev ihlal edilir . Resmen, en çok kullanılan platform kümesinin şimdilik "Android" olarak tanımlandığı ve isMostPopularbu veri setini yetersiz şekilde kullandığı anlamına gelir . Veri kümesi anlamsal olarak sabit değildir ve zaman içinde serbestçe değişebilir. HumanReadablePlatformSerializerDeğişiklik durumunda kaynak kodunun güncellenmesi gerekir.

Bu örnekte, Tek Sorumluluk ihlalini de fark edebilirsiniz. Aynı konuda her iki prensibi de gösterebilmek için bilerek yaptım. SRP'yi düzeltmek için, isMostPopularişlevi bazı harici cihazlara çıkarabilir Helperve için bir parametre ekleyebilirsiniz PlatformSerializer.toJson. Ama bu başka bir hikaye.


0

LSP ve OCP aynı değildir.

LSP, programın doğruluğu hakkında durur . Bir alt tür örneği, ata türleri için koda eklendiğinde program doğruluğunu kırarsa, o zaman LSP ihlalini kanıtladınız. Bunu göstermek için bir test yapmak zorunda kalabilirsiniz, ancak alttaki kod tabanını değiştirmek zorunda kalmazsınız. LSP'ye uyup uymadığını görmek için programın kendisini onaylıyorsunuz.

OCP , program kodundaki değişikliklerin doğruluğunu, bir kaynak sürümden diğerine olan deltadan bahseder . Davranış değiştirilmemelidir. Sadece uzatılmalıdır. Klasik örnek alan eklemedir. Mevcut alanların tümü eskisi gibi çalışmaya devam eder. Yeni alan sadece işlevsellik ekler. Bununla birlikte, bir alanı silmek tipik olarak OCP'nin ihlalidir. Burada , OCP'ye uyup uymadığını görmek için delta program sürümünü doğrulıyorsunuz.

Demek ki LSP ile OCP arasındaki anahtar fark bu. İlki, yalnızca kod tabanını dururken doğrular, ikincisi yalnızca bir sürümden diğerine yalnızca kod tabanı deltasını doğrular . Bu yüzden aynı şey olamazlar, farklı şeyleri onaylamak olarak tanımlanırlar .

Size daha resmi bir kanıt vereceğim: "LSP'nin OCP'yi ima ettiğini" söylemek bir delta anlamına gelecektir (çünkü OCP önemsiz durumda olandan başka bir tane gerektirir), ancak LSP bir tane gerektirmez. Yani bu açıkça yanlıştır. Bunun tersine, OCP'nin deltalarla ilgili bir ifade olduğunu söyleyerek "OCP'nin LSP'yi ima ettiğini" ispatlayabiliriz, bu nedenle yerinde bir programa ilişkin bir ifade hakkında hiçbir şey söylemez. Bu, HERHANGİ bir programdan başlamak üzere HERHANGİ bir delta oluşturabildiğiniz gerçeğinden kaynaklanmaktadır. Tamamen bağımsızlar.


-1

Buna müşterinin bakış açısından bakardım. İstemci bir arabirimin özelliklerini kullanıyorsa ve dahili olarak bu özellik A Sınıfı tarafından uygulandıysa, A sınıfını genişleten bir B sınıfı olduğunu varsayalım, o zaman yarın A sınıfını bu arabirimden çıkarır ve B sınıfını koyarsam, o zaman B sınıfı Ayrıca müşteriye aynı özellikleri sunar. Standart örnek, yüzen bir Duck sınıfıdır ve ToyDuck Duck'ı uzatırsa, o zaman da yüzmeli ve yüzemeyeceğinden şikayet etmemelidir, aksi takdirde ToyDuck, Duck sınıfını uzatmamalıydı.


Herhangi bir cevabı oylamada bulunan kişilerin de yorum yazması çok yapıcı olacaktır. Ne de olsa hepimiz burada bilgiyi paylaşmak için varız ve uygun bir sebep olmadan karar vermek sadece bir amaca hizmet etmeyecek.
AKS

Bu, önceki 6
cevapta

1
Sanırım ilkelerden birini, sanırım L'yi açıklıyor gibisiniz. Sorun değil, ancak soru iki farklı ilkenin karşılaştırılması / karşıtlığını istedi. Muhtemelen bu yüzden biri bunu reddetti.
StarWeaver
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.