Bu Liskov Değişim İlkesinin ihlali midir?


132

Diyelim ki bir Görev varlıkları listesi ve bir ProjectTaskalt tür var. Görevler, ProjectTasksBaşlama statüsüne sahip olduklarında kapatılamaz olmaları haricinde, herhangi bir zamanda kapatılabilir. Kullanıcı Arabirimi, bir başlatmayı kapatma seçeneğinin ProjectTaskhiçbir zaman kullanılabilir olmadığından emin olmalıdır , ancak alanda bazı güvenlik önlemleri vardır:

public class Task
{
     public Status Status { get; set; }

     public virtual void Close()
     {
         Status = Status.Closed;
     }
}

public class ProjectTask : Task
{
     public override void Close()
     {
          if (Status == Status.Started) 
              throw new Exception("Cannot close a started Project Task");

          base.Close();
     }
}

Şimdi Close()bir Görevi çağırırken , başlangıç ProjectTaskdurumuna sahipse, bir temel Görev olmasaydı aramanın başarısız olması ihtimali var . Ancak bu iş gereksinimleridir. Başarısız olmalı. Bu Liskov ikame ilkesinin ihlali olarak kabul edilebilir mi?


14
Liskov ikamesini ihlal eden bir T örneği için mükemmel. Kalıtım kullanmayın burada ve iyi olacak.
Jimmy Hoffa

8
Bunu değiştirmek isteyebilirsiniz public Status Status { get; private set; }:; Aksi takdirde, Close()yöntem çalışılabilir.
İş

5
Belki bu sadece bir örnek, ancak LSP'ye uymanın maddi bir faydası göremiyorum. Bana göre, sorudaki bu çözüm net, anlaşılması kolay ve bakımı LSP ile uyumlu olandan daha kolay.
Ben Lee,

2
@BenLee Bakımı kolay değil. Sadece öyle görünüyor çünkü bunu yalıtılmış olarak görüyorsunuz. Sistem büyük olduğunda, alt Tasktürlerinin polimorfik kodda tuhaf uyumsuzluklar ortaya koymadığından emin olmak sadece bilmesi Taskgereken bir şeydir. LSP bir heves değil, büyük sistemlerde sürdürülebilirliği sağlamak için tam olarak tanıtıldı.
Andres F.

8
@BenLee Bir TaskClosersürecin olduğunu hayal edin closesAllTasks(tasks). Bu süreç açıkça istisnalar yakalamaya çalışmıyor; ne de olsa, bu açık sözleşmenin bir parçası değil Task.Close(). Şimdi ProjectTaskaniden TaskCloser(muhtemelen işlenmemiş) istisnalar atmaya başlarsınız. Bu büyük bir anlaşma!
Andres F.

Yanıtlar:


173

Evet, bu LSP'nin ihlalidir. Liskov değiştirme İlke gerektirir olduğu

  • Bir alt türde ön koşullar güçlendirilemez.
  • Son koşullar bir alt tipte zayıflayamaz.
  • Üst tipin değişmezleri bir alt tipte korunmalıdır.
  • Tarih kısıtlaması ("tarih kuralı"). Nesnelerin sadece yöntemleri ile değiştirilebildiği kabul edilir (kapsülleme). Alt tipler, üst tipte bulunmayan yöntemleri ortaya koyabildiğinden, bu yöntemlerin eklenmesi, alt tipte üst tipte izin verilmeyen durum değişikliklerine izin verebilir. Tarih kısıtlaması bunu yasaklar.

Örneğiniz, Close()yöntemi çağırmak için bir ön koşulu güçlendirerek ilk gereksinimi ihlal eder .

Güçlendirilmiş ön koşulu miras hiyerarşisinin en üst seviyesine getirerek düzeltebilirsiniz:

public class Task {
    public Status Status { get; set; }
    public virtual bool CanClose() {
        return true;
    }
    public virtual void Close() {
        Status = Status.Closed;
    }
}

Bir çağrının Close()yalnızca CanClose()iadeler truedurumunda geçerli olacağını öngörerek, ön koşulu LSP ihlalini düzelterek için Taskde geçerli kılacağınızı belirtin ProjectTask:

public class ProjectTask : Task {
    public override bool CanClose() {
        return Status != Status.Started;
    }
    public override void Close() {
        if (Status == Status.Started) 
            throw new Exception("Cannot close a started Project Task");
        base.Close();
    }
}

