Làm thế nào để một người quyết định nếu một loại đối tượng dữ liệu nên được thiết kế là bất biến?


23

Tôi yêu "mẫu" bất biến vì những điểm mạnh của nó, và trong quá khứ tôi đã thấy nó có lợi khi thiết kế hệ thống với các kiểu dữ liệu bất biến (một số, hầu hết hoặc thậm chí là tất cả). Thông thường khi tôi làm như vậy, tôi thấy mình viết ít lỗi hơn và gỡ lỗi dễ dàng hơn nhiều.

Tuy nhiên, đồng nghiệp của tôi nói chung né tránh bất biến. Họ hoàn toàn không có kinh nghiệm (cách xa nó), nhưng họ viết các đối tượng dữ liệu theo cách cổ điển - các thành viên riêng với một getter và setter cho mọi thành viên. Sau đó, thông thường các nhà xây dựng của họ không có đối số, hoặc có thể chỉ lấy một số đối số cho thuận tiện. Vì vậy, thường xuyên, tạo một đối tượng trông như thế này:

Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Có lẽ họ làm điều đó ở khắp mọi nơi. Có lẽ họ thậm chí không định nghĩa một hàm tạo lấy hai chuỗi đó, bất kể chúng quan trọng như thế nào. Và có thể họ không thay đổi giá trị của các chuỗi đó sau đó và không bao giờ cần. Rõ ràng nếu những điều đó là đúng, đối tượng sẽ được thiết kế tốt hơn là bất biến, phải không? (constructor có hai thuộc tính, không có setters nào cả).

Làm thế nào để bạn quyết định nếu một loại đối tượng nên được thiết kế là bất biến? Có một bộ tiêu chí tốt để đánh giá nó?

Hiện tại tôi đang tranh luận về việc có nên chuyển một vài loại dữ liệu trong dự án của riêng tôi thành bất biến hay không, nhưng tôi sẽ phải chứng minh nó với các đồng nghiệp của mình và dữ liệu trong các loại có thể thay đổi (RẤT hiếm khi) đó là cách bất biến (tạo một cái mới, sao chép các thuộc tính từ đối tượng cũ ngoại trừ các thuộc tính mà bạn muốn thay đổi). Nhưng tôi không chắc liệu đây chỉ là tình yêu của tôi đối với những thứ bất biến thể hiện qua, hay nếu có nhu cầu thực sự cho / lợi ích từ họ.


1
Chương trình ở Erlang và toàn bộ vấn đề đã được giải quyết, mọi thứ đều bất biến
Zachary K

1
@ZacharyK Tôi thực sự đã nghĩ đến việc đề cập đến một cái gì đó về lập trình chức năng, nhưng bị hạn chế do kinh nghiệm hạn chế của tôi với các ngôn ngữ lập trình chức năng.
Ricket

Câu trả lời:


27

Có vẻ như bạn đang tiếp cận nó ngược. Người ta nên mặc định là bất biến. Chỉ làm cho một đối tượng có thể thay đổi nếu bạn hoàn toàn phải / chỉ không thể làm cho nó hoạt động như một đối tượng bất biến.


11

Lợi ích chính của các đối tượng bất biến là đảm bảo an toàn luồng. Trong một thế giới nơi nhiều lõi và luồng là tiêu chuẩn, lợi ích này đã trở nên rất quan trọng.

Nhưng sử dụng các đối tượng có thể thay đổi là rất thuận tiện. Chúng hoạt động tốt và miễn là bạn không sửa đổi chúng từ các luồng riêng biệt và bạn hiểu rõ về những gì bạn đang làm, chúng khá đáng tin cậy.


15
Lợi ích của các đối tượng bất biến là chúng dễ dàng lý luận hơn. Trong môi trường đa luồng, điều này dễ dàng hơn nhiều; trong các thuật toán đơn luồng đơn giản, nó vẫn dễ dàng hơn. Ví dụ, bạn không bao giờ phải quan tâm xem trạng thái có nhất quán hay không, đối tượng có vượt qua tất cả các đột biến cần sử dụng trong một ngữ cảnh nhất định không, v.v. Các đối tượng có thể thay đổi tốt nhất cho phép bạn quan tâm rất ít về trạng thái chính xác của chúng, ví dụ: cache.
9000

