Có một chiến lược thiết kế cụ thể nào có thể được áp dụng để giải quyết hầu hết các vấn đề về trứng và trứng trong khi sử dụng các vật thể bất biến?


17

Đến từ nền tảng OOP (Java), tôi đang tự học Scala. Mặc dù tôi có thể dễ dàng nhìn thấy những lợi thế của việc sử dụng các đối tượng bất biến riêng lẻ, tôi gặp khó khăn khi xem cách người ta có thể thiết kế toàn bộ ứng dụng như thế. Tôi sẽ đưa ra một ví dụ:

Giả sử tôi có các vật thể đại diện cho "vật liệu" và thuộc tính của chúng (Tôi đang thiết kế một trò chơi, vì vậy tôi thực sự có vấn đề đó), như nước và băng. Tôi sẽ có một "người quản lý" sở hữu tất cả các trường hợp vật liệu như vậy. Một đặc tính sẽ là điểm đóng băng và nóng chảy, và những gì vật liệu đóng băng hoặc tan chảy.

[EDIT] Tất cả các cá thể vật chất là "singleton", giống như một Enum Java.

Tôi muốn "nước" nói rằng nó đóng băng thành "băng" ở 0C và "băng" để nói rằng nó tan chảy thành "nước" ở 1C. Nhưng nếu nước và băng là bất biến, chúng không thể lấy tham chiếu cho nhau dưới dạng tham số của hàm tạo, bởi vì một trong số chúng phải được tạo trước và không thể tham chiếu đến tham số khác chưa có trong tham số của hàm tạo. Tôi có thể giải quyết điều này bằng cách cung cấp cho họ cả tài liệu tham khảo cho người quản lý để họ có thể truy vấn nó để tìm phiên bản vật liệu khác mà họ cần mỗi khi họ được yêu cầu về các đặc tính đóng băng / tan chảy của họ, nhưng sau đó tôi gặp vấn đề tương tự giữa người quản lý và các tài liệu, rằng chúng cần một tài liệu tham khảo cho nhau, nhưng nó chỉ có thể được cung cấp trong hàm tạo cho một trong số chúng, vì vậy người quản lý hoặc tài liệu không thể thay đổi được.

Là họ không có cách nào xung quanh vấn đề này, hay tôi cần sử dụng các kỹ thuật lập trình "chức năng", hoặc một số mô hình khác để giải quyết nó?


2
Đối với tôi, theo cách bạn nói, không có nước cũng không có nước đá. Chỉ có h2onguyên liệu
gnat

1
Tôi biết điều này sẽ có ý nghĩa "khoa học" hơn, nhưng trong một trò chơi, việc mô hình hóa nó thành hai vật liệu khác nhau với các thuộc tính "cố định", thay vì một vật liệu có thuộc tính "biến" tùy thuộc vào ngữ cảnh.
Sebastien Diot

Singleton là một ý tưởng ngu ngốc.
DeadMG

@DeadMG Vâng, OK. Họ không phải là người Singletons Java thực sự. Tôi chỉ có nghĩa là không có điểm nào để tạo nhiều hơn một thể hiện của mỗi trường hợp, vì chúng là bất biến và sẽ bằng nhau. Trong thực tế, tôi sẽ không có bất kỳ trường hợp tĩnh thực sự. Các đối tượng "root" của tôi là các dịch vụ OSGi.
Sebastien Diot

Câu trả lời cho câu hỏi này khác dường như để khẳng định nghi ngờ của tôi rằng mọi thứ được phức tạp thực sự nhanh chóng với immutables: programmers.stackexchange.com/questions/68058/...
Sebastien Diot

Câu trả lời:


2

Giải pháp là gian lận một chút. Đặc biệt:

  • Tạo A, nhưng để lại tham chiếu đến B chưa được khởi tạo (vì B chưa tồn tại).

  • Tạo B và chỉ ra A.

  • Cập nhật A để trỏ đến B. Không cập nhật A hoặc B sau này.

Điều này có thể được thực hiện rõ ràng (ví dụ trong C ++):

struct List {
    int n;
    List *next;

    List(int n, List *next)
        : n(n), next(next);
};

