Mengurangi Aturan dalam Penyihir dan Pejuang

9

Dalam seri posting blog ini , Eric Lippert menjelaskan masalah dalam desain berorientasi objek menggunakan penyihir dan prajurit sebagai contoh, di mana:

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

dan kemudian menambahkan beberapa aturan:

  • Seorang prajurit hanya bisa menggunakan pedang.
  • Seorang penyihir hanya bisa menggunakan staf.

Dia kemudian melanjutkan untuk mendemonstrasikan masalah yang Anda hadapi jika Anda mencoba untuk menegakkan aturan-aturan ini menggunakan sistem tipe C # (misalnya membuat Wizardkelas bertanggung jawab untuk memastikan bahwa seorang penyihir hanya dapat menggunakan staf). Anda melanggar Prinsip Substitusi Liskov, mengambil risiko pengecualian waktu berjalan atau berakhir dengan kode yang sulit diperpanjang.

Solusi yang ia temukan adalah tidak ada validasi yang dilakukan oleh kelas Player. Ini hanya digunakan untuk melacak status. Kemudian, alih-alih memberikan senjata kepada pemain dengan:

player.Weapon = new Sword();

negara diubah oleh Commands dan menurut Rules:

... kami membuat Commandobjek bernama Wieldyang membutuhkan dua objek kondisi permainan, a Playerdan a Weapon. Ketika pengguna mengeluarkan perintah ke sistem "wizard ini harus menggunakan pedang itu", maka perintah itu dievaluasi dalam konteks seperangkat Rules, yang menghasilkan urutan Effects. Kami memiliki satu Ruleyang mengatakan bahwa ketika seorang pemain mencoba untuk menggunakan senjata, efeknya adalah bahwa senjata yang ada, jika ada, dijatuhkan dan senjata baru menjadi senjata pemain. Kami memiliki aturan lain yang memperkuat aturan pertama, yang mengatakan bahwa efek aturan pertama tidak berlaku ketika seorang penyihir mencoba memegang pedang.

Saya menyukai ide ini pada prinsipnya, tetapi memiliki keprihatinan tentang bagaimana itu dapat digunakan dalam praktik.

Sepertinya tidak ada yang menghalangi pengembang untuk melewati Commandsdan Rules dengan hanya mengatur Weaponpada a Player. The Weaponproperti perlu diakses oleh Wieldperintah, sehingga tidak dapat dibuat private set.

Jadi, apa yang dilakukannya mencegah pengembang dari melakukan hal ini? Apakah mereka hanya perlu ingat untuk tidak melakukannya?

