Interface vazia para combinar várias interfaces

20

Suponha que você tenha duas interfaces:

interface Readable {
    public void read();
}

interface Writable {
    public void write();
}

Em alguns casos, os objetos de implementação podem suportar apenas um deles, mas em muitos casos as implementações suportam as duas interfaces. As pessoas que usam as interfaces terão que fazer algo como:

// 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();

Nenhuma dessas opções é extremamente conveniente, ainda mais quando se considera que você deseja isso como um parâmetro de método.

Uma solução seria declarar uma interface de agrupamento:

interface TheWholeShabam extends Readable, Writable {}

Mas isso tem um problema específico: todas as implementações que suportam Readable e Writable precisam implementar o TheWholeShabam se quiserem ser compatíveis com as pessoas que usam a interface. Mesmo que não ofereça nada além da presença garantida de ambas as interfaces.

Existe uma solução limpa para esse problema ou devo usar a interface do wrapper?

ATUALIZAR

De fato, muitas vezes é necessário ter um objeto que seja legível e gravável, de modo que simplesmente separar as preocupações dos argumentos nem sempre seja uma solução limpa.

UPDATE2

(extraído como resposta, para ficar mais fácil comentar)

UPDATE3

Por favor, tome cuidado para que a principal base de dados para isso não seja fluxos (embora eles também devam ser suportados). Os fluxos fazem uma distinção muito específica entre entrada e saída e há uma clara separação de responsabilidades. Em vez disso, pense em algo como um bytebuffer no qual você precisa de um objeto no qual possa escrever e ler, um objeto que tenha um estado muito específico anexado. Esses objetos existem porque são muito úteis para algumas coisas como E / S assíncrona, codificações, ...

UPDATE4

Uma das primeiras coisas que tentei foi a mesma sugerida abaixo (verifique a resposta aceita), mas provou ser muito frágil.

Suponha que você tenha uma classe que precise retornar o tipo:

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

Se você chamar esse método, o RW genérico será determinado pela variável que recebe o objeto, portanto, você precisará de uma maneira de descrever essa var.

MyObject myObject = someInstance.getItAll();

Isso funcionaria, mas, mais uma vez, vincula-o a uma implementação e pode realmente lançar as exceções de classes de classe em tempo de execução (dependendo do que é retornado).

Além disso, se você deseja uma variável de classe do tipo RW, precisa definir o genérico no nível da classe.

nablex
fonte
5
A frase é "shebang inteiro"
kevin cline 16/10
Esta é uma boa pergunta, mas eu acho que usar legível e 'gravável' como seu exemplo de interfaces é turvando as águas um pouco uma vez que são geralmente papéis diferentes ...
vaughandroid
@Basueta Embora o nome tenha sido simplificado, legível e gravável realmente transmitem muito bem o meu caso. Em alguns casos, você deseja apenas leitura, em alguns casos, somente gravação e, em uma quantidade surpreendente de casos, leitura e gravação.
Nablex #
Não consigo pensar em um momento em que precisei de um único fluxo que seja legível e gravável e, a julgar pelas respostas / comentários de outras pessoas, acho que não sou o único. Só estou dizendo que poderia ser mais útil para escolher um par menos controverso de interfaces ...
vaughandroid
@Baqueta Algo a ver com os pacotes java.nio *? Se você seguir os fluxos, o caso de uso é realmente limitado aos locais onde você usaria o ByteArray * Stream.
Nablex 17/10/2013

Respostas:

19

Sim, você pode declarar seu parâmetro de método como um tipo subespecificado que estende ambos Readablee Writable:

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

A declaração do método parece terrível, mas usá-la é mais fácil do que saber sobre a interface unificada.

Kilian Foth
fonte
2
Eu prefiro muito a segunda abordagem do Konrads aqui: process(Readable readThing, Writable writeThing)e se você deve invocá-lo usando process(foo, foo).
Joachim Sauer
1
A sintaxe correta não é <RW extends Readable&Writable>?
Venha de
1
@JoachimSauer: Por que você prefere uma abordagem que se rompe facilmente sobre uma que é apenas visualmente feia? Se eu chamar process (foo, bar) e foo e bar forem diferentes, o método poderá falhar.
Michael Shaw
@ MichaelShaw: o que estou dizendo é que não deve falhar quando são objetos diferentes. Por que deveria? Se isso acontecer, eu argumentaria que processfaz várias coisas diferentes e viola o princípio de responsabilidade única.
Joachim Sauer
@JoachimSauer: Por que não iria falhar? para (i = 0; j <100; i ++) não é um loop tão útil quanto para (i = 0; i <100; i ++). O loop for lê e grava na mesma variável e não viola o SRP para fazer isso.
Michael Shaw
12

