Quali sono i modi per evitare la duplicazione della logica tra classi di dominio e query SQL?


21

L'esempio che segue è totalmente artificiale e il suo unico scopo è quello di far capire il mio punto di vista.

Supponiamo che io abbia una tabella SQL:

CREATE TABLE rectangles (
  width int,
  height int 
);

Classe di dominio:

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

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

Supponiamo ora che ho l'obbligo di mostrare all'utente l'area totale di tutti i rettangoli nel database. Posso farlo recuperando tutte le righe della tabella, trasformandole in oggetti e ripetendole. Ma questo sembra semplicemente stupido, perché ho molti rettangoli nella mia tavola.

Quindi faccio questo:

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

Questo è facile, veloce e utilizza i punti di forza del database. Tuttavia, introduce una logica duplicata, perché ho lo stesso calcolo anche nella mia classe di dominio.

Naturalmente, per questo esempio, la duplicazione della logica non è affatto fatale. Tuttavia, devo affrontare lo stesso problema con le altre mie classi di dominio, che sono più complesse.


1
Ho il sospetto che la soluzione ottimale varierà piuttosto selvaggiamente da base di codice a base di codice, quindi potresti descrivere brevemente uno degli esempi più complessi che ti stanno dando problemi?
Ixrec,

2
@lxrec: rapporti. Un'applicazione aziendale che ha delle regole che sto acquisendo in classe e che devo anche creare report che mostrano le stesse informazioni, ma condensate. Calcoli IVA, pagamenti, guadagni, questo tipo di cose.
Escape Velocity,

1
Non si tratta anche di distribuire il carico tra server e client? Certo, semplicemente scaricare il risultato memorizzato nella cache del calcolo per un cliente è la soluzione migliore, ma se i dati cambiano spesso e ci sono molte richieste, potrebbe essere vantaggioso poter semplicemente lanciare gli ingredienti e la ricetta sul client anziché cucinando il pasto per loro. Penso che non sia necessariamente una brutta cosa avere più di un nodo in un sistema distribuito in grado di fornire una certa funzionalità.
null

Penso che il modo migliore sia generare tali codici. Spiegherò più avanti.
Xavier Combelle,

Risposte:


11

Come ha sottolineato lxrec, varierà da base di codice a base di codice. Alcune applicazioni ti permetteranno di inserire quel tipo di logica aziendale in Funzioni e / o query SQL e ti permetteranno di eseguirle ogni volta che dovrai mostrare tali valori all'utente.

A volte può sembrare stupido, ma è meglio codificare per correttezza che prestazioni come obiettivo primario.

Nel tuo esempio, se stai mostrando il valore dell'area per un utente in un modulo web, dovresti:

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.

È stupido per cose semplici come quella sul campione, ma potrebbe essere necessario per cose più complesse come il calcolo dell'IRR di un investimento di un cliente in un sistema bancario.

Codice per correttezza . Se il tuo software è corretto, ma lento, avrai la possibilità di ottimizzare dove ti serve (dopo la profilazione). Se ciò significa mantenere parte della logica aziendale nel database, così sia. Ecco perché abbiamo tecniche di refactoring.

Se diventa lento, o non risponde, allora potresti avere alcune ottimizzazioni da fare, come violare il principio DRY, il che non è un peccato se ti circondi del corretto test unitario e test di coerenza.


1
Il problema con l'inserimento della logica aziendale (procedurale) in SQL è che è estremamente doloroso da considerare. Anche se hai strumenti di refactoring SQL di altissimo livello, di solito non si interfacciano con gli strumenti di refactoring del codice nel tuo IDE (o almeno non ho ancora visto un tale set di strumenti)
Roland Tepp,

2

Dici che l'esempio è artificiale, quindi non so se quello che sto dicendo qui si adatta alla tua situazione reale, ma la mia risposta è: usa un livello ORM (Object-relational mapping) per definire la struttura e la query / manipolazione di il tuo database. In questo modo non hai una logica duplicata, poiché tutto sarà definito nei modelli.

Ad esempio, usando il framework Django (python), definiresti la tua classe di dominio rectangle come il seguente modello :

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

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

Per calcolare l'area totale (senza alcun filtro) è necessario definire:

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

Come altri hanno già detto, dovresti prima codificare per correttezza e ottimizzare solo quando colpisci davvero un collo di bottiglia. Quindi, se in un secondo momento decidi, devi assolutamente ottimizzare, puoi passare alla definizione di una query non elaborata, come ad esempio:

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

1

Ho scritto un esempio sciocco per spiegare un'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";
    }
}

Quindi, se hai qualche logica:

var logic = "MULTIPLY:0,1";

Puoi riutilizzarlo in classi di dominio:

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

O nel tuo livello di generazione sql:

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

E, naturalmente, puoi cambiarlo facilmente. Prova questo:

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

-1

Come ha detto @Machado, il modo più semplice per farlo è evitarlo e fare tutte le tue elaborazioni nel tuo java principale. Tuttavia, è ancora possibile dover codificare la base con un codice simile senza ripetersi generando il codice per entrambi i code base.

Ad esempio, usando l' attivazione del dente per generare i tre frammenti da una definizione comune

frammento 1:

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

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

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

da un file di riferimento

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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.