LSP'yi ihlal etmek hiç uygun mu?


10

Bu soruyu takip ediyorum , ama odağımı koddan ilkeye geçiriyorum.

Liskov ikame ilkesini (LSP) anladığımdan, temel sınıfımda ne olursa olsun, alt sınıfımda uygulanmaları gerekir ve bu sayfaya göre , temel sınıftaki bir yöntemi geçersiz kılarsanız ve hiçbir şey yapmazsa veya istisna, prensibi ihlal ediyorsunuz.

Şimdi, sorunum şu şekilde özetlenebilir: Bir özetim Weapon classve iki dersim var Swordve Reloadable. Denilen Reloadablebelirli bir içeriğe sahipse method, buna Reload()erişmek için mahzun etmem gerekir methodve ideal olarak bundan kaçınmak istersiniz.

Sonra kullanmayı düşündüm Strategy Pattern. Bu şekilde, her silah yalnızca gerçekleştirebileceği eylemlerin farkındaydı, örneğin bir Reloadablesilah, açıkça yeniden yüklenebilir, ancak bir Swordyapamaz ve hatta bir farkında bile değildir Reload class/method. Stack Overflow yayınımda belirttiğim gibi, mahzun etmek zorunda değilim ve bir List<Weapon>koleksiyon tutabilirim .

On başka forumda ilk cevap izin önerilen Swordfarkında olmak Reloadsadece bir şey yapmayın. Aynı cevap, yukarıda bağlandığım Yığın Taşması sayfasında da verildi.

Nedenini tam olarak anlamıyorum. Prensibi ihlal edip Kılıç'ın farkında olmasına Reloadve boş bırakmasına neden izin vermeliyim ? Stack Overflow yayınımda söylediğim gibi, SP, sorunlarımı hemen hemen çözdü.

Neden uygun bir çözüm değil?

public final Weapon{

    private final String name;
    private final int damage;
    private final List<AttackStrategy> validactions;
    private final List<Actions> standardActions;

    private Weapon(String name, int damage, List<AttackStrategy> standardActions, List<Actions> attacks)
    {
        this.name = name;
        this.damage = damage;
        standardActions = new ArrayList<Actions>(standardActions);
        validAttacks = new ArrayList<AttackStrategy>(validActions);
    }

    public void standardAction(String action){} // -- Can call reload or aim here.  

    public int attack(String action){} // - Call any actions that are attacks. 

    public static Weapon Sword(String name, damage, List<AttackStrategy> standardActions, List<Actions> attacks){
        return new Weapon(name, damage,standardActions, attacks) ;
    }

}

Saldırı Arayüzü ve Uygulaması:

public interface AttackStrategy{
    void attack(Enemy enemy);
}

public class Shoot implements AttackStrategy {
    public void attack(Enemy enemy){
        //code to shoot
    }
}

public class Strike implements AttackStrategy {
    public void attack(Enemy enemy){
        //code to strike
    }
}

2
Yapabilirsin class Weapon { bool supportsReload(); void reload(); }. İstemciler yeniden yüklemeden önce desteklenip desteklenmediklerini test ederler. reloadiff atmak için sözleşmeye bağlı olarak tanımlanmıştır !supportsReload(). Bu LSP iff tahrikli sınıflara uyduğu, az önce özetlediğim protokole bağlı.
usr

3
reload()Boş bırakıp bırakma veya standardActionsyeniden yükleme eylemi içermeme, sadece farklı bir mekanizmadır. Temel bir fark yok. Her ikisini de yapabilirsiniz. => Sizin çözüm olduğunu (sizin soruydu olan) yaşayabilir .; Silah boş bir varsayılan uygulama içeriyorsa, kılıç yeniden yükleme hakkında bilgi sahibi olmak zorunda değildir.
usr

27
Bu problemi çözmek için çeşitli tekniklerle çeşitli problemleri araştıran bir dizi makale yazdım. Sonuç: oyununuzun kurallarını dilin tip sisteminde yakalamaya çalışmayın . Oyunun kurallarını, tür sistemi seviyesinde değil, oyun mantığı düzeyinde temsil eden ve uygulayan nesnelerde yakalayın . Kullandığınız herhangi bir tür sisteminin oyun mantığınızı temsil edecek kadar sofistike olduğuna inanmak için hiçbir neden yoktur. ericlippert.com/2015/04/27/wizards-and-warriors-part-one
Eric Lippert

