Không khai báo giao diện cho các đối tượng bất biến


27

Không khai báo giao diện cho các đối tượng bất biến

[EDIT] Trường hợp các đối tượng trong câu hỏi đại diện cho Đối tượng truyền dữ liệu (DTO) hoặc Dữ liệu cũ đơn giản (POD)

Đó có phải là một hướng dẫn hợp lý?

Cho đến nay, tôi thường tạo giao diện cho các lớp kín không thay đổi (dữ liệu không thể thay đổi). Tôi đã cố gắng cẩn thận để không sử dụng giao diện bất cứ nơi nào tôi quan tâm đến sự bất biến.

Thật không may, giao diện bắt đầu tràn ngập mã (và đó không chỉ là mã của tôi mà tôi lo lắng). Bạn kết thúc việc được thông qua một giao diện, và sau đó muốn chuyển nó đến một mã nào đó thực sự muốn cho rằng thứ được truyền cho nó là bất biến.

Vì vấn đề này, tôi đang cân nhắc không bao giờ khai báo giao diện cho các đối tượng bất biến.

Điều này có thể có sự phân nhánh đối với Kiểm thử đơn vị, nhưng ngoài ra, điều này có vẻ là một hướng dẫn hợp lý?

Hoặc có một mẫu nào khác mà tôi nên sử dụng để tránh sự cố "giao diện trải rộng" mà tôi đang gặp?

(Tôi đang sử dụng các đối tượng không thay đổi này vì nhiều lý do: Chủ yếu là vì sự an toàn của luồng vì tôi viết rất nhiều mã đa luồng; nhưng cũng vì điều đó có nghĩa là tôi có thể tránh tạo các bản sao phòng thủ của các đối tượng được truyền cho các phương thức. Mã trở nên đơn giản hơn rất nhiều nhiều trường hợp khi bạn biết một thứ gì đó là bất biến - mà bạn không biết nếu bạn đã được giao một giao diện. Thực tế, bạn thường không thể tạo một bản sao phòng thủ của một đối tượng được tham chiếu qua giao diện nếu nó không cung cấp hoạt động nhân bản hoặc bất kỳ cách nào để tuần tự hóa nó ...)

[CHỈNH SỬA]

Để cung cấp nhiều ngữ cảnh hơn cho lý do tôi muốn làm cho các đối tượng trở nên bất biến, hãy xem bài đăng trên blog này từ Eric Lippert:

http://bloss.msdn.com/b/ericlippert/archive/tags/immutability/

Tôi cũng nên chỉ ra rằng tôi đang làm việc với một số khái niệm cấp thấp hơn ở đây, chẳng hạn như các mục đang bị thao túng / chuyển xung quanh trong hàng đợi công việc đa luồng. Đây thực chất là các DTO.

Ngoài ra Joshua Bloch khuyên bạn nên sử dụng các đối tượng bất biến trong cuốn sách Java hiệu quả .


Theo sát

Cảm ơn về tất cả các phản hồi. Tôi đã quyết định đi trước và sử dụng hướng dẫn này cho DTO và ilk của họ. Nó hoạt động tốt cho đến nay, nhưng chỉ mới được một tuần ... Tuy nhiên, nó trông vẫn ổn.

Có một số vấn đề khác liên quan đến vấn đề này mà tôi muốn hỏi về; đáng chú ý là một cái gì đó tôi đang gọi là "Bất biến sâu hoặc nông" (danh pháp tôi đã đánh cắp từ nhân bản Deep và Shallow) - nhưng đó là một câu hỏi cho một thời điểm khác.


3
+1 câu hỏi tuyệt vời. Vui lòng giải thích lý do tại sao nó rất quan trọng với bạn rằng đối tượng thuộc lớp bất biến cụ thể đó chứ không phải là thứ chỉ thực hiện cùng một giao diện. Có thể vấn đề của bạn bắt nguồn từ nơi khác. Dependency Injection cực kỳ quan trọng đối với thử nghiệm đơn vị, nhưng nó có nhiều lợi ích quan trọng khác mà tất cả sẽ biến mất nếu bạn buộc mã của mình yêu cầu một lớp bất biến cụ thể.
Steven Doggart