Se houver um local em que você precise myObjectcomo a Readablee Writablevocê pode:

  • Refatorar esse lugar? Ler e escrever são duas coisas diferentes. Se um método faz as duas coisas, talvez não siga o princípio da responsabilidade única.

  • Passe myObjectduas vezes, como Readablee como Writable(dois argumentos). O que o método se importa se é ou não o mesmo objeto?

Konrad Morawski
fonte
1
Isso pode funcionar quando você o usa como argumento e são preocupações separadas. No entanto, às vezes você realmente quer um objeto que é legível e gravável, ao mesmo tempo (pelo mesmo motivo que você deseja usar ByteArrayOutputStream por exemplo)
nablex
Por quê? Os fluxos de saída gravam, como o nome indica - são os fluxos de entrada que podem ler. Mesmo em C # - há uma StreamWritervs. StreamReader(e muitos outros)
Konrad Morawski
Você escreve em um ByteArrayOutputStream para obter os bytes (toByteArray ()). Isso é equivalente a escrever + ler. A funcionalidade real por trás das interfaces é praticamente a mesma, mas de uma maneira mais genérica. Às vezes você só quer ler ou escrever, às vezes você quer os dois. Outro exemplo é o ByteBuffer.
Nablex #
2
Recuei um pouco nesse segundo ponto, mas depois de um momento pensei que realmente não parecia uma má idéia. Você não apenas está separando a leitura e a gravação, mas também criando uma função mais flexível e reduzindo a quantidade de estados de entrada que ela sofre.
Phoshi
2
@ Phoshi O problema é que nem sempre as preocupações são separadas. Às vezes você quer um objeto que pode tanto ler e escrever e que pretende a garantia de que ele é o mesmo objeto (por exemplo ByteArrayOutputStream, ByteBuffer, ...)
nablex
4

Atualmente, nenhuma das respostas aborda a situação quando você não precisa de um texto legível ou gravável, mas ambos . Você precisa de garantias de que, ao escrever para A, possa ler esses dados de A, não para A e ler de B e apenas torcer para que sejam realmente o mesmo objeto. Usecases são abundantes, por exemplo, em todos os lugares que você usaria um ByteBuffer.

De qualquer forma, eu quase terminei o módulo em que estou trabalhando e atualmente optei pela interface do wrapper:

interface Container extends Readable, Writable {}

Agora você pode pelo menos fazer:

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

Minhas próprias implementações de contêiner (atualmente 3) todas implementam Contêiner em oposição às interfaces separadas, mas se alguém esquecer isso em uma implementação, o IOUtils fornece um método utilitário:

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

Sei que essa não é a solução ideal, mas ainda é a melhor saída no momento, porque o Container ainda é um caso de uso bastante grande.

nablex
fonte
1
Eu acho isso absolutamente bom. Melhor do que algumas das outras abordagens propostas em outras respostas.
Tom Anderson
0

Dada uma instância genérica do MyObject, você sempre precisará saber se ela suporta leituras ou gravações. Então você teria um código como:

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

No caso simples, não acho que você possa melhorar isso. Mas se você readThisReadabledeseja gravar o Readable em outro arquivo depois de lê-lo, fica estranho.

Então eu provavelmente iria com isso:

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

Tomando isso como um parâmetro, readThisReadableagora readThisWholeShabam, pode lidar com qualquer classe que implemente TheWholeShabam, não apenas MyObject. E pode escrevê-lo se for gravável e não escrevê-lo se não for. (Temos um verdadeiro "polimorfismo".)

Portanto, o primeiro conjunto de códigos se torna:

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

E você pode salvar uma linha aqui fazendo com que readThisWholeShebam () faça a verificação de legibilidade.

Isso significa que nosso antigo Readable-only precisa implementar isWriteable () (retornando false ) e write () (sem fazer nada), mas agora ele pode ir a todos os tipos de lugares que não podia ir antes e todo o código que lida com TheWholeShabam objetos irão lidar com isso sem nenhum esforço adicional de nossa parte.

Outra coisa: se você pode lidar com uma chamada para read () em uma classe que não lê e uma chamada para write () em uma classe que não escreve sem lixeira, você pode pular isReadable () e isWriteable () métodos. Essa seria a maneira mais elegante de lidar com isso - se funcionar.

RalphChapin
fonte