2
@EricLippert - Bağlantınız için teşekkürler. Bu bloga pek çok kez rastladım, ama bazı noktaları tam olarak anlamadım, ama bu senin hatan değil. Ben kendi kendime OOP öğreniyorum ve SOLID prensipleri ile karşılaştım. Blogunuzla ilk karşılaştığımda, hiç anlamadım, ama biraz daha öğrendim ve blogunuzu tekrar okudum ve yavaş yavaş söylenen kısımları anlamaya başladım. Bir gün, o dizideki her şeyi tam olarak anlayacağım. Umarım: D

6
@SR "hiçbir şey yapmazsa veya istisna atarsa, ihlal edersiniz" - Sanırım bu makaledeki mesajı yanlış okudunuz. Sorun doğrudan setAltitude'un hiçbir şey yapmadığı değil, "ayarlanan rakımda kuş çizilecek" koşulunu yerine getirememiş olmasıydı. "Yeniden yükleme" nin son koşulunu "yeterli cephane varsa silah tekrar saldırabilir" olarak tanımlarsanız, hiçbir şey yapmamak cephane kullanmayan bir silah için tamamen geçerli bir uygulamadır.
Sebastian Redl

Yanıtlar:


16

LSP, alt tipleme ve polimorfizm ile ilgilenmektedir. Tüm kodlar aslında bu özellikleri kullanmaz, bu durumda LSP önemsizdir. Alt tipleme olmayan iki yaygın miras dili yapısı örneği şunlardır:

  • Kalıtım, temel sınıfın uygulanmasını devralır, ancak arabirimini değil. Hemen hemen tüm durumlarda bileşim tercih edilmelidir. Java gibi diller, uygulama ve arayüz devralmalarını ayıramaz, ancak örneğin C ++ privatedevralma özelliğine sahiptir .

  • Bir toplam türünü / birleşimini modellemek için kullanılan kalıtım, örneğin: a Baseya CaseAyadır CaseB. Temel tür ilgili bir arabirim bildirmez. Örneklerini kullanmak için, bunları doğru beton tipine dökmeniz gerekir. Döküm güvenli bir şekilde yapılabilir ve sorun değildir. Ne yazık ki, birçok OOP dili temel sınıf alt türlerini yalnızca amaçlanan alt türlerle sınırlayamaz. Harici kod bir a oluşturabilirse CaseC, a'nın Baseyalnızca a CaseAveya CaseByanlış olabileceğini varsayan kod . Scala bunu case classkonseptiyle güvenle yapabilir . Java'da bu, Baseözel bir kurucuya sahip soyut bir sınıf olduğunda ve iç içe statik sınıflardan sonra tabandan devralındığında modellenebilir .

Gerçek dünyadaki nesnelerin kavramsal hiyerarşileri gibi bazı kavramlar, nesne yönelimli modellerle çok kötü bir şekilde eşleşir. “Silah bir silah ve kılıç bir silahtır, bu yüzden miras ve miras Weaponalınan bir temel sınıfım olacak ” gibi düşünceler : gerçek-kelime-ilişkiler, modelimizde böyle bir ilişki anlamına gelmez. İlgili bir sorun, nesnelerin birden fazla kavramsal hiyerarşiye ait olabilmeleri veya çalışma süresi boyunca hiyerarşi bağlantılarını değiştirebilmeleridir, çünkü çoğu dil miras genellikle nesne başına değil sınıf başına olduğundan ve çalışma zamanında değil çalışma zamanında tanımlanır.GunSword

OOP modelleri tasarlarken hiyerarşi veya bir sınıfın diğerini nasıl “genişlettiği” düşünmemeliyiz. Temel sınıf, birden çok sınıfın ortak bölümlerini hesaba katan bir yer değildir . Bunun yerine, nesnelerinizin nasıl kullanılacağını, yani bu nesnelerin kullanıcılarının ne tür bir davranışa ihtiyacı olduğunu düşünün.