-1, tôi đồng ý với @ 9000. An toàn luồng chỉ là thứ yếu (xem xét các đối tượng xuất hiện ở trạng thái bất biến nhưng có trạng thái đột biến bên trong vì ví dụ như ghi nhớ). Ngoài ra, nâng cao hiệu suất có thể thay đổi là tối ưu hóa sớm và có thể bảo vệ mọi thứ nếu bạn yêu cầu người dùng "biết họ đang làm gì". Nếu tôi biết những gì tôi đang làm mọi lúc, tôi sẽ không bao giờ viết một chương trình có lỗi.
Doval

4
@Doval: Không có gì để không đồng ý. 9000 là hoàn toàn chính xác; đối tượng bất biến dễ dàng hơn để lý do về. Đó là một phần những gì làm cho chúng rất hữu ích cho lập trình đồng thời. Phần khác là bạn không phải lo lắng về việc thay đổi trạng thái. Tối ưu hóa sớm là không liên quan ở đây; việc sử dụng các đối tượng bất biến là về thiết kế, không phải hiệu năng và sự lựa chọn kém về cấu trúc dữ liệu trước mắt là sự bi quan sớm.
Robert Harvey

@RobertHarvey Tôi không chắc ý của bạn là gì bởi "sự lựa chọn kém về cấu trúc dữ liệu ở phía trước." Có những phiên bản bất biến của hầu hết các cấu trúc dữ liệu có thể thay đổi ngoài kia cung cấp hiệu suất tương tự. Nếu bạn cần một danh sách, và bạn có lựa chọn sử dụng danh sách bất biến và danh sách có thể thay đổi, bạn đi với danh sách bất biến cho đến khi bạn biết chắc chắn đó là nút cổ chai trong ứng dụng của bạn và phiên bản có thể thay đổi sẽ hoạt động tốt hơn.
Doval

2
@Doval: Tôi đã đọc Okasaki. Những cấu trúc dữ liệu đó thường không được sử dụng trừ khi bạn sử dụng ngôn ngữ hỗ trợ đầy đủ mô hình chức năng, như Haskell hoặc Lisp. Và tôi tranh luận quan điểm rằng các cấu trúc bất biến là sự lựa chọn mặc định; phần lớn các hệ thống máy tính kinh doanh vẫn được thiết kế xung quanh các cấu trúc có thể thay đổi (tức là cơ sở dữ liệu quan hệ). Bắt đầu với các cấu trúc dữ liệu bất biến là một ý tưởng hay, nhưng nó vẫn rất ngà.
Robert Harvey

6

Có hai cách chính để quyết định xem một đối tượng là bất biến.

a) Dựa vào bản chất của Đối tượng

Rất dễ để nắm bắt những tình huống này bởi vì chúng tôi biết rằng những đối tượng này sẽ không thay đổi sau khi nó được xây dựng. Ví dụ: nếu bạn có một RequestHistorythực thể và bản chất lịch sử các thực thể không thay đổi một khi nó được xây dựng. Những đối tượng này có thể được thiết kế thẳng như các lớp bất biến. Hãy nhớ rằng Object Object có thể thay đổi vì nó có thể thay đổi trạng thái của nó và người được gán cho nó theo thời gian nhưng lịch sử yêu cầu không thay đổi. Ví dụ, có một yếu tố lịch sử được tạo ra vào tuần trước khi nó chuyển từ trạng thái được gửi sang trạng thái được chỉ định VÀ quyền lợi lịch sử này không bao giờ có thể thay đổi. Vì vậy, đây là một trường hợp bất biến cổ điển.

b) Dựa trên sự lựa chọn thiết kế, các yếu tố bên ngoài

Điều này tương tự như ví dụ java.lang.String. Các chuỗi thực sự có thể thay đổi theo thời gian nhưng theo thiết kế, chúng đã làm cho nó trở nên bất biến do các yếu tố bộ nhớ đệm / chuỗi chuỗi / đồng thời. Similary bộ nhớ đệm / đồng thời, vv có thể đóng một vai trò tốt trong việc làm cho một đối tượng không bị biến dạng nếu bộ nhớ đệm / đồng thời và hiệu suất liên quan là quan trọng trong ứng dụng. Nhưng quyết định này nên được thực hiện rất cẩn thận sau khi làm lạnh tất cả các tác động.

