2010-11-30 7 views
5

Supongamos que tengo una interfaz que soporta unas pocas operaciones posibles:¿Qué es un buen diseño para una interfaz con componentes opcionales?

interface Frobnicator { 
    int doFoo(double v); 
    int doBar(); 
} 

Ahora, algunos casos sólo apoyará una u otra de estas operaciones. Ellos pueden apoyar ambos. El código del cliente no necesariamente lo sabrá hasta que obtenga uno de la fábrica relevante, a través de la inyección de dependencia, o desde donde obtenga instancias.

Veo algunas maneras de manejar esto. Una, que parece ser la táctica general tomada en la API de Java, es simplemente tener la interfaz como se muestra arriba y tener métodos no compatibles aumentar UnsupportedOperationException. Esto tiene la desventaja, sin embargo, de no ser rápido: el código del cliente no puede decir si doFoo funcionará hasta que intente llamar al doFoo.

Esto podría ser aumentado con supportsFoo() y supportsBar() métodos, definidos para devolver verdadero si el método correspondiente do funciona.

Otra estrategia es factorizar los métodos doFoo y doBar en los métodos FooFrobnicator y BarFrobnicator, respectivamente. Estos métodos devolverían null si la operación no es compatible. Para mantener el código de cliente de tener que hacer instanceof cheques, defino una interfaz de Frobnicator como sigue:

interface Frobnicator { 
    /* Get a foo frobnicator, returning null if not possible */ 
    FooFrobnicator getFooFrobnicator(); 
    /* Get a bar frobnicator, returning null if not possible */ 
    BarFrobnicator getBarFrobnicator(); 
} 

interface FooFrobnicator { 
    int doFoo(double v); 
} 

interface BarFrobnicator { 
    int doBar(); 
} 

Alternativamente, FooFrobnicator y BarFrobnicator podría ampliar Frobnicator, y los métodos get* posiblemente ser renombrado as*.

Un problema con esto es nombrar: el Frobnicator realmente no es un frobnicator, es una forma de obtener frobnicators (a menos que use el as* nombrando). También es un poco difícil de manejar. La nomenclatura puede ser más complicada, ya que el Frobnicator se recuperará de un servicio FrobnicatorEngine.

¿Alguien tiene alguna idea de una solución buena, preferiblemente bien aceptada para este problema? ¿Hay un patrón de diseño apropiado? El visitante no es apropiado en este caso, ya que el código del cliente necesita un tipo particular de interfaz (y preferiblemente debe fallar rápido si no puede obtenerlo), en lugar de despachar el tipo de objeto que obtuvo. Si se soportan o no características diferentes puede variar en una variedad de cosas: la implementación de Frobnicator, la configuración en tiempo de ejecución de esa implementación (por ejemplo, admite doFoo solo si hay algún servicio de sistema disponible para habilitar Foo), etc.

Actualización: La configuración en tiempo de ejecución es la otra llave inglesa en este negocio. Es posible transportar los tipos FooFrobnicator y BarFrobnicator para evitar el problema, especialmente si uso más los Guice-modules-as-configuration, pero introduce complejidad en otras interfaces circundantes (como la fábrica/constructor que produce Frobnicators). en primer lugar). Básicamente, la implementación de la fábrica que produce frobnicators se configura en tiempo de ejecución (ya sea a través de propiedades o un módulo de Guice), y quiero que sea bastante fácil para el usuario decir "conectar este proveedor de frobnicator con este cliente" . Admito que es un problema con posibles problemas de diseño inherentes, y que también podría estar pensando demasiado en algunos de los problemas de generalización, pero prefiero una combinación de lo menos feo y lo menos asombroso.

+5

De hecho, existe una tercera opción: tener 'Frobnicator extiende FooFrobnicator, BarFrobnicator'. Un componente que es solo un 'FooFrobnicator' solo admitirá' doFoo() ', mientras que' BarFrobnicator' solo admitirá 'doBar()'. ¿Cuál es el problema con este? – Riduidel

+0

@Riduidel luego una clase que proporciona 'Frobnicator' pero solo admite' doFoo() 'también debe implementar' BarFrobnicator', reduciendo el problema a la primera interfaz que presenté. He pensado algo sobre cómo llevar los tipos para permitir que las interfaces específicas se usen directamente, pero hace que la configuración en tiempo de ejecución sea una pesadilla. –

Respuesta

3