17
Bu çekin çoğaltılmasını sevmiyorum. Task.'a girerek istisna atmayı tercih ederim.
Öforik

4
@Euphoric Bu doğru, Closekontrolün en üst seviyeye çıkması ve korunan eklenmesi DoClosegeçerli bir alternatif olacaktır. Ancak, OP örneğine mümkün olduğunca yakın kalmak istedim; bunun üzerinde gelişmek ayrı bir sorudur.
dasblinkenlight 16:12

5
@Euphoric: Ama şimdi şu soruyu yanıtlamanın bir yolu yok, "Bu görev kapatılabilir mi?" kapatmaya çalışmadan. Bu gereksiz yere akış kontrolü için istisnaların kullanılmasını zorlar. Bununla birlikte, bu tür bir şeyin çok fazla alınabileceğini itiraf edeceğim. Çok ileri götürüldüğünde, bu tür bir çözüm girişimci bir karışıklığa yol açabilir. Ne olursa olsun, OP'nin sorusu beni ilkeler konusunda daha fazla vuruyor, bu yüzden fildişi bir kule cevabı çok uygun. +1
Brian

30
@Brian CanClose hala orada. Görev'in kapatılıp kapatılamayacağını kontrol etmek için hala çağrılabilir. Kapat girişindeki kontrol de bunu çağırmalıdır.
Öforik

5
@Euphoric: Ah, yanlış anladım. Haklısın, bu çok daha temiz bir çözüm yapar.
Brian

82

Evet. Bu, LSP'yi ihlal ediyor.

Önerim, CanClosetemel göreve yöntem / özellik eklemektir , böylece herhangi bir görev bu durumdaki görevin kapatılıp kapatılamayacağını söyleyebilir. Ayrıca nedenini de sağlayabilir. Ve sanal olanı kaldırın Close.

Yorumuma göre:

public class Task {
    public Status Status { get; private set; }

    public virtual bool CanClose(out String reason) {
        reason = null;
        return true;
    }
    public void Close() {
        String reason;
        if (!CanClose(out reason))
            throw new Exception(reason);

        Status = Status.Closed;
    }
}

public ProjectTask : Task {
    public override bool CanClose(out String reason) {
        if (Status != Status.Started)
        {
            reason = "Cannot close a started Project Task";
            return false;
        }
        return base.CanClose(out reason);
    }
}

3
Bunun için dasblinkenlight'ın örneğini bir adım daha aldınız, ancak açıklamasını açıkladım. Üzgünüm 2 cevabı kabul edemiyorum!
Paul T Davies

İmzanın neden halka açık sanal bool olduğunu bilmekle ilgileniyorum CanClose (out Dize nedeni) - kullanarak, yalnızca geleceğe yönelik bir prova mı yapıyorsunuz? Yoksa kaçırdığım daha ince bir şey mi var?
Reacher Gilt,

3
@ReacherGilt Ne yaptığınızı kontrol etmeli / ref yazmalı ve kodumu tekrar okumalısınız. Kafan karışmış. Basitçe "Görev kapanamazsa nedenini bilmek istiyorum."
Öforik

2
out, tüm dillerde bulunmazsa, bir tuple (veya nedeni ve boole içeren basit bir nesne döndürmek), doğrudan boolma kolaylığını kaybetme pahasına olsa bile, OO dilleri arasında daha iyi taşınabilir hale getirecektir.
Newtopian

1
Ve CanClose mülkünün ön koşullarını güçlendirmek uygun mudur? Durumu ekleyelim mi?
John V

24

Liskov ikame prensibi, bir temel sınıfın, programın istenen özelliklerinden herhangi birini değiştirmeden, alt sınıflarından herhangi biri ile değiştirilebilir olması gerektiğini belirtir. Sadece ProjectTaskkapalı olduğunda bir istisna yarattığından, ProjectTaskbunun yerine bir programın değiştirilmesi gerekecekti , yerine kullanılmalı Task. Bu yüzden bir ihlaldir.

Eğer değiştirirseniz Ama Taskonun imzası belirten edebilir kapatıldığında bir istisna, o zaman ilkesini ihlal olmaz.


Bu olasılığı olmadığını düşündüğüm c # 'yu kullanıyorum, fakat Java' nın bildiğini biliyorum.
Paul T Davies

