Method Objects

Publicado 31-05-2019

Debemos admitir que los métodos cortos y simples tienen su cuota de belleza… pero ¿qué sucede cuando la lógica que requieren es inherentemente extensa? Existe una técnica de refactor que permite extraer comportamientos complejos en un objeto aparte, y delegar la lógica para poder realizar cambios que de otro modo, serían muy engorrosos.

El código fuente del video (separado en diferentes commits según el incremento) está disponible en Github para su consulta.

Tenemos el siguiente extracto de código:

class Invoice
    # more code that doesn't matter right now
    def barcode
        cuit = owner.cuit
        type = invoice_type.to_s.rjust(2, '0')
        sales_point = owner.sales_point.to_s.rjust(4, '0')
        due_date = cae_due_date.strfdate("%Y%M%d")

        number_string = "#{cuit}#{type}#{sales_point}#{cae}#{due_date}"

        odd_sum = number_string.chars.select.with_index { |_, i|
            i.odd?
        }.inject(0) { |sum, x|
            sum + x.to_i
        }
        odd_sum *= 3

        even_sum = number_string.chars.select.with_index { |_, i|
            i.even?
        }.inject(0) { |sum, x|
            sum + x.to_i
        }

        sum = odd_sum + even_sum
        verification_digit = 10 - sum % 10

        "#{number_string}#{verification_digit}"
    end
end

El código que les presenté es el encargado de calcular el número con el cual se procesará el código de barras en los comprobantes electrónicos en la República Argentina. Hay algunos conceptos que no vienen al caso en este momento, y probablemente este algoritmo pueda mejorarse. Sin embargo puede verse que el tipo de algoritmo que está plasmado aquí poco tiene que ver con el concepto de Factura.

Luego de aplicar este patrón, obtenemos:

class Invoice
    # more code that doesn't matter right now
    def barcode
        BarcodeCalculator.new(self).call
    end
end

class BarcodeCalculator
    def initialize(invoice)
        @invoice = invoice
    end

    def call
        cuit = @invoice.owner.cuit
        type = @invoice.invoice_type.to_s.rjust(2, '0')
        sales_point = @invoice.owner.sales_point.to_s.rjust(4, '0')
        cae = @invoice.cae
        due_date = @invoice.cae_due_date.strfdate("%Y%M%d")

        number_string = "#{cuit}#{type}#{sales_point}#{cae}#{due_date}"

        odd_sum = number_string.chars.select.with_index { |_, i|
            i.odd?
        }.inject(0) { |sum, x|
            sum + x.to_i
        }
        odd_sum *= 3

        even_sum = number_string.chars.select.with_index { |_, i|
            i.even?
        }.inject(0) { |sum, x|
            sum + x.to_i
        }

        sum = odd_sum + even_sum
        verification_digit = (10 - sum % 10) % 10

        "#{number_string}#{verification_digit}"
    end
end

El código del Invoice está mucho más limpio, y el algoritmo (que poco tenía que ver con el comprobante) se ha ido a un nuevo Method Object llamado BarcodeCalculator por el momento. Es nuestra oportunidad para mejorarlo con técnicas que iremos viendo en sucesivas publicaciones.

Nota: Hemos agregado parámetros a la inicialización de la calculadora, conforme se sugiere. Esto es, agregar el objeto anfitrión para poder acceder a los atributos necesarios, y todo parámetro. En este caso no había parámetros, por lo que no es necesario más que el objeto anfitrión.

Características

Este patrón surge de la necesidad puntual de mejorar la escritura de un método que se volvió complejo y probablemente extenso. Llegado dicho momento, nos encontramos ante la imposibilidad de hacerlo dentro de la clase en la que se encuentra, y por ello preferimos extraer el comportamiento hacia una nueva clase, para allí poder mejorar el código sin tener conflictos con la clase anfitriona anterior.

Consideraciones

Es necesario ser cuidadoso al momento de implementar este patrón. La fórmula exitosa, según Kent Beck y adaptada por mí, es la siguiente:

  1. Crear una clase nueva, y nombrarla según el método
  2. Darle a esta clase un campo inmutable que refiera al objeto anfitrión
  3. Darle un constructor que tome al objeto anfitrión y a cada parámetro
  4. Agregar un método compute, call, do o similar, que realice la acción. Conviene ser consistente.
  5. Copiar el código del método original en este nuevo método.
  6. Compilar (si fuera necesario)
  7. Reemplazar el método original por una llamada a este nuevo método, luego de la necesaria inicialización del Method Object

Notemos que son varios pasos, pero cada uno de ellos es simple. En el video que acompaña esta nota podemos ver una ejecución paso a paso de esta aplicación del patrón.

Ventajas

La principal ventaja de este patrón es que nos permite simplificar clases, ya que removemos lógica conforme creamos nuevos Method Objects, y la colocamos en su propia clase, cuyo único fin es el de realizar un algoritmo. Esto cumple y conviene bajo los preceptos del Principio de Responsabilidad Única.

Por otro lado, nos beneficia aislando contextos que pueden ser ligeramente disjuntos. Si bien es cierto que probablemente la lógica de cierta clase sea mayormente cohesiva, es probable que si un algoritmo requere de muchos pasos, variables temporales e información contextual, es mejor separar esas operaciones del contexto principal. Este aislamiento generará una ventaja al momento de no contaminar nuestro contexto general (la clase) con variables y posiblemente atributos sólamente necesarios para un contexto particular (método que extraemos).

Finalmente, cuando aislamos código dentro de su propia clase, se nos brinda la posibilidad de reinventar conceptos, nombrar atributos, variables y parámetros, generar métodos auxiliares, entre otras cosas. Es por ello, que este nuevo contexto nos permite utilizar metáforas apropiadas al algoritmos que estamos implementando, más allá de las preexistentes en el objeto que lo poseía anteriormente.

Desventajas

La primera desventaja que vemos a simple vista es que crea más clases en nuestro dominio. Esto no siempre es un problema, ya que si cada clase paga el costo de su existencia, el sistema se vuelve balanceado y mantenible. Sin embargo, muchas veces no sabemos equilibrar la cantidad y la complejidad de interacciones termina funcionando de lastre digital.

Derivada de la primera desventaja, encontramos la explosión de clases ocasionada por el uso excesivo de este patrón. No todo método requiere de un Method Object, ni toda lógica es suficientemente compleja como para llevarla a su propia clase. Mucho menos para despachar más mensajes sólo por resumir cinco líneas de código en una sola. Todo es cuestión de medida, parafraseando a Alberto Cortez.

Siempre existe el peligro latente de utilizar este patrón para mover un fragmento de código sucio, a un contexto más distante para ignorarlo permanentemente. De ese modo, se da una sensación de simplicidad y brevedad, aunque en el fondo todo lo que tenemos es una reubicación de código, sin el adecuado refactor. Vamos, me refiero a “esconder mugre bajo la alformbra”. Tarde o temprano, ese código va a tener que ser revisado, y no habremos ganado nada con la indirección ocasionada. Es más, puede que lo hayamos ensuciado aún más.

Lectura profunda