Ben L
sumber
2
Saya tidak berpikir pertanyaan ini spesifik bahasa (C #) karena ini benar-benar pertanyaan tentang desain OOP. Harap pertimbangkan untuk menghapus tag C #.
Maybe_Factor
1
@maybe_factor tag c # baik-baik saja karena kode yang diposting adalah c #.
CodingYoshi
Mengapa Anda tidak meminta @EricLippert secara langsung? Dia tampaknya muncul di sini di situs ini dari waktu ke waktu.
Doc Brown
@ Maybe_Factor - Saya ragu dengan tag C #, tetapi memutuskan untuk tetap menggunakannya jika ada solusi khusus bahasa.
Ben L
1
@DocBrown - Saya memang memposting pertanyaan ini di blog-nya (hanya beberapa hari yang lalu diakui; Saya belum menunggu selama itu untuk jawaban). Apakah ada cara untuk membawa pertanyaan saya ke sini untuk diperhatikan?
Ben L

Jawaban:

9

Seluruh argumen yang mengarah pada serangkaian posting blog ada di Bagian Lima :

Kami tidak memiliki alasan untuk percaya bahwa sistem tipe C # dirancang untuk memiliki sifat umum yang cukup untuk menyandikan aturan Dungeons & Dragons, jadi mengapa kita bahkan mencoba?

Kami telah memecahkan masalah "ke mana kode pergi yang mengekspresikan aturan sistem?" Ini masuk dalam objek yang mewakili aturan sistem, bukan pada objek yang mewakili kondisi permainan; keprihatinan objek negara adalah menjaga negara mereka konsisten, bukan dalam mengevaluasi aturan permainan.

Senjata, karakter, monster, dan objek game lainnya tidak bertanggung jawab untuk memeriksa apa yang dapat atau tidak bisa mereka lakukan. Sistem aturan bertanggung jawab untuk itu. The Commandobjek tidak melakukan apa-apa dengan objek permainan baik. Itu hanya merupakan upaya untuk melakukan sesuatu dengan mereka. Sistem aturan kemudian memeriksa apakah perintah itu mungkin, dan ketika itu sistem aturan menjalankan perintah dengan memanggil metode yang sesuai pada objek permainan.

Jika pengembang ingin membuat sistem aturan kedua yang melakukan hal-hal dengan karakter dan senjata yang tidak diizinkan oleh sistem aturan pertama, mereka bisa melakukannya karena di C # Anda tidak dapat (tanpa peretasan refleksi jahat) mencari tahu dari mana panggilan metode datang dari.

Solusi yang mungkin berhasil dalam beberapa situasi adalah menempatkan objek game (atau antarmuka mereka) dalam satu rakitan dengan mesin aturan dan tandai metode mutator apa pun sebagai internal. Sistem apa pun yang membutuhkan akses hanya baca ke objek game akan berada dalam majelis berbeda, yang berarti mereka hanya akan dapat mengakses publicmetode. Ini masih menyisakan celah objek permainan memanggil metode internal masing-masing. Tetapi melakukan hal itu akan menjadi bau kode yang jelas, karena Anda setuju bahwa kelas objek game seharusnya adalah negara-bodoh.

Philipp
sumber
4

Masalah yang jelas dari kode asli adalah melakukan Pemodelan Data, bukan Pemodelan Objek . Harap dicatat bahwa sama sekali tidak disebutkan persyaratan bisnis aktual dalam artikel tertaut!

Saya akan mulai dengan mencoba mendapatkan persyaratan fungsional yang sebenarnya. Misalnya: "Pemain mana saja dapat menyerang pemain lain, ...". Sini:

interface Player {
    void Attack(Player enemy);
}

"Pemain dapat menggunakan senjata yang digunakan dalam serangan, Penyihir dapat menggunakan Staf, Warriors a Sword":

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

"Setiap senjata memberikan Damage pada musuh yang diserang". Oke, sekarang kita harus memiliki antarmuka umum untuk Senjata:

interface Weapon {
    void dealDamageTo(Player enemy);
}

Dan seterusnya ... Mengapa tidak ada Wield()di dalamnya Player? Karena tidak ada persyaratan bahwa Pemain dapat menggunakan Senjata apa pun.

Saya dapat membayangkan, bahwa akan ada persyaratan yang mengatakan: "Siapa pun Playerdapat mencoba menggunakan apa pun Weapon." Namun ini akan menjadi hal yang sangat berbeda. Saya akan memodelkannya seperti ini:

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

Ringkasan: Modelkan persyaratan, dan hanya persyaratan. Jangan melakukan pemodelan data, itu bukan pemodelan oo.

Robert Bräutigam
sumber
1
Apakah Anda membaca seri? Mungkin Anda ingin memberi tahu penulis seri itu untuk tidak memodelkan data tetapi persyaratan. Rquirements yang Anda miliki dalam jawaban Anda adalah persyaratan buatan Anda BUKAN persyaratan yang penulis miliki ketika membangun kompiler C #.
CodingYoshi
2
Eric Lippert merinci masalah teknis dalam seri itu, yang baik-baik saja. Namun pertanyaan ini adalah tentang masalah sebenarnya, bukan fitur C #. Maksud saya adalah, dalam proyek nyata kita seharusnya mengikuti persyaratan bisnis (yang saya berikan contoh, ya), tidak mengasumsikan hubungan dan properti. Begitulah cara Anda mendapatkan model yang cocok. Yang merupakan pertanyaan.
Robert Bräutigam
Itulah hal pertama yang saya pikirkan ketika membaca seri itu. Penulis baru saja membuat beberapa abstraksi, tidak pernah mengevaluasi mereka lebih jauh, hanya bertahan dengan mereka. Berusaha memecahkan masalah secara mekanis, berulang kali. Alih-alih memikirkan domain dan abstraksi yang berguna, yang tampaknya harus pergi dulu. Suara positif saya.
Vadim Samokhin
Ini jawaban yang benar. Artikel ini menyatakan persyaratan yang saling bertentangan (satu persyaratan mengatakan bahwa pemain dapat menggunakan senjata [apa saja], sementara persyaratan lain mengatakan bahwa ini bukan masalahnya.) Dan kemudian merinci seberapa sulitnya bagi sistem untuk mengekspresikan konflik dengan benar. Satu-satunya jawaban yang benar adalah menghapus konflik. Dalam hal ini, itu berarti menghapus persyaratan bahwa pemain dapat memegang senjata apa pun.
Daniel T.
2

Salah satu cara adalah dengan meneruskan Wieldperintah ke Player. Pemain kemudian menjalankan Wieldperintah, yang memeriksa aturan yang sesuai dan mengembalikan Weapon, yang Playerkemudian menetapkan bidang Senjata sendiri. Dengan cara ini, bidang Senjata dapat memiliki setter pribadi dan hanya dapat diselesaikan melalui pemberian Wieldperintah kepada pemain.

Maybe_Factor
sumber
Sebenarnya ini tidak menyelesaikan masalah. Pengembang yang membuat objek perintah dapat melewati senjata apa pun dan pemain akan mengaturnya. Baca seri karena masalahnya lebih sulit daripada yang Anda pikirkan. Sebenarnya dia membuat seri itu karena dia menemukan masalah desain ini saat mengembangkan kompiler Roslyn C #.
CodingYoshi
2

Tidak ada yang mencegah pengembang melakukan itu. Sebenarnya Eric Lippert mencoba banyak teknik berbeda tetapi mereka semua memiliki kelemahan. Itulah inti dari seri itu yang menghentikan pengembang dari melakukan itu tidak mudah dan semua yang ia coba memiliki kelemahan. Akhirnya ia memutuskan bahwa menggunakan Commandobjek dengan aturan adalah jalan yang harus ditempuh.

Dengan aturan, Anda dapat mengatur Weaponproperti a Wizardmenjadi Swordtetapi ketika Anda meminta Wizarduntuk menggunakan senjata (Pedang) dan menyerang itu tidak akan memiliki efek dan karenanya tidak akan mengubah keadaan apa pun. Seperti yang dia katakan di bawah ini:

Kami memiliki aturan lain yang memperkuat aturan pertama, yang mengatakan bahwa efek aturan pertama tidak berlaku ketika seorang penyihir mencoba memegang pedang. Efek untuk situasi itu adalah “membuat suara trombone yang menyedihkan, pengguna kehilangan aksinya untuk giliran ini, tidak ada status permainan yang dimutasi

Dengan kata lain, kita tidak dapat menegakkan aturan seperti itu melalui typehubungan yang dia coba berbagai cara tetapi entah tidak suka atau tidak berhasil. Jadi satu-satunya hal yang dia katakan bisa kita lakukan adalah melakukan sesuatu tentang hal itu saat runtime. Melempar pengecualian itu tidak baik karena ia tidak menganggapnya sebagai pengecualian.

Dia akhirnya memilih untuk pergi dengan solusi di atas. Solusi ini pada dasarnya mengatakan Anda dapat mengatur senjata apa pun tetapi ketika Anda menghasilkannya, jika bukan senjata yang tepat, itu pada dasarnya tidak berguna. Tapi tidak terkecuali akan terlempar.

Saya pikir ini solusi yang bagus. Meskipun dalam beberapa kasus saya akan pergi dengan pola coba-atur juga.

CodingYoshi
sumber
This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.Saya gagal menemukannya dalam seri itu, bisakah Anda menunjukkan kepada saya di mana solusi ini diusulkan?
Vadim Samokhin
@zapadlo Dia mengatakannya secara tidak langsung. Saya telah menyalin bagian itu dalam jawaban saya dan mengutipnya. Ini dia lagi. Dalam kutipannya dia berkata: ketika seorang penyihir mencoba memegang pedang. Bagaimana bisa seorang penyihir memegang pedang jika pedang tidak dipasang? Pasti sudah diatur. Kemudian jika seorang penyihir memegang pedang . Efek untuk situasi itu adalah “membuat suara trombon yang menyedihkan, pengguna kehilangan aksinya untuk belokan ini
CodingYoshi
Hmm, saya pikir menggunakan pedang pada dasarnya berarti harus diatur, bukan? Ketika saya membaca paragraf itu, saya menafsirkannya bahwa efek aturan pertama adalah that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon. Sedangkan aturan kedua yaitu that strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.Jadi saya pikir ada aturan yang memeriksa apakah senjata itu adalah pedang, jadi tidak bisa dikuasai wizzard, jadi tidak disetel. Alih-alih terdengar trombone yang sedih.
Vadim Samokhin
Menurut saya, akan aneh jika beberapa aturan perintah dilanggar. Ini seperti mengaburkan masalah. Mengapa menangani masalah ini setelah aturan dilanggar dan menetapkan wisaya ke keadaan tidak valid? Si penyihir tidak bisa memiliki pedang, tetapi pedang itu punya! Mengapa tidak membiarkannya terjadi?
Vadim Samokhin
Saya setuju dengan @Zapadlo tentang cara menafsirkan di Wieldsini. Saya pikir itu adalah nama yang agak menyesatkan untuk perintah. Sesuatu seperti ChangeWeaponitu akan lebih akurat. Saya kira Anda bisa memiliki model yang berbeda di mana Anda dapat mengatur senjata apa pun tetapi ketika Anda menghasilkannya, jika bukan senjata yang tepat, itu pada dasarnya tidak berguna . Kedengarannya menarik, tapi menurut saya itu yang dijelaskan Eric Lippert.
Ben L
2

Solusi pertama yang dibuang penulis adalah merepresentasikan aturan oleh sistem tipe. Sistem tipe dievaluasi pada waktu kompilasi. Jika Anda melepaskan aturan dari sistem tipe, mereka tidak diperiksa lagi oleh kompiler, jadi tidak ada yang mencegah pengembang membuat kesalahan sendiri.

Tetapi masalah ini dihadapi oleh setiap bagian dari logika / pemodelan yang tidak diperiksa oleh kompiler dan jawaban umum untuk ini adalah (unit) pengujian. Oleh karena itu, solusi yang diusulkan penulis membutuhkan uji coba yang kuat untuk menghindari kesalahan oleh pengembang. Untuk menggarisbawahi titik ini membutuhkan uji coba yang kuat untuk kesalahan yang hanya terdeteksi saat runtime, lihat artikel ini oleh Bruce Eckel, yang membuat argumen bahwa Anda perlu menukar pengetikan yang kuat untuk pengujian yang lebih kuat dalam bahasa dinamis.

Sebagai kesimpulan, satu-satunya hal yang dapat mencegah pengembang membuat kesalahan adalah memiliki serangkaian (unit) tes yang memeriksa bahwa semua aturan dipatuhi.

larsbe
sumber
Anda harus menggunakan API dan API seharusnya memastikan Anda akan melakukan pengujian unit atau membuat asumsi itu? Seluruh tantangan adalah tentang pemodelan sehingga model tidak pecah bahkan jika pengembang menggunakan model itu ceroboh.
CodingYoshi
1
Maksud saya mencoba untuk membuat adalah bahwa tidak ada yang mencegah pengembang membuat kesalahan. Dalam solusi yang diajukan, aturan dilepaskan dari data, jadi jika Anda tidak membuat cek sendiri, tidak ada yang mencegah Anda menggunakan objek data tanpa menerapkan aturan.
larsbe
1

Saya mungkin telah melewatkan kehalusan di sini, tapi saya tidak yakin masalahnya dengan sistem tipe. Mungkin dengan konvensi dalam C #.

Misalnya, Anda bisa membuat ini sepenuhnya aman dengan membuat Weaponsetter terlindungi Player. Kemudian tambahkan setSword(Sword)dan setStaff(Staff)ke Warriordan Wizardmasing-masing yang memanggil setter yang dilindungi.

Dengan begitu, Player/ Weaponhubungan yang statis diperiksa dan kode yang tidak peduli hanya dapat menggunakan Playeruntuk mendapatkan Weapon.

Alex
sumber
Eric Lippert tidak ingin melempar pengecualian. Apakah Anda membaca seri? Solusinya harus memenuhi persyaratan dan persyaratan ini dinyatakan dengan jelas dalam seri.
CodingYoshi
@CodingYoshi Mengapa ini membuat pengecualian? Ini jenis aman yaitu dapat diperiksa pada waktu kompilasi.
Alex
Maaf tidak dapat mengubah komentar saya setelah saya menyadari Anda tidak melemparkan kutukan. Namun, Anda melanggar warisan dengan melakukan itu. Lihat masalah yang penulis coba selesaikan adalah Anda tidak bisa hanya menambahkan metode seperti yang Anda lakukan karena sekarang jenisnya tidak dapat diperlakukan secara polimorfik.
CodingYoshi
@CodingYoshi Persyaratan polimorfik adalah bahwa Pemain memiliki Senjata. Dan dalam skema ini, Player memang memiliki Senjata. Tidak ada warisan yang rusak. Solusi ini hanya akan dikompilasi jika aturannya benar.
Alex
@CodingYoshi Sekarang, itu tidak berarti Anda tidak dapat menulis kode yang akan memerlukan pemeriksaan runtime misalnya jika Anda mencoba menambahkan a Weaponke Player. Tetapi tidak ada sistem tipe di mana Anda tidak tahu jenis beton pada waktu kompilasi yang dapat bertindak pada jenis beton tersebut pada waktu kompilasi. Menurut definisi. Skema ini berarti bahwa hanya kasus itulah yang perlu ditangani saat runtime, Karena itu sebenarnya lebih baik daripada skema Eric.
Alex
0

Jadi, apa yang mencegah pengembang melakukan ini? Apakah mereka hanya perlu ingat untuk tidak melakukannya?

Pertanyaan ini secara efektif sama dengan topik perang suci yang disebut "di mana harus meletakkan validasi " (kemungkinan besar juga mencatat ddd).

Jadi, sebelum menjawab pertanyaan ini, seseorang harus bertanya pada diri sendiri: apa sifat aturan yang ingin Anda ikuti? Apakah mereka diukir di atas batu dan mendefinisikan entitas? Apakah melanggar aturan-aturan itu mengakibatkan entitas berhenti menjadi seperti apa adanya? Jika ya, bersama dengan menjaga aturan-aturan ini dalam validasi perintah , taruh juga di entitas. Jadi, jika pengembang lupa untuk memvalidasi perintah, entitas Anda tidak akan berada dalam keadaan tidak valid.

Jika tidak - yah, itu secara inheren menyiratkan bahwa aturan ini spesifik untuk perintah dan tidak boleh berada di entitas domain. Jadi, melanggar aturan ini menghasilkan tindakan yang seharusnya tidak boleh terjadi, tetapi tidak dalam keadaan model yang tidak valid.

Vadim Samokhin
sumber