2
@PaulTDavies Ne gibi istisnalar attığı bir yöntemi dekore edebilirsiniz, msdn.microsoft.com/en-us/library/5ast78ax.aspx . Bunu, temel sınıf kütüphanesindeki bir yönteme götürdüğünüzde, bir istisnalar listesi elde edersiniz. Zorunlu değildir, ancak arayanı yine de farkında kılar.
Despertar

18

Bir LSP ihlali üç taraf gerektirir. T Tipi, Alt Tipi S ve T'yi kullanan P programı ancak S örneği verilmiştir.

Sorunuz T (Görev) ve S (ProjectTask) sağladı, ancak P sağladı. Yani sorunuz tamamlanmadı ve cevap nitelikli: Eğer bir istisna beklemeyen bir P varsa, o zaman P için bir LSP'niz var. ihlal. Her P bir istisna beklerse, LSP ihlali olmaz.

Ancak, bunu bir var SRP ihlali. Bir görev durumunun değiştirilebilmesi ve belirli eyaletlerdeki belirli görevlerin başka devletlere değiştirilmemesi gerektiği politikası , iki farklı sorumluluğa sahiptir.

  • Sorumluluk 1: Bir görevi temsil eder.
  • Sorumluluk 2: Görevlerin durumunu değiştiren politikaları uygulayın.

Bu iki sorumluluk farklı nedenlerden dolayı değişir ve bu nedenle ayrı sınıflarda olması gerekir. Görevler, bir görev olma gerçeğini ve bir görevle ilişkili verileri ele almalıdır. TaskStatePolicy, verilen bir uygulamada görevlerin durumdan duruma geçiş yolunu kullanmalıdır.


2
Sorumluluklar büyük ölçüde alana ve (bu örnekte) karmaşık görevin ve onun değiştiricilerin durumlarına bağlıdır. Bu durumda, böyle bir şey belirtisi yoktur, bu yüzden SRP ile ilgili bir sorun yoktur. LSP ihlaline gelince, hepimizin arayanın bir istisna beklemeyeceğini ve uygulamanın hatalı duruma geçmek yerine makul bir mesaj göstermesi gerektiğini düşündüğümüzü düşünüyorum.
Öforik

Unca 'Bob cevap veriyor mu? "Biz layık değiliz! Biz layık değiliz!" Her neyse ... Her P bir istisna beklerse LSP ihlali olmaz. AMA T örneğini şart koşarsak OpenTaskException(ipucu, ipucu) atamazsak ve her P bir istisna beklerse, uygulama için değil, arabirim koduyla ilgili ne diyor ? Ben neden bahsediyorum Bilmiyorum. Sadece bir Unca 'Bob cevabı üzerine yorum yapıyorum.
radarbob

3
Bir LSP ihlalini kanıtlamanın üç nesne gerektirdiği doğrudur. Bununla birlikte, eğer S'nin yokluğunda doğru olan bir P programı varsa, ancak S
Mart'ta

16

Bu , LSP'nin ihlali olabilir veya olmayabilir .

Ciddi anlamda. Bana kulak ver.

LSP'yi izlerseniz, tür ProjectTasknesnelerinin Taskdavranması beklendiği için tür nesnelerinin davranması gerekir .

Kodunuzla ilgili sorun, türdeki nesnelerin nasıl Taskdavranması beklendiğini belgelememenizdir . Kod yazdınız, ancak sözleşmeniz yok. İçin bir sözleşme ekleyeceğim Task.Close. Eklemiş olduğum sözleşmeye bağlı olarak, kodun ProjectTask.Closeya LSP'yi izlemesi ya da izlememesi.

Task.Close için aşağıdaki sözleşme göz önüne alındığında, için kod LSP takip ProjectTask.Close etmez :

     // Behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

Task.Close için kodu için aşağıdaki sözleşme Verilen ProjectTask.Close gelmez LSP'yi izleyin:

     // Behaviour: Moves the task to the closed status if possible.
     // If this is not possible, this method throws an Exception
     // and leaves the status unchanged.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