// Return a list containing [0,1,0,1,...].
List *binary(void)
{
    List *a = new List(0, NULL);
    List *b = new List(1, a);
    a->next = b; // Evil, but necessary.
    return a;
}

hoặc ngầm (ví dụ trong Haskell):

binary :: [Int]
binary = a where
    a = 0 : b
    b = 1 : a

Ví dụ Haskell sử dụng đánh giá lười biếng để đạt được ảo tưởng về các giá trị bất biến phụ thuộc lẫn nhau. Các giá trị bắt đầu là:

a = 0 : <thunk>
b = 1 : a

ablà cả hai hình thức bình thường đầu hợp lệ độc lập. Mỗi khuyết điểm có thể được xây dựng mà không cần giá trị cuối cùng của biến khác. Khi thunk được ước tính, sau đó nó sẽ trỏ đến cùng một bđiểm dữ liệu .

Do đó, nếu bạn muốn hai giá trị bất biến trỏ đến nhau, bạn phải cập nhật giá trị thứ nhất sau khi xây dựng giá trị thứ hai hoặc sử dụng cơ chế cấp cao hơn để thực hiện tương tự.


Trong ví dụ cụ thể của bạn, tôi có thể diễn đạt nó bằng Haskell như:

data Material = Water {temperature :: Double}
              | Ice   {temperature :: Double}

setTemperature :: Double -> Material -> Material
setTemperature newTemp (Water _) | newTemp <= 0.0 = Ice newTemp
                                 | otherwise      = Water newTemp
setTemperature newTemp (Ice _)   | newTemp >= 1.0 = Water newTemp
                                 | otherwise      = Ice newTemp

Tuy nhiên, tôi đang đẩy mạnh vấn đề. Tôi tưởng tượng rằng trong một cách tiếp cận hướng đối tượng, trong đó một setTemperaturephương thức được gắn vào kết quả của mỗi hàm Materialtạo, bạn sẽ phải có các hàm tạo trỏ vào nhau. Nếu các hàm tạo được coi là giá trị bất biến, bạn có thể sử dụng cách tiếp cận được nêu ở trên.


(giả sử tôi đã hiểu cú pháp của Haskell) Tôi nghĩ rằng giải pháp hiện tại của tôi thực sự rất giống nhau, nhưng tôi đã tự hỏi liệu đó có phải là "đúng" hay không, nếu có thứ gì đó tốt hơn tồn tại. Đầu tiên tôi tạo một "tay cầm" (tham chiếu) cho mọi đối tượng (chưa được tạo), sau đó tạo tất cả các đối tượng, cung cấp cho chúng các tay cầm mà chúng cần và cuối cùng khởi tạo tay cầm cho các đối tượng. Các đối tượng là bất biến, nhưng không phải là tay cầm.
Sebastien Diot

6

Trong ví dụ của bạn, bạn đang áp dụng một phép biến đổi cho một đối tượng nên tôi sẽ sử dụng một cái gì đó giống như một ApplyTransform()phương thức trả về một BlockBasethay vì cố gắng thay đổi đối tượng hiện tại.

Ví dụ: để thay đổi IceBlock thành WaterBlock bằng cách áp dụng một chút nhiệt, tôi sẽ gọi một cái gì đó như

BlockBase currentBlock = new IceBlock();
currentBlock = currentBlock.ApplyTemperature(1); 
// currentBlock is now a WaterBlock 

IceBlock.ApplyTemperature()phương thức sẽ trông giống như thế này:

public class IceBlock() : BlockBase
{
    public BlockBase ApplyTemperature(int temp)
    {
        return (temp > 0 ? new WaterBlock((BlockBase)this) : this);
    }
}

Đây là một câu trả lời tốt, nhưng thật không may chỉ vì tôi đã không đề cập đến việc "tài liệu" của tôi, thực sự là "khối" của tôi, đều là đơn lẻ, vì vậy WaterBlock () mới không phải là một lựa chọn. Đó là lợi ích chính của bất biến, bạn có thể tái sử dụng chúng vô hạn. Thay vì có 500.000 khối trong ram, tôi có 500.000 tham chiếu đến 100 khối. Rẻ hơn nhiều!
Sebastien Diot

Vì vậy, những gì về trở lại BlockList.WaterBlockthay vì tạo ra một khối mới?
Rachel