1
Tôi hơi bối rối khi bạn sử dụng thuật ngữ bất biến . Nói rõ hơn, bạn có nghĩa là một lớp niêm phong, không thể được kế thừa và ghi đè, hoặc bạn có nghĩa là dữ liệu mà nó phơi bày là chỉ đọc và không thể thay đổi khi đối tượng được tạo. Nói cách khác, bạn có muốn đảm bảo rằng đối tượng thuộc một loại cụ thể hoặc giá trị của nó sẽ không bao giờ thay đổi. Trong bình luận đầu tiên của tôi, tôi giả sử có nghĩa là cái trước, nhưng bây giờ với chỉnh sửa của bạn, nó nghe giống như cái sau. Hay bạn quan tâm đến cả hai?
Steven Doggart

3
Tôi đang bối rối, tại sao không bỏ setters khỏi giao diện? Nhưng mặt khác, tôi rất mệt mỏi khi nhìn thấy các giao diện cho các đối tượng thực sự là các đối tượng miền và / hoặc DTO yêu cầu các nhà máy cho các đối tượng đó ... gây ra sự bất đồng về nhận thức đối với tôi.
Michael Brown

2
Điều gì về giao diện cho các lớp mà tất cả là bất biến? Ví dụ, Java Số lớp cho phép một để xác định một List<Number>mà có thể giữ Integer, Float, Long, BigDecimal, vv ... Tất cả những điều là không thay đổi bản thân.

1
@MikeBrown Tôi đoán rằng chỉ vì giao diện ngụ ý sự bất biến không có nghĩa là việc triển khai thực thi nó. Bạn chỉ có thể để nó theo quy ước (tức là ghi lại giao diện rằng tính bất biến là một yêu cầu) nhưng bạn có thể kết thúc với một số vấn đề thực sự khó chịu nếu ai đó vi phạm quy ước.
vaughandroid

Câu trả lời:


17

Theo tôi, quy tắc của bạn là một quy tắc tốt (hoặc ít nhất nó không phải là một quy tắc xấu), nhưng chỉ vì tình huống bạn đang mô tả. Tôi sẽ không nói rằng tôi đồng ý với nó trong mọi tình huống, vì vậy, từ quan điểm của người giáo viên nội tâm của tôi, tôi phải nói rằng quy tắc của bạn về mặt kỹ thuật quá rộng.

Thông thường, bạn sẽ không xác định các đối tượng bất biến trừ khi về cơ bản chúng được sử dụng làm đối tượng truyền dữ liệu (DTO), có nghĩa là chúng chứa các thuộc tính dữ liệu nhưng rất ít logic và không phụ thuộc. Nếu đó là trường hợp, vì có vẻ như nó ở đây, tôi muốn nói rằng bạn an toàn khi sử dụng các loại cụ thể trực tiếp hơn là các giao diện.

Tôi chắc chắn sẽ có một số người theo chủ nghĩa thử nghiệm đơn vị sẽ không đồng ý, nhưng theo tôi, các lớp DTO có thể được loại trừ một cách an toàn khỏi các yêu cầu kiểm tra đơn vị và tiêm phụ thuộc. Không cần sử dụng nhà máy để tạo DTO, vì nó không có phụ thuộc. Nếu mọi thứ tạo ra các DTO trực tiếp khi cần, thì thực sự không có cách nào để tiêm một loại khác, vì vậy không cần giao diện. Và vì chúng không chứa logic, nên không có gì để kiểm tra đơn vị. Ngay cả khi chúng có chứa một số logic, miễn là chúng không có phụ thuộc, thì việc kiểm tra đơn vị logic là điều không quan trọng, nếu cần thiết.

Như vậy, tôi nghĩ rằng việc đưa ra một quy tắc rằng tất cả các lớp DTO sẽ không thực hiện giao diện, trong khi có khả năng không cần thiết, sẽ không làm tổn hại đến thiết kế phần mềm của bạn. Vì bạn có yêu cầu này là dữ liệu cần phải bất biến và bạn không thể thực thi điều đó thông qua một giao diện, nên tôi sẽ nói rằng hoàn toàn hợp pháp để thiết lập quy tắc đó như một tiêu chuẩn mã hóa.

Tuy nhiên, vấn đề lớn hơn là cần phải thực thi nghiêm ngặt một lớp DTO sạch. Miễn là các lớp không thay đổi giao diện của bạn chỉ tồn tại trong lớp DTO và lớp DTO của bạn vẫn không có logic và phụ thuộc, thì bạn sẽ an toàn. Tuy nhiên, nếu bạn bắt đầu trộn các lớp của mình và bạn có các lớp không có giao diện tăng gấp đôi so với các lớp của lớp nghiệp vụ, thì tôi nghĩ bạn sẽ bắt đầu gặp nhiều rắc rối.


