¿Implementar una interfaz definida en un subpaquete es un antipatrón?

9

Digamos que tengo lo siguiente:

package me.my.pkg;
public interface Something {
    /* ... couple of methods go here ... */
}

y:

package me.my;
import me.my.pkg.Something;
public class SomeClass implements Something {
    /* ... implementation of Something goes here ... */
    /* ... some more method implementations go here too ... */
}

Es decir, la clase que implementa una interfaz vive más cerca de la raíz de la jerarquía de paquetes que la interfaz que implementa, pero ambas pertenecen a la misma jerarquía de paquetes.

La razón de esto en el caso particular que tengo en mente es que hay un paquete previamente existente que agrupa la funcionalidad a la que Somethingpertenece lógicamente la interfaz y la lógica (como en "la que esperarías" y "la única donde debe ir dada la clase de implementación de arquitectura actual) y existe un nivel "arriba" desde la ubicación lógica de la interfaz. La clase implementadora no pertenece lógicamente a ningún lugar debajo de me.my.pkg.

En mi caso particular, la clase en cuestión implementa varias interfaces, pero parece que no hace ninguna diferencia (o al menos no significativa) aquí.

No puedo decidir si este es un patrón aceptable o no. ¿Es o no es y por qué?

un CVn
fuente

Respuestas:

6

Sí, es aceptable, siempre y cuando la clase y la interfaz estén definitivamente en los paquetes correctos.

Organizar clases en un conjunto jerárquico de paquetes es un poco como organizar clases en una jerarquía de herencia. Funciona bastante bien la mayor parte del tiempo, pero hay muchos casos legítimos que simplemente no encajan.

Tenga en cuenta que Java ni siquiera tiene una jerarquía de paquetes : la naturaleza jerárquica no se extiende más allá de la estructura de carpetas. No puedo hablar por los diseñadores del lenguaje Java, ¡pero dudo que hayan tomado esta decisión por accidente!

A veces me parece útil pensar en la 'jerarquía de paquetes' como un ayudante para ayudar a los programadores a encontrar el paquete que están buscando.

vaughandroid
fuente
3

Aunque formalmente legítimo, esto puede indicar un olor a código . Para saber si esto es así en su caso, hágase tres preguntas:

  1. ¿Lo vas a necesitar?
  2. ¿Por qué lo empacaste de esa manera?
  3. ¿Cómo saber si su paquete API es conveniente de usar?

¿Lo vas a necesitar?

Si no ve la razón para invertir un esfuerzo adicional en el mantenimiento del código , considere dejarlo como está. Este enfoque se basa en el principio YAGNI

el programador no debe agregar funcionalidad hasta que se considere necesario ... "Implemente siempre las cosas cuando realmente las necesite, nunca cuando prevea que las necesita".

Las consideraciones que se proporcionan a continuación suponen que el esfuerzo de inversión en análisis posteriores está justificado, digamos que espera que el código se mantenga a largo plazo, o que esté interesado en practicar y perfeccionar habilidades para escribir código que se pueda mantener. Si esto no aplica, no dude en omitir el resto, porque no lo va a necesitar .

¿Por qué lo empacaste de esa manera?

Lo primero que vale la pena señalar es que ninguna de las convenciones oficiales de codificación y el tutorial brindan orientación al respecto, enviando una fuerte señal de que se espera que este tipo de decisiones sean a discreción de un programador.

Pero si observa el diseño implementado por los desarrolladores de JDK , notará que la API de subpaquetes "con fugas" en los de nivel superior no se practica allí. Por ejemplo, mire el paquete de utilidad concurrente y sus subpaquetes: bloqueos y atómico .

  • Vale la pena señalar que el uso de subpaquetes API en el interior se considera correcto, por ejemplo, la implementación de ConcurrentHashMap importa bloqueos y usa ReentrantLock. Simplemente no expone los bloqueos API como públicos.

De acuerdo, desarrolladores del núcleo de la API parecen evitar eso, y para averiguar por qué es tan pregúntese, ¿por qué te empaquetar esa manera? La razón natural para eso sería el deseo de comunicar a los usuarios de paquetes algún tipo de jerarquía, tipo de capas , de modo que el subpaquete corresponda a conceptos de uso de menor nivel / más especializados / más estrechos.

Esto a menudo se hace para hacer que la API sea más fácil de usar y comprender, para ayudar a los usuarios de API a operar dentro de una capa en particular, evitando molestarlos con preocupaciones que pertenecen a otras capas. Si este es el caso, exponer la API de nivel inferior de los subpaquetes seguramente iría en contra de su intención.

  • Nota al margen, si no puede decir por qué lo empaqueta de esta manera, considere volver a su diseño y analizarlo a fondo hasta que lo entienda. Piénselo, si es difícil de explicarle a usted, incluso ahora, ¿qué tan difícil sería para aquellos que mantendrán y usarán su paquete en el futuro?

¿Cómo saber si su paquete API es conveniente de usar?

Para eso, recomiendo escribir pruebas que ejerciten la API proporcionada por su paquete. Pedirle a alguien que revise tampoco estaría de más, pero el revisor de API experimentado probablemente le pedirá que proporcione tales pruebas de todos modos. La cuestión es que es difícil superar ver un ejemplo de uso real al revisar API.

  • Uno puede mirar la clase con un método único que tiene un parámetro único y soñar wow lo simple , pero si la prueba para invocarlo requiere la creación de otros 10 objetos, invocando como 20 métodos con 50 parámetros en total, esto rompe una ilusión peligrosa y revela la verdadera complejidad de el diseño.

Otra ventaja de tener pruebas es que estas pueden simplificar enormemente la evaluación de varias ideas que considera para mejorar su diseño.

A veces me ocurrió que cambios de implementación relativamente menores desencadenaron simplificaciones importantes en las pruebas, reduciendo la cantidad de importaciones, objetos, invocaciones de métodos y parámetros. Incluso antes de ejecutar las pruebas, esto sirve como una buena indicación de la forma en que vale la pena seguir adelante.

Lo opuesto también me sucedió a mí, es decir, cuando grandes cambios ambiciosos en mis implementaciones destinadas a simplificar la API se probaron incorrectos por un impacto menor e insignificante en las pruebas, lo que me ayudó a descartar las ideas infructuosas y dejar el código como está (tal vez solo con un comentario agregado para ayudar a comprender los antecedentes del diseño y ayudar a otros a aprender sobre el camino equivocado).


Para su caso concreto, lo primero que viene a la mente es considerar cambiar la herencia a la composición , es decir, en lugar de SomeClassimplementar directamente Something, incluir un objeto que implemente esa interfaz dentro de la clase,

package me.my;
import me.my.pkg.Something;
public class SomeClass {
    private Something something = new Something() {
        /* ... implementation of Something goes here ... */
    }
    /* ... some more method implementations go here too ... */
}

Este enfoque puede ser rentable en el futuro, evitando el riesgo de que una subclase SomeClassanule accidentalmente un método de Something, haciendo que se comporte de una manera que no planificó.

mosquito
fuente