Cách thích hợp để mô hình hóa hoạt động trong thế giới thực này dường như cần tham chiếu vòng tròn trong OOP là gì?


24

Tôi đã vật lộn với một vấn đề trong một dự án Java về các tham chiếu vòng tròn. Tôi đang cố gắng mô hình hóa một tình huống trong thế giới thực, trong đó có vẻ như các đối tượng trong câu hỏi phụ thuộc lẫn nhau và cần biết về nhau.

Dự án này là một mô hình chung để chơi một trò chơi cờ. Các lớp cơ bản là không cụ thể, nhưng được mở rộng để đối phó với các chi tiết cụ thể về cờ vua, cờ thỏ cáo và các trò chơi khác. Tôi đã mã hóa nó như một applet 11 năm trước với nửa tá trò chơi khác nhau, nhưng vấn đề là nó có đầy đủ các tài liệu tham khảo vòng tròn. Tôi đã triển khai nó sau đó bằng cách nhồi tất cả các lớp đan xen vào một tệp nguồn duy nhất, nhưng tôi có ý tưởng rằng đó là hình thức xấu trong Java. Bây giờ tôi muốn thực hiện một điều tương tự như một ứng dụng Android và tôi muốn làm mọi thứ đúng cách.

Các lớp học là:

  • Quy tắc: một đối tượng có thể được thẩm vấn cho những thứ như bố cục ban đầu của Hội đồng quản trị, thông tin trạng thái ban đầu khác như ai di chuyển trước, Di chuyển có sẵn, những gì xảy ra với Trạng thái trò chơi sau khi Di chuyển được đề xuất và đánh giá một vị trí hội đồng hiện tại hoặc đề xuất.

  • Bảng: một đại diện đơn giản của một bảng trò chơi, có thể được hướng dẫn để phản ánh Di chuyển.

  • MoveList: danh sách các Moves. Đây là mục đích kép: một lựa chọn di chuyển có sẵn tại một điểm nhất định hoặc danh sách các động tác đã được thực hiện trong trò chơi. Nó có thể được chia thành hai lớp gần giống nhau, nhưng điều đó không liên quan đến câu hỏi tôi đang hỏi và có thể làm phức tạp thêm.

  • Di chuyển: một động tác duy nhất. Nó bao gồm mọi thứ về việc di chuyển như một danh sách các nguyên tử: nhặt một mảnh từ đây, đặt nó xuống đó, loại bỏ một mảnh bị bắt từ đó.

  • Bang: thông tin trạng thái đầy đủ của một trò chơi đang diễn ra. Không chỉ vị trí Hội đồng, mà cả MoveList và các thông tin trạng thái khác như ai sẽ di chuyển ngay bây giờ. Trong cờ vua, người ta sẽ ghi lại liệu vua và tân binh của mỗi người chơi đã được di chuyển hay chưa.

Ví dụ, có rất nhiều tài liệu tham khảo về Thông tư: Quy tắc cần biết về Trạng thái trò chơi để xác định các bước di chuyển có sẵn tại một thời điểm nhất định, nhưng Trạng thái trò chơi cần truy vấn Quy tắc bắt đầu để bố trí ban đầu và cho các tác dụng phụ đi kèm với di chuyển một lần nó được thực hiện (ví dụ ai di chuyển tiếp).

Tôi đã thử tổ chức tập hợp các lớp mới theo thứ bậc, với RuleBook ở trên cùng vì nó cần biết về mọi thứ. Nhưng điều này dẫn đến việc phải di chuyển rất nhiều phương thức vào lớp RuleBook (chẳng hạn như thực hiện di chuyển) làm cho nó trở nên nguyên khối và không đặc biệt đại diện cho những gì một RuleBook nên có.

Vì vậy, cách thích hợp để tổ chức này là gì? Tôi có nên biến RuleBook thành BigClassThatDoesAl MaximumEverythingInTheGame để tránh các tham chiếu vòng tròn, từ bỏ nỗ lực mô hình hóa trò chơi trong thế giới thực một cách chính xác không? Hay tôi nên gắn bó với các lớp phụ thuộc lẫn nhau và dỗ trình biên dịch biên dịch chúng bằng cách nào đó, giữ lại mô hình thế giới thực của tôi? Hoặc có một số cấu trúc hợp lệ rõ ràng tôi đang thiếu?

Thanks cho bất kỳ giúp bạn có thể cung cấp cho!


7
Điều gì xảy ra nếu RuleBooklấy ví dụ Statenhư là một đối số và trả về giá trị hợp lệ MoveList, tức là "đây là nơi chúng ta đang ở, điều gì có thể được thực hiện tiếp theo?"
jonrsharpe