Burada, kullanıcıların attack()silahlarla ve belki de silahlarla ihtiyacı olabilir reload(). Bir tür hiyerarşisi oluşturacaksak, bu yöntemlerin her ikisi de taban türünde olmalıdır, ancak geri yüklenemez silahlar bu yöntemi görmezden gelebilir ve çağrıldığında hiçbir şey yapamaz. Böylece temel sınıf ortak parçaları içermez, ancak tüm alt sınıfların birleşik arayüzünü içerir. Alt sınıflar arayüzlerinde farklılık göstermez, sadece bu arayüzü uygulamalarında farklılık gösterir.

Bir hiyerarşi oluşturmak gerekli değildir. İki tiptir Gunve Swordtamamen ilgisiz olabilir. Oysa sadece bir Gunkutu fire()ve reload()bir Swordmayıs strike(). Bu nesneleri polimorf olarak yönetmeniz gerekiyorsa, ilgili yönleri yakalamak için Bağdaştırıcı Deseni'ni kullanabilirsiniz. Java 8'de bu, fonksiyonel arayüzler ve lambdas / yöntem referansları ile oldukça uygun bir şekilde mümkündür. Örneğin, Attacktedarik ettiğiniz bir stratejiniz olabilir myGun::fireveya () -> mySword.strike().

Son olarak, bazen herhangi bir alt sınıftan kaçınmak mantıklıdır, ancak tüm nesneleri tek bir türle modelleyin. Bu, özellikle oyunlarla ilgilidir, çünkü birçok oyun nesnesi herhangi bir hiyerarşiye iyi uymaz ve birçok farklı özelliğe sahip olabilir. Örneğin, rol yapma oyunu, hem görev öğesi olan, hem de donatıldığında istatistiklerinizi +2 güçle güçlendiren, alınan herhangi bir hasarı görmezden gelme şansı% 20 olan ve yakın dövüş saldırısı sağlayan bir öğeye sahip olabilir. Ya da belki de yeniden yüklenebilir bir kılıç çünkü bu * büyü *. Hikayenin neye ihtiyacı olduğunu kim bilebilir.

Bu karışıklık için bir sınıf hiyerarşisi bulmaya çalışmak yerine, çeşitli yetenekler için yuvalar sağlayan bir sınıfa sahip olmak daha iyidir. Bu yuvalar çalışma zamanında değiştirilebilir. Her alan OnDamageReceivedveya gibi bir strateji / geri arama olacaktır Attack. Silah ile, olabilir MeleeAttack, RangedAttackve Reloadyuvaları. Bu yuvalar boş olabilir, bu durumda nesne bu özelliği sağlamaz. Yuvaları sonra koşullu olarak adlandırılır: if (item.attack != null) item.attack.perform().


Bir şekilde SP gibi. Yuva neden boşalmak zorunda? Sözlük eylemi içermiyorsa, hiçbir şey yapmayın

@SR Bir yuvanın boş olup olmadığı önemli değildir ve bu yuvaları uygulamak için kullanılan mekanizmaya bağlıdır. Bu cevabı, yuvaların örnek alanlar olduğu ve her zaman var olduğu oldukça statik bir dilin varsayımlarıyla yazdım (yani Java'da normal sınıf tasarımı). Yuvaların sözlükteki girişler olduğu daha dinamik bir model seçerseniz (Java'da HashMap kullanmak veya normal bir Python nesnesi gibi), yuvaların mevcut olması gerekmez. Daha dinamik yaklaşımların, genellikle arzu edilmeyen çok fazla tür güvenliğinden vazgeçtiğini unutmayın.
amon

Gerçek dünyadaki nesnelerin iyi modellenmediğini kabul ediyorum. Gönderinizi anlarsam, Strateji Desenini kullanabileceğimi mi söylüyorsunuz?

2
@SR Evet, bir şekilde Strateji Paterni muhtemelen mantıklı bir yaklaşımdır. Ayrıca ilgili Tür Nesne Deseni karşılaştırın: gameprogrammingpatterns.com/type-object.html
amon

3

Çünkü bir stratejiye sahip olmak attackihtiyaçlarınız için yeterli değil. Elbette, öğenin hangi eylemleri yapabileceğini soyutlamanızı sağlar, ancak silahın aralığını bilmeniz gerektiğinde ne olur? Veya cephane kapasitesi? Veya ne tür bir cephane gerekir? Buna ulaşmak için aşağı inmeye geri döndün. Ve bu esneklik seviyesine sahip olmak, kullanıcı arayüzünün uygulanmasını biraz daha zorlaştıracaktır, çünkü tüm yeteneklerle başa çıkmak için benzer bir strateji modeline sahip olması gerekecektir.

