Quais são as maneiras de evitar duplicação de lógica entre classes de domínio e consultas SQL?

21

O exemplo abaixo é totalmente artificial e seu único objetivo é transmitir meu ponto de vista.

Suponha que eu tenha uma tabela SQL:

CREATE TABLE rectangles (
  width int,
  height int 
);

Classe de domínio:

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

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

Agora, suponha que eu tenha um requisito para mostrar ao usuário a área total de todos os retângulos no banco de dados. Eu posso fazer isso buscando todas as linhas da tabela, transformando-as em objetos e iterando sobre elas. Mas isso parece estúpido, porque eu tenho muitos e muitos retângulos na minha mesa.

Então eu faço isso:

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

Isso é fácil, rápido e utiliza os pontos fortes do banco de dados. No entanto, ele introduz uma lógica duplicada, porque também tenho o mesmo cálculo na minha classe de domínio.

Obviamente, neste exemplo, a duplicação da lógica não é fatal. No entanto, enfrento o mesmo problema com minhas outras classes de domínio, que são mais complexas.

Velocidade de escape
fonte
1
Eu suspeito que a solução ideal variará bastante de base para base de código, então você poderia descrever brevemente um dos exemplos mais complexos que estão lhe causando problemas?
Ixrec 21/07
2
@lxrec: Relatórios. Um aplicativo de negócios que possui regras que estou capturando nas classes e também preciso criar relatórios que mostrem as mesmas informações, mas condensadas. Cálculos de IVA, pagamentos, ganhos, esse tipo de coisa.
Escape Velocity
1
Não se trata também de distribuir a carga entre servidor e clientes? Certamente, apenas despejar o resultado em cache do cálculo para um cliente é sua melhor aposta, mas se os dados mudarem com frequência e houver muitas solicitações, pode ser vantajoso poder lançar os ingredientes e a receita no cliente em vez de cozinhar a refeição para eles. Eu acho que não é necessariamente uma coisa ruim ter mais de um nó em um sistema distribuído que pode fornecer uma certa funcionalidade.
null
Eu acho que a melhor maneira é gerar esses códigos. Vou explicar mais tarde.
Xavier Combelle

Respostas:

11

Como o lxrec apontou, ele varia de base de código para base de código. Alguns aplicativos permitem que você coloque esse tipo de lógica de negócios em Funções e / ou consultas SQL e permitem que você execute essas sempre que precisar mostrar esses valores ao usuário.

Às vezes, pode parecer estúpido, mas é melhor codificar a correção do que o desempenho como objetivo principal.

Na sua amostra, se você estiver mostrando o valor da área para um usuário em um formulário da web, precisará:

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.

É estúpido para coisas simples como a da amostra, mas pode ser necessário coisas mais complexas, como calcular a TIR de um investimento de um cliente em um sistema bancário.

Código de correção . Se o seu software estiver correto, mas lento, você terá chances de otimizar onde precisa (após a criação de perfil). Se isso significa manter parte da lógica de negócios no banco de dados, que assim seja. É por isso que temos técnicas de refatoração.

Se ficar lento ou não responder, você poderá ter algumas otimizações, como violar o princípio DRY, que não é pecado se você se envolver nos testes de unidade e de consistência adequados.

Machado
fonte
1
O problema de colocar a lógica de negócios (processual) no SQL é extremamente doloroso para refatorar. Mesmo se você tem top notch SQL refatoração ferramentas, eles geralmente não interface com código de refatoração ferramentas em seu IDE (ou pelo menos eu ainda não vi tal conjunto de ferramentas)
Roland Tepp
2

Você diz que o exemplo é artificial, então não sei se o que estou dizendo aqui se adequa à sua situação real, mas minha resposta é - use uma camada ORM (Mapeamento Relacional Objeto) para definir a estrutura e a consulta / manipulação de seu banco de dados. Dessa forma, você não tem lógica duplicada, pois tudo será definido nos modelos.

Por exemplo, usando a estrutura do Django (python), você definiria sua classe de domínio retangular como o seguinte modelo :

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

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

Para calcular a área total (sem filtragem), você definiria:

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

Como outros já mencionaram, você deve primeiro codificar a correção e otimizar apenas quando realmente atingir um gargalo. Portanto, se em uma data posterior você decidir, absolutamente precisa otimizar, poderá mudar para definir uma consulta bruta, como:

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

Eu escrevi um exemplo bobo para explicar uma ideia:

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";
    }
}

Então, se você tem alguma lógica:

var logic = "MULTIPLY:0,1";

Você pode reutilizá-lo nas classes de domínio:

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

Ou na sua camada de geração sql:

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

E, claro, você pode alterá-lo facilmente. Tente o seguinte:

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

Como o @Machado disse, a maneira mais fácil de fazer isso é evitá-lo e fazer todo o processamento no seu java principal. No entanto, ainda é possível ter que codificar a base com um código semelhante sem se repetir, gerando o código para ambos.

Por exemplo, usando o cog enable para gerar os três trechos de uma definição comum

snippet 1:

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

snippet 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]]]*/
}

snippet 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 um arquivo de referência

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
fonte