Sihirbazlarda ve Savaşçılarda Kuralların Aşılması


9

Bu blog gönderileri dizisinde Eric Lippert, sihirbazları ve savaşçıları kullanarak nesne odaklı tasarımda bir sorunu şöyle açıklıyor:

abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }

abstract class Player 
{ 
  public Weapon Weapon { get; set; }
}
sealed class Wizard : Player { }
sealed class Warrior : Player { }

ve sonra birkaç kural ekler:

  • Bir savaşçı sadece kılıç kullanabilir.
  • Bir sihirbaz yalnızca bir personel kullanabilir.

Daha sonra, C # tipi sistemi kullanarak bu kuralları uygulamaya çalıştığınızda karşılaştığınız sorunları göstermeye devam eder (örneğin Wizard, bir sihirbazın yalnızca bir personel kullanabileceğinden emin olmak için sınıfı sorumlu tutmak ). Liskov İkame İlkesini ihlal edersiniz, çalışma zamanı istisnalarını riske eder veya uzatılması zor olan kodlarla sonuçlanırsınız.

Ortaya koyduğu çözüm, Player sınıfı tarafından hiçbir doğrulama yapılmamasıdır. Yalnızca durumu izlemek için kullanılır. Sonra, bir oyuncuya şu silahı vermek yerine:

player.Weapon = new Sword();

durumu Commands ile değiştirilir ve s'ye göre Rule:

... a ve a olmak üzere iki oyun durumu nesnesi alan bir Commandnesne yaparız . Kullanıcı sisteme “bu sihirbaz o kılıcı kullanmalıdır” komutunu verdiğinde, bu komut s dizisi üreten bir s kümesi bağlamında değerlendirilir . Biz bir var bir oyuncu girişimleri silah ellerinde ne zaman, etkisi mevcut silah, eğer varsa, düşürülür ve yeni silah oyuncunun silah haline gelmesi olduğunu söylüyor. Birinci kuralı güçlendiren başka bir kuralımız var, yani bir sihirbaz bir kılıç kullanmaya çalıştığında ilk kuralın etkilerinin geçerli olmadığını söylüyor.WieldPlayerWeaponRuleEffectRule

Bu fikri prensipte seviyorum, ama pratikte nasıl kullanılabileceğiyle ilgili bir endişem var.

Hiçbir şey atlatabilmesini bir geliştirici önlemek gibi görünüyor Commandsve Rulesadece ayarlayarak s Weaponbir üstünde Player. WeaponMülkiyet tarafından erişilebilir olmalıdır Wieldo yapılamaz bu yüzden, komuta private set.

Peki, ne yapar bunu bir geliştirici önlemek? Sadece hatırlamamaları gerekiyor mu?