Mã dự kiến ​​đặc biệt để xử lý một đối tượng DTO hoặc giá trị bất biến nên sử dụng các tính năng của loại đó thay vì các giao diện, nhưng điều đó không có nghĩa là sẽ không có trường hợp sử dụng cho một giao diện được thực hiện bởi một lớp như vậy và cả bởi các lớp khác, với các phương thức hứa trả về các giá trị sẽ hợp lệ trong một thời gian tối thiểu (ví dụ: nếu giao diện được truyền cho một hàm, các phương thức sẽ trả về các giá trị hợp lệ cho đến khi hàm đó trả về). Giao diện như vậy có thể hữu ích nếu có một số loại đóng gói dữ liệu tương tự và một điều ước ...
supercat

... Để có một phương tiện sao chép dữ liệu từ cái này sang cái khác. Nếu tất cả các loại bao gồm một số dữ liệu nhất định thực hiện cùng một giao diện để đọc nó, thì dữ liệu đó có thể được sao chép dễ dàng giữa tất cả các loại, mà không yêu cầu mỗi loại phải biết cách nhập từ mỗi loại khác.
supercat

Tại thời điểm đó, bạn cũng có thể loại bỏ getters của mình (và setters, nếu DTO của bạn có thể thay đổi vì một lý do ngu ngốc nào đó) và làm cho các trường công khai. Bạn sẽ không bao giờ đặt bất kỳ logic ở đó, phải không?
Kevin

@Kevin Tôi cho rằng, nhưng với sự tiện lợi hiện đại của các thuộc tính tự động, thật dễ dàng để biến chúng thành các thuộc tính, tại sao không? Tôi cho rằng nếu hiệu suất và hiệu quả là mối quan tâm lớn nhất thì có lẽ đó là vấn đề. Trong mọi trường hợp, câu hỏi là, mức độ "logic" nào bạn cho phép trong DTO? Ngay cả khi bạn đặt một số logic xác thực vào các thuộc tính của nó, sẽ không có vấn đề gì khi kiểm tra đơn vị nó miễn là nó không có phụ thuộc sẽ yêu cầu chế nhạo. Miễn là DTO không sử dụng bất kỳ đối tượng kinh doanh phụ thuộc nào, thì sẽ an toàn khi có một chút logic được tích hợp vào chúng bởi vì chúng vẫn có thể kiểm tra được.
Steven Doggart

Cá nhân, tôi thích giữ chúng sạch sẽ nhất có thể, nhưng luôn luôn có lúc cần phải bẻ cong các quy tắc và vì vậy tốt hơn hết là để cánh cửa mở cho nó trong trường hợp cần thiết.
Steven Doggart

7

Tôi đã từng làm ầm ĩ về việc làm cho mã của tôi trở nên bất khả xâm phạm để sử dụng sai. Tôi đã thực hiện các giao diện chỉ đọc để ẩn các thành viên đột biến, thêm nhiều ràng buộc cho chữ ký chung của mình, v.v., hóa ra hầu hết thời gian tôi đưa ra quyết định thiết kế vì tôi không tin tưởng đồng nghiệp tưởng tượng của mình. "Có lẽ một ngày nào đó họ sẽ thuê một anh chàng mới vào cấp và anh ta sẽ không biết rằng lớp XYZ không thể cập nhật DTO ABC. Ôi không!" Lần khác, tôi đang tập trung vào vấn đề sai - bỏ qua giải pháp rõ ràng - không nhìn thấy khu rừng qua những tán cây.

Tôi không bao giờ tạo giao diện cho các DTO của mình nữa. Tôi làm việc theo giả định rằng những người chạm vào mã của tôi (chủ yếu là bản thân tôi) biết những gì được phép và những gì có ý nghĩa. Nếu tôi cứ mắc phải một lỗi ngu ngốc như vậy, tôi thường không cố gắng làm cứng giao diện của mình. Bây giờ tôi dành phần lớn thời gian của mình để cố gắng hiểu tại sao tôi cứ mắc lỗi tương tự. Đó thường là vì tôi đang phân tích quá mức một cái gì đó hoặc thiếu một khái niệm quan trọng. Mã của tôi đã được làm việc dễ dàng hơn nhiều kể từ khi tôi từ bỏ việc bị hoang tưởng. Tôi cũng kết thúc với ít "khung" hơn đòi hỏi kiến ​​thức của người trong cuộc để làm việc trên hệ thống.

