Domar las clases de 'funciones de utilidad'

15

En nuestra base de código Java sigo viendo el siguiente patrón:

/**
 This is a stateless utility class
 that groups useful foo-related operations, often with side effects.
*/
public class FooUtil {
  public int foo(...) {...}
  public void bar(...) {...}
}

/**
 This class does applied foo-related things.
*/
class FooSomething {
  int DoBusinessWithFoo(FooUtil fooUtil, ...) {
    if (fooUtil.foo(...)) fooUtil.bar(...);
  }
}

Lo que me molesta es que tengo que pasar una instancia de FooUtiltodas partes, porque las pruebas.

  • No puedo hacer que FooUtillos métodos sean estáticos, porque no podré burlarme de ellos para probar ambos FooUtily sus clases de cliente.
  • No puedo crear una instancia FooUtilen el lugar de consumo con a new, de nuevo porque no podré burlarme de ella para probarla.

Supongo que mi mejor opción es usar inyección (y lo hago), pero agrega su propio conjunto de problemas. Además, pasar varias instancias de utilidades infla el tamaño de las listas de parámetros del método.

¿Hay alguna manera de manejar esto mejor que no estoy viendo?

Actualización: dado que las clases de utilidad no tienen estado, probablemente podría agregar un INSTANCEmiembro singleton estático , o un getInstance()método estático , al tiempo que conserva la capacidad de actualizar el campo estático subyacente de la clase para la prueba. Tampoco parece súper limpio.

9000
fuente
44
Y su suposición de que es necesario burlarse de todas las dependencias para realizar pruebas unitarias de manera efectiva no es correcta (aunque todavía estoy buscando la pregunta que discute eso).
Telastyn
44
¿Por qué estos métodos no son miembros de la clase cuyos datos manipulan?
Jules
3
Tenga un conjunto separado de Junit que llame a las fuentes estáticas si es FooUtility. Entonces puedes usarlo sin burlarte. Si es algo de lo que necesita burlarse, sospecho que no es solo una utilidad, sino que está haciendo algo de lógica de negocio o IO y, de hecho, debería ser una referencia de miembro de clase e inicializada a través del método constructor, init o setter.
tgkprog
2
+1 para el comentario de Jules. Las clases de util son un código de olor. Dependiendo de la semántica, debería haber una mejor solución. Tal vez los FooUtilmétodos deberían ser incorporados FooSomething, tal vez hay propiedades FooSomethingque deberían ser cambiadas / extendidas para que los FooUtilmétodos ya no sean necesarios. Danos un ejemplo más concreto de qué FooSomethingnos puede ayudar a dar una buena respuesta a estas preguntas.
valenterry
13
@valenterry: la única razón por la que las clases de utilidades son un olor a código es porque Java no proporciona una buena manera de tener funciones independientes. Hay muy buenas razones para tener funciones que no son específicas de las clases.
Robert Harvey

Respuestas:

16

En primer lugar, permítanme decir que existen diferentes enfoques de programación para diferentes problemas. Para mi trabajo, prefiero un diseño OO sólido para la organización y el mantenimiento, y mi respuesta lo refleja. Un enfoque funcional tendría una respuesta muy diferente.

Tiendo a encontrar que la mayoría de las clases de utilidad se crean porque el objeto en el que deberían estar los métodos no existe. Esto casi siempre proviene de uno de dos casos, ya sea que alguien está pasando colecciones o alguien está pasando frijoles (a menudo se los llama POJO, pero creo que un montón de setters y getters se describe mejor como un bean).

Cuando tienes una colección que estás pasando, debe haber un código que quiera ser parte de esa colección, y esto termina siendo esparcido por todo tu sistema y eventualmente recopilado en clases de utilidad.

Mi sugerencia es envolver sus colecciones (o beans) con cualquier otro dato estrechamente asociado en nuevas clases y poner el código de "utilidad" en objetos reales.

Puede extender la colección y agregar los métodos allí, pero pierde el control del estado de su objeto de esa manera. Le sugiero que lo encapsule completamente y solo exponga los métodos de lógica de negocios que sean necesarios (piense "No pregunte a sus objetos por sus datos, dar comandos a sus objetos y dejar que actúen sobre sus datos privados ", un inquilino importante de OO y uno que, cuando lo piensa, requiere el enfoque que sugiero aquí).

Este "ajuste" es útil para cualquier objeto de biblioteca de propósito general que contenga datos pero a los que no se les puede agregar código: poner código con los datos que manipula es otro concepto importante en el diseño OO.

Hay muchos estilos de codificación donde este concepto de envoltura sería una mala idea: ¿quién quiere escribir una clase completa cuando solo implementa un script o prueba de una sola vez? Pero creo que la encapsulación de cualquier tipo / colección básica que no pueda agregar código es obligatoria para OO.

Bill K
fuente
Esta respuesta es tan asombrosa. Tuve este problema (obviamente), leí esta respuesta con cierta duda, volví y miré mi código y pregunté "qué objeto falta que debería tener estos métodos". He aquí, se encontró una solución elegante y se rellenó el modelo de objeto.
GreenAsJade
@Deduplicator En 40 años de programación, rara vez (¿nunca?) He visto el caso en el que estaba bloqueado por demasiados niveles de indirección (aparte de la pelea ocasional con mi IDE para saltar a una implementación en lugar de una interfaz) pero yo ' A menudo (muy a menudo) hemos visto el caso en que las personas escribieron códigos horribles en lugar de crear una clase claramente necesaria. Aunque creo que demasiada indirección podría haber mordido a algunas personas, me equivocaría al lado de los principios apropiados de OO, como que cada clase haga bien una cosa, contención adecuada, etc.
Bill K
@BillK En general, una buena idea. Pero sin una escotilla de escape, la encapsulación realmente puede interferir. Y existe una cierta belleza en los algoritmos gratuitos que puede combinar con cualquier tipo que desee, aunque es cierto que en lenguajes estrictos de OO cualquier cosa menos OO es bastante incómodo.
Deduplicador
@Deduplicator Al escribir funciones de 'utilidad' que deben escribirse fuera de los requisitos comerciales específicos, está en lo correcto. Las clases de utilidad llenas de funciones (como colecciones) son simplemente incómodas en OO porque no están asociadas con datos. estos son los "Escape Hatch" que usan las personas que no se sienten cómodas con OO (también, los proveedores de herramientas deben usarlos para una funcionalidad muy genérica). Mi sugerencia anterior fue envolver estos hacks en clases cuando está trabajando en lógica de negocios para que pueda volver a asociar su código con sus datos.
Bill K
1

Puede usar un patrón de proveedor e inyectarle FooSomethingun FooUtilProviderobjeto (podría estar en un constructor; solo una vez).

FooUtilProvidersólo tendría un método: FooUtils get();. La clase luego usaría cualquier FooUtilsinstancia que proporcione.

Ahora que está a un paso de usar un marco de Inyección de dependencias, cuando solo tendrías que conectar dos veces el codificador DI: para el código de producción y para tu conjunto de pruebas. Simplemente vincula la FooUtilsinterfaz a cualquiera RealFooUtilso MockedFooUtils, y el resto sucede de forma automática. Cada objeto que depende FooUtilsProviderobtiene la versión adecuada de FooUtils.

Konrad Morawski
fuente