Geçersiz kılınabilecek yöntemler iki şekilde belgelenmelidir:

  • "Davranış", alıcı nesnesinin bir olduğunu bilen Task, ancak hangi sınıfın doğrudan olduğunu bilmeyen bir müşterinin neye güvenebileceğini belgelemektedir . Ayrıca, geçersiz kılan ve makul olmayan alt sınıf tasarımcılarına da söyler.

  • "Varsayılan davranış", alıcı nesnenin doğrudan bir örneği olduğunu bilen bir müşteri tarafından neye güvenebileceğini belgelemektedir Task(yani, eğer kullanırsanız ne elde edersiniz new Task(). Ayrıca, alt sınıf tasarımcılarına, eğer yapmazlarsa hangi davranışların kalıtsal olacağını da söyler. yöntemi geçersiz kıl.

Şimdi aşağıdaki ilişkiler geçerli olmalı:

  • S, T'nin bir alt türü ise, S'nin belgelenmiş davranışı, T'nin belgelenmiş davranışını düzeltmelidir.
  • S, T'nin bir alt türü (veya eşittir) ise, S'nin kodunun davranışı, T'nin belgelenmiş davranışını düzeltmelidir.
  • S, T'nin bir alt türü (veya eşittir) ise, S'nin varsayılan davranışı, T'nin belgelenmiş davranışını düzeltmelidir.
  • Bir sınıfın kodunun gerçek davranışı, belgelenen varsayılan davranışını iyileştirmelidir.

@ user61852, yöntemin imzasında bir istisna oluşturabildiğini belirtebileceğiniz noktayı ortaya koydu ve basitçe bunu yaparak (gerçek efekt koduna sahip olmayan bir şey) artık LSP'yi çiğniyorsunuz.
Paul T Davies

@PaulTDavies Haklısın. Ancak birçok dilde imza, bir rutinin bir istisna atabileceğini ilan etmenin iyi bir yolu değildir. Örneğin OP'de (C # da sanırım) ikinci uygulama Closeatıyor. Yani imza bir istisna olduğunu beyan edebilir o kimse olmaz demiyor - atılmasına. Java bu konuda daha iyi bir iş çıkarır. Öyle olsa bile, bir yöntemin bir istisna olduğunu beyan ederseniz, bunun altında olabileceği (veya olacak) koşulları belgelemelisiniz. Bu yüzden, LSP'nin ihlal edilip edilmediğinden emin olmak için imzanın ötesinde belgelere ihtiyacımız olduğunu savunuyorum.
Theodore Norvell

4
Buradaki birçok cevap, sözleşmeyi bilmiyorsanız bir sözleşmenin onaylanıp onaylanmadığını bilemeyeceğinizi tamamen görmezden geliyor gibi görünmektedir. Cevabınız için teşekkürler.
gnasher729

İyi cevap, ama diğer cevaplar da iyi. Temel sınıfın istisna atmadığı sonucuna varırlar çünkü o sınıfta bunun belirtilerini gösteren hiçbir şey yoktur. Dolayısıyla temel sınıfı kullanan program istisnalar için hazırlanmamalıdır.
inf3rno

İstisna listesinin bir yerde belgelenmesi konusunda haklısın. Bence en iyi yer kodda. Burada ilgili bir soru var: stackoverflow.com/questions/16700130/… Fakat bunu açıklama olmadan da yapabilirsiniz, vb., Sadece if (false) throw new Exception("cannot start")temel sınıfa benzer bir şeyler yazın . Derleyici onu kaldıracak ve hala gerekli olanı içeriyor. Btw. Bu geçici çözümlerle ilgili hala bir LSP ihlali var, çünkü önkoşul hala güçlendiriliyor ...
inf3rno

6

Liskov Değişim İlkesinin ihlali değildir.

Liskov Değişim Prensibi şöyle diyor:

Let q (X) nesneler hakkında kanıtlanabilir bir özellik olabilir x tipi T . S , T'nin bir alt türü olsun . Tip G bir amacı ise Liskov değiştirme prensibi ihlal y tipi S bulunacak şekilde, q, (y) kanıtlanabilir değildir.

Alt tipi uygulamanızın Liskov Yerine Getirme İlkesini ihlal etmemesinin sebebi oldukça basittir: Task::Close()gerçekte ne yaptığı konusunda hiçbir şey kanıtlanamaz . Tabii, ProjectTask::Close()ne zaman bir istisna atar Status == Status.Started, ama çok olabilir Status = Status.Closedde Task::Close().


4

Evet, bir ihlaldir.

Hiyerarşinizi geri almanızı öneriyorum. Her biri kapalı değilse Task, o close()zamanTask . Belki de CloseableTasktüm olmayanların ProjectTasksuygulayabileceği bir arayüz istiyorsunuz .


3
Her Görev kapatılabilir, ancak her koşulda değil.
Paul T Davies

Bu yaklaşım benim için riskli gözüküyor çünkü insanlar tüm Task'ın ClosableTask uygulamasını yapmasını beklerken kod yazabiliyor, ancak problemi doğru bir şekilde modelliyor. Bu yaklaşımla durum makinesi arasında parçalandım çünkü durum makinelerinden nefret ediyorum.
Jimmy Hoffa

Eğer Taskkendisi uygulamıyor CloseableTasksonra da güvenli olmayan bir döküm yapıyoruz yerde bile çağırmak için Close().
Tom G,

@TomG korktuğum şey bu
Jimmy Hoffa

1
Halen bir devlet makinesi var. Nesne kapatılamıyor çünkü yanlış durumda.
Kaz

3

Bir LSP sorununa ek olarak, program akışını kontrol etmek için istisnalar kullanıyor gibi görünüyor (Bu önemsiz istisnayı bir yerde yakaladığınızı ve uygulamanızın çökmesine izin vermek yerine bazı özel akışlar yaptığınızı varsaymalıyım).

TaskState için Devlet modelini uygulamak ve durum nesnelerinin geçerli geçişleri yönetmesine izin vermek için iyi bir yer gibi görünüyor.


1

Burada, LSP ve Sözleşmeye Göre Tasarım ile ilgili önemli bir şeyi özlüyorum - önkoşullarda, önkoşulların yerine getirildiğinden emin olmak, arayanın sorumluluğundadır. DbC teorisinde çağrılan kod ön koşulu doğrulamamalıdır. Sözleşme, bir görevin ne zaman kapanabileceğini (örneğin CanClose, True döndürür) belirtmeli ve daha sonra arama kodu, ön koşulun Kapat () çağrılmadan önce yerine getirilmesini sağlamalıdır.


Sözleşme, işletmenin ihtiyaç duyduğu davranışları belirtmelidir. Bu durumda, bu Kapat () başlatıldığında çağrıldığında bir istisna oluşturacaktır ProjectTask. Bu bir post-koşuldur ( yöntem çağrıldıktan sonra ne olduğunu söyler ) ve bunu yerine getirmek kodun sorumluluğundadır.
Goyo

@Goyo Evet, ancak diğerleri gibi istisnanın ön koşulu güçlendiren alt tipte gündeme geldiğini ve böylece Close () 'u çağırmanın görevi kapattığı (ima edilen) sözleşmeyi ihlal ettiği belirtildi.
Ezoela Vacca

Hangi önkoşul? Hiç birşey göremiyorum.
Goyo

@Goyo Kabul edilen cevabı kontrol et, örneğin :) Temel sınıfta, Kapat'ın önkoşulu yoktur, çağrılır ve görevi kapatır. Bununla birlikte, çocukta, durumun başlatılmaması ile ilgili bir önkoşul vardır. Diğerlerinin de belirttiği gibi, bu daha güçlü kriterlerdir ve davranış bu nedenle yerine geçemez.
Ezoela Vacca