Kết luận của tôi là tìm ra thứ đơn giản nhất có hiệu quả. Sự phức tạp thêm vào của việc tạo các giao diện an toàn chỉ làm lãng phí thời gian phát triển và làm phức tạp mã đơn giản. Lo lắng về những điều như thế này khi bạn có 10.000 nhà phát triển sử dụng thư viện của mình. Tin tôi đi, nó sẽ cứu bạn khỏi rất nhiều căng thẳng không cần thiết.


Tôi thường lưu các giao diện khi tôi cần tiêm phụ thuộc để kiểm tra đơn vị.
Công viên Travis

5

Có vẻ như một hướng dẫn ổn, nhưng vì lý do kỳ lạ. Tôi đã có một số nơi mà một giao diện (hoặc lớp cơ sở trừu tượng) cung cấp quyền truy cập thống nhất vào một loạt các đối tượng bất biến. Các chiến lược có xu hướng giảm ở đây. Các đối tượng nhà nước có xu hướng rơi ở đây. Tôi không nghĩ rằng quá vô lý khi định hình một giao diện dường như bất biến và ghi lại nó như vậy trong API của bạn.

Điều đó nói rằng, mọi người thường có xu hướng giao diện quá mức đối tượng Plain Old Data (sau đây, POD) và thậm chí các cấu trúc đơn giản (thường không thay đổi). Nếu mã của bạn không có lựa chọn thay thế lành mạnh cho một số cấu trúc cơ bản, thì nó không cần giao diện. Không, kiểm tra đơn vị không đủ lý do để thay đổi thiết kế của bạn (truy cập cơ sở dữ liệu không phải là lý do bạn cung cấp giao diện cho điều đó, đó là tính linh hoạt cho thay đổi trong tương lai) - đó không phải là kết thúc của thế giới nếu các thử nghiệm của bạn sử dụng cấu trúc cơ bản cơ bản như vốn có.


2

Mã trở nên đơn giản hơn rất nhiều trong nhiều trường hợp khi bạn biết điều gì đó là bất biến - điều mà bạn không làm nếu bạn được trao một giao diện.

Tôi không nghĩ đó là mối lo ngại mà người thực hiện truy cập phải lo lắng. Nếu điều đó interface Xcó nghĩa là bất biến, thì đó không phải là trách nhiệm của người thực hiện giao diện để đảm bảo rằng họ thực hiện giao diện theo cách không thay đổi?

Tuy nhiên, trong suy nghĩ của tôi, không có thứ gọi là giao diện bất biến - hợp đồng mã được chỉ định bởi giao diện chỉ áp dụng cho các phương thức được hiển thị của một đối tượng và không có gì về bên trong của một đối tượng.

Thông thường hơn nhiều khi thấy tính bất biến được triển khai như một trang trí thay vì giao diện, nhưng tính khả thi của giải pháp đó thực sự phụ thuộc vào cấu trúc của đối tượng của bạn và mức độ phức tạp của việc triển khai.


1
Bạn có thể cung cấp một ví dụ về việc thực hiện bất biến như một người trang trí?
vaughandroid

Không quan trọng, tôi không nghĩ, nhưng đó là một bài tập suy nghĩ tương đối đơn giản - nếu MutableObjectncác phương pháp thay đổi trạng thái và mphương thức trả về trạng thái, ImmutableDecoratorcó thể tiếp tục hiển thị các phương thức trả về trạng thái ( m) và, tùy thuộc vào môi trường, khẳng định hoặc ném một ngoại lệ khi một trong các phương thức có thể thay đổi được gọi.
Jonathan Rich

Tôi thực sự không thích chuyển đổi sự chắc chắn của thời gian biên dịch thành khả năng ngoại lệ trong thời gian chạy ...
Matthew Watson

OK, nhưng làm thế nào mà ImmutableDecorator có thể biết liệu một phương thức đã cho có thay đổi trạng thái hay không?
vaughandroid