Ưu điểm chính của các đối tượng không thay đổi là chúng không phải chịu mô hình cỏ dại. Đối tượng sẽ không nhận bất kỳ thay đổi nào trong suốt thời gian sử dụng và điều đó làm cho việc mã hóa và bảo trì rất dễ dàng hơn.


4

Hiện tại tôi đang tranh luận về việc có nên chuyển một vài loại dữ liệu trong dự án của riêng tôi thành bất biến hay không, nhưng tôi sẽ phải chứng minh nó với các đồng nghiệp của mình và dữ liệu trong các loại có thể thay đổi (RẤT hiếm khi) đó là cách bất biến (tạo một cái mới, sao chép các thuộc tính từ đối tượng cũ ngoại trừ các thuộc tính mà bạn muốn thay đổi).

Giảm thiểu trạng thái của một chương trình rất có lợi.

Hỏi họ nếu họ muốn sử dụng một loại giá trị có thể thay đổi trong một trong các lớp của bạn để lưu trữ tạm thời từ một lớp khách.

Nếu họ nói có, hỏi tại sao? Trạng thái có thể thay đổi không thuộc về một kịch bản như thế này. Buộc họ tạo trạng thái nơi nó thực sự thuộc về và làm cho trạng thái của các loại dữ liệu của bạn rõ ràng nhất có thể là những lý do tuyệt vời.


4

Câu trả lời là hơi phụ thuộc vào ngôn ngữ. Mã của bạn trông giống như Java, nơi vấn đề này càng khó khăn càng tốt. Trong Java, các đối tượng chỉ có thể được truyền bằng tham chiếu và bản sao bị phá vỡ hoàn toàn.

Không có câu trả lời đơn giản, nhưng chắc chắn bạn muốn làm cho các đối tượng giá trị nhỏ không thay đổi. Java đã tạo các chuỗi không thay đổi, nhưng ngày và lịch không chính xác có thể thay đổi.

Vì vậy, chắc chắn làm cho các đối tượng giá trị nhỏ không thay đổi và thực hiện một hàm tạo sao chép. Quên tất cả về Clonizable, nó được thiết kế tồi đến mức vô dụng.

Đối với các đối tượng có giá trị lớn hơn, nếu nó bất tiện để làm cho chúng bất biến, thì hãy làm cho chúng dễ dàng sao chép.


Nghe có vẻ giống như cách chọn giữa stack và heap khi viết C hoặc C ++ :)
Ricket

@Ricket: IMO không nhiều lắm. Stack / heap phụ thuộc vào tuổi thọ của đối tượng. Nó rất phổ biến trong C ++ khi có các đối tượng có thể thay đổi trên ngăn xếp.
kevin cline

1

Và có thể họ không thay đổi giá trị của các chuỗi đó sau đó và không bao giờ cần. Rõ ràng nếu những điều đó là đúng, đối tượng sẽ được thiết kế tốt hơn là bất biến, phải không?

Một số phản ứng trực giác, không bao giờ cần phải thay đổi các chuỗi sau này là một lập luận khá tốt rằng nó không quan trọng nếu các đối tượng là bất biến hay không. Các lập trình viên đã coi họ là bất biến một cách hiệu quả cho dù trình biên dịch có thực thi nó hay không.

Sự bất biến thường không gây tổn thương, nhưng nó cũng không giúp được gì. Cách dễ dàng để biết liệu đối tượng của bạn có thể được hưởng lợi từ tính bất biến hay không là nếu bạn cần tạo một bản sao của đối tượng hoặc có được một mutex trước khi thay đổi nó . Nếu nó không bao giờ thay đổi, thì sự bất biến không thực sự mua cho bạn bất cứ thứ gì, và đôi khi làm cho mọi thứ trở nên phức tạp hơn.

Bạn làm có một điểm tốt về nguy cơ xây dựng một đối tượng trong một trạng thái không hợp lệ, nhưng đó thực sự là một vấn đề riêng biệt từ tính bất biến. Một đối tượng có thể vừa có thể thay đổi vừa luôn ở trạng thái hợp lệ sau khi xây dựng.

Ngoại lệ cho quy tắc đó là do Java không hỗ trợ các tham số được đặt tên cũng như tham số mặc định, đôi khi có thể gặp khó khăn khi thiết kế một lớp đảm bảo một đối tượng hợp lệ chỉ sử dụng các hàm tạo quá tải. Không quá nhiều với trường hợp hai thuộc tính, nhưng sau đó cũng có điều cần nói về tính nhất quán, nếu mô hình đó thường xuyên xảy ra với các lớp tương tự nhưng lớn hơn trong các phần khác của mã của bạn.