Vâng, đây là những gì tôi làm, nhưng làm thế nào để tôi có được danh sách chặn? Rõ ràng, các khối phải được tạo trước danh sách các khối, và do đó, nếu khối thực sự bất biến, nó không thể nhận danh sách các khối làm tham số. Vì vậy, doe nó nhận được danh sách từ đâu? Quan điểm chung của tôi là, bằng cách làm cho mã trở nên phức tạp hơn, bạn giải quyết vấn đề trứng gà ở một cấp độ, chỉ để lấy lại mã ở lần tiếp theo. Về cơ bản, tôi thấy không có cách nào để tạo ra một ứng dụng hoàn toàn dựa trên sự bất biến. Nó dường như chỉ áp dụng cho "các đối tượng nhỏ", nhưng không áp dụng cho các container.
Sebastien Diot

@Sebastien Tôi nghĩ rằng đó BlockListchỉ là một staticlớp chịu trách nhiệm cho các phiên bản duy nhất của mỗi khối, vì vậy bạn không cần phải tạo một thể hiện của BlockList(Tôi đã quen với C #)
Rachel

@Sebastien: Nếu bạn sử dụng Singeltons, bạn phải trả giá.
DeadMG

6

Một cách khác để phá vỡ chu trình là tách các mối quan tâm về vật chất và biến đổi, trong một số ngôn ngữ tạo thành:

water = new Block("water");
ice = new Block("ice");

transitions = new Transitions([
    new transitions.temperature.Below(0.0, water, ice),
    new transitions.temperature.Above(0.0, ice, water),
]);

Huh, thật khó để tôi đọc cái này ban đầu, nhưng tôi nghĩ về cơ bản đó là cách tiếp cận mà tôi chủ trương.
Aidan Cully

1

Nếu bạn sẽ sử dụng một ngôn ngữ chức năng và bạn muốn nhận ra lợi ích của sự bất biến, thì bạn nên tiếp cận vấn đề với suy nghĩ đó. Bạn đang cố gắng xác định loại đối tượng "băng" hoặc "nước" có thể hỗ trợ một phạm vi nhiệt độ - để hỗ trợ tính không thay đổi, sau đó bạn cần tạo một đối tượng mới mỗi khi nhiệt độ thay đổi, gây lãng phí. Vì vậy, cố gắng làm cho các khái niệm về loại khối và nhiệt độ độc lập hơn. Tôi không biết Scala (nó nằm trong danh sách cần học của tôi :-)), nhưng mượn từ Joey Adams Trả lời bằng Haskell , tôi đề xuất một cái gì đó như:

data Material = Water | Ice

blockForTemperature :: Double -> Material
blockForTemperature x = 
  if x < 0 then Ice else Water

hoặc có thể:

transitionForTemperature :: Material -> Double -> Material
transitionForTemperature oldMaterial newTemp = 
  case (oldMaterial, newTemp) of
    (Ice, _) | newTemp > 0 -> Water
    (Water, _) | newTemp <= 0 -> Ice

(Lưu ý: Tôi chưa thử chạy cái này và Haskell của tôi hơi rỉ sét.) Bây giờ, logic chuyển đổi được tách ra khỏi loại vật liệu, vì vậy nó không lãng phí nhiều bộ nhớ và (theo ý kiến ​​của tôi) nó khá một chút chức năng định hướng.


Tôi thực sự không cố gắng sử dụng "ngôn ngữ chức năng" bởi vì tôi không hiểu nó! Điều duy nhất tôi thường giữ lại từ bất kỳ ví dụ lập trình chức năng không tầm thường nào là: "Chết tiệt, tôi thông minh hơn!" Nó vượt xa tôi như thế nào điều này có thể có ý nghĩa với bất cứ ai. Từ những ngày còn là sinh viên, tôi nhớ rằng Prolog (dựa trên logic), Occam (mọi thứ chạy song song theo mặc định) và thậm chí cả trình biên dịch đều có ý nghĩa, nhưng Lisp chỉ là thứ điên rồ. Nhưng tôi thực sự hiểu ý bạn về việc di chuyển mã gây ra "thay đổi trạng thái" bên ngoài "trạng thái".
Sebastien Diot
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.