Trong mọi trường hợp, bạn không thể chắc chắn nếu một lớp thực hiện giao diện của bạn là bất biến đối với các phương thức được xác định bởi giao diện của bạn. @Baqueta Người trang trí phải có kiến ​​thức về việc thực hiện lớp cơ sở.
Jonathan Rich

0

Bạn kết thúc việc được thông qua một giao diện, và sau đó muốn chuyển nó đến một mã nào đó thực sự muốn cho rằng thứ được truyền cho nó là bất biến.

Trong C #, một phương thức mong đợi một đối tượng thuộc loại "bất biến" không nên có tham số của loại giao diện vì giao diện C # không thể chỉ định hợp đồng bất biến. Do đó, hướng dẫn mà bạn đề xuất tự nó là vô nghĩa trong C # vì bạn không thể thực hiện nó ngay từ đầu (và các phiên bản tương lai của ngôn ngữ không có khả năng cho phép bạn thực hiện điều đó).

Câu hỏi của bạn bắt nguồn từ một sự hiểu lầm tinh tế về các bài viết của Eric Lippert về tính bất biến. Eric đã không xác định các giao diện IStack<T>IQueue<T>chỉ định hợp đồng cho các ngăn xếp và hàng đợi bất biến. Họ không. Ông định nghĩa chúng cho thuận tiện. Các giao diện này cho phép anh ta xác định các loại khác nhau cho các ngăn xếp và hàng đợi trống. Chúng ta có thể đề xuất một thiết kế khác và thực hiện một ngăn xếp bất biến bằng cách sử dụng một loại duy nhất mà không yêu cầu giao diện hoặc một loại riêng để biểu diễn ngăn xếp trống, nhưng mã kết quả sẽ không sạch và sẽ kém hiệu quả hơn một chút.

Bây giờ hãy bám sát thiết kế của Eric. Một phương thức yêu cầu một ngăn xếp bất biến phải có tham số về kiểu Stack<T>chứ không phải giao diện chung IStack<T>đại diện cho kiểu dữ liệu trừu tượng của ngăn xếp theo nghĩa chung. Không rõ ràng làm thế nào để làm điều này khi sử dụng ngăn xếp bất biến của Eric và anh ấy đã không thảo luận về điều này trong các bài viết của mình, nhưng điều đó là có thể. Vấn đề là với loại ngăn xếp trống. Bạn có thể giải quyết vấn đề này bằng cách đảm bảo rằng bạn không bao giờ có được một ngăn xếp trống. Điều này có thể được đảm bảo bằng cách đẩy một giá trị giả làm giá trị đầu tiên trên ngăn xếp và không bao giờ bật nó. Bằng cách này, bạn có thể bỏ kết quả PushPopđến một cách an toàn Stack<T>.

Stack<T>dụng cụ IStack<T>có thể hữu ích. Bạn có thể định nghĩa các phương thức yêu cầu một ngăn xếp, bất kỳ ngăn xếp nào, không nhất thiết là một ngăn xếp bất biến. Các phương thức này có thể có một tham số của loại IStack<T>. Điều này cho phép bạn vượt qua ngăn xếp bất biến cho nó. Lý tưởng nhất, IStack<T>sẽ là một phần của chính thư viện tiêu chuẩn. Trong .NET, không có IStack<T>, nhưng có các giao diện tiêu chuẩn khác mà ngăn xếp bất biến có thể thực hiện, làm cho kiểu này trở nên hữu ích hơn.

Thiết kế thay thế và thực hiện ngăn xếp bất biến mà tôi đã đề cập trước đó sử dụng một giao diện được gọi là IImmutableStack<T>. Tất nhiên, việc chèn "Bất biến" trong tên giao diện sẽ không khiến mọi loại thực hiện nó trở nên bất biến. Tuy nhiên, trong giao diện này, hợp đồng bất biến chỉ bằng lời nói. Một nhà phát triển tốt nên tôn trọng nó.

Nếu bạn đang phát triển một thư viện nhỏ, nội bộ, thì bạn có thể đồng ý với mọi người trong nhóm để tôn trọng hợp đồng này và bạn có thể sử dụng IImmutableStack<T>làm loại tham số. Nếu không, bạn không nên sử dụng loại giao diện.

Tôi muốn thêm, vì bạn đã gắn thẻ câu hỏi C #, rằng trong đặc tả C #, không có thứ gọi là DTO và POD. Do đó, loại bỏ chúng hoặc xác định chính xác chúng sẽ cải thiện câu hỏi.

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.