Bütün bunlar, diğer sorularınızın cevaplarına özellikle katılmıyorum. Sahip swordden kaynaklanan devralma weaponbir ameliyat yöntemleri ya da tip kontroller için her zaman açar kodu ile ilgili saçılmış korkunç, naif OO olup.

Ancak konunun temelinde iki çözüm de yanlış değildir . Oynamak için eğlenceli bir oyun yapmak için her iki çözümü de kullanabilirsiniz. Her biri, seçtiğiniz herhangi bir çözüm gibi, kendi takasları ile birlikte gelir.


Bence bu mükemmel. SP'yi kullanabilirim, ancak takas yapıyorlar, sadece onların farkında olmak zorundalar. Aklımdakiler için düzenlememe bakın.

1
Fwiw: Bir kılıç sonsuz cephaneye sahiptir: sonsuza dek okumadan kullanmaya devam edebilirsiniz; reload hiçbir şey yapmaz çünkü başlangıç ​​için sonsuz kullanımınız vardır; bir çeşit / yakın dövüş silahı: bir yakın dövüş silahı. Tüm istatistikleri / eylemleri hem yakın hem menzil için uygun şekilde düşünmek imkansız değildir. Yine de, yaşlandıkça arayüzler, rekabet ve Weaponkılıç ve silah örneğiyle tek bir sınıf kullanmak için ne olursa olsun, daha az kalıtım kullanıyorum .
CAD97

Destiny 2 kılıçlarındaki Fwiw herhangi bir nedenle cephane kullanıyor!

@ CAD97 - Bu sorunla ilgili olarak gördüğüm düşünce türüdür. Sonsuz cephane ile kılıç var, bu yüzden yeniden yükleme yok. Bu sadece sorunu etrafa iter veya gizler. Bir el bombası getirirsem ne olur? El bombalarının cephanesi veya ateşi yoktur ve bu tür yöntemlerin farkında olmamalıdır.

1
Bu konuda CAD97 ile beraberim. Ve WeaponBuilderbir strateji silahı oluşturarak kılıç ve silahlar inşa edebilecek bir tane yaratacaktı .
Chris Wohlert

3

Tabii ki uygulanabilir bir çözüm; bu sadece çok kötü bir fikir.

Eğer temel sınıf yeniden yükleme koymak tek bir örneği varsa sorun değildir. Sorun "salıncak", "ateş", "savuşturmak", "vurmak", "lehçe", "sökmek", "keskinleştirmek" ve "kulübün sivri ucunun nailes yerine" koymak zorunda olmasıdır temel sınıfınızda.

LSP'nin amacı, üst düzey algoritmalarınızın çalışması ve anlamlı olması gerektiğidir. Yani böyle bir kod varsa:

if (isEquipped(weapon)) {
   reload();
}

Şimdi bu uygulanmayan bir istisna atar ve programınızın çökmesine neden olursa, bu çok kötü bir fikirdir.

Kodunuz böyle görünüyorsa,

if (canReload(weapon)) {
   reload();
}
else if (canSharpen(weapon)) {
  sharpen();
}
else if (canPollish(weapon)) {
  polish();
}

kodunuz soyut 'silah' fikriyle hiçbir ilgisi olmayan çok spesifik özelliklerle karmaşıklaşabilir.

Ancak bir birinci şahıs nişancı uygulıyorsanız ve tüm silahlarınız bir bıçak hariç (belirli bir bağlamda) ateş edebilir / yeniden yükleyebilirse, bıçağınızın yeniden yüklenmesinin hiçbir şey yapmaması çok mantıklıdır, çünkü istisna ve oranlar temel sınıfınızın belirli özelliklerle darmadağın olması düşüktür.

Güncelleme: Soyut vaka / terimler üzerinde düşünmeye çalışın. Örneğin, her silahın silahlar için yeniden yükleme ve kılıçlar için bir kılıf olmayan bir "hazırlık" eylemi vardır.


Diyelim ki silahların eylemlerini tutan bir iç silah sözlüğüm var ve kullanıcı "Yeniden Yükle" den geçtiğinde sözlüğü denetler, örneğin, weaponActions.containsKey (eylem) öyleyse, ilişkili nesneyi al ve yap o. Birden fazla if ifadesine sahip bir silah sınıfından ziyade