Những gì @jonrsharpe nói. Khi chơi một trò chơi thực sự, cuốn sách quy tắc cũng không biết về bất kỳ trò chơi thực tế nào đang được chơi. Tôi thậm chí có thể giới thiệu một lớp khác để thực sự tính toán các bước di chuyển, nhưng điều đó có thể phụ thuộc vào mức độ lớn của lớp RuleBook này.
Sebastiaan van den Broek

4
Tránh đối tượng thần (BigClassThatDoesAl MaximumEverythingInTheGame) quan trọng hơn nhiều so với việc tránh các tham chiếu vòng tròn.
user281377

2
@ user281377, tuy nhiên, chúng không nhất thiết phải là mục tiêu loại trừ lẫn nhau!
jonrsharpe

1
Bạn có thể hiển thị các nỗ lực mô hình? Một sơ đồ chẳng hạn?
Người dùng

Câu trả lời:


47

Tôi đã vật lộn với một vấn đề trong một dự án Java về các tham chiếu vòng tròn.

Trình thu gom rác của Java không dựa vào các kỹ thuật đếm tham chiếu. Tài liệu tham khảo thông tư không gây ra bất kỳ loại vấn đề nào trong Java. Thời gian dành cho việc loại bỏ các tham chiếu vòng tròn hoàn toàn tự nhiên trong Java là lãng phí thời gian.

Tôi đã mã hóa điều này lên [...] nhưng vấn đề là nó chứa đầy các tài liệu tham khảo vòng tròn. Tôi đã triển khai nó sau đó bằng cách nhồi tất cả các lớp đan xen vào một tệp nguồn duy nhất , [...]

Không cần thiết. Nếu bạn chỉ biên dịch tất cả các tệp nguồn cùng một lúc (ví dụ javac *.java:), trình biên dịch sẽ giải quyết tất cả các tham chiếu chuyển tiếp mà không gặp vấn đề gì.

Hoặc tôi nên gắn bó với các lớp phụ thuộc lẫn nhau và dỗ trình biên dịch biên dịch chúng bằng cách nào đó, [...]

Vâng. Các lớp ứng dụng dự kiến ​​sẽ phụ thuộc lẫn nhau. Biên dịch tất cả các tệp nguồn Java thuộc cùng một gói không phải là một cách hack thông minh, đó chính xác là cách Java được cho là hoạt động.


24
"Tham chiếu tròn không gây ra bất kỳ loại vấn đề nào trong Java." Về mặt biên soạn, điều này là đúng. Tham khảo thông tư được coi là thiết kế xấu , mặc dù.
Chặt

22
Tham chiếu tròn là hoàn toàn tự nhiên trong nhiều tình huống, đó là lý do tại sao Java và các ngôn ngữ hiện đại khác sử dụng trình thu gom rác tinh vi thay vì bộ đếm tham chiếu đơn giản.
user281377

3
Java có thể giải quyết các tham chiếu vòng tròn là điều tuyệt vời và điều chắc chắn là chúng tự nhiên trong nhiều tình huống. Nhưng OP đã đưa ra một tình huống cụ thể và điều đó nên được xem xét. Mã spaghetti rối có lẽ không phải là cách tốt nhất để xử lý vấn đề này.
Matthew đọc

3
Vui lòng không truyền bá FUD không có căn cứ về các ngôn ngữ lập trình không liên quan. Python đã hỗ trợ GC cho các chu kỳ tham chiếu từ các thời đại ( tài liệu , cũng trên SO: tại đâytại đây ).
Christian Aichinger

2
IMHO câu trả lời này chỉ là tầm thường, vì không có một từ nào về tham chiếu vòng tròn ong hữu ích, ví dụ như OP.
Doc Brown

22

Cấp, phụ thuộc vòng tròn là một thực tiễn đáng nghi ngờ từ quan điểm thiết kế, nhưng chúng không bị cấm, và từ quan điểm thuần túy kỹ thuật, chúng thậm chí không nhất thiết phải có vấn đề , vì bạn dường như coi chúng là: chúng hoàn toàn hợp pháp trong hầu hết các kịch bản, chúng không thể tránh khỏi trong một số tình huống, và trong một số trường hợp hiếm hoi, chúng thậm chí có thể được coi là một điều hữu ích để có.

