Interface vide pour combiner plusieurs interfaces

20

Supposons que vous ayez deux interfaces:

interface Readable {
    public void read();
}

interface Writable {
    public void write();
}

Dans certains cas, les objets d'implémentation ne peuvent prendre en charge que l'un d'entre eux, mais dans de nombreux cas, les implémentations prendront en charge les deux interfaces. Les personnes qui utilisent les interfaces devront faire quelque chose comme:

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

Aucune de ces options n'est terriblement pratique, d'autant plus si l'on considère que vous le souhaitez comme paramètre de méthode.

Une solution serait de déclarer une interface d'encapsulation:

interface TheWholeShabam extends Readable, Writable {}

Mais cela a un problème spécifique: toutes les implémentations qui prennent en charge Readable et Writable doivent implémenter TheWholeShabam si elles veulent être compatibles avec les personnes utilisant l'interface. Même s'il n'offre rien à part la présence garantie des deux interfaces.

Existe-t-il une solution propre à ce problème ou dois-je opter pour l'interface wrapper?

MISE À JOUR

En fait, il est souvent nécessaire d'avoir un objet à la fois lisible et inscriptible, donc séparer simplement les préoccupations dans les arguments n'est pas toujours une solution claire.

UPDATE2

(extrait comme réponse, il est donc plus facile de commenter)

UPDATE3