Yukarıdaki düzenlemeye bakın. SP kullanırken aklımda olan budur

0

Açıkçası, temel sınıfın bir örneğini değiştirmek amacıyla bir alt sınıf oluşturmazsanız, ancak temel sınıfı işlevselliğin uygun bir havuzu olarak kullanarak bir alt sınıf oluşturursanız sorun olmaz.

Şimdi bunun iyi bir fikir olup olmadığı çok tartışmalıdır, ancak alt sınıfı asla taban sınıfının yerine koymazsanız, işe yaramadığı gerçeği sorun değildir. Sorunlarınız olabilir, ancak bu durumda LSP sorun değildir.


0

LSP iyidir, çünkü arama kodunun sınıfın nasıl çalıştığı konusunda endişelenmemesine izin verir.

Örneğin. BattleMech'e takılan tüm Silahlara Weapon.Attack () diyebilirim ve bazılarının bir istisna atabileceği ve oyunumu çökertebileceğinden endişe etmiyorum.

Şimdi sizin durumunuzda taban türünüzü yeni işlevlerle genişletmek istiyorsunuz. Saldırı () bir sorun değildir, çünkü Gun sınıfı cephanesini takip edebilir ve bittiğinde ateş etmeyi durdurabilir. Ancak Reload () yeni bir şey ve silah olmanın bir parçası değil.

Kolay çözüm mahzun, aşırı performans konusunda endişelenmeniz gerektiğini düşünmüyorum, her karede yapmayacaksınız.

Alternatif olarak, mimarinizi yeniden değerlendirebilir ve özette tüm Silahların yeniden yüklenebilir olduğunu ve bazı silahların asla yeniden yüklenmesine gerek olmadığını düşünebilirsiniz.

O zaman artık silahlar için sınıfı genişletmiyorsunuz ya da LSP'yi ihlal etmiyorsunuz.

Ama bu uzun vadede sorunlu çünkü daha özel durumlar, Gun.SafteyOn (), Sword.WipeOffBlood () vb. değişmek zorunda.

düzenleme: strateji modeli neden kötü (tm)

Değil, ancak kurulum, performans ve genel kodu düşünün.

Bir yerde bir silahın yeniden yüklenebileceğini söyleyen bir konfigürasyonum olmalı. Bir silahı başlattığımda, bu yapılandırmayı okumalı ve tüm yöntemleri dinamik olarak eklemeliyim, yinelenen adlar vb.

Bir yöntemi çağırdığımda, bu eylemler listesinde döngü yapmam ve hangisini çağıracağını görmek için bir dize eşleşmesi yapmam gerekiyor.

Ben kodu derlemek ve "saldırı" yerine Weapon.Do ("atack") çağırdığımda derleme hata almayacak.

Bazı problemler için uygun bir çözüm olabilir, örneğin rastgele yöntemlerin farklı kombinasyonlarına sahip yüzlerce silahınız var, ancak OO ve güçlü yazmanın birçok faydasını kaybedersiniz. Gerçekten inişli çıkışlı bir şeyden tasarruf etmiyor


SP'nin silahın sahip olduğu SafteyOn()ve Swordsahip olacağı her şeyi (yukarıdaki düzenlemeye bakınız) ele alabileceğini düşünüyorum wipeOffBlood(). Her silah diğer yöntemlerin farkında değildir (ve olmamalıdır)

SP gayet iyi, ancak tip güvenliği olmadan downcastinge eşdeğer. Sanırım başka bir soruya cevap
Ewan

2
Tek başına strateji modeli, bir stratejinin bir listede veya sözlükte dinamik olarak aranması anlamına gelmez. Yani her ikisi weapon.do("attack")de tip güvenli weapon.attack.perform()strateji modeline örnek olabilir. Stratejileri ada göre aramak, yalnızca bir yapılandırma dosyasından nesneyi yapılandırırken gereklidir, ancak yansıma kullanmak eşit derecede güvenlidir.
amon

bazı kullanıcı girişlerine bağlamanız gereken iki ayrı eylem saldırısı ve yeniden yükleme olduğu için bu durumda işe yaramayacak
Ewan
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.