Creo que tienes un problema si usted está diciendo que la clase implementa la interfaz FooImpl Foo, pero que en realidad no implementar todas Foo. Se supone que la interfaz es un contrato que indica qué métodos implementa la clase implementadora (como mínimo).

Si algo llama a FooFactory para obtener un objeto Foo, el objeto debería ser capaz de hacer lo que hace Foos.

Entonces, ¿a dónde vas desde allí? Sugeriría que tu idea de composición sea buena, usando la herencia un poco menos.

La composición funcionaría exactamente como la tiene con los métodos getFooFrobnicator() y getBarFrobnicator(). Si no le gusta que su Frobnicator en realidad no lo haga, llámelo FrobnicatorHolder.

Herencia tendría su Frobnicator conteniendo solo los métodos que tienen todos los Frobnicators, y las subinterfaces tendrían los métodos adicionales.

public interface Frobnicator{ 
    public void frobnicate(); 
} 

public interface FooFrobnicator{ 
    public void doFoo(); 
} 

public interface BarFrobnicator{ 
    public void doBar(); 
} 

A continuación, podría utilizar if (frobnicator instanceof FooFrobnicator) { ((FooFrobnicator) frobnicator).doFoo() } y la vida sigue.

Favorecer la composición por herencia. Estarás más feliz.

+0

Hasta ahora, la composición está ganando a menos que encuentre una buena manera de impulsar las interfaces reales a través del sistema. –

+0

En última instancia, opté por el enfoque de composición, con un método 'FrobnicatorEngine' con' getFooFrobnicator' y 'getBarFrobnicator' (cada uno regresa nulo si no es compatible con ese motor en particular). Tienes razón, intentar heredar la bocina donde la composición es más apropiada es un dolor innecesario e inapropiado. La API basada en composición es mucho más limpia. Todavía puedo tratar de encontrar una forma de impulsar a los tipos reales, por lo que las cosas hacen que los FooFrobnicators, etc. califiquen más que los motores, pero ese es el trabajo futuro. –

1

¿Qué le parece simplemente agregar métodos vacíos de no implementación en lugar de arrojar UnsupportedOperationExceptions? De esta forma, llamar a un método no compatible no tendrá efectos secundarios y no causará un error en tiempo de ejecución.

Si desea poder saber si un determinado objeto admite un método o no, le sugiero que divida su interfaz en dos con una interfaz super común, y luego escriba verificar el objeto que reciba para determinar qué métodos son compatibles. Esto es probablemente más limpio y más aconsejable que mi primera sugerencia.

interface Frobnicator {} 

interface FooFrobnicator extends Frobnicator { 
    void doFoo(); 
} 

interface BarFrobnicator extends Frobnicator { 
    void doBar(); 
} 

Editar:

Otra forma de hacerlo sería añadir un tipo de retorno booleano a sus métodos, y asegúrese de que sólo se devuelven false si no se admite el método. Por lo tanto:

interface Frobnicator 
{ 
    boolean doFoo(); 
    boolean doBar(); 
} 

class FooFrobnicator implements Frobnicator 
{ 
    public boolean doFoo() { code, code, code; return true; } 
    public boolean doBar() { return false; } 
} 
+0

Los métodos son un poco más complejos; no-ops no funcionará. He actualizado la pregunta para reflejar esto. Además, si el objeto no admite 'doFoo', entonces el cliente puede evitar hacer trabajo para prepararse para un' doFoo' que no funcionará. También he pensado en la solución de interfaz; el segundo método mejorado que describo en la pregunta es esencialmente un refinamiento de esto (para evitar la necesidad de que el código del cliente haga verificaciones y conversiones de 'instancia de'). –

+0

Básicamente idéntico a mi respuesta, así que lo he eliminado. +1 –

+0

@Michael E - se evita la necesidad de que el cliente haga 'instanceof', pero aún tienen que verificar las interfaces devueltas para' null'. ¿Por qué inventar una forma más detallada y personalizada de hacer algo que el lenguaje ya admite? –

4

Veo algunas maneras de manejar esto. Una, que parece ser la táctica general tomada en la API de Java, es simplemente tener la interfaz como se muestra arriba y tener métodos no compatibles aumentar UnsupportedOperationException. Sin embargo, esto tiene la desventaja de no ser a prueba de fallas: el código del cliente no puede decir si doFoo funcionará hasta que intente llamar a doFoo.

Como dijiste, la táctica general es utilizar el patrón de diseño template method. Un excelente ejemplo es el HttpServlet.

Así es como podría lograr lo mismo.

