Giao diện trống để kết hợp nhiều giao diện


20

Giả sử bạn có hai giao diện:

interface Readable {
    public void read();
}

interface Writable {
    public void write();
}

Trong một số trường hợp, các đối tượng triển khai chỉ có thể hỗ trợ một trong những điều này nhưng trong rất nhiều trường hợp, việc triển khai sẽ hỗ trợ cả hai giao diện. Những người sử dụng các giao diện sẽ phải làm một cái gì đó như:

// can't write to it without explicit casting
Readable myObject = new MyObject();

// can't read from it without explicit casting
Writable myObject = new MyObject();

// tight coupling to actual implementation
MyObject myObject = new MyObject();

Không có tùy chọn nào trong số này là thuận tiện khủng khiếp, thậm chí còn nhiều hơn khi xem xét rằng bạn muốn đây là một tham số phương thức.

Một giải pháp sẽ là khai báo giao diện gói:

interface TheWholeShabam extends Readable, Writable {}

Nhưng điều này có một vấn đề cụ thể: tất cả các triển khai hỗ trợ cả Readable và Writable đều phải triển khai TheWholeShabam nếu chúng muốn tương thích với mọi người sử dụng giao diện. Mặc dù nó không cung cấp gì ngoài sự hiện diện được đảm bảo của cả hai giao diện.

Có một giải pháp rõ ràng cho vấn đề này hay tôi nên sử dụng giao diện trình bao bọc?

CẬP NHẬT

Trên thực tế, thường là cần thiết để có một đối tượng vừa có thể đọc vừa có thể ghi được, vì vậy chỉ cần tách biệt các mối quan tâm trong các đối số không phải lúc nào cũng là một giải pháp sạch.

CẬP NHẬT2

(được trích xuất dưới dạng câu trả lời để dễ nhận xét hơn)

CẬP NHẬT3

Xin lưu ý rằng usecase chính cho việc này là không phải luồng (mặc dù chúng cũng phải được hỗ trợ). Các luồng tạo ra sự phân biệt rất cụ thể giữa đầu vào và đầu ra và có sự phân tách trách nhiệm rõ ràng. Thay vào đó, hãy nghĩ về một cái gì đó giống như một bytebuffer nơi bạn cần một đối tượng bạn có thể viết và đọc từ đó, một đối tượng có trạng thái rất cụ thể gắn liền với nó. Các đối tượng này tồn tại bởi vì chúng rất hữu ích cho một số thứ như I / O không đồng bộ, mã hóa, ...

CẬP NHẬT4

Một trong những điều đầu tiên tôi đã thử giống như gợi ý được đưa ra dưới đây (kiểm tra câu trả lời được chấp nhận) nhưng nó tỏ ra quá mong manh.

Giả sử bạn có một lớp phải trả về kiểu:

public <RW extends Readable & Writable> RW getItAll();

Nếu bạn gọi phương thức này, RW chung được xác định bởi biến nhận đối tượng, vì vậy bạn cần một cách để mô tả var này.

MyObject myObject = someInstance.getItAll();

Điều này sẽ hoạt động nhưng một lần nữa liên kết nó với một triển khai và thực sự có thể ném các ngoại lệ classcast khi chạy (tùy thuộc vào những gì được trả về).

Ngoài ra, nếu bạn muốn một biến lớp thuộc loại RW, bạn cần xác định chung ở cấp độ lớp.


5
Cụm từ này là "toàn bộ shebang"
kevin cline

Đây là một câu hỏi hay, nhưng tôi nghĩ rằng việc sử dụng Readable và 'Writable' vì các giao diện ví dụ của bạn đang làm vấy bẩn vùng nước vì chúng thường có vai trò khác nhau ...
vaughandroid

@Basueta Trong khi việc đặt tên đã được đơn giản hóa, có thể đọc và ghi được thực sự truyền tải usecase của tôi khá tốt. Trong một số trường hợp bạn chỉ muốn đọc, trong một số trường hợp chỉ viết và trong một số lượng đáng ngạc nhiên các trường hợp đọc & viết.
nablex

