Cada vez que veo un método en el que el comportamiento activa el tipo de su parámetro, inmediatamente considero primero si ese método realmente pertenece al parámetro del método. Por ejemplo, en lugar de tener un método como:
public void sort(List values) {
if (values instanceof LinkedList) {
// do efficient linked list sort
} else { // ArrayList
// do efficient array list sort
}
}
Yo haría esto:
values.sort();
// ...
class ArrayList {
public void sort() {
// do efficient array list sort
}
}
class LinkedList {
public void sort() {
// do efficient linked list sort
}
}
Llevamos el comportamiento al lugar que sabe cuándo usarlo. Creamos una abstracción real donde no necesita conocer los tipos o los detalles de la implementación. Para su situación, podría tener más sentido mover este método desde la clase original (que llamaré O) para escribir Ay anularlo en tipo B. Si el método se llama doIten algún objeto, mover doIta Ay anulación con el diferente comportamiento en B. Si hay bits de datos desde donde doItse llama originalmente, o si el método se usa en suficientes lugares, puede dejar el método original y delegar:
class O {
int x;
int y;
public void doIt(A a) {
a.doIt(this.x, this.y);
}
}
Sin embargo, podemos sumergirnos un poco más. Veamos la sugerencia de usar un parámetro booleano y veamos qué podemos aprender sobre la forma en que piensa su compañero de trabajo. Su propuesta es hacer:
public void doIt(A a, boolean isTypeB) {
if (isTypeB) {
// do B stuff
} else {
// do A stuff
}
}
Esto se parece mucho al instanceofque usé en mi primer ejemplo, excepto que estamos externalizando esa verificación. Esto significa que tendríamos que llamarlo de dos maneras:
o.doIt(a, a instanceof B);
o:
o.doIt(a, true); //or false
En la primera forma, el punto de llamada no tiene idea de qué tipo Atiene. Por lo tanto, ¿deberíamos pasar booleanos hasta el fondo? ¿Es realmente un patrón que queremos en toda la base de código? ¿Qué sucede si hay un tercer tipo que debemos tener en cuenta? Si así es como se llama el método, deberíamos moverlo al tipo y dejar que el sistema elija la implementación para nosotros polimórficamente.
En la segunda forma, ya debemos saber el tipo de aen el punto de llamada. Por lo general, eso significa que estamos creando la instancia allí o tomando una instancia de ese tipo como parámetro. Crear un método Oque tome un Baquí funcionaría. El compilador sabría qué método elegir. Cuando estamos manejando cambios como este, la duplicación es mejor que crear la abstracción incorrecta , al menos hasta que descubramos a dónde vamos realmente. Por supuesto, sugiero que no hayamos terminado realmente sin importar lo que hayamos cambiado hasta este punto.
Necesitamos mirar más de cerca la relación entre Ay B. En general, se nos dice que debemos favorecer la composición sobre la herencia . Esto no es cierto en todos los casos, pero es cierto en un sorprendente número de casos una vez que profundizamos. BHereda de A, lo que significa que creemos que Bes un A. Bdebe usarse igual que A, excepto que funciona un poco diferente. ¿Pero cuáles son esas diferencias? ¿Podemos dar a las diferencias un nombre más concreto? ¿No Bes un A, pero realmente Atiene un Xque podría ser A'o B'? ¿Cómo sería nuestro código si hiciéramos eso?
Si nos trasladamos en el método Acomo se sugirió anteriormente, podríamos inyectar una instancia de Xdentro A, y delegar ese método para la X:
class A {
X x;
A(X x) {
this.x = x;
}
public void doIt(int x, int y) {
x.doIt(x, y);
}
}
Podemos implementar A'y B'deshacernos B. Hemos mejorado el código dando un nombre a un concepto que podría haber sido más implícito, y nos permitimos establecer ese comportamiento en tiempo de ejecución en lugar de tiempo de compilación. Aen realidad se ha vuelto menos abstracto también. En lugar de una relación de herencia extendida, está llamando a métodos en un objeto delegado. Ese objeto es abstracto, pero está más enfocado solo en las diferencias en la implementación.
Sin embargo, hay una última cosa para mirar. Volvamos a la propuesta de su compañero de trabajo. Si en todos los sitios de llamadas conocemos explícitamente el tipo Aque tenemos, entonces deberíamos hacer llamadas como:
B b = new B();
o.doIt(b, true);
Asumimos anteriormente al componer que Atiene un Xque es A'o B'. Pero tal vez incluso esta suposición no es correcta. ¿Es este el único lugar donde esta diferencia entre Ay Bimporta? Si es así, entonces quizás podamos adoptar un enfoque ligeramente diferente. Todavía tenemos uno Xque es A'o B', pero no pertenece A. Solo O.doItle importa, así que solo pasémoslo a O.doIt:
class O {
int x;
int y;
public void doIt(A a, X x) {
x.doIt(a, x, y);
}
}
Ahora nuestro sitio de llamadas se ve así:
A a = new A();
o.doIt(a, new B'());
Una vez más, Bdesaparece, y la abstracción se mueve hacia lo más enfocado X. Esta vez, sin embargo, Aes aún más simple al saber menos. Es aún menos abstracto.
Es importante reducir la duplicación en una base de código, pero debemos considerar por qué la duplicación ocurre en primer lugar. La duplicación puede ser un signo de abstracciones más profundas que están tratando de salir.