Trên thực tế, có rất ít kịch bản mà trình biên dịch java sẽ từ chối một phụ thuộc vòng tròn. (Lưu ý: có thể có nhiều hơn, tôi chỉ có thể nghĩ về những điều sau đây ngay bây giờ.)

  1. Kế thừa: Bạn không thể có lớp A mở rộng lớp B, lần lượt mở rộng lớp A, và điều hoàn toàn hợp lý là bạn không thể có điều này, vì sự thay thế sẽ hoàn toàn vô nghĩa theo quan điểm logic.

  2. Trong số các lớp phương thức-cục bộ: các lớp được khai báo trong một phương thức có thể không tham chiếu tuần hoàn lẫn nhau. Điều này có lẽ không có gì ngoài một hạn chế của trình biên dịch java, có thể là do khả năng làm một việc như vậy không đủ hữu ích để biện minh cho sự phức tạp bổ sung sẽ phải đi vào trình biên dịch để hỗ trợ nó. (Hầu hết các lập trình viên java thậm chí không nhận thức được thực tế rằng bạn có thể khai báo một lớp trong một phương thức, chứ đừng nói đến việc khai báo nhiều lớp và sau đó có các lớp này tham chiếu với nhau theo vòng tròn.)

Vì vậy, điều quan trọng là phải nhận ra và đưa nó ra khỏi cách mà nhiệm vụ giảm thiểu sự phụ thuộc vòng tròn là một cuộc tìm kiếm sự tinh khiết trong thiết kế, không phải là một cuộc tìm kiếm sự đúng đắn về kỹ thuật.

Theo như tôi biết, không tồn tại cách tiếp cận giảm thiểu để loại bỏ các phụ thuộc vòng tròn, có nghĩa là không có công thức bao gồm không có gì ngoài các bước "không có trí tuệ" đơn giản được xác định trước để thực hiện một hệ thống với các tham chiếu vòng tròn, áp dụng chúng lần lượt và kết thúc với một hệ thống miễn phí tham chiếu vòng tròn. Bạn phải đặt tâm trí của bạn để làm việc, và bạn phải thực hiện các bước tái cấu trúc phụ thuộc vào bản chất của thiết kế của bạn.

Trong tình huống cụ thể mà bạn có trong tay, dường như điều tôi cần là một thực thể mới, có thể gọi là "Trò chơi" hoặc "GameLogic", biết tất cả các thực thể khác, (không có bất kỳ thực thể nào khác biết về nó, ) để các thực thể khác không phải biết nhau.

Ví dụ, đối với tôi, có vẻ không hợp lý khi thực thể RuleBook của bạn cần biết bất cứ điều gì về thực thể GameState, bởi vì một cuốn sách quy tắc là thứ mà chúng tôi tham khảo để chơi, nó không phải là thứ tích cực trong việc chơi. Vì vậy, chính thực thể "Trò chơi" mới này cần tham khảo cả sách quy tắc và trạng thái trò chơi để xác định những động thái nào có sẵn và điều này giúp loại bỏ các phụ thuộc vòng tròn.

Bây giờ, tôi nghĩ rằng tôi có thể đoán được vấn đề của bạn sẽ là gì với cách tiếp cận này: mã hóa thực thể "Trò chơi" theo cách không tin tưởng trò chơi sẽ rất khó khăn, vì vậy rất có thể bạn sẽ kết thúc với không chỉ một mà là hai các thực thể sẽ cần phải có các triển khai tùy chỉnh cho từng loại trò chơi khác nhau: thực thể "Quy tắc" và thực thể "Trò chơi". Lần lượt đánh bại mục đích có một thực thể "Quy tắc" ở vị trí đầu tiên. Chà, tất cả những gì tôi có thể nói về điều này là có lẽ, chỉ có thể, nguyện vọng ban đầu của bạn là viết một hệ thống có thể chơi nhiều loại trò chơi khác nhau có thể là cao cả, nhưng có lẽ không được hình dung. Nếu tôi ở trong đôi giày của bạn, tôi sẽ tập trung vào việc sử dụng một cơ chế chung để hiển thị trạng thái của tất cả các trò chơi khác nhau và một cơ chế chung để nhận đầu vào của người dùng cho tất cả các trò chơi này,


1
Cảm ơn Mike. Bạn nói đúng về những nhược điểm của thực thể Trò chơi; với mã applet cũ Tôi đã có thể tạo ra các trò chơi mới với ít hơn một lớp con RuleBook mới và thiết kế đồ họa phù hợp.
Damian Walker

10

Lý thuyết trò chơi coi các trò chơi như một danh sách các di chuyển trước đó (các loại giá trị bao gồm cả những người đã chơi chúng) và một hàm RationalMoves (trướcMoves)

Tôi sẽ thử và làm theo mô hình này cho phần không có UI của trò chơi và coi những thứ như thiết lập bảng như là di chuyển.