Tôi không thể nghĩ về một thời gian khi tôi cần một luồng duy nhất có thể đọc và ghi được và đánh giá từ các câu trả lời / nhận xét của người khác Tôi không nghĩ tôi là người duy nhất. Tôi chỉ nói rằng có thể hữu ích hơn khi chọn một cặp giao diện ít gây tranh cãi hơn ...
vaughandroid

@Baqueta Bất cứ điều gì để làm với các gói java.nio *? Nếu bạn dính vào luồng, usecase thực sự bị giới hạn ở những nơi bạn sẽ sử dụng ByteArray * Stream.
nablex

Câu trả lời:


19

Có, bạn có thể khai báo tham số phương thức của mình dưới dạng không xác định được mở rộng cả ReadableWritable:

public <RW extends Readable & Writable> void process(RW thing);

Khai báo phương thức có vẻ khủng khiếp, nhưng sử dụng nó dễ hơn là phải biết về giao diện hợp nhất.


2
Tôi rất thích cách tiếp cận thứ hai của Konrads ở đây: process(Readable readThing, Writable writeThing)và nếu bạn phải gọi nó bằng cách sử dụng process(foo, foo).
Joachim Sauer

1
Không phải là cú pháp đúng <RW extends Readable&Writable>?
ĐẾN TỪ

1
@JoachimSauer: Tại sao bạn muốn một cách tiếp cận dễ dàng vượt qua một cách đơn giản là xấu về mặt thị giác? Nếu tôi gọi tiến trình (foo, bar) và foo và bar khác nhau, phương thức có thể thất bại.
Michael Shaw

@MichaelShaw: điều tôi đang nói là không nên thất bại khi chúng là những đối tượng khác nhau. Tại sao nên làm thế? Nếu đúng như vậy, thì tôi cho rằng processlàm nhiều việc khác nhau và vi phạm nguyên tắc trách nhiệm đơn lẻ.
Joachim Sauer

@JoachimSauer: Tại sao nó không thất bại? for (i = 0; j <100; i ++) không hữu ích như một vòng lặp như đối với (i = 0; i <100; i ++). Vòng lặp for cả đọc và ghi vào cùng một biến và nó không vi phạm SRP để làm như vậy.
Michael Shaw

12

Nếu có một nơi mà bạn cần myObjectcả a ReadableWritablebạn có thể:

  • Tái cấu trúc nơi đó? Đọc và viết là hai điều khác nhau. Nếu một phương thức thực hiện cả hai, có lẽ nó không tuân theo nguyên tắc trách nhiệm duy nhất.

  • Vượt qua myObjecthai lần, như một Readablevà như mộtWritable (hai đối số). Phương thức quan tâm xem nó có phải hay không cùng một đối tượng?


1
Điều này có thể hoạt động khi bạn sử dụng nó như một đối số và chúng là những mối quan tâm riêng biệt. Tuy nhiên, đôi khi bạn thực sự muốn một đối tượng có thể đọc và ghi được cùng một lúc (với cùng lý do bạn muốn sử dụng ByteArrayOutputStream chẳng hạn)
nablex

Làm thế nào mà? Luồng đầu ra ghi, như tên cho biết - đó là luồng đầu vào có thể đọc. Tương tự trong C # - có một StreamWriterso với StreamReader(và nhiều người khác)
Konrad Morawski

Bạn viết thư cho ByteArrayOutputStream để lấy byte (toByteArray ()). Điều này tương đương với viết + đọc. Các chức năng thực tế đằng sau các giao diện là nhiều như nhau nhưng theo cách chung hơn. Đôi khi bạn sẽ chỉ muốn đọc hoặc viết, đôi khi bạn sẽ muốn cả hai. Một ví dụ khác là ByteBuffer.
nablex

2
Tôi đã thu lại một chút ở điểm thứ hai, nhưng sau một lúc nghĩ rằng đó thực sự không phải là một ý tưởng tồi. Bạn không chỉ tách biệt việc đọc và viết, bạn đang tạo ra một chức năng linh hoạt hơn và giảm lượng trạng thái đầu vào mà nó biến đổi.
Phoshi

