¿Cuáles son las formas de evitar la duplicación de lógica entre clases de dominio y consultas SQL?

21

El siguiente ejemplo es totalmente artificial y su único propósito es hacerme entender.

Supongamos que tengo una tabla SQL:

CREATE TABLE rectangles (
  width int,
  height int 
);

Clase de dominio:

public class Rectangle {
  private int width;
  private int height;

  /* My business logic */
  public int area() {
    return width * height;
  }
}

Ahora suponga que tengo el requisito de mostrarle al usuario el área total de todos los rectángulos en la base de datos. Puedo hacer eso buscando todas las filas de la tabla, convirtiéndolas en objetos e iterando sobre ellas. Pero esto parece simplemente estúpido, porque tengo muchos rectángulos en mi mesa.

Entonces hago esto:

SELECT sum(r.width * r.height)
FROM rectangles r

Esto es fácil, rápido y utiliza las fortalezas de la base de datos. Sin embargo, introduce lógica duplicada, porque también tengo el mismo cálculo en mi clase de dominio.

Por supuesto, para este ejemplo, la duplicación de la lógica no es fatal en absoluto. Sin embargo, me enfrento al mismo problema con mis otras clases de dominio, que son más complejas.

Velocidad de escape
fuente
1
Sospecho que la solución óptima variará bastante de una base de código a otra, así que ¿podría describir brevemente uno de los ejemplos más complejos que le está causando problemas?
Ixrec
2
@lxrec: Informes. Una aplicación de negocios que tiene reglas que estoy capturando en clases, y también necesito crear informes que muestren la misma información, pero condensada. Cálculos de IVA, pagos, ganancias, ese tipo de cosas.
Velocidad de escape
1
¿No se trata también de distribuir la carga entre el servidor y los clientes? Claro, lo mejor es tirar el resultado en caché del cálculo a un cliente, pero si los datos cambian con frecuencia y hay muchas solicitudes, podría ser ventajoso poder lanzar los ingredientes y la receta al cliente en lugar de cocinando la comida para ellos. Creo que no es necesariamente malo tener más de un nodo en un sistema distribuido que pueda proporcionar una cierta funcionalidad.
nulo
Creo que la mejor manera es generar dichos códigos. Te lo explicaré más tarde.
Xavier Combelle

Respuestas:

11

Como señaló lxrec, variará de una base de código a otra. Algunas aplicaciones le permitirán poner ese tipo de lógica de negocios en funciones y / o consultas SQL y le permitirán ejecutarlas en cualquier momento que necesite mostrar esos valores al usuario.

A veces puede parecer estúpido, pero es mejor codificar la corrección que el rendimiento como objetivo principal.

En su muestra, si muestra el valor del área para un usuario en un formulario web, tendría que:

1) Do a post/get to the server with the values of x and y;
2) The server would have to create a query to the DB Server to run the calculations;
3) The DB server would make the calculations and return;
4) The webserver would return the POST or GET to the user;
5) Final result shown.

Es estúpido para cosas simples como la de la muestra, pero puede ser necesario para cosas más complejas como calcular la TIR de una inversión de un cliente en un sistema bancario.

Código de corrección . Si su software es correcto, pero lento, tendrá la oportunidad de optimizar donde lo necesite (después de la creación de perfiles). Si eso significa mantener parte de la lógica empresarial en la base de datos, que así sea. Por eso tenemos técnicas de refactorización.

Si se vuelve lento o no responde, puede que tenga que hacer algunas optimizaciones, como violar el principio DRY, lo cual no es un pecado si se rodea de la prueba de unidad adecuada y la prueba de consistencia.

Machado
fuente
1
El problema con poner lógica empresarial (de procedimiento) en SQL es que es extremadamente doloroso refactorizarlo. Incluso si tiene herramientas de refactorización SQL de primer nivel, generalmente no interactúan con las herramientas de refactorización de código en su IDE (o al menos aún no he visto un conjunto de herramientas de este tipo)
Roland Tepp
2

Usted dice que el ejemplo es artificial, por lo que no sé si lo que digo aquí se adapta a su situación real, pero mi respuesta es: use una capa ORM (mapeo relacional de objetos) para definir la estructura y la consulta / manipulación de su base de datos De esa manera no tiene lógica duplicada, ya que todo se definirá en los modelos.

Por ejemplo, utilizando el marco Django (python), definiría su clase de dominio rectangular como el siguiente modelo :

class Rectangle(models.Model):
    width = models.IntegerField()
    height = models.IntegerField()

    def area(self):
        return self.width * self.height

Para calcular el área total (sin ningún filtro) definiría:

def total_area():
    return sum(rect.area() for rect in Rectangle.objects.all())

Como otros han mencionado, primero debe codificar la corrección y solo optimizar cuando realmente llegue a un cuello de botella. Entonces, si en una fecha posterior decide, absolutamente tiene que optimizar, puede cambiar a definir una consulta sin procesar, como:

