Mối quan hệ giữa các đối tượng


8

Trong một vài tuần, tôi đã suy nghĩ về mối quan hệ giữa các đối tượng - đặc biệt là các đối tượng của OOP. Ví dụ, trong C ++ , chúng ta thường thể hiện điều đó bằng cách phân lớp các con trỏ hoặc vùng chứa các con trỏ trong cấu trúc cần quyền truy cập vào đối tượng khác. Nếu một đối tượng Acần phải có quyền truy cập vào B, nó không phải là hiếm để tìm thấy một B *pBtrong A.

Nhưng tôi không còn là lập trình viên C ++ nữa, tôi viết chương trình bằng các ngôn ngữ chức năng và đặc biệt hơn là trong Haskell , đây là một ngôn ngữ chức năng thuần túy. Có thể sử dụng con trỏ, tài liệu tham khảo hoặc loại công cụ đó, nhưng tôi cảm thấy lạ với điều đó, giống như cách thực hiện nó theo cách không phải Haskell.

Sau đó, tôi suy nghĩ sâu hơn một chút về tất cả những thứ liên quan và đi đến điểm:

Tại sao chúng ta thậm chí đại diện cho mối quan hệ như vậy bằng cách xếp lớp?

Tôi đọc một số người đã nghĩ về điều đó ( ở đây ). Theo quan điểm của tôi, việc thể hiện các mối quan hệ thông qua các biểu đồ rõ ràng là cách tốt hơn vì nó cho phép chúng ta tập trung vào cốt lõi của loại hình của mình và thể hiện quan hệ sau đó thông qua các tổ hợp (giống như SQL ).

Theo cốt lõi, ý tôi là khi chúng ta xác định A, chúng ta sẽ xác định những gì Ađược tạo ra , chứ không phải những gì nó phụ thuộc vào . Ví dụ, trong một trò chơi video, nếu chúng ta có một loại Character, đó là hợp pháp để nói về Trait, Skillhoặc loại công cụ, nhưng là nó nếu chúng ta nói về Weaponhay Items? Tôi không còn chắc chắn nữa. Sau đó:

data Character = {
    chSkills :: [Skill]
  , chTraits :: [Traits]
  , chName   :: String
  , chWeapon :: IORef Weapon -- or STRef, or whatever
  , chItems  :: IORef [Item] -- ditto
  }

Nghe có vẻ thực sự sai về mặt thiết kế đối với tôi. Tôi thích một cái gì đó như:

data Character = {
    chSkills :: [Skill]
  , chTraits :: [Traits]
  , chName   :: String
  }

-- link our character to a Weapon using a Graph Character Weapon
-- link our character to Items using a Graph Character [Item] or that kind of stuff

Hơn nữa, khi một ngày đến để thêm các tính năng mới, chúng ta chỉ có thể tạo các loại mới, biểu đồ và liên kết mới. Trong thiết kế đầu tiên, chúng ta sẽ phải phá vỡ Characterloại hoặc sử dụng một số loại công việc xung quanh để mở rộng nó.

Bạn nghĩ gì về ý tưởng đó? Bạn nghĩ gì là tốt nhất để giải quyết vấn đề đó trong Haskell , một ngôn ngữ chức năng thuần túy?


2
Bỏ phiếu cho ý kiến ​​không được phép ở đây. Bạn nên viết lại câu hỏi cho một cái gì đó dọc theo dòng chữ "có bất kỳ nhược điểm nào đối với phương pháp này không?" thay vì "bạn nghĩ gì về điều này?" hoặc "cách tốt nhất để ... là gì?" Đối với bất cứ điều gì đáng giá, không có nghĩa lý gì để hỏi tại sao mọi thứ được thực hiện theo một cách nhất định trong C ++, trái ngược với cách nó được thực hiện trong các ngôn ngữ chức năng, bởi vì C ++ thiếu bộ sưu tập rác, bất cứ thứ gì tương tự như các lớp loại (trừ khi bạn sử dụng các mẫu, mở ra một lon giun thực sự lớn) và nhiều tính năng khác hỗ trợ lập trình chức năng.
Doval

Một điều đáng giá là trong cách tiếp cận của bạn, không thể nói bằng cách sử dụng hệ thống loại liệu có Character nên giữ vũ khí hay không. Khi bạn có một bản đồ từ Charactersđến Weaponsvà một nhân vật vắng mặt trên bản đồ, điều đó có nghĩa là nhân vật đó hiện không cầm vũ khí hay nhân vật đó không thể giữ vũ khí? Hơn nữa, bạn sẽ thực hiện các tra cứu không cần thiết bởi vì bạn không biết tiên nghiệm liệu Charactercó thể giữ vũ khí hay không.
Doval

@Doval Nếu các nhân vật không thể cầm vũ khí là một tính năng trong trò chơi của bạn, thì sẽ không có ý nghĩa gì khi đưa cho nhân vật một canHoldWeapons :: Boollá cờ, cho phép bạn biết ngay nếu nó có thể giữ vũ khí, và nếu nhân vật của bạn không trong biểu đồ bạn có thể nói rằng nhân vật hiện không cầm vũ khí. Hãy giveCharacterWeapon :: WeaponGraph -> Character -> Weapon -> WeaponGraphhành động như idthể nếu cờ đó là Falsedành cho nhân vật được cung cấp hoặc sử dụng Maybeđể thể hiện sự thất bại.
bheklilr