Tôi thấy tò mò rằng các cuộc thảo luận về tính đột biến không thể nhận ra mối quan hệ giữa tính bất biến và cách thức mà một đối tượng có thể được phơi bày hoặc chia sẻ một cách an toàn. Một tham chiếu đến một đối tượng lớp bất biến sâu sắc có thể được chia sẻ một cách an toàn với mã không tin cậy. Một tham chiếu đến một thể hiện của một lớp có thể thay đổi có thể được chia sẻ nếu mọi người giữ tham chiếu có thể được tin cậy không bao giờ sửa đổi đối tượng hoặc đưa nó ra mã có thể làm như vậy. Một tham chiếu đến một thể hiện của lớp có thể bị đột biến thường không nên được chia sẻ. Nếu chỉ có một tham chiếu đến một vật thể tồn tại ở bất cứ đâu trong vũ trụ ...
supercat

... và không có gì đã truy vấn "mã băm nhận dạng" của nó, đã sử dụng nó để khóa hoặc truy cập vào "các Objecttính năng ẩn" của nó , sau đó thay đổi một đối tượng trực tiếp sẽ không khác gì so với ghi đè tham chiếu với một tham chiếu đến một đối tượng mới. giống hệt nhau nhưng cho sự thay đổi được chỉ định. Một trong những điểm yếu lớn nhất về ngữ nghĩa trong Java, IMHO, là nó không có nghĩa là mã có thể chỉ ra rằng một biến chỉ nên là tham chiếu không phù hợp duy nhất cho một cái gì đó; tài liệu tham khảo chỉ có thể được chuyển qua các phương thức không thể giữ một bản sao sau khi chúng trở lại.
supercat

0

Tôi có thể có một cái nhìn quá thấp về điều này và có thể bởi vì tôi đang sử dụng C và C ++, điều này không chính xác để làm cho mọi thứ trở nên bất biến, nhưng tôi thấy các loại dữ liệu bất biến như một chi tiết tối ưu hóa để viết thêm chức năng hiệu quả không có tác dụng phụ và có thể rất dễ dàng cung cấp các tính năng như hoàn tác hệ thống và chỉnh sửa không phá hủy.

Ví dụ, điều này có thể rất tốn kém:

/// @return A new mesh whose vertices have been transformed
/// by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

... nếu Meshkhông được thiết kế để trở thành một cấu trúc dữ liệu bền vững và thay vào đó, là loại dữ liệu yêu cầu sao chép toàn bộ (có thể kéo dài hàng gigabyte trong một số trường hợp) ngay cả khi tất cả chúng ta sẽ làm đang thay đổi một phần của nó (như trong kịch bản trên, nơi chúng tôi chỉ sửa đổi các vị trí đỉnh).

Vì vậy, đó là khi tôi đạt được sự bất biến và thiết kế cấu trúc dữ liệu để cho phép các phần không được sửa đổi của nó được sao chép và tham chiếu nông, để cho phép chức năng trên có hiệu quả hợp lý mà không phải sao chép sâu toàn bộ các lưới xung quanh trong khi vẫn có thể viết Chức năng không có tác dụng phụ giúp đơn giản hóa đáng kể tính an toàn của luồng, an toàn ngoại lệ, khả năng hoàn tác thao tác, áp dụng nó không phá hủy, v.v.

Trong trường hợp của tôi, nó quá tốn kém (ít nhất là từ quan điểm năng suất) để làm cho mọi thứ trở nên bất biến, vì vậy tôi lưu nó cho các lớp quá đắt để sao chép toàn bộ. Các lớp đó thường là các cấu trúc dữ liệu khổng lồ như mắt lưới và hình ảnh và tôi thường sử dụng giao diện có thể thay đổi để thể hiện các thay đổi đối với chúng thông qua một đối tượng "trình tạo" để có được một bản sao bất biến mới. Và tôi không làm điều đó quá nhiều để cố gắng đạt được những đảm bảo bất biến ở cấp trung tâm của lớp, cũng như giúp tôi sử dụng lớp trong các chức năng có thể không có tác dụng phụ. Mong muốn của tôi để làm cho Meshbất biến ở trên không phải là trực tiếp để tạo ra các mắt lưới bất biến, mà là cho phép dễ dàng viết các hàm không có tác dụng phụ nhập vào lưới và xuất ra một lưới mới mà không phải trả một bộ nhớ lớn và chi phí tính toán để trao đổi.