def total_area_optimized():
    return Rectangle.objects.raw(
        'select sum(width * height) from myapp_rectangle')
yoniLavi
fuente
1

He escrito un ejemplo tonto para explicar una idea:

class BinaryIntegerOperation
{
    public int Execute(string operation, int operand1, int operand2)
    {
        var split = operation.Split(':');
        var opCode = split[0];
        if (opCode == "MULTIPLY")
        {
            var args = split[1].Split(',');
            var result = IsFirstOperand(args[0]) ? operand1 : operand2;
            for (var i = 1; i < args.Length; i++)
            {
                result *= IsFirstOperand(args[i]) ? operand1 : operand2;
            }
            return result;
        }
        else
        {
            throw new NotImplementedException();
        }
    }
    public string ToSqlExpression(string operation, string operand1Name, string operand2Name)
    {
        var split = operation.Split(':');
        var opCode = split[0];
        if (opCode == "MULTIPLY")
        {
            return string.Join("*", split[1].Split(',').Select(a => IsFirstOperand(a) ? operand1Name : operand2Name));
        }
        else
        {
            throw new NotImplementedException();
        }
    }
    private bool IsFirstOperand(string code)
    {
        return code == "0";
    }
}

Entonces, si tienes algo de lógica:

var logic = "MULTIPLY:0,1";

Puede reutilizarlo en clases de dominio:

var op = new BinaryIntegerOperation();
Console.WriteLine(op.Execute(logic, 3, 6));

O en su capa de generación SQL:

Console.WriteLine(op.ToSqlExpression(logic, "r.width", "r.height"));

Y, por supuesto, puedes cambiarlo fácilmente. Prueba esto:

logic = "MULTIPLY:0,1,1,1";
astef
fuente
-1

Como dijo @Machado, la forma más fácil de hacerlo es evitarlo y hacer todo el procesamiento en su Java principal. Sin embargo, aún es posible tener que codificar la base con un código similar sin repetirlo generando el código para ambos.

Por ejemplo, usando cog enable para generar los tres fragmentos de una definición común

fragmento 1:

/*[[[cog
from generate import generate_sql_table
cog.outl(generate_sql_table("rectangle"))
]]]*/
CREATE TABLE rectangles (
    width int,
    height int
);
/*[[[end]]]*/

fragmento 2:

public class Rectangle {
    /*[[[cog
      from generate import generate_domain_attributes,generate_domain_logic
      cog.outl(generate_domain_attributes("rectangle"))
      cog.outl(generate_domain_logic("rectangle"))
      ]]]*/
    private int width;
    private int height;
    public int area {
        return width * heigh;
    }
    /*[[[end]]]*/
}

fragmento 3:

/*[[[cog
from generate import generate_sql
cog.outl(generate_sql("rectangle","""
                       SELECT sum({area})
                       FROM rectangles r"""))
]]]*/
SELECT sum((r.width * r.heigh))
FROM rectangles r
/*[[[end]]]*/

de un archivo de referencia

import textwrap
import pprint

# the common definition 

types = {"rectangle":
    {"sql_table_name": "rectangles",
     "sql_alias": "r",
     "attributes": [
         ["width", "int"],
         ["height", "int"],
     ],
    "methods": [
        ["area","int","this.width * this.heigh"],
    ]
    }
 }

# the utilities functions

def generate_sql_table(name):
    type = types[name]
    attributes =",\n    ".join("{attr_name} {attr_type}".format(
        attr_name=attr_name,
        attr_type=attr_type)
                   for (attr_name,attr_type)
                   in type["attributes"])
    return """
CREATE TABLE {table_name} (
    {attributes}
);""".format(
    table_name=type["sql_table_name"],
    attributes = attributes
).lstrip("\n")


def generate_method(method_def):
    name,type,value =method_def
    value = value.replace("this.","")
    return textwrap.dedent("""
    public %(type)s %(name)s {
        return %(value)s;
    }""".lstrip("\n"))% {"name":name,"type":type,"value":value}


def generate_sql_method(type,method_def):
    name,_,value =method_def
    value = value.replace("this.",type["sql_alias"]+".")
    return name,"""(%(value)s)"""% {"value":value}

def generate_domain_logic(name):
    type = types[name]
    attributes ="\n".join(generate_method(method_def)
                   for method_def
                   in type["methods"])

    return attributes


def generate_domain_attributes(name):
    type = types[name]
    attributes ="\n".join("private {attr_type} {attr_name};".format(
        attr_name=attr_name,
        attr_type=attr_type)
                   for (attr_name,attr_type)
                   in type["attributes"])

    return attributes

def generate_sql(name,sql):
    type = types[name]
    fields ={name:value
             for name,value in
             (generate_sql_method(type,method_def)
              for method_def in type["methods"])}
    sql=textwrap.dedent(sql.lstrip("\n"))
    print (sql)
    return sql.format(**fields)
Xavier Combelle
fuente