public interface Frobnicator { 
    int doFoo(double v); 
    int doBar(); 
} 

public abstract class BaseFrobnicator implements Frobnicator { 
    public int doFoo(double v) { 
     throw new UnsupportedOperationException(); 
    } 
    public int doBar() { 
     throw new UnsupportedOperationException(); 
    } 
} 

/** 
* This concrete frobnicator only supports the {@link #doBar()} method. 
*/ 
public class ConcreteFrobnicator extends BaseFrobnicator { 
    public int doBar() { 
     return 42; 
    } 
} 

El cliente acaba de leer los documentos y manejar la UnsupportedOperationException en consecuencia. Como es un RuntimeException, es un caso perfecto para un "error de programador". Es cierto que no es a prueba de fallas (es decir, no es tiempo de compilación), pero es por eso que se le paga como desarrollador. Solo prevenlo o atrapa y maneja.

+0

¡¿Le pagan ?! – willcodejavaforfood

+0

Fail-fast no es lo mismo que comprobado en tiempo de compilación. Fail-fast permite que el código del cliente pregunte con anticipación "¿funcionará esto?" por lo tanto, no se molesta en intentarlo (lo que puede implicar costosas configuraciones) si no funciona. Además, 'UnsupportedOperationException' es una violación del espíritu de sustituibilidad de Liskov (de alguna manera, toda mi configuración en la pregunta también lo es, pero estoy buscando la manera menos espantosa de resolverlo). –

+0

En cuanto a la falla rápida, no quise dar a entender que eso significa exactamente eso. Solo me refería al patrón de método de la plantilla en general. En cuanto a Liskov, eso se refiere en este caso al requisito funcional, no a los posibles enfoques. Por cierto, el 'HttpServlet' ofrece un método' doOptions() 'que comprueba los métodos soportados por reflexión. Es posible que desee considerar lo mismo. Una alternativa completamente diferente es rediseñar todo para tener un solo método que tenga otra interfaz como argumento para que el cliente pueda suministrar el algoritmo por sí mismo, eche un vistazo al patrón de estrategia. – BalusC

2

La solución más obvia es simplemente definir dos interfaces, FooFrobnicator y BarFrobnicator. Esto debería estar bien ya que la clase puede implementar múltiples interfaces.

A continuación, podría proporcionar una interfaz super (marcador) si es necesario.

+1

+1 Si está diciendo eso, para satisfacer la interfaz de Frobnicator, un objeto debería hacer foo o bar, o tal vez ambas cosas: esa es una mala interfaz. Eso es un mal diseño. Tienes Fooers and Barrers, y algunos de tus objetos son ambos, deja que el diseño de la clase lo refleje. –

0

La solución más sencilla es añadir supports*() métodos,

Si usted piensa que no es un buen estilo, y luego olvidarse de Frobnicator (que le da muy poco, ya que no es una interfaz que puede confiar) y el código directamente contra FooFrobnicator y BarFrobnicator, y si resultan ser implementados por el mismo objeto, bien. Esto también le da la posibilidad de exigir una interfaz específica en el código de cliente:

interface FooFrobnicator { 
    doFoo(); 
} 

interface BarFrobnicator { 
    doBar(); 
} 

public class Client { 
    ... 
    FooFrobnicator fooFrobnicator; 
    public void setFooFrobnicator(FooFrobnicator fooFrobnicator) { 
     this.fooFrobnicator = fooFrobnicator; 
    } 

    BarFrobnicator barFrobnicator; 
    public void setBarFrobnicator(BarFrobnicator barFrobnicator) { 
     this.barFrobnicator = barFrobnicator; 
    } 
    ... 
    public void doSomething() { 
     ... 
     if (fooFrobnicator != null) { ... } 
     ... 
     if (barFrobnicator != null) { ... } 
    } 
} 
... 
public class FrobnicatorImpl implements FooFrobnicator, BarFrobnicator { ... } 
... 
public void doSomething() { 
    ... 
    FrobnicatorImpl impl = new FrobnicatorImpl(); 
    Client client = new Client();   
    client.setFooFrobnicator(impl); 
    client.setBarFrobnicator(impl); 
    ... 
} 
-1

en mi humilde opinión, que está haciendo de esta manera demasiado complicado. Termina con la interfaz. Cree una clase base con los métodos comúnmente utilizados (ni siquiera pueden hacer nada en la clase base si no saben cómo implementarlos allí). A continuación, amplíe la clase base & para implementar/anular los métodos que necesite.

Cuestiones relacionadas