UI sau đó có thể là công cụ OO tiêu chuẩn với một cách tham chiếu logic


Cập nhật để bình luận

Hãy xem xét cờ vua. Các trò chơi cờ vua thường được ghi lại dưới dạng danh sách các nước đi. http://en.wikipedia.org/wiki/Portable_Game_Notation

danh sách các nước đi xác định trạng thái hoàn chỉnh của trò chơi tốt hơn nhiều so với hình ảnh của bảng.

Ví dụ: chúng ta bắt đầu tạo các đối tượng cho Board, Piece, Move, v.v. và các Phương thức như Piece.GetValidMoves ()

đầu tiên chúng tôi thấy chúng tôi phải có tài liệu tham khảo bảng, nhưng sau đó chúng tôi xem xét đúc. điều mà bạn chỉ có thể làm nếu bạn chưa chuyển vua hoặc tân binh của mình. Vì vậy, chúng tôi cần một lá cờ MovedAl ready trên vua và các tân binh. Tương tự những con tốt có thể di chuyển 2 hình vuông trong lần di chuyển đầu tiên của chúng.

Sau đó, chúng ta thấy rằng trong việc đúc di chuyển hợp lệ của nhà vua phụ thuộc vào sự tồn tại và trạng thái của tân binh, vì vậy hội đồng quản trị cần phải có những mảnh ghép trên đó và tham chiếu những quân cờ đó. chúng tôi đang nhận được vào vấn đề ref tròn của bạn.

Tuy nhiên, nếu chúng ta định nghĩa Move là một trạng thái cấu trúc và trò chơi bất biến là danh sách các bước di chuyển trước đó, chúng ta sẽ thấy những vấn đề này biến mất. Để xem liệu castling có hợp lệ không, chúng ta có thể kiểm tra danh sách di chuyển về sự tồn tại của di chuyển lâu đài và vua. Để xem người cầm đồ có thể mất thời gian không, chúng ta có thể kiểm tra xem người cầm đồ kia có di chuyển gấp đôi khi di chuyển trước đó không. Không cần tham khảo ngoại trừ Quy tắc -> Di chuyển

Bây giờ cờ vua có một bảng tĩnh, và các hoa mẫu đơn luôn được thiết lập theo cùng một cách. Nhưng giả sử chúng ta có một biến thể trong đó chúng ta cho phép thiết lập thay thế. có lẽ bỏ sót một số phần như một khuyết tật.

Nếu chúng ta Thêm thiết lập di chuyển dưới dạng di chuyển, 'từ hộp sang ô vuông X' và điều chỉnh đối tượng Quy tắc để hiểu di chuyển đó, thì chúng ta vẫn có thể biểu diễn trò chơi dưới dạng một chuỗi các bước di chuyển.

Tương tự như vậy nếu trong trò chơi của bạn, bản thân bảng không tĩnh, giả sử chúng ta có thể thêm hình vuông vào cờ vua hoặc xóa hình vuông khỏi bảng để chúng không thể được di chuyển qua. Những thay đổi này cũng có thể được biểu diễn dưới dạng Di chuyển mà không thay đổi cấu trúc tổng thể của công cụ Quy tắc của bạn hoặc phải tham chiếu một đối tượng Boardsetup tương tự


Điều này sẽ có xu hướng làm phức tạp việc triển khai RationalMoves, điều này sẽ làm chậm logic của bạn.
Taemyr

không thực sự, tôi giả sử thiết lập bảng là biến, vì vậy bạn cần xác định nó bằng cách nào đó. Nếu bạn chuyển đổi thiết lập di chuyển sang một số cấu trúc hoặc đối tượng khác để hỗ trợ tính toán, bạn có thể lưu trữ kết quả nếu cần. Một số trò chơi có bảng thay đổi khi chơi và một số nước đi hợp lệ có thể phụ thuộc vào các nước đi trước thay vì vị trí hiện tại (ví dụ: ném bóng trong cờ vua)
Ewan

1
Thêm cờ và công cụ là sự phức tạp bạn tránh bằng cách chỉ có lịch sử di chuyển. Nó không tốn kém để lặp đi lặp lại khi nói 100 nước cờ để có được thiết lập bảng hiện tại và bạn có thể lưu trữ kết quả giữa các lần di chuyển
Ewan

1
bạn cũng tránh thay đổi mô hình đối tượng của mình để phản ánh các quy tắc. tức là đối với cờ vua, nếu bạn thực hiện các hợp lệ Bạn cũng mất ý tưởng về việc ai sẽ đi và các khái niệm như kiểm tra được phát hiện
Ewan

