¿Estamos abusando de los métodos estáticos?

13

Hace un par de meses comencé a trabajar en un nuevo proyecto, y cuando revisé el código me sorprendió la cantidad de métodos estáticos utilizados. No solo se utilizan métodos de utilidad collectionToCsvString(Collection<E> elements), sino también mucha lógica de negocios.

Cuando le pregunté al tipo responsable de la razón detrás de esto, dijo que era una forma de escapar de la tiranía de Spring . Va en torno a este proceso de pensamiento: para implementar un método de creación de recibo del cliente, podríamos tener un servicio

@Service
public class CustomerReceiptCreationService {

    public CustomerReceipt createReceipt(Object... args) {
        CustomerReceipt receipt = new CustomerReceipt();
        // creation logic
        return receipt;
    }
}

Ahora, el tipo dijo que no le gusta que Spring administre clases innecesariamente, básicamente porque impone la restricción de que las clases de clientes deben ser Spring beans. Terminamos teniendo todo administrado por Spring, lo que nos obliga a trabajar con objetos sin estado de una manera procesal. Más o menos lo que se indica aquí https://www.javacodegeeks.com/2011/02/domain-driven-design-spring-aspectj.html

Entonces, en lugar del código anterior, tiene

public class CustomerReceiptCreator {

    public static CustomerReceipt createReceipt(Object... args) {
        CustomerReceipt receipt = new CustomerReceipt();
        // creation logic
        return receipt;
    }
}

Podría argumentar hasta el punto de evitar que Spring administre nuestras clases cuando sea posible, pero lo que no veo es el beneficio de tener todo estático. Estos métodos estáticos también son apátridas, por lo que tampoco son muy OO. Me sentiría más cómodo con algo como

new CustomerReceiptCreator().createReceipt()

Afirma que los métodos estáticos tienen algunos beneficios adicionales. A saber:

  • Más fácil de leer. Importe el método estático y solo debemos preocuparnos por la acción, sin importar qué clase lo esté haciendo.
  • Obviamente, es un método libre de llamadas a DB, por lo que es económico en términos de rendimiento; y es bueno dejarlo claro, para que el cliente potencial necesite ingresar al código y verificarlo.
  • Más fácil de escribir pruebas.

Pero siento que hay algo que no está del todo bien con esto, por lo que me gustaría escuchar algunos pensamientos más experimentados de los desarrolladores sobre esto.

Entonces mi pregunta es, ¿cuáles son las posibles trampas de esta forma de programación?

usuario3748908
fuente
44
El staticmétodo que está ilustrando arriba es solo un método de fábrica común. Hacer que los métodos de fábrica sean estáticos es la convención generalmente aceptada, por varias razones convincentes. Si el método de fábrica es apropiado aquí es una cuestión diferente.
Robert Harvey

Respuestas:

23

¿Cuál es la diferencia entre new CustomerReceiptCreator().createReceipt()y CustomerReceiptCreator.createReceipt()? Casi ninguno. La única diferencia significativa es que el primer caso tiene una sintaxis considerablemente más incómoda. Si sigue lo primero en la creencia de que de alguna manera evitar los métodos estáticos hace que su código sea mejor OO, está gravemente equivocado. Crear un objeto solo para llamar a un método único en él es un método estático por sintaxis obtusa.

Donde las cosas se ponen diferentes es cuando se inyecta en CustomerReceiptCreatorlugar de inyectarlo new. Consideremos un ejemplo:

class OrderProcessor {
    @Inject CustomerReceiptCreator customerReceiptCreator;

    void processOrder(Order order) {
        ...
        CustomerReceipt receipt = customerReceiptCreator.createReceipt(order);
        ...
    }
}

Comparemos esto con una versión de método estático:

void processOrder(Order order) {
    ...
    CustomerReceipt receipt = CustomerReceiptCreator.createReceipt(order);
    ...
}

La ventaja de la versión estática es que puedo decirte fácilmente cómo interactúa con el resto del sistema. No lo hace. Si no he usado variables globales, sé que el resto del sistema no ha cambiado de alguna manera. Sé que ninguna otra parte del sistema podría estar afectando el recibo aquí. Si he usado objetos inmutables, sé que el orden no ha cambiado, y createReceipt es una función pura. En ese caso, puedo mover / eliminar / etc. libremente esta llamada sin preocuparme por los efectos aleatorios impredecibles en otros lugares.

No puedo hacer las mismas garantías si me he inyectado el CustomerReceiptCreator. Es posible que tenga un estado interno modificado por la llamada, podría estar afectado o cambiar otro estado. Puede haber relaciones impredecibles entre declaraciones en mi función, de modo que cambiar el orden introducirá errores sorprendentes.

Por otro lado, ¿qué sucede si de CustomerReceiptCreatorrepente necesita una nueva dependencia? Digamos que necesita verificar un indicador de función. Si estuviéramos inyectando, podríamos hacer algo como:

public class CustomerReceiptCreator {
    @Injected FeatureFlags featureFlags;

    public CustomerReceipt createReceipt(Order order) {
        CustomerReceipt receipt = new CustomerReceipt();
        // creation logic
        if (featureFlags.isFlagSet(Flags::FOOBAR)) {
           ...
        }
        return receipt;
    }
}

Entonces hemos terminado, porque el código de llamada se inyectará con un CustomerReceiptCreatorque se inyectará automáticamente a FeatureFlags.

¿Y si estuviéramos usando un método estático?

public class CustomerReceiptCreator {
    public static CustomerReceipt createReceipt(Order order, FeatureFlags featureFlags) {
        CustomerReceipt receipt = new CustomerReceipt();
        // creation logic
        if (featureFlags.isFlagSet(Flags::FOOBAR)) {
           ...
        }
        return receipt;
    }
}

¡Pero espera! el código de llamada también necesita ser actualizado:

void processOrder(Order order) {
    ...
    CustomerReceipt receipt = CustomerReceiptCreator.createReceipt(order, featureFlags);
    ...
}

Por supuesto, esto todavía deja la cuestión de dónde processOrderobtiene su origen FeatureFlags. Si tenemos suerte, el camino termina aquí, si no, la necesidad de pasar por FeatureFlags se ve empujada más arriba en la pila.

Hay una compensación aquí. Métodos estáticos que requieren pasar explícitamente las dependencias, lo que resulta en más trabajo. El método inyectado reduce el trabajo, pero hace que las dependencias sean implícitas y, por lo tanto, ocultas, lo que hace que el código sea más difícil de razonar.

Winston Ewert
fuente