Các cách để tránh trùng lặp logic giữa các lớp miền và truy vấn SQL là gì?


21

Ví dụ dưới đây là hoàn toàn nhân tạo và mục đích duy nhất của nó là đưa quan điểm của tôi đi qua.

Giả sử tôi có một bảng SQL:

CREATE TABLE rectangles (
  width int,
  height int 
);

Lớp tên miền:

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

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

Bây giờ giả sử rằng tôi có một yêu cầu hiển thị cho người dùng tổng diện tích của tất cả các hình chữ nhật trong cơ sở dữ liệu. Tôi có thể làm điều đó bằng cách tìm nạp tất cả các hàng của bảng, biến chúng thành các đối tượng và lặp lại trên chúng. Nhưng điều này có vẻ ngu ngốc, bởi vì tôi có rất nhiều hình chữ nhật trong bảng.

Vì vậy, tôi làm điều này:

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

Điều này là dễ dàng, nhanh chóng và sử dụng các điểm mạnh của cơ sở dữ liệu. Tuy nhiên, nó giới thiệu logic trùng lặp, bởi vì tôi cũng có tính toán tương tự trong lớp miền của mình.

Tất nhiên, với ví dụ này, sự trùng lặp của logic hoàn toàn không gây tử vong. Tuy nhiên, tôi phải đối mặt với cùng một vấn đề với các lớp miền khác, phức tạp hơn.


1
Tôi nghi ngờ giải pháp tối ưu sẽ thay đổi khá nhiều từ codebase sang codebase, vì vậy bạn có thể mô tả ngắn gọn một trong những ví dụ phức tạp hơn gây rắc rối cho bạn không?
Ixrec

2
@lxrec: Báo cáo. Một ứng dụng kinh doanh có các quy tắc mà tôi đang nắm bắt trong các lớp và tôi cũng cần tạo các báo cáo hiển thị cùng thông tin, nhưng cô đọng. Tính toán VAT, thanh toán, thu nhập, loại công cụ như vậy.
Escape Velocity

1
Đây có phải cũng là một câu hỏi về phân phối tải giữa máy chủ và máy khách không? Chắc chắn, chỉ cần bỏ kết quả được lưu trong bộ nhớ cache của máy tính vào máy khách là đặt cược tốt nhất của bạn, nhưng nếu dữ liệu thay đổi thường xuyên và có nhiều yêu cầu, có thể thuận lợi khi chỉ có thể ném các thành phần và công thức vào máy khách thay vì nấu bữa ăn cho họ Tôi nghĩ rằng không nhất thiết phải có nhiều hơn một nút trong một hệ thống phân tán có thể cung cấp một chức năng nhất định.
null

Tôi nghĩ rằng cách tốt nhất là tạo ra các mã như vậy. Tôi sẽ giải thích sau.
Xavier Combelle

Câu trả lời:


11

Như lxrec đã chỉ ra, nó sẽ thay đổi từ codebase sang codebase. Một số ứng dụng sẽ cho phép bạn đưa các loại logic nghiệp vụ đó vào các Hàm và / hoặc truy vấn SQL và cho phép bạn chạy các giá trị đó bất cứ lúc nào bạn cần để hiển thị các giá trị đó cho người dùng.

Đôi khi nó có vẻ ngu ngốc, nhưng tốt hơn là viết mã cho tính chính xác hơn là hiệu suất làm mục tiêu chính.

Trong mẫu của bạn, nếu bạn đang hiển thị giá trị của khu vực cho người dùng ở dạng web, bạn phải:

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.

Thật ngu ngốc cho những thứ đơn giản như mẫu trên mẫu, nhưng có thể cần những thứ phức tạp hơn như tính IRR của khoản đầu tư của khách hàng trong hệ thống ngân hàng.

Mã cho sự chính xác . Nếu phần mềm của bạn đúng, nhưng chậm, bạn sẽ có cơ hội tối ưu hóa nơi bạn cần (sau khi định hình). Nếu điều đó có nghĩa là giữ một số logic kinh doanh trong cơ sở dữ liệu, thì hãy là nó. Đó là lý do tại sao chúng tôi có các kỹ thuật tái cấu trúc.

Nếu nó trở nên chậm hoặc không phản hồi, bạn có thể phải thực hiện một số tối ưu hóa, như vi phạm nguyên tắc DRY, đó không phải là tội lỗi nếu bạn bao quanh bản thân kiểm tra đơn vị phù hợp và kiểm tra tính nhất quán.


1
Rắc rối với việc đưa logic kinh doanh (thủ tục) vào SQL là cực kỳ đau đớn để tái cấu trúc. Ngay cả khi bạn có các công cụ tái cấu trúc SQL hàng đầu, chúng thường không giao diện với các công cụ tái cấu trúc mã trong IDE của bạn (hoặc ít nhất là tôi chưa thấy một bộ công cụ như vậy)
Roland Tepp

2

Bạn nói rằng ví dụ này là giả tạo, vì vậy tôi không biết những gì tôi nói ở đây có phù hợp với tình huống thực tế của bạn không, nhưng câu trả lời của tôi là - sử dụng lớp ORM (ánh xạ quan hệ đối tượng) để xác định cấu trúc và truy vấn / thao tác của cơ sở dữ liệu của bạn. Bằng cách đó, bạn không có logic trùng lặp, vì mọi thứ sẽ được xác định trong các mô hình.

Ví dụ: sử dụng khung Django (python), bạn sẽ xác định lớp miền hình chữ nhật của mình là mô hình sau :

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

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

Để tính tổng diện tích (không có bất kỳ bộ lọc nào) bạn xác định:

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

Như những người khác đã đề cập, trước tiên bạn nên viết mã cho chính xác và chỉ tối ưu hóa khi bạn thực sự gặp phải nút cổ chai. Vì vậy, nếu vào một ngày sau đó bạn quyết định, bạn hoàn toàn phải tối ưu hóa, bạn có thể chuyển sang xác định truy vấn thô, chẳng hạn như:

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

1

Tôi đã viết một ví dụ ngớ ngẩn để giải thích một ý tưởng:

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

Vì vậy, nếu bạn có một số logic:

var logic = "MULTIPLY:0,1";

Bạn có thể sử dụng lại nó trong các lớp miền:

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

Hoặc trong lớp thế hệ sql của bạn:

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

Và, tất nhiên, bạn có thể thay đổi nó một cách dễ dàng. Thử đi:

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

-1

Như @Machado đã nói, cách dễ nhất để làm điều đó là tránh nó và thực hiện tất cả quá trình xử lý của bạn trong java chính của bạn. Tuy nhiên, vẫn có thể phải mã cơ sở với mã tương tự mà không lặp lại chính bạn bằng cách tạo mã cho cả hai cơ sở mã.

Ví dụ: sử dụng cog enable để tạo ba đoạn trích từ một định nghĩa chung

đoạn 1:

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

đoạn 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]]]*/
}

đoạn 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]]]*/

từ một tập tin tham khảo

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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.