Tôi thích điều này và thẳng thắn nghĩ rằng những gì bạn thiết kế rất phù hợp với phong cách ứng dụng - điều mà tôi có xu hướng thấy phổ biến khi xem xét các vấn đề về mô hình hóa OO trong Haskell. Các ứng dụng cho phép bạn soạn thảo các hành vi kiểu CRUD độc đáo và trôi chảy, do đó, ý tưởng rằng bạn sẽ mô hình hóa các bit một cách độc lập, sau đó có một số triển khai ứng dụng cho phép bạn kết hợp các loại đó với các hoạt động được đưa vào giữa các tác phẩm cho các sự kiện như xác nhận, kiên trì, phản ứng bắn v.v ... Điều này rất phù hợp với những gì bạn đề nghị ở đó. Đồ thị làm việc tuyệt vời với các ứng dụng.
Jimmy Hoffa

3
@Doval Cụ thể với các kiểu dữ liệu kiểu bản ghi, nếu bạn có nhiều hàm tạo, mã sau đây là hoàn toàn hợp pháp và không phát ra cảnh báo với -Walltrên GHC : data Foo = Foo { foo :: Int } | Bar { bar :: String }. Sau đó, gõ kiểm tra để gọi foo (Bar "")hoặc bar (Foo 0), nhưng cả hai sẽ đưa ra ngoại lệ. Nếu bạn đang sử dụng các hàm tạo dữ liệu kiểu vị trí thì không có vấn đề gì vì trình biên dịch không tạo ra các trình truy cập cho bạn, nhưng bạn không có được sự tiện lợi của các loại bản ghi cho các cấu trúc lớn và phải sử dụng nhiều bản tóm tắt hơn thư viện như ống kính.
bheklilr

Câu trả lời:


2

Bạn đã thực sự trả lời câu hỏi của riêng bạn mà bạn chưa biết nó. Câu hỏi bạn đặt ra không phải là về Haskell mà là về lập trình nói chung. Bạn đang thực sự tự hỏi mình những câu hỏi rất hay (kudos).

Cách tiếp cận của tôi đối với vấn đề bạn có trong tay phân chia cơ bản theo hai khía cạnh chính: thiết kế mô hình miền và sự đánh đổi.

Hãy để tôi giải thích.

Thiết kế mô hình miền : Đây là cách bạn quyết định tổ chức mô hình cốt lõi của ứng dụng. Tôi có thể thấy bạn đã làm điều đó bằng cách nghĩ về Nhân vật, Vũ khí, v.v. Thêm vào đó bạn cần xác định mối quan hệ giữa chúng. Cách tiếp cận của bạn về việc xác định các đối tượng bằng những gì chúng là thay vì những gì chúng phụ thuộc là hoàn toàn hợp lệ. Mặc dù vậy, hãy cẩn thận vì đó không phải là viên đạn bạc cho mọi quyết định liên quan đến thiết kế của bạn.

Bạn chắc chắn đang làm điều đúng đắn bằng cách nghĩ trước về điều này nhưng đến một lúc nào đó bạn cần ngừng suy nghĩ và bắt đầu viết một số mã. Bạn sẽ thấy sau đó nếu quyết định ban đầu của bạn là đúng hay không. Rất có thể là không vì bạn chưa có kiến ​​thức đầy đủ về ứng dụng của mình. Sau đó, đừng sợ tái cấu trúc khi bạn nhận ra một số quyết định đang trở thành một vấn đề không phải là một giải pháp. Điều quan trọng là viết mã khi bạn nghĩ về những quyết định đó để bạn có thể xác nhận chúng và tránh phải viết lại toàn bộ.

Có một số nguyên tắc tốt bạn có thể làm theo, chỉ cần google cho "nguyên tắc phần mềm" và bạn sẽ tìm thấy một loạt các nguyên tắc đó.

Đánh đổi : Mọi thứ đều là sự đánh đổi. Một mặt có quá nhiều phụ thuộc là không tốt tuy nhiên bạn sẽ phải đối phó với sự phức tạp thêm của việc quản lý các phụ thuộc ở một nơi khác chứ không phải trong các đối tượng miền của bạn. Không có quyết định đúng đắn. Nếu bạn có một cảm giác ruột đi cho nó. Bạn sẽ học được rất nhiều bằng cách đi theo con đường đó và nhìn thấy kết quả.

Như tôi đã nói, bạn đang hỏi đúng câu hỏi và đó là điều quan trọng nhất. Cá nhân tôi đồng ý với lý luận của bạn nhưng có vẻ như bạn đã dành quá nhiều thời gian để suy nghĩ về nó. Chỉ cần ngừng sợ sai lầm và phạm sai lầm . Chúng thực sự có giá trị và bạn luôn có thể cấu trúc lại mã của mình bất cứ khi nào bạn thấy phù hợp.


Cảm ơn bạn vì câu trả lời. Kể từ khi tôi đăng nó, tôi đã đi rất nhiều con đường. Tôi cố gắng AST, AST ràng buộc, monads miễn phí, bây giờ tôi đang cố gắng để đại diện cho toàn bộ điều qua typeclasses . Thật vậy, không có giải pháp tuyệt đối. Tôi chỉ cảm thấy cần phải thách thức cách làm việc thông thường, điều này đối với tôi không phải là mạnh mẽ và linh hoạt.
phaazon

@phaazon Điều đó thật tuyệt và tôi ước có nhiều nhà phát triển có cách tiếp cận tương tự. Thử nghiệm là công cụ học tập tốt nhất. Chúc may mắn với dự án của bạn.
Alex
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.