2
Bu sorunun dile özgü (C #) olduğunu düşünmüyorum çünkü gerçekten OOP tasarımı ile ilgili bir soru. Lütfen C # etiketini kaldırmayı düşünün.
Maybe_Factor

1
@maybe_factor c # etiketi iyidir çünkü gönderilen kod c #.
CodingYoshi

Neden @EricLippert'e doğrudan sormuyorsun? Burada zaman zaman bu sitede görünmektedir.
Doc Brown

@Maybe_Factor - C # etiketi üzerinde tereddüt ettim, ancak dile özgü bir çözüm olması durumunda tutmaya karar verdim.
Ben L

1
@DocBrown - Bu soruyu bloguna gönderdim (sadece birkaç gün önce itiraf ettim; bir yanıt için bu kadar beklemedim). Sorumu dikkatine sunmanın bir yolu var mı?
Ben L

Yanıtlar:


9

Blog gönderileri dizisinin yol açtığı argümanın tamamı Beşinci Bölüm'dedir :

C # tipi sistemin, Zindanlar ve Ejderhaların kurallarını kodlamak için yeterli genelliğe sahip olacak şekilde tasarlandığına inanmak için hiçbir nedenimiz yok, neden çalışıyoruz?

“Sistemin kurallarını ifade eden kod nereye gidiyor?” Sorununu çözdük. Oyun durumunu temsil eden nesnelere değil, sistemin kurallarını temsil eden nesnelere gider; devlet nesnelerinin endişesi, oyunun kurallarını değerlendirirken değil, devletlerini tutarlı tutmaktır.

Silahlar, karakterler, canavarlar ve diğer oyun nesneleri ne yapabileceklerini veya yapamayacaklarını kontrol etmekten sorumlu değildir. Kural sistemi bundan sorumludur. CommandNesne ya oyun nesnelerle hiçbir şey yapmıyor. Sadece onlarla bir şeyler yapma girişimini temsil ediyor . Kural sistemi daha sonra komutun mümkün olup olmadığını kontrol eder ve kural sistemi, oyun nesneleri üzerinde uygun yöntemleri çağırarak komutu yürütür.

Bir geliştirici , ilk kural sisteminin izin vermediği karakterler ve silahlarla bir şeyler yapan ikinci bir kural sistemi oluşturmak istiyorsa, bunu yapabilirler çünkü C #'da (kötü yansıma kesmek olmadan) bir yöntem çağrısının nereden geldiğini bulamazsınız dan.

Bir geçici çözüm olabilir çalışmak bazı durumlarda kural motoru ile montaj birinde oyun nesneleri (ya da arayüzler) koyarak ve herhangi bir Mutator-yöntemlerini işaretlemek edilir internal. Oyun nesnelerine salt okunur erişime ihtiyaç duyan tüm sistemler farklı bir montajda olur, yani yalnızca publicyöntemlere erişebilirler . Bu, yine de birbirlerinin dahili yöntemlerini çağıran oyun nesnelerinin boşluklarını bırakır. Ancak bunu yapmak bariz bir kod kokusu olacaktır, çünkü oyun nesnesi sınıflarının aptal devlet sahipleri olması gerektiğini kabul ettiniz.


4

Orijinal kodun bariz sorunu , Nesne Modelleme yerine Veri Modelleme yapıyor olmasıdır . Bağlantılı makalede gerçek iş gereksinimlerinden kesinlikle bahsetmediğini lütfen unutmayın!

Ben gerçek fonksiyonel gereksinimleri almaya çalışarak başlayacaktı. Örneğin: "Herhangi bir oyuncu diğer oyunculara saldırabilir, ...". Buraya:

interface Player {
    void Attack(Player enemy);
}

"Oyuncular saldırıda kullanılan bir silah kullanabilirler, Sihirbazlar bir Personel, Bir Savaşçı Kılıç kullanabilir":

public class Wizard: Player {
    ...
    public void Wield(Staff weapon) { ... }
    ...
}
public class Warrior: Player {
    ...
    public void Wield(Sword sword) { ... }
    ...
}

"Her silah saldırıya uğrayan düşmana hasar verir". Tamam, şimdi Silah için ortak bir arayüzümüz olmalı:

interface Weapon {
    void dealDamageTo(Player enemy);
}

Ve böylece ... Neden hiçbir var Wield()içinde Player? Çünkü herhangi bir Oyuncunun herhangi bir Silah kullanma zorunluluğu yoktu.

Şunu söyleyebilirim ki, "Herhangi Playerbiri herhangi birini kullanmaya çalışabilirWeapon ." Ancak bu tamamen farklı bir şey olacaktır. Belki de bu şekilde modellenirim:

interface Player {
    void Attack(Player enemy);
    void TryWielding(Weapon weapon); // Throws UnwieldableException
}

Özet: Gereksinimleri ve yalnızca gereksinimleri modelleyin. Veri modelleme yapmayın, yani oo modelleme değildir.


1
Diziyi okudun mu? Belki de bu dizinin yazarına verileri modellemesini değil, gereksinimleri modellemesini istersiniz. Eğer cevap var Rquirements sizin olan uydurma şartlar DEĞİL C # derleyicisi oluştururken yazar vardı gereksinimleri.
CodingYoshi

2
Eric Lippert bu seride teknik bir problemi detaylandırıyor, ki bu iyi. Bu soru asıl sorunla ilgilidir, ancak C # özellikleri ile ilgili değildir. Demek istediğim, gerçek projelerde ilişkiler ve mülkler üstlenmemek için iş gereksinimlerini (tamamlanmış örnekler verdiğim, evet) izlememiz gerekiyor . Bu şekilde uyan bir model elde edersiniz. Hangi soru sordu.
Robert Bräutigam

Bu diziyi okurken ilk düşündüğüm şey bu. Yazar sadece bazı soyutlamalarla geldi, onları asla daha fazla değerlendirmedi, sadece onlara bağlı kaldı. Problemi tekrar tekrar mekanik olarak çözmeye çalışıyorum. Görünüşe göre önce gitmesi gereken bir alan ve soyutlamalar hakkında düşünmek yerine. Benim oyum.
Vadim Samokhin

Bu doğru cevap. Makale, çelişkili gereksinimleri ifade eder (bir gereklilik, bir oyuncunun [herhangi] bir silahı kullanabileceğini söylerken, diğer gereksinimler durumun böyle olmadığını söyler.) Ve ardından sistemin çakışmayı doğru bir şekilde ifade etmesinin ne kadar zor olduğunu açıklar. Tek doğru cevap çatışmayı kaldırmaktır. Bu durumda, bu, bir oyuncunun herhangi bir silahı kullanabileceği gereksinimini ortadan kaldırmak anlamına gelir.
Daniel T.

2

Bunun bir yolu Wieldkomutu Player. Oyuncu daha sonra Wielduygun kuralları kontrol Weaponeden ve Playerardından kendi Silah alanını ayarlayan komutu veren komutu yürütür . Bu şekilde, Silah alanı özel bir ayarlayıcıya sahip olabilir ve yalnızca Wieldoyuncuya bir komut iletilerek ayarlanabilir .


Aslında bu sorunu çözmez. Komut nesnesini yapan geliştirici herhangi bir silahı geçebilir ve oyuncu onu ayarlayacaktır. Dizi okuyun çünkü sorun düşündüğünüzden daha zor. Aslında bu diziyi yaptı çünkü Roslyn C # derleyicisini geliştirirken bu tasarım sorunuyla karşılaştı.
CodingYoshi

2

Hiçbir şey geliştiricinin bunu yapmasını engellemez. Aslında Eric Lippert birçok farklı teknik denedi ama hepsinin zayıflığı vardı. Bu serinin, geliştiriciyi bunu durdurmanın kolay olmadığı ve denediği her şeyin dezavantajları vardı. Sonunda Commandkuralları olan bir nesneyi kullanmanın bir yol olduğuna karar verdi .

Kurallarla, a'nın Weaponözelliğini a Wizardolarak ayarlayabilirsiniz , Swordancak Wizardsilahı (Kılıç) kullanmasını ve saldırmasını istediğinizde herhangi bir etkisi olmaz ve bu nedenle herhangi bir durumu değiştirmez. Aşağıda dediği gibi:

İlk kuralı güçlendiren başka bir kuralımız var, yani bir sihirbaz bir kılıç kullanmaya çalıştığında ilk kuralın etkilerinin geçerli olmadığını söylüyor. Bu durumun etkileri “üzgün bir trombon sesi çıkar, kullanıcı bu dönüş için eylemlerini kaybeder, oyun durumu mutasyona uğramaz

Başka bir deyişle, böyle bir kuralı, typepek çok farklı yol denediği ancak sevmediği ya da çalışmadığı ilişkiler yoluyla uygulayamayız . Bu yüzden yapabileceğimiz tek şey çalışma zamanında bu konuda bir şeyler yapmaktır. İstisna atmak iyi değildi çünkü bunu istisna olarak görmüyor.

Sonunda yukarıdaki çözüme gitmeyi seçti. Bu çözüm temelde herhangi bir silah ayarlayabileceğinizi söylüyor, ancak silahı verdiğinizde, doğru silah olmasa bile, aslında işe yaramaz olurdu. Ancak hiçbir istisna atılmaz.

Bence bu iyi bir çözüm. Bazı durumlarda ben de denemek-set desen gitmek istiyorum.


This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.Bu seride bulamadım, lütfen bu çözümün nerede önerildiğini söyleyebilir misiniz?
Vadim Samokhin

@zapadlo Dolaylı söylüyor. Cevabımda bu kısmı kopyaladım ve alıntıladım. İşte yine. Alıntıda şöyle diyor: bir sihirbaz bir kılıç kullanmaya çalıştığında. Bir kılıç ayarlanmadıysa bir sihirbaz bir kılıcı nasıl kullanabilir? Ayarlanmış olmalıdır. Eğer bir sihirbaz kılıcı kullanırsa Bu durumun etkileri “hüzünlü bir trombon sesi çıkarır”, kullanıcı bu dönüş için eylemlerini kaybeder
CodingYoshi

Hmm, sanırım bir kılıç kullanmak temel olarak ayarlanması gerektiği anlamına geliyor, değil mi? Bu paragrafı okurken, ilk kuralın etkisinin olduğunu yorumluyorum that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon. İkinci kural olan that strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.Silahın kılıç olup olmadığını kontrol eden bir kural olduğunu düşünüyorum, bu yüzden bir sihirbaz tarafından kullanılamaz, bu yüzden ayarlanmadı. Bunun yerine üzgün bir trombon sesi duyulur.
Vadim Samokhin

Benim düşünceme göre, bazı komut kurallarının ihlal edilmesi garip olurdu. Bir problem üzerinde bulanıklaşma gibi. Bir kural zaten ihlal edildikten ve bir sihirbazı geçersiz duruma getirdikten sonra bu sorunu neden ele almalı? Sihirbazın kılıcı olamaz, ama var! Neden olmasına izin vermiyorsun?
Vadim Samokhin

@Zapadlo ile nasıl yorumlanacağı konusunda hemfikirim Wield. Komuta için biraz yanıltıcı bir isim olduğunu düşünüyorum. Gibi bir şey ChangeWeapondaha doğru olurdu. Sanırım herhangi bir silah ayarlayabileceğiniz farklı bir modeliniz olabilir, ancak silahı verdiğinizde, doğru silah olmasaydı, aslında işe yaramaz olurdu . Kulağa ilginç geliyor, ama Eric Lippert'in açıkladığı şeyin bu olduğunu sanmıyorum.
Ben L

2

Yazarın ilk atılan çözümü, kuralları tip sistemi ile temsil etmekti. Tip sistemi derleme zamanında değerlendirilir. Kuralları tür sisteminden ayırırsanız, bunlar artık derleyici tarafından denetlenmez, bu nedenle bir geliştiricinin kendi başına hata yapmasını engelleyen hiçbir şey yoktur.

Ancak bu sorun, derleyici tarafından kontrol edilmeyen her mantık / modellemeyle karşı karşıyadır ve bunun genel cevabı (birim) testidir. Bu nedenle, yazarın önerdiği çözümün, geliştiricilerin hatalarını atlatmak için güçlü bir test kayışına ihtiyacı vardır. Yalnızca çalışma zamanında algılanan hatalar için güçlü bir test koşumuna ihtiyaç duymanın bu noktasını vurgulamak için, dinamik dilde daha güçlü testler için güçlü yazımları değiştirmeniz gerektiğini savunan Bruce Eckel'in bu makalesine bakın.

Sonuç olarak, geliştiricilerin hata yapmasını önleyebilecek tek şey, tüm kurallara uyulduğunu kontrol eden bir dizi (birim) test yapmaktır.


Bir API kullanacaksınız ve API, birim testi yapacağınızdan veya bu varsayımı yaptığınızdan emin mi? Bütün zorluk modelleme ile ilgilidir, bu yüzden modeli kullanan geliştirici dikkatsiz olsa bile model kırılmaz.
CodingYoshi

1
Yapmaya çalıştığım nokta , geliştiricinin hata yapmasını engelleyen hiçbir şey olmamasıydı . Önerilen çözümde kurallar verilerden ayrılmıştır, bu nedenle kendi kontrollerinizi oluşturmazsanız, kuralları uygulamadan veri nesnelerini kullanmanızı engelleyen hiçbir şey yoktur.
larsbe

1

Burada bir inceliği kaçırmış olabilirim, ancak sorunun tip sistemiyle ilgili olduğundan emin değilim. Belki de C # konvansiyonu ile.

Örneğin, Weaponayarlayıcıyı korumalı hale getirerek bu tamamen türü güvenli hale getirebilirsiniz Player. Sonra eklemek setSword(Sword)ve setStaff(Staff)için Warriorve Wizardbu çağrının korumalı ayarlayıcı sırasıyla.

Bu şekilde, Player/ Weaponrelation statik olarak kontrol edilir ve umursamayan kod sadece a Playeralmak için a'yı kullanabilir Weapon.


Eric Lippert istisnalar atmak istemedi. Diziyi okudun mu? Çözüm gereklilikleri karşılamalıdır ve bu gereklilikler seride açıkça belirtilmiştir.
KodlamaYoshi

@CodingYoshi Bu neden bir istisna oluşturur? Tür güvenli yani derleme zamanında kontrol edilebilir.
Alex

Ben heyecan atmadığınızı anladıktan sonra üzgünüm yorumumu değiştiremedi. Ancak, mirasınızı bunu yaparak kırdınız. Yazarın çözmeye çalıştığı meseleye bakın, sadece sizin gibi bir yöntem ekleyemezsiniz, çünkü şimdi türler polimorfik olarak tedavi edilemez.
KodlamaYoshi

@CodingYoshi Polimorfik gereksinim, bir Oyuncunun Silahına sahip olmasıdır. Ve bu şemada Oyuncunun gerçekten de bir Silahı var. Kalıtım yok. Bu çözüm, yalnızca kuralları doğru bulduğunuzda derlenir.
Alex

@CodingYoshi Bu, örneğin bir Weapona eklemeye çalışırsanız çalışma zamanı denetimi gerektiren bir kod yazamayacağınız anlamına gelmez Player. Ancak derleme zamanında beton türlerini bilmediğiniz tipte bir sistem yoktur . Tanım olarak. Bu şema, çalışma zamanında ele alınması gereken sadece bu durum olduğu anlamına gelir, çünkü Eric'in şemalarından aslında daha iyidir.
Alex

0

Peki, bir geliştiricinin bunu yapmasını ne engeller? Sadece hatırlamamaları gerekiyor mu?

Bu soru, " nerede doğrulama yapılır " denilen oldukça kutsal savaş-ish konusuyla aynıdır (büyük olasılıkla ddd'ye de dikkat çeker).

Yani, bu soruyu cevaplamadan önce, kendinize şunu sormalısınız: takip etmek istediğiniz kuralların doğası nedir? Taşa oyulmuşlar ve varlığı tanımlarlar mı? Bu kuralların çiğnenmesi, bir varlığın ne olduğunu bırakmasına neden olur mu? Evetse, bu kuralları komut doğrulamasında tutmanın yanı sıra , bunları bir varlıkta da koyun. Dolayısıyla, bir geliştirici komutu doğrulamayı unutursa, varlıklarınız geçersiz bir durumda olmaz.

Hayır - iyi ise, doğal olarak bu kuralların komuta özgü olduğunu ve alan varlıklarında bulunmaması gerektiğini ima eder. Bu nedenle, bu kuralların ihlali, gerçekleştirilmesine izin verilmemesi gereken, ancak geçersiz model durumunda olmayan bir eylemle sonuçlanır.

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.