Kết quả là tôi chỉ có 4 loại dữ liệu bất biến trong toàn bộ cơ sở mã của mình và chúng đều là các cấu trúc dữ liệu khổng lồ, nhưng tôi sử dụng chúng rất nhiều để giúp tôi viết các hàm không có tác dụng phụ. Câu trả lời này có thể được áp dụng nếu như tôi, bạn đang làm việc trong một ngôn ngữ không dễ khiến mọi thứ trở nên bất biến. Trong trường hợp đó, bạn có thể chuyển trọng tâm của mình theo hướng làm cho phần lớn các chức năng của bạn tránh được các tác dụng phụ, tại thời điểm đó bạn có thể muốn tạo các cấu trúc dữ liệu được chọn không thay đổi, các loại PDS, như một chi tiết tối ưu hóa để tránh các bản sao đầy đủ đắt tiền. Trong khi đó khi tôi có một chức năng như thế này:

/// @return The v1 * v2.
Vector3f vec_mul(Vector3f v1, Vector3f v2);

... sau đó tôi không có cám dỗ để làm cho các vectơ trở nên bất biến vì chúng đủ rẻ để chỉ sao chép đầy đủ. Không có lợi thế về hiệu suất để đạt được ở đây bằng cách biến vectơ thành các cấu trúc bất biến có thể sao chép nông các phần chưa sửa đổi. Chi phí như vậy sẽ lớn hơn chi phí chỉ cần sao chép toàn bộ vector.


-1
Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Nếu Foo chỉ có hai thuộc tính, thì thật dễ dàng để viết một hàm tạo có hai đối số. Nhưng giả sử bạn thêm một tài sản khác. Sau đó, bạn phải thêm một hàm tạo khác:

public Foo(String a, String b, SomethingElse c)

Tại thời điểm này, nó vẫn có thể quản lý được. Nhưng nếu có 10 tài sản thì sao? Bạn không muốn một nhà xây dựng với 10 đối số. Bạn có thể sử dụng mẫu trình xây dựng để xây dựng các thể hiện, nhưng điều đó làm tăng thêm độ phức tạp. Đến lúc này, bạn sẽ nghĩ "tại sao tôi không thêm setters cho tất cả các thuộc tính như người bình thường làm"?


4
Là một hàm tạo có mười đối số tồi tệ hơn NullPulumException xảy ra khi property7 không được đặt? Là mẫu xây dựng phức tạp hơn mã để kiểm tra các thuộc tính chưa được khởi tạo trong mỗi phương thức? Tốt hơn để kiểm tra một lần khi đối tượng được xây dựng.
kevin cline

2
Như đã nói cách đây hàng thập kỷ chính xác cho trường hợp này, "Nếu bạn có một thủ tục với mười tham số, có lẽ bạn đã bỏ lỡ một số."
9000

1
Tại sao bạn không muốn một nhà xây dựng với 10 đối số? Tại điểm nào bạn vẽ đường thẳng? Tôi nghĩ rằng một nhà xây dựng với 10 đối số là một cái giá nhỏ để trả cho những lợi ích của sự bất biến. Ngoài ra, bạn có một dòng với 10 điều được phân tách bằng dấu phẩy (tùy ý trải thành nhiều dòng hoặc thậm chí 10 dòng, nếu nó trông tốt hơn) hoặc dù sao bạn cũng có 10 dòng khi bạn thiết lập từng thuộc tính riêng lẻ ...
Ricket

1
@Ricket: Bởi vì nó làm tăng nguy cơ đặt các đối số theo thứ tự sai . Nếu bạn đang sử dụng setters hoặc mẫu xây dựng, điều đó khá khó xảy ra.
Mike Baranczak

4
Nếu bạn có một hàm tạo với 10 đối số, có lẽ đã đến lúc bạn nên suy nghĩ về việc gói các đối số đó vào một lớp (hoặc các lớp) của riêng chúng và / hoặc xem xét quan trọng về thiết kế lớp của bạn.
Adam Lear
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.