1
@Gabe Đây boardLayoutlà một chức năng của tất cả priorMoves(tức là nếu chúng tôi duy trì nó như một trạng thái, không có gì được đóng góp ngoài mỗi cái thisMove). Do đó, đề xuất của Ewan về cơ bản là "cắt người giữa" - thay vào đó là một chức năng trực tiếp của chức năng trực tiếp validMoves( boardLayout( priorMoves ) ).
OJFord

8

Cách tiêu chuẩn để loại bỏ một tham chiếu vòng tròn giữa hai lớp trong lập trình hướng đối tượng là giới thiệu một giao diện mà sau đó có thể được thực hiện bởi một trong số chúng. Vì vậy, trong trường hợp của bạn, bạn có thể đã RuleBookđề cập đến Statecái mà sau đó đề cập đến một InitialPositionProvider(sẽ là một giao diện được triển khai bởi RuleBook). Điều này cũng làm cho việc kiểm tra dễ dàng hơn, vì sau đó bạn có thể tạo một vị trí Statesử dụng vị trí ban đầu khác (có lẽ đơn giản hơn) cho mục đích thử nghiệm.


6

Tôi tin rằng các tham chiếu tròn và đối tượng thần trong trường hợp của bạn có thể dễ dàng được loại bỏ bằng cách tách quyền kiểm soát luồng trò chơi khỏi các mô hình trạng thái và quy tắc của trò chơi. Bằng cách đó, bạn có thể có được rất nhiều sự linh hoạt và thoát khỏi sự phức tạp không cần thiết.

Tôi nghĩ bạn nên có một bộ điều khiển ("bậc thầy trò chơi" nếu muốn) điều khiển luồng trò chơi và xử lý các thay đổi trạng thái thực tế thay vì đưa ra quy tắc hoặc trò chơi nêu rõ trách nhiệm này.

Một đối tượng trạng thái trò chơi không cần phải thay đổi chính nó hoặc phải nhận thức được các quy tắc. Lớp chỉ cần cung cấp một mô hình dễ xử lý (được tạo, kiểm tra, thay đổi, lưu giữ, ghi nhật ký, sao chép, lưu trữ, v.v.) và các đối tượng trạng thái trò chơi hiệu quả cho phần còn lại của ứng dụng.

Cuốn sách quy tắc không cần phải biết hoặc mân mê với bất kỳ trò chơi đang diễn ra. Nó chỉ cần một chế độ xem trạng thái trò chơi để có thể biết được động thái nào là hợp pháp và nó chỉ cần trả lời với trạng thái trò chơi kết quả khi được hỏi điều gì xảy ra khi di chuyển được áp dụng cho trạng thái trò chơi. Nó cũng có thể cung cấp trạng thái trò chơi bắt đầu khi được yêu cầu bố trí ban đầu.

Bộ điều khiển phải nhận thức được các trạng thái trò chơi và sách quy tắc và có lẽ một số đối tượng khác của mô hình trò chơi, nhưng không cần phải lộn xộn với các chi tiết.


4
Chính xác suy nghĩ của tôi. OP đang trộn quá nhiều dữ liệuthủ tục trong cùng một lớp. Tốt hơn hết là chia chúng ra nhiều hơn. Đây là một bài nói chuyện tốt về chủ đề này. Btw, khi tôi đọc "xem trạng thái trò chơi", tôi nghĩ "đối số với chức năng." +100 nếu tôi có thể.
jpmc26

5

Tôi nghĩ vấn đề ở đây là bạn chưa đưa ra một mô tả rõ ràng về các nhiệm vụ sẽ được xử lý bởi các lớp nào. Tôi sẽ mô tả những gì tôi nghĩ là một mô tả tốt về những gì mỗi lớp nên làm, sau đó tôi sẽ đưa ra một ví dụ về mã chung minh họa các ý tưởng. Chúng ta sẽ thấy rằng mã ít được ghép nối hơn và do đó nó không thực sự có các tham chiếu tròn.

Hãy bắt đầu với việc mô tả những gì mỗi lớp làm.

Các GameStatelớp học chỉ nên chứa các thông tin về tình trạng hiện tại của trò chơi. Nó không nên chứa bất kỳ thông tin nào về những trạng thái trong quá khứ của trò chơi hoặc những động thái trong tương lai là có thể. Nó chỉ nên chứa thông tin về những quân cờ trên ô vuông nào, hoặc bao nhiêu và loại cờ nào trên những điểm nào trong backgammon. Các GameStatesẽ phải chứa một số thông tin thêm, như thông tin về castling trong cờ vua hay về khối tăng gấp đôi trong backgammon.