Boş ver, soruyu önkoşulu buldum. Ancak daha sonra (DbC-bilge), önkoşulları kontrol eden ve karşılanmadıklarında istisnalar oluşturan kod ile ilgili yanlış bir şey yoktur. Buna "savunma programlama" denir. Ayrıca, ön koşulun bu durumda olduğu gibi karşılanmadığında ne olacağını belirten bir post-koşul varsa, uygulama post-koşulunun yerine getirildiğinden emin olmak için ön koşulu doğrulamak zorundadır.
Goyo

0

Evet, bu açıkça bir LSP ihlalidir.

Bazı insanlar burada temel sınıfta alt sınıfların istisnalar ortaya koyabileceğini açıkça belirtmenin bunu kabul edilebilir kılacağını savunuyorlar, ancak bunun doğru olduğunu sanmıyorum. Temel sınıfta ne belgelemiş olursanız olun veya kodu hangi soyutlama seviyesine götürürseniz götürün, önkoşullar alt sınıfta da güçlenecektir, çünkü ona "Başlatılan Proje Görevi kapatılamıyor" bölümünü eklediniz. Bu, geçici bir çözümle çözebileceğiniz bir şey değildir, LSP'yi ihlal etmeyen farklı bir modele ihtiyacınız vardır (veya "önkoşullar güçlendirilmez" kısıtlamasını gevşetmemiz gerekir).

Bu durumda, LSP ihlalini önlemek istiyorsanız, dekoratör desenini deneyebilirsiniz. İşe yarayabilir, bilmiyorum.

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.