2
@Phoshi Vấn đề là mối quan tâm không phải lúc nào cũng riêng biệt. Đôi khi bạn muốn một đối tượng vừa có thể đọc và viết và bạn muốn đảm bảo rằng đó là cùng một đối tượng (ví dụ: ByteArrayOutputStream, ByteBuffer, ...)
nablex

4

Không có câu trả lời nào hiện đang giải quyết tình huống khi bạn không cần đọc hoặc có thể ghi nhưng cả hai . Bạn cần đảm bảo rằng khi ghi vào A, bạn có thể đọc dữ liệu đó từ A, không ghi vào A và đọc từ B và chỉ hy vọng chúng thực sự là cùng một đối tượng. Usecase rất phong phú, ví dụ như mọi nơi bạn sẽ sử dụng ByteBuffer.

Dù sao, tôi đã gần hoàn thành mô-đun tôi đang làm việc và hiện tại tôi đã chọn giao diện trình bao bọc:

interface Container extends Readable, Writable {}

Bây giờ bạn ít nhất có thể làm:

Container container = IOUtils.newContainer();
container.write("something".getBytes());
System.out.println(IOUtils.toString(container));

Tất cả các triển khai container của riêng tôi (hiện tại 3) đều thực hiện Container trái ngược với các giao diện riêng biệt nhưng nếu ai đó quên điều này trong một triển khai, IOUtils cung cấp một phương thức tiện ích:

Readable myReadable = ...;
// assuming myReadable is also Writable you can do this:
Container container = IOUtils.toByteContainer(myReadable);

Tôi biết đây không phải là giải pháp tối ưu nhưng hiện tại vẫn là cách tốt nhất vì Container vẫn là một usecase khá lớn.


1
Tôi nghĩ rằng điều này hoàn toàn tốt. Tốt hơn so với một số phương pháp khác được đề xuất trong các câu trả lời khác.
Tom Anderson

0

Đưa ra một ví dụ MyObject chung, bạn sẽ luôn cần biết liệu nó có hỗ trợ đọc hay ghi hay không. Vì vậy, bạn sẽ có mã như:

if (myObject instanceof Readable)  {
    Readable  r = (Readable) myObject;
    readThisReadable( r );
}

Trong trường hợp đơn giản, tôi không nghĩ bạn có thể cải thiện điều này. Nhưng nếu readThisReadablemuốn viết Readable sang tập tin khác sau khi đọc nó, điều đó thật khó xử.

Vì vậy, tôi có thể đi với điều này:

interface TheWholeShabam  {
    public boolean  isReadable();
    public boolean  isWriteable();
    public void     read();
    public void     write();
}

readThisReadableBây giờ readThisWholeShabam, lấy điều này làm tham số, có thể xử lý bất kỳ lớp nào thực hiện TheWholeShabam, không chỉ MyObject. nó có thể viết nó nếu nó có thể ghi và không viết nó nếu không. (Chúng ta đã có "Đa hình" thực sự.)

Vì vậy, bộ mã đầu tiên trở thành:

TheWholeShabam  myObject = ...;
if (myObject.isReadable()
    readThisWholeShebam( myObject );

Và bạn có thể lưu một dòng ở đây bằng cách đọc ThisWholeShebam () để kiểm tra khả năng đọc.

Điều này không có nghĩa là trước đây chỉ đọc được của chúng tôi phải thực hiện isWritizable () (trả về false ) và viết () (không làm gì), nhưng bây giờ nó có thể đi đến tất cả các nơi mà nó không thể đi trước và tất cả các mã xử lý TheWholeShabam các đối tượng sẽ đối phó với nó mà không cần nỗ lực hơn nữa về phía chúng tôi.

Một điều khác: nếu bạn có thể xử lý một cuộc gọi để đọc () trong một lớp không đọc và một cuộc gọi để viết () trong một lớp không viết mà không bỏ qua một cái gì đó, bạn có thể bỏ qua isReadable () và có thể ghi được () phương thức. Đây sẽ là cách thanh lịch nhất để xử lý nó - nếu nó hoạt động.

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.