Các Movelớp học là một chút khéo léo. Tôi sẽ nói rằng tôi có thể chỉ định di chuyển để chơi bằng cách chỉ định GameStatekết quả từ việc chơi di chuyển. Vì vậy, bạn có thể tưởng tượng rằng một động thái chỉ có thể được thực hiện như một GameState. Tuy nhiên, trong khi đi (ví dụ) bạn có thể tưởng tượng rằng việc chỉ định di chuyển sẽ dễ dàng hơn rất nhiều bằng cách chỉ định một điểm duy nhất trên bảng. Chúng tôi muốn Movelớp học của chúng tôi đủ linh hoạt để xử lý một trong hai trường hợp này. Do đó, Movelớp thực sự sẽ là một giao diện với một phương thức thực hiện bước di chuyển trước GameStatevà trả về một bước di chuyển mới GameState.

Bây giờ RuleBooklớp có trách nhiệm biết mọi thứ về các quy tắc. Điều này có thể được chia thành ba điều. Nó cần phải biết những gì ban đầu GameStatelà gì, nó cần phải biết những động thái nào là hợp pháp và nó cần phải có khả năng để biết nếu một trong những người chơi đã giành chiến thắng.

Bạn cũng có thể tạo một GameHistorylớp để theo dõi tất cả các động thái đã được thực hiện và tất cả những GameStatesđiều đã xảy ra. Một lớp mới là cần thiết bởi vì chúng tôi đã quyết định rằng một người GameStatekhông nên chịu trách nhiệm về việc biết tất cả những GameStategì xuất hiện trước nó.

Điều này kết luận các lớp / giao diện tôi sẽ thảo luận. Bạn cũng có một Boardlớp học. Nhưng tôi nghĩ rằng các bảng trong các trò chơi khác nhau đủ khác nhau để khó có thể nhìn thấy những gì có thể được thực hiện chung với các bảng. Bây giờ tôi sẽ tiếp tục cung cấp các giao diện chung và triển khai các lớp chung.

Đầu tiên là GameState. Vì lớp này hoàn toàn phụ thuộc vào trò chơi cụ thể, không có Gamestategiao diện chung hoặc lớp.

Tiếp theo là Move. Như tôi đã nói, điều này có thể được biểu diễn bằng một giao diện có một phương thức duy nhất có trạng thái trước khi di chuyển và tạo ra trạng thái sau khi di chuyển. Đây là mã cho giao diện này:

package boardgame;

/**
 *
 * @param <T> The type of GameState
 */
public interface Move<T> {

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

Lưu ý rằng có một tham số loại. Điều này là do, ví dụ, một ChessMoveý chí cần phải biết về các chi tiết của việc di chuyển trước ChessGameState. Vì vậy, ví dụ, khai báo lớp ChessMovesẽ là

class ChessMove extends Move<ChessGameState>,

nơi bạn đã xác định một ChessGameStatelớp.

Tiếp theo tôi sẽ thảo luận về RuleBooklớp học chung . Đây là mã:

package boardgame;

import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public interface RuleBook<T> {

    T makeInitialState();

    List<Move<T>> makeMoveList(T gameState);

    StateEvaluation evaluateState(T gameState);

    boolean isMoveLegal(Move<T> move, T currentState);

}

Một lần nữa có một tham số loại cho GameStatelớp. Vì RuleBookphải biết trạng thái ban đầu là gì, chúng tôi đã đặt một phương thức để đưa ra trạng thái ban đầu. Vì RuleBookcần phải biết những động thái nào là hợp pháp, chúng tôi có các phương pháp để kiểm tra xem một động thái đó có hợp pháp ở một quốc gia nhất định hay không và đưa ra một danh sách các động thái hợp pháp cho một quốc gia nhất định. Cuối cùng, có một phương pháp để đánh giá GameState. Lưu ý rằng RuleBookchỉ nên chịu trách nhiệm mô tả nếu một hoặc những người chơi khác đã giành chiến thắng, nhưng không phải là người ở vị trí tốt hơn ở giữa trò chơi. Quyết định ai ở vị trí tốt hơn là một điều phức tạp nên được chuyển vào lớp của chính nó. Do đó, StateEvaluationlớp học thực sự chỉ là một enum đơn giản được đưa ra như sau:

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

Cuối cùng, hãy mô tả GameHistorylớp học. Lớp này có trách nhiệm ghi nhớ tất cả các vị trí đã đạt được trong trò chơi cũng như các động tác được chơi. Điều chính nó có thể làm là ghi lại Movenhư đã chơi. Bạn cũng có thể thêm chức năng cho hoàn tác Moves. Tôi có một thực hiện dưới đây.

package boardgame;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public class GameHistory<T> {

    private List<T> states;
    private List<Move<T>> moves;

    public GameHistory(T initialState) {
        states = new ArrayList<>();
        states.add(initialState);
        moves = new ArrayList<>();
    }

    void recordMove(Move<T> move) throws IllegalArgumentException {
        moves.add(move);
        states.add(move.makeResultingState(getMostRecentState()));
    }

    void resetToNthState(int n) {
        states = states.subList(0, n + 1);
        moves = moves.subList(0, n);
    }

    void undoLastMove() {
        resetToNthState(getNumberOfMoves() - 1);
    }

    T getMostRecentState() {
        return states.get(getNumberOfMoves());
    }

    T getStateAfterNthMove(int n) {
        return states.get(n + 1);
    }

    Move<T> getNthMove(int n) {
        return moves.get(n);
    }

    int getNumberOfMoves() {
        return moves.size();
    }

}

Cuối cùng, chúng ta có thể tưởng tượng làm một Gamelớp để gắn kết mọi thứ lại với nhau. Đây Gamelớp có nghĩa vụ phải tiếp xúc với các phương pháp mà làm cho nó có thể cho mọi người thấy những gì hiện nay GameStatelà, xem ai, nếu có ai có một, xem những gì di chuyển có thể được chơi, và chơi một di chuyển. Tôi có một triển khai dưới đây

package boardgame;

import java.util.List;

/**
 *
 * @author brian
 * @param <T> The type of GameState
 */
public class Game<T> {

