Phá vỡ các quy tắc trong Pháp sư và Chiến binh


9

Trong loạt bài đăng trên blog này , Eric Lippert mô tả một vấn đề trong thiết kế hướng đối tượng bằng cách sử dụng thuật sĩ và chiến binh làm ví dụ, trong đó:

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

và sau đó thêm một vài quy tắc:

  • Một chiến binh chỉ có thể sử dụng một thanh kiếm.
  • Một thuật sĩ chỉ có thể sử dụng một nhân viên.

Sau đó, anh ta tiếp tục chứng minh các vấn đề bạn gặp phải nếu bạn cố thực thi các quy tắc này bằng hệ thống loại C # (ví dụ: làm cho Wizardlớp chịu trách nhiệm đảm bảo rằng một trình hướng dẫn chỉ có thể sử dụng một nhân viên). Bạn vi phạm Nguyên tắc thay thế Liskov, ngoại lệ rủi ro trong thời gian chạy hoặc kết thúc bằng mã khó gia hạn.

Giải pháp mà anh ta đưa ra là không có xác nhận nào được thực hiện bởi lớp Người chơi. Nó chỉ được sử dụng để theo dõi trạng thái. Sau đó, thay vì cho người chơi một vũ khí bằng cách:

player.Weapon = new Sword();

trạng thái được sửa đổi bởi Commands và theo Rules:

... Chúng tôi tạo ra một Commandđối tượng được gọi là Wieldcó hai đối tượng trạng thái trò chơi, a Playervà a Weapon. Khi người dùng đưa ra lệnh cho hệ thống, thuật sĩ này sẽ sử dụng thanh kiếm đó, khi đó lệnh đó được đánh giá trong bối cảnh của một tập hợp Rules, tạo ra một chuỗi Effects. Chúng ta có một Rulecâu nói rằng khi người chơi cố gắng sử dụng vũ khí, thì hiệu quả là vũ khí hiện có, nếu có, bị rơi và vũ khí mới trở thành vũ khí của người chơi. Chúng tôi có một quy tắc khác củng cố quy tắc đầu tiên, đó là các hiệu ứng của quy tắc đầu tiên không áp dụng khi thuật sĩ cố gắng sử dụng thanh kiếm.

Tôi thích ý tưởng này về nguyên tắc, nhưng có một mối quan tâm về cách nó có thể được sử dụng trong thực tế.

Dường như không có gì ngăn cản nhà phát triển phá vỡ các CommandsRulechỉ bằng cách đặt Weapontrên a Player. Tài Weaponsản cần phải được truy cập bằng Wieldlệnh, vì vậy nó không thể được thực hiện private set.

Vì vậy, những gì làm ngăn chặn một nhà phát triển từ việc này? Có phải họ chỉ cần nhớ không?


