Comment éviter la duplication de la logique entre les classes de domaine et les requêtes SQL?


21

L'exemple ci-dessous est totalement artificiel et son seul but est de faire passer mon message.

Supposons que j'ai une table SQL:

CREATE TABLE rectangles (
  width int,
  height int 
);

Classe de domaine:

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

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

Supposons maintenant que j'ai l'obligation de montrer à l'utilisateur la surface totale de tous les rectangles de la base de données. Je peux le faire en récupérant toutes les lignes du tableau, en les transformant en objets et en les itérant. Mais cela semble juste stupide, car j'ai beaucoup, beaucoup de rectangles dans ma table.

Alors je fais ça:

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

C'est simple, rapide et utilise les points forts de la base de données. Cependant, il introduit une logique dupliquée, car j'ai également le même calcul dans ma classe de domaine.

Bien sûr, pour cet exemple, la duplication de la logique n'est pas du tout fatale. Cependant, je suis confronté au même problème avec mes autres classes de domaine, qui sont plus complexes.


1
Je soupçonne que la solution optimale variera considérablement d'une base de code à l'autre, alors pourriez-vous décrire brièvement l'un des exemples les plus complexes qui vous pose problème?
Ixrec

2
@lxrec: Rapports. Une application métier qui a des règles que je capture dans les classes, et je dois également créer des rapports qui affichent les mêmes informations, mais condensés. Calculs de TVA, paiements, gains, ce genre de choses.
Escape Velocity

1
N'est-ce pas aussi une question de répartition de la charge entre serveur et clients? Bien sûr, le simple fait de vider le résultat du calcul en cache sur un client est votre meilleur choix, mais si les données changent souvent et qu'il y a de nombreuses demandes, il pourrait être avantageux de pouvoir simplement jeter les ingrédients et la recette au client au lieu de préparer le repas pour eux. Je pense que ce n'est pas nécessairement une mauvaise chose d'avoir plus d'un nœud dans un système distribué qui peut fournir une certaine fonctionnalité.
null

Je pense que la meilleure façon est de générer de tels codes. J'expliquerai plus tard.
Xavier Combelle

Réponses:


11

Comme l'a souligné lxrec, cela variera d'une base de code à l'autre. Certaines applications vous permettront d'intégrer ce type de logique métier dans des fonctions SQL et / ou des requêtes et vous permettront de les exécuter à tout moment pour montrer ces valeurs à l'utilisateur.

Parfois, cela peut sembler stupide, mais il vaut mieux coder pour l'exactitude que la performance comme objectif principal.

Dans votre exemple, si vous affichez la valeur de la zone pour un utilisateur dans un formulaire Web, vous devez:

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.

C'est stupide pour des choses simples comme celle de l'échantillon, mais il peut être nécessaire de faire des choses plus complexes comme le calcul du TRI d'un investissement d'un client dans un système bancaire.

Code de correction . Si votre logiciel est correct, mais lent, vous aurez des chances d'optimiser où vous en avez besoin (après profilage). Si cela signifie conserver une partie de la logique métier dans la base de données, tant pis. C'est pourquoi nous avons des techniques de refactoring.

Si cela devient lent ou ne répond plus, vous pouvez avoir des optimisations à faire, comme violer le principe DRY, ce qui n'est pas un péché si vous vous entourez des tests unitaires et des tests de cohérence appropriés.


1
Le problème avec la mise en logique métier (procédurale) dans SQL est extrêmement pénible à refactoriser. Même si vous avez des outils de refactorisation SQL de premier ordre, ils ne s'interfacent généralement pas avec les outils de refactorisation de code dans votre IDE (ou du moins je n'ai pas encore vu un tel ensemble d'outils)
Roland Tepp

2

Vous dites que l'exemple est artificiel, donc je ne sais pas si ce que je dis ici convient à votre situation réelle, mais ma réponse est - utilisez une couche ORM (Object-relationnel mapping) pour définir la structure et l'interrogation / manipulation de votre base de données. De cette façon, vous n'avez pas de logique dupliquée, car tout sera défini dans les modèles.

Par exemple, en utilisant le framework Django (python), vous définiriez votre classe de domaine rectangle comme le modèle suivant :

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

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

Pour calculer la superficie totale (sans aucun filtrage), vous devez définir:

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

Comme d'autres l'ont mentionné, vous devez d'abord coder pour l'exactitude et n'optimiser que lorsque vous rencontrez vraiment un goulot d'étranglement. Donc, si à une date ultérieure vous décidez, vous devez absolument optimiser, vous pouvez passer à la définition d'une requête brute, telle que:

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

1

J'ai écrit un exemple stupide pour expliquer une idée:

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

Donc, si vous avez une logique:

var logic = "MULTIPLY:0,1";

Vous pouvez le réutiliser dans les classes de domaine:

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

Ou dans votre couche de génération SQL:

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

Et, bien sûr, vous pouvez le changer facilement. Essaye ça:

logic = "MULTIPLY:0,1,1,1";

-1

Comme l'a dit @Machado, la façon la plus simple de le faire est de l'éviter et de faire tout votre traitement dans votre java principal. Cependant, il est toujours possible d'avoir à coder la base avec un code similaire sans se répéter soi-même en générant le code pour les deux bases de code.

Par exemple, en utilisant cog enable pour générer les trois extraits à partir d'une définition commune

extrait 1:

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

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

extrait 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]]]*/

à partir d'un fichier de référence

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)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.