Veuillez noter que le principal cas d'utilisation n'est pas les flux (bien qu'ils doivent également être pris en charge). Les flux font une distinction très spécifique entre les entrées et les sorties et il y a une séparation claire des responsabilités. Pensez plutôt à quelque chose comme un bytebuffer où vous avez besoin d'un objet sur lequel vous pouvez écrire et lire, un objet qui a un état très spécifique qui lui est attaché. Ces objets existent car ils sont très utiles pour certaines choses comme les E / S asynchrones, les encodages, ...

UPDATE4

L'une des premières choses que j'ai essayées était la même que la suggestion donnée ci-dessous (vérifiez la réponse acceptée) mais elle s'est avérée trop fragile.

Supposons que vous ayez une classe qui doit retourner le type:

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

Si vous appelez cette méthode, le RW générique est déterminé par la variable qui reçoit l'objet, vous avez donc besoin d'un moyen de décrire cette var.

MyObject myObject = someInstance.getItAll();

Cela fonctionnerait, mais une fois de plus le lie à une implémentation et peut en fait lever des exceptions classcastex au moment de l'exécution (en fonction de ce qui est retourné).

De plus, si vous souhaitez une variable de classe de type RW, vous devez définir le générique au niveau de la classe.

nablex
la source
5
La phrase est "shebang entier"
kevin cline
C'est une bonne question, mais je pense que l'utilisation de Readable et 'Writable' comme exemples d'interfaces brouille un peu les eaux car ce sont généralement des rôles différents ...
vaughandroid
@Basueta Bien que la dénomination ait été simplifiée, lisible et inscriptible transmet en fait assez bien mon cas d'utilisation. Dans certains cas, vous souhaitez lire uniquement, dans certains cas, écrire uniquement et dans un nombre surprenant de cas, lire et écrire.
nablex
Je ne peux pas penser à un moment où j'ai eu besoin d'un seul flux qui est à la fois lisible et inscriptible, et à en juger par les réponses / commentaires des autres, je ne pense pas que je suis le seul. Je dis juste qu'il pourrait être plus utile de choisir une paire d'interfaces moins controversée ...
vaughandroid
@Baqueta Quelque chose à voir avec les packages java.nio *? Si vous vous en tenez aux flux, le cas d'utilisation est en effet limité aux endroits où vous utiliseriez ByteArray * Stream.
nablex

Réponses:

19

Oui, vous pouvez déclarer votre paramètre de méthode comme un type sous-spécifié qui étend à la fois Readableet Writable:

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

La déclaration de méthode semble terrible, mais son utilisation est plus facile que d'avoir à connaître l'interface unifiée.

Kilian Foth
la source
2
Je préfère de loin la deuxième approche de Konrads: process(Readable readThing, Writable writeThing)et si vous devez l'invoquer en utilisant process(foo, foo).
Joachim Sauer
1
N'est-ce pas la bonne syntaxe <RW extends Readable&Writable>?
VENIR DU
1
@JoachimSauer: Pourquoi préféreriez-vous une approche qui se casse facilement plutôt qu'une approche simplement laide visuellement? Si j'appelle process (foo, bar) et foo et bar sont différents, la méthode peut échouer.
Michael Shaw
@MichaelShaw: ce que je dis, c'est que ça ne devrait pas échouer quand ce sont des objets différents. Pourquoi cela? Si c'est le cas, je dirais que cela processfait plusieurs choses différentes et viole le principe de la responsabilité unique.
Joachim Sauer
@JoachimSauer: Pourquoi cela n'échouerait-il pas? for (i = 0; j <100; i ++) n'est pas une boucle aussi utile que pour (i = 0; i <100; i ++). La boucle for lit et écrit dans la même variable, et elle ne viole pas le SRP pour le faire.
Michael Shaw
12

S'il y a un endroit où vous avez besoin myObjectà la fois d'un Readableet Writablevous pouvez:

  • Refactoriser cet endroit? La lecture et l'écriture sont deux choses différentes. Si une méthode fait les deux, elle ne suit peut-être pas le principe de la responsabilité unique.

  • Passez myObjectdeux fois, en tant que a Readableet en tant que Writable(deux arguments). Que la méthode se soucie-t-elle de savoir s'il s'agit ou non du même objet?

Konrad Morawski
la source
1
Cela pourrait fonctionner lorsque vous l'utilisez comme argument et ce sont des préoccupations distinctes. Cependant, parfois, vous voulez vraiment un objet qui est lisible et inscriptible en même temps (pour la même raison que vous voulez utiliser ByteArrayOutputStream par exemple)
nablex
Comment venir? Les flux de sortie écrivent, comme son nom l'indique - ce sont les flux d'entrée qui peuvent lire. Idem en C # - il y a un StreamWritervs. StreamReader(et bien d'autres)
Konrad Morawski
Vous écrivez dans un ByteArrayOutputStream afin d'obtenir les octets (toByteArray ()). Cela équivaut à écrire + lire. La fonctionnalité réelle derrière les interfaces est sensiblement la même, mais de manière plus générique. Parfois, vous ne voudrez que lire ou écrire, parfois vous voudrez les deux. Un autre exemple est ByteBuffer.
nablex
2
J'ai reculé un peu à ce deuxième point, mais après un moment de réflexion, cela ne semble vraiment pas être une mauvaise idée. Non seulement vous séparez la lecture et l'écriture, vous créez une fonction plus flexible et réduisez la quantité d'état d'entrée qu'elle mute.
Phoshi
2
@Phoshi Le problème est que les préoccupations ne sont pas toujours distinctes. Parfois, vous voulez un objet qui peut à la fois lire et écrire et vous voulez avoir la garantie qu'il s'agit du même objet (par exemple ByteArrayOutputStream, ByteBuffer, ...)
nablex
4

Aucune des réponses ne traite actuellement de la situation lorsque vous n'avez pas besoin d'un élément lisible ou inscriptible mais des deux . Vous avez besoin de garanties que lorsque vous écrivez dans A, vous pouvez lire ces données depuis A, pas écrire vers A et lire depuis B et espérer simplement qu'elles sont en fait le même objet. Les cas d'utilisation sont nombreux, par exemple partout où vous utiliseriez un ByteBuffer.

Quoi qu'il en soit, j'ai presque terminé le module sur lequel je travaille et actuellement j'ai opté pour l'interface wrapper:

interface Container extends Readable, Writable {}

Maintenant, vous pouvez au moins faire:

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

Mes propres implémentations de conteneur (actuellement 3) implémentent toutes Container par opposition aux interfaces séparées, mais si quelqu'un oublie cela dans une implémentation, IOUtils fournit une méthode utilitaire:

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

Je sais que ce n'est pas la solution optimale, mais c'est toujours la meilleure solution pour le moment, car le conteneur est toujours un cas d'utilisation assez important.

nablex
la source
1
Je pense que c'est très bien. Mieux que certaines des autres approches proposées dans d'autres réponses.
Tom Anderson
0

Étant donné une instance générique de MyObject, vous aurez toujours besoin de savoir si elle prend en charge les lectures ou les écritures. Vous auriez donc du code comme:

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

Dans le cas simple, je ne pense pas que vous puissiez améliorer cela. Mais si vous readThisReadablevoulez écrire le Readable dans un autre fichier après l'avoir lu, cela devient gênant.

Donc j'irais probablement avec ceci:

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

En prenant ceci comme paramètre, readThisReadablemaintenant readThisWholeShabam, vous pouvez gérer n'importe quelle classe qui implémente TheWholeShabam, pas seulement MyObject. Et il peut l'écrire s'il est accessible en écriture et ne pas l'écrire s'il ne l'est pas. (Nous avons un vrai "polymorphisme".)

Ainsi, le premier ensemble de codes devient:

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

Et vous pouvez enregistrer une ligne ici en demandant à readThisWholeShebam () de vérifier la lisibilité.

Cela signifie que notre ancien Readable-only doit implémenter isWriteable () (renvoyer false ) et write () (ne rien faire), mais maintenant il peut aller dans toutes sortes d'endroits où il ne pouvait pas aller auparavant et tout le code qui gère TheWholeShabam les objets s'en occuperont sans autre effort de notre part.

Une autre chose: si vous pouvez gérer un appel à read () dans une classe qui ne lit pas et un appel à write () dans une classe qui n'écrit pas sans jeter quelque chose, vous pouvez ignorer isReadable () et isWriteable () méthodes. Ce serait la façon la plus élégante de le gérer - si cela fonctionne.

RalphChapin
la source