2
Tôi không nghĩ câu hỏi này là ngôn ngữ cụ thể (C #) vì đây thực sự là một câu hỏi về thiết kế OOP. Vui lòng xem xét việc xóa thẻ C #.
Có thể là

1
@maybe_factor c # tag vẫn ổn vì mã được đăng là c #.
CodingYoshi

Tại sao bạn không hỏi trực tiếp @EricLippert? Ông dường như xuất hiện ở đây trên trang web này theo thời gian.
Doc Brown

@Maybe_Factor - Tôi đã làm lung lay thẻ C #, nhưng quyết định giữ nó trong trường hợp có một giải pháp dành riêng cho ngôn ngữ.
Ben L

1
@DocBrown - Tôi đã đăng câu hỏi này lên blog của anh ấy (chỉ một vài ngày trước đây đã thừa nhận; tôi đã không chờ đợi câu trả lời đó lâu). Có cách nào để đưa câu hỏi của tôi đến đây để anh ấy chú ý không?
Ben L

Câu trả lời:


9

Toàn bộ lập luận mà hàng loạt bài đăng trên blog dẫn đến là trong Phần thứ năm :

Chúng tôi không có lý do để tin rằng hệ thống loại C # được thiết kế để có đủ tính tổng quát để mã hóa các quy tắc của Dungeons & Dragons, vậy tại sao chúng tôi thậm chí còn cố gắng?

Chúng tôi đã giải quyết vấn đề của Code, nơi mã đi đến đâu thể hiện các quy tắc của hệ thống? Nó đi vào các đối tượng đại diện cho các quy tắc của hệ thống, không phải trong các đối tượng đại diện cho trạng thái trò chơi; mối quan tâm của các đối tượng nhà nước là giữ cho trạng thái của họ nhất quán, không phải trong việc đánh giá các quy tắc của trò chơi.

Vũ khí, nhân vật, quái vật và các đối tượng trò chơi khác không chịu trách nhiệm kiểm tra những gì họ có thể hoặc không thể làm. Hệ thống quy tắc chịu trách nhiệm cho điều đó. Đối Commandtượng cũng không làm gì với các đối tượng trò chơi. Nó chỉ đại diện cho nỗ lực để làm một cái gì đó với họ. Sau đó, hệ thống quy tắc sẽ kiểm tra xem lệnh có khả thi hay không và khi đó là hệ thống quy tắc thực thi lệnh bằng cách gọi các phương thức thích hợp trên các đối tượng trò chơi.

Nếu nhà phát triển muốn tạo một hệ thống quy tắc thứ hai thực hiện mọi thứ với các ký tự và vũ khí mà hệ thống quy tắc thứ nhất không cho phép, họ có thể làm điều đó bởi vì trong C #, bạn không thể (không có hack phản xạ khó chịu) tìm ra nơi gọi phương thức từ.

Một cách giải quyết có thể hoạt động trong một số tình huống là đưa các đối tượng trò chơi (hoặc giao diện của chúng) vào một tổ hợp với công cụ quy tắc và đánh dấu bất kỳ phương thức biến đổi nào là internal. Bất kỳ hệ thống nào cần truy cập chỉ đọc vào các đối tượng trò chơi sẽ nằm trong một tổ hợp khác, điều đó có nghĩa là chúng chỉ có thể truy cập các publicphương thức. Điều này vẫn để lại kẽ hở của các đối tượng trò chơi gọi các phương thức nội bộ của nhau. Nhưng làm điều đó sẽ có mùi mã rõ ràng, bởi vì bạn đã đồng ý rằng các lớp đối tượng trò chơi được coi là chủ sở hữu nhà nước câm.


4

Vấn đề rõ ràng của mã gốc là nó đang thực hiện Mô hình hóa dữ liệu thay vì Mô hình đối tượng . Xin lưu ý rằng hoàn toàn không có đề cập đến các yêu cầu kinh doanh thực tế trong bài viết được liên kết!

Tôi sẽ bắt đầu bằng cách cố gắng để có được yêu cầu chức năng thực tế. Ví dụ: "Bất kỳ người chơi nào cũng có thể tấn công bất kỳ người chơi nào khác, ...". Đây:

interface Player {
    void Attack(Player enemy);
}

"Người chơi có thể sử dụng vũ khí được sử dụng trong cuộc tấn công, Phù thủy có thể sử dụng Nhân viên, Chiến binh một thanh kiếm":

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

"Mỗi vũ khí gây sát thương cho kẻ thù bị tấn công". Ok, bây giờ chúng ta phải có một giao diện chung cho Weapon:

interface Weapon {
    void dealDamageTo(Player enemy);
}

Và như vậy ... Tại sao không có Wield()trong Player? Bởi vì không có yêu cầu rằng bất kỳ Người chơi nào cũng có thể sử dụng bất kỳ Vũ khí nào.

Tôi có thể hình ảnh, rằng sẽ có một yêu cầu nói rằng: "Bất kỳ ai Playercũng có thể cố gắng sử dụng bất kỳ Weapon." Đây sẽ là một điều hoàn toàn khác nhau tuy nhiên. Tôi sẽ mô hình hóa nó có lẽ theo cách này:

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

Tóm tắt: Mô hình hóa các yêu cầu, và chỉ các yêu cầu. Không làm mô hình dữ liệu, đó không phải là mô hình oo.


1
Bạn đã đọc bộ truyện này chưa? Có thể bạn muốn nói với tác giả của loạt bài đó không phải mô hình hóa dữ liệu mà là các yêu cầu. Các câu hỏi bạn có trong câu trả lời của bạn là các yêu cầu được tạo ra của bạn KHÔNG phải là các yêu cầu mà tác giả có khi xây dựng trình biên dịch C #.
CodingYoshi

2
Eric Lippert đang nêu chi tiết một vấn đề kỹ thuật trong loạt bài đó, điều này là tốt. Câu hỏi này là về vấn đề thực tế, tuy nhiên, không phải là tính năng C #. Quan điểm của tôi là, trong các dự án thực tế, chúng tôi phải tuân thủ các yêu cầu kinh doanh (mà tôi đã đưa ra các ví dụ trang điểm, có), không giả định các mối quan hệ và tài sản. Đó là cách bạn có được một mô hình phù hợp. Đó là câu hỏi.
Robert Bräutigam

Đó là điều đầu tiên tôi nghĩ khi đọc bộ truyện đó. Tác giả chỉ đưa ra một số trừu tượng, không bao giờ đánh giá chúng thêm, chỉ gắn bó với chúng. Cố gắng giải quyết vấn đề một cách máy móc, hết lần này đến lần khác. Thay vì suy nghĩ về một miền và trừu tượng là hữu ích, điều đó rõ ràng nên đi trước. Upvote của tôi.
Vadim Samokhin

Đây là câu trả lời chính xác. Bài báo thể hiện các yêu cầu mâu thuẫn (một yêu cầu nói rằng người chơi có thể sử dụng vũ khí [bất kỳ], trong khi các yêu cầu khác nói rằng đây không phải là trường hợp.) Và sau đó nêu chi tiết mức độ khó của hệ thống để thể hiện chính xác xung đột. Câu trả lời đúng duy nhất là loại bỏ xung đột. Trong trường hợp này, điều đó có nghĩa là loại bỏ yêu cầu người chơi có thể sử dụng bất kỳ vũ khí nào.
Daniel T.

2

Một cách sẽ là truyền Wieldlệnh cho Player. Sau đó, người chơi thực thi Wieldlệnh, kiểm tra các quy tắc phù hợp và trả về Weapon, Playersau đó đặt trường Vũ khí của riêng mình. Bằng cách này, trường Vũ khí có thể có bộ cài đặt riêng và chỉ có thể cài đặt được thông qua việc truyền Wieldlệnh cho người chơi.


Trên thực tế điều này không giải quyết được vấn đề. Nhà phát triển đang chế tạo đối tượng chỉ huy có thể vượt qua bất kỳ vũ khí nào và người chơi sẽ đặt nó. Đi đọc bộ truyện vì vấn đề khó khăn hơn bạn nghĩ. Trên thực tế, ông đã thực hiện loạt bài đó bởi vì ông đã gặp phải vấn đề thiết kế này trong khi phát triển trình biên dịch Roslyn C #.
CodingYoshi

2

Không có gì ngăn cản nhà phát triển làm điều đó. Thật ra Eric Lippert đã thử nhiều kỹ thuật khác nhau nhưng tất cả đều có điểm yếu. Đó là toàn bộ quan điểm của loạt bài đó rằng việc ngăn chặn nhà phát triển làm như vậy là không dễ dàng và mọi thứ anh ta thử đều có nhược điểm. Cuối cùng, ông quyết định rằng sử dụng một Commandđối tượng với các quy tắc là cách để đi.

Với các quy tắc, bạn có thể đặt Weaponthuộc tính của a Wizardthành một Swordnhưng khi bạn yêu cầu Wizardsử dụng vũ khí (Kiếm) và tấn công thì nó sẽ không có tác dụng gì và do đó nó sẽ không thay đổi bất kỳ trạng thái nào. Như ông nói dưới đây:

Chúng tôi có một quy tắc khác củng cố quy tắc đầu tiên, đó là các hiệu ứng của quy tắc đầu tiên không áp dụng khi thuật sĩ cố gắng sử dụng thanh kiếm. Hiệu ứng cho tình huống đó là tạo ra âm thanh trombone buồn, người dùng mất hành động cho lượt này, không có trạng thái trò chơi nào bị đột biến

Nói cách khác, chúng ta không thể thực thi quy tắc như vậy thông qua các typemối quan hệ mà anh ta đã thử nhiều cách khác nhau nhưng không thích hoặc không hoạt động. Do đó, điều duy nhất anh ấy nói chúng ta có thể làm là làm một cái gì đó về nó trong thời gian chạy. Ném một ngoại lệ là không tốt vì anh ta không coi đó là ngoại lệ.

Cuối cùng anh đã chọn đi với giải pháp trên. Giải pháp này về cơ bản nói rằng bạn có thể thiết lập bất kỳ vũ khí nào nhưng khi bạn mang lại nó, nếu không phải là vũ khí phù hợp, về cơ bản nó sẽ vô dụng. Nhưng không có ngoại lệ sẽ được ném.

Tôi nghĩ đó là một giải pháp tốt. Mặc dù trong một số trường hợp, tôi cũng sẽ sử dụng mẫu thử.


This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.Tôi đã không tìm thấy nó trong loạt bài đó, bạn có thể vui lòng chỉ cho tôi nơi giải pháp này được đề xuất không?
Vadim Samokhin

@zapadlo Anh nói gián tiếp. Tôi đã sao chép phần đó trong câu trả lời của tôi và trích dẫn nó. Lại một lân nưa. Trong trích dẫn, ông nói: khi một phù thủy cố gắng sử dụng một thanh kiếm. Làm thế nào một phù thủy có thể sử dụng một thanh kiếm nếu một thanh kiếm không được thiết lập? Nó phải được thiết lập. Sau đó, nếu một thuật sĩ sử dụng một thanh kiếm Các hiệu ứng cho tình huống đó là chất tạo ra âm thanh trombone buồn, người dùng sẽ mất hành động cho lượt này
CodingYoshi

Hmm, tôi nghĩ rằng việc cầm một thanh kiếm về cơ bản có nghĩa là nó nên được đặt, phải không? Khi tôi đọc đoạn đó, tôi giải thích rằng hiệu ứng của quy tắc đầu tiên là that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon. Trong khi quy tắc thứ hai mà that strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.tôi nghĩ rằng có một quy tắc kiểm tra xem vũ khí có phải là kiếm không, do đó, nó không thể được sử dụng bởi một wizzard, vì vậy nó không được thiết lập. Thay vào đó là một âm thanh trombone buồn.
Vadim Samokhin

Theo suy nghĩ của tôi, thật lạ khi một số quy tắc lệnh bị vi phạm. Nó giống như làm mờ đi một vấn đề. Tại sao xử lý vấn đề này sau khi quy tắc đã bị vi phạm và đặt trình hướng dẫn vào trạng thái không hợp lệ? Thuật sĩ không thể có một thanh kiếm, nhưng nó có! Tại sao không để nó xảy ra?
Vadim Samokhin

Tôi đồng ý với @Zapadlo về cách diễn giải Wieldở đây. Tôi nghĩ rằng đó là một tên hơi sai cho lệnh. Một cái gì đó như ChangeWeaponsẽ chính xác hơn. Tôi đoán bạn có thể có một mô hình khác, nơi bạn có thể đặt bất kỳ vũ khí nào nhưng khi bạn mang lại nó, nếu không phải là vũ khí phù hợp, về cơ bản nó sẽ vô dụng . Nghe có vẻ thú vị, nhưng tôi không nghĩ đó là những gì Eric Lippert mô tả.
Ben L

2

Giải pháp bị loại bỏ đầu tiên của tác giả là thể hiện các quy tắc theo hệ thống loại. Hệ thống loại được đánh giá tại thời gian biên dịch. Nếu bạn tách các quy tắc khỏi hệ thống loại, chúng sẽ không được trình biên dịch kiểm tra nữa, vì vậy không có gì ngăn cản nhà phát triển mắc lỗi mỗi lần.

Nhưng vấn đề này phải đối mặt với mọi phần logic / mô hình không được trình biên dịch kiểm tra và câu trả lời chung cho vấn đề này là (đơn vị) kiểm tra. Do đó, giải pháp đề xuất của tác giả cần một khai thác thử nghiệm mạnh mẽ để tránh lỗi của các nhà phát triển. Để nhấn mạnh quan điểm này cần một khai thác thử nghiệm mạnh mẽ đối với các lỗi mà chỉ có thể được phát hiện trong thời gian chạy, xem này bài viết bởi Bruce Eckel, mà làm cho một cuộc tranh cãi mà bạn cần trao đổi gõ mạnh để thử nghiệm mạnh mẽ hơn trong các ngôn ngữ động.

Tóm lại, điều duy nhất có thể ngăn các nhà phát triển mắc lỗi là có một bộ (đơn vị) kiểm tra kiểm tra xem tất cả các quy tắc có được tuân thủ hay không.


Bạn phải sử dụng API và API được cho là đảm bảo bạn sẽ thực hiện kiểm tra đơn vị hoặc đưa ra giả định đó? Toàn bộ thách thức là về mô hình hóa để mô hình không bị hỏng ngay cả khi nhà phát triển sử dụng mô hình không cẩn thận.
CodingYoshi

1
Điểm tôi đang cố gắng đưa ra là không có gì ngăn cản nhà phát triển mắc lỗi. Trong giải pháp được đề xuất, các quy tắc được tách ra khỏi dữ liệu, vì vậy nếu bạn không tạo séc cho riêng mình, không có gì ngăn bạn sử dụng các đối tượng dữ liệu mà không áp dụng quy tắc.
larsbe

1

Tôi có thể đã bỏ lỡ một sự tinh tế ở đây, nhưng tôi không chắc vấn đề là do hệ thống loại. Có lẽ đó là quy ước trong C #.

Ví dụ, bạn có thể làm cho loại này hoàn toàn an toàn bằng cách làm cho trình cài Weaponđặt được bảo vệ Player. Sau đó thêm setSword(Sword)setStaff(Staff)đến WarriorWizardtương ứng mà gọi setter được bảo vệ.

Theo cách đó, mối quan hệ Player/ Weaponđược kiểm tra tĩnh và mã không quan tâm chỉ có thể sử dụng a Playerđể lấy a Weapon.


Eric Lippert không muốn ném ngoại lệ. Bạn đã đọc bộ truyện này chưa? Giải pháp phải đáp ứng các yêu cầu và yêu cầu này được nêu rõ trong loạt bài.
CodingYoshi

@CodingYoshi Tại sao điều này sẽ ném một ngoại lệ? Đó là loại an toàn tức là có thể kiểm tra tại thời gian biên dịch.
Alex

Xin lỗi không thể thay đổi nhận xét của tôi một khi tôi nhận ra bạn không ném ngoại lệ. Tuy nhiên, Bạn đã phá vỡ sự kế thừa bằng cách làm điều đó. Xem vấn đề mà tác giả đã cố gắng giải quyết là bạn không thể chỉ thêm một phương thức như bạn đã làm vì bây giờ các loại có thể được xử lý đa hình.
CodingYoshi

@CodingYoshi Yêu cầu đa hình là Người chơi có Vũ khí. Và trong sơ đồ này, Người chơi thực sự có Vũ khí. Không có di sản bị phá vỡ. Giải pháp này sẽ chỉ biên dịch nếu bạn hiểu đúng các quy tắc.
Alex

@CodingYoshi Bây giờ, điều đó không có nghĩa là bạn không thể viết mã yêu cầu kiểm tra thời gian chạy, ví dụ: nếu bạn cố gắng thêm a Weaponvào a Player. Nhưng không có hệ thống loại nào mà bạn không biết các loại cụ thể tại thời gian biên dịch có thể hoạt động trên các loại cụ thể đó vào thời gian biên dịch. Theo định nghĩa. Lược đồ này có nghĩa là chỉ có trường hợp đó cần được xử lý trong thời gian chạy, vì như vậy thực sự tốt hơn bất kỳ kế hoạch nào của Eric.
Alex

0

Vì vậy, điều gì ngăn cản một nhà phát triển làm điều này? Có phải họ chỉ cần nhớ không?

Câu hỏi này thực sự giống với chủ đề khá là chiến tranh thần thánh có tên là " nơi đặt xác nhận " (có lẽ cũng đáng chú ý nhất là ddd).

Vì vậy, trước khi trả lời câu hỏi này, người ta nên tự hỏi: bản chất của các quy tắc mà bạn muốn tuân theo là gì? Có phải họ được khắc trên đá và xác định thực thể? Liệu việc phá vỡ các quy tắc đó dẫn đến một thực thể dừng lại như nó là gì? Nếu có, cùng với việc giữ các quy tắc này trong xác thực lệnh , hãy đặt chúng trong một thực thể. Vì vậy, nếu nhà phát triển quên xác thực lệnh, các thực thể của bạn sẽ không ở trạng thái không hợp lệ.

Nếu không - tốt, nó vốn ngụ ý rằng các quy tắc này là lệnh cụ thể và không nên nằm trong các thực thể miền. Vì vậy, vi phạm quy tắc này dẫn đến một hành động không nên được phép thực hiện, nhưng không ở trạng thái mô hình không hợp lệ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.