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.
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
@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. –