    GameHistory<T> gameHistory;
    RuleBook<T> ruleBook;

    public Game(RuleBook<T> ruleBook) {
        this.ruleBook = ruleBook;
        final T initialState = ruleBook.makeInitialState();
        gameHistory = new GameHistory<>(initialState);
    }

    T getCurrentState() {
        return gameHistory.getMostRecentState();
    }

    List<Move<T>> getLegalMoves() {
        return ruleBook.makeMoveList(getCurrentState());
    }

    void doMove(Move<T> move) throws IllegalArgumentException {
        if (!ruleBook.isMoveLegal(move, getCurrentState())) {
            throw new IllegalArgumentException("Move is not legal in this position");
        }
        gameHistory.recordMove(move);
    }

    void undoMove() {
        gameHistory.undoLastMove();
    }

    StateEvaluation evaluateState() {
        return ruleBook.evaluateState(getCurrentState());
    }

}

Lưu ý trong lớp này rằng RuleBookkhông chịu trách nhiệm cho việc biết hiện tại GameStatelà gì. Đó là GameHistorycông việc của. Vì vậy, các Gamecâu hỏi về GameHistorytình trạng hiện tại là gì và cung cấp thông tin này cho RuleBookkhi nào Gamecần nói các động thái pháp lý là gì hoặc nếu có ai giành chiến thắng.

Dù sao, quan điểm của câu trả lời này là một khi bạn đã xác định hợp lý về trách nhiệm của mỗi lớp và bạn làm cho mỗi lớp tập trung vào một số ít trách nhiệm và bạn giao mỗi trách nhiệm cho một lớp duy nhất, sau đó là các lớp có xu hướng được tách rời và mọi thứ trở nên dễ dàng để viết mã. Hy vọng rằng đó là rõ ràng từ các ví dụ mã tôi đã đưa ra.


3

Theo kinh nghiệm của tôi, các tài liệu tham khảo vòng tròn thường chỉ ra rằng thiết kế của bạn không được cân nhắc kỹ lưỡng.

Trong thiết kế của bạn, tôi không hiểu tại sao RuleBook cần "biết" về Bang. Chắc chắn, nó có thể nhận Trạng thái làm tham số cho một phương thức nào đó, nhưng tại sao nó cần phải biết (nghĩa là giữ như một biến thể hiện) một tham chiếu đến Trạng thái? Điều đó không có ý nghĩa với tôi. Một RuleBook không cần phải "biết" về trạng thái của bất kỳ trò chơi cụ thể nào để thực hiện công việc của nó; luật chơi không thay đổi tùy theo trạng thái hiện tại của trò chơi. Vì vậy, hoặc bạn đã thiết kế nó không chính xác, hoặc bạn đã thiết kế nó một cách chính xác nhưng đang giải thích nó không chính xác.


+1. Bạn mua một trò chơi bảng vật lý, bạn nhận được một cuốn sách quy tắc có thể mô tả các quy tắc mà không cần nhà nước.
unperson325680

1

Sự phụ thuộc vòng tròn không nhất thiết là một vấn đề kỹ thuật, nhưng nó nên được coi là mùi mã, thường là vi phạm Nguyên tắc Trách nhiệm duy nhất .

Sự phụ thuộc vòng tròn của bạn xuất phát từ thực tế là bạn đang cố gắng làm quá nhiều từ Stateđối tượng của mình .

Bất kỳ đối tượng trạng thái nào chỉ nên cung cấp các phương thức liên quan trực tiếp đến việc quản lý trạng thái cục bộ đó. Nếu nó đòi hỏi bất cứ điều gì nhiều hơn logic cơ bản nhất, thì có lẽ nó nên được chia thành một mô hình lớn hơn. Một số người có ý kiến ​​khác nhau về vấn đề này, nhưng theo nguyên tắc chung, nếu bạn đang làm bất cứ điều gì ngoài getters và setters trên dữ liệu, bạn đang làm quá nhiều.

Trong trường hợp này, bạn nên có một StateFactory, có thể biết về a Rulebook. Bạn có thể có một lớp trình điều khiển khác sử dụng của bạn StateFactoryđể tạo một trò chơi mới. Statechắc chắn không nên biết về Rulebook. Rulebookcó thể biết về một Statetùy thuộc vào việc thực hiện các quy tắc của bạn.


0

Có cần một đối tượng quy tắc bị ràng buộc với một trạng thái trò chơi cụ thể không, hoặc sẽ có ý nghĩa hơn khi có một đối tượng quy tắc với một phương thức, được đưa ra một trạng thái trò chơi, sẽ báo cáo những chuyển động có sẵn từ trạng thái đó (và, có báo cáo rằng, không nhớ gì về trạng thái trong câu hỏi)? Trừ khi có một cái gì đó để đạt được bằng cách có đối tượng được hỏi về các bước di chuyển có sẵn giữ lại một bộ nhớ về trạng thái trò chơi, không cần thiết phải duy trì tham chiếu.

Trong một số trường hợp, có thể có những lợi thế khi có trạng thái duy trì đối tượng đánh giá quy tắc. Nếu bạn nghĩ rằng một tình huống như vậy có thể phát sinh, tôi sẽ đề nghị thêm một lớp "trọng tài" và để quy tắc cung cấp một phương thức "createReferee". Không giống như quy tắc, không quan tâm đến việc nó hỏi về một trò chơi hay năm mươi, một đối tượng trọng tài sẽ mong đợi điều hành một trò chơi. Nó sẽ không được dự kiến ​​sẽ gói gọn tất cả các trạng thái liên quan đến trò chơi mà nó đang thực hiện, nhưng có thể lưu trữ bất kỳ thông tin nào về trò chơi mà nó cho là hữu ích. Nếu một trò chơi hỗ trợ chức năng "hoàn tác", có thể hữu ích khi có trọng tài bao gồm một phương tiện sản xuất một đối tượng "ảnh chụp nhanh" có thể được lưu trữ cùng với các trạng thái trò chơi trước đó; đối tượng đó nên,

Nếu một số khớp nối có thể là cần thiết giữa các khía cạnh xử lý quy tắc và xử lý trạng thái trò chơi của mã, sử dụng một đối tượng trọng tài sẽ có thể giữ cho khớp nối đó ra khỏi sách quy tắc chính và các lớp trạng thái trò chơi. Cũng có thể có các quy tắc mới xem xét các khía cạnh của trạng thái trò chơi mà lớp trạng thái trò chơi sẽ không xem xét có liên quan (ví dụ: nếu một quy tắc được thêm vào có nội dung "Đối tượng X không thể làm Y nếu nó từng ở vị trí Z ", Trọng tài có thể được thay đổi để theo dõi đối tượng nào đã đến vị trí Z mà không phải thay đổi lớp trạng thái trò chơi).


-2

Cách thích hợp để đối phó với điều này là sử dụng các giao diện. Thay vì có hai lớp biết về nhau, hãy để mỗi lớp thực hiện một giao diện và tham chiếu trong lớp kia. Giả sử bạn có lớp A và lớp B cần tham khảo nhau. Có lớp A thực hiện giao diện A và lớp B thực hiện giao diện B thì bạn có thể tham chiếu giao diện B từ lớp A và giao diện A từ lớp B. Lớp A có thể nằm trong dự án riêng của nó, cũng như lớp B. Các giao diện nằm trong một dự án riêng biệt mà cả hai dự án khác tham khảo.


2
điều này dường như chỉ lặp lại các điểm được thực hiện và giải thích trong câu trả lời trước được đăng vài giờ trước khi câu này
gnat
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.