Thiết kế phù hợp cho một lớp học với một phương pháp có thể khác nhau giữa các khách hàng


12

Tôi có một lớp được sử dụng để xử lý thanh toán của khách hàng. Tất cả trừ một trong những phương thức của lớp này đều giống nhau cho mọi khách hàng, ngoại trừ một phương pháp tính toán (ví dụ) số tiền mà người dùng của khách hàng nợ. Điều này có thể khác nhau rất nhiều từ khách hàng này sang khách hàng khác và không có cách nào dễ dàng để nắm bắt logic của các tính toán trong một cái gì đó như tệp thuộc tính, vì có thể có bất kỳ số lượng các yếu tố tùy chỉnh nào.

Tôi có thể viết mã xấu mà chuyển đổi dựa trên ID khách hàng:

switch(customerID) {
 case 101:
  .. do calculations for customer 101
 case 102:
  .. do calculations for customer 102
 case 103:
  .. do calculations for customer 103
 etc
}

nhưng điều này đòi hỏi phải xây dựng lại lớp mỗi khi chúng ta có một khách hàng mới. Cách tốt hơn là gì?

[Chỉnh sửa] Bài viết "trùng lặp" là hoàn toàn khác nhau. Tôi không hỏi làm thế nào để tránh câu lệnh chuyển đổi, tôi đang hỏi về thiết kế hiện đại áp dụng tốt nhất cho trường hợp này - điều mà tôi có thể giải quyết bằng câu lệnh chuyển đổi nếu tôi muốn viết mã khủng long. Các ví dụ được cung cấp có chung chung và không hữu ích, vì về cơ bản chúng nói "Này, công tắc hoạt động khá tốt trong một số trường hợp, không phải trong một số trường hợp khác."


[Chỉnh sửa] Tôi quyết định chọn câu trả lời được xếp hạng hàng đầu (tạo một lớp "Khách hàng" riêng cho từng khách hàng thực hiện giao diện chuẩn) vì các lý do sau:

  1. Tính nhất quán: Tôi có thể tạo giao diện đảm bảo tất cả các lớp Khách hàng nhận và trả lại cùng một đầu ra, ngay cả khi được tạo bởi nhà phát triển khác

  2. Khả năng bảo trì: Tất cả các mã được viết bằng cùng một ngôn ngữ (Java), do đó không cần ai khác phải học một ngôn ngữ mã hóa riêng biệt để duy trì tính năng đơn giản.

  3. Tái sử dụng: Trong trường hợp một vấn đề tương tự xuất hiện trong mã, tôi có thể sử dụng lại lớp Khách hàng để giữ bất kỳ số phương thức nào để thực hiện logic "tùy chỉnh".

  4. Tính quen thuộc: Tôi đã biết cách thực hiện việc này, vì vậy tôi có thể hoàn thành nhanh chóng và chuyển sang các vấn đề khác, cấp bách hơn.

Hạn chế:

  1. Mỗi khách hàng mới yêu cầu biên dịch lớp Khách hàng mới, có thể thêm một số phức tạp vào cách chúng tôi biên dịch và triển khai các thay đổi.

  2. Mỗi khách hàng mới phải được thêm bởi nhà phát triển - một người hỗ trợ không thể thêm logic vào một cái gì đó như tệp thuộc tính. Điều này không lý tưởng ... nhưng sau đó tôi cũng không chắc chắn làm thế nào một Người hỗ trợ có thể viết ra logic kinh doanh cần thiết, đặc biệt là nếu nó phức tạp với nhiều ngoại lệ (rất có thể).

  3. Sẽ không có quy mô tốt nếu chúng ta thêm nhiều, nhiều khách hàng mới. Điều này không được mong đợi, nhưng nếu nó xảy ra, chúng ta sẽ phải suy nghĩ lại về nhiều phần khác của mã cũng như phần này.

Đối với những người bạn quan tâm, bạn có thể sử dụng Java Reflection để gọi một lớp theo tên:

Payment payment = getPaymentFromSomewhere();

try {
    String nameOfCustomClass = propertiesFile.get("customClassName");
    Class<?> cpp = Class.forName(nameOfCustomClass);
    CustomPaymentProcess pp = (CustomPaymentProcess) cpp.newInstance();

    payment = pp.processPayment(payment);
} catch (Exception e) {
    //handle the various exceptions
} 

doSomethingElseWithThePayment(payment);

1
Có lẽ bạn nên giải thích về các loại tính toán. Nó chỉ là một khoản giảm giá được tính toán hay nó là một luồng công việc khác nhau?
qwerty_so

@ThomasKilian Đó chỉ là một ví dụ. Hãy tưởng tượng một đối tượng Thanh toán và các phép tính có thể giống như "nhân số Thanh toán.percentage với Payment.total - nhưng không phải nếu Payment.id bắt đầu bằng 'R'". Đó là loại chi tiết. Mỗi khách hàng có quy tắc riêng của họ.
Andrew


Bạn đã thử một cái gì đó như thế này . Thiết lập các công thức khác nhau cho mỗi khách hàng.
Laiv

1
Các plugin được đề xuất từ ​​các câu trả lời khác nhau sẽ chỉ hoạt động nếu bạn có sản phẩm dành riêng cho mỗi khách hàng. Nếu bạn có một giải pháp máy chủ nơi bạn kiểm tra id khách hàng một cách linh hoạt, nó sẽ không hoạt động. Vậy môi trường của bạn là gì?
qwerty_so

Câu trả lời:


14

Tôi có một lớp được sử dụng để xử lý thanh toán của khách hàng. Tất cả trừ một trong những phương thức của lớp này đều giống nhau cho mọi khách hàng, ngoại trừ một phương pháp tính toán (ví dụ) số tiền mà người dùng của khách hàng nợ.

Hai lựa chọn đến với tâm trí của tôi.

Tùy chọn 1: Biến lớp của bạn thành một lớp trừu tượng, trong đó phương thức khác nhau giữa các khách hàng là một phương thức trừu tượng. Sau đó tạo một lớp con cho mỗi khách hàng.

Tùy chọn 2: Tạo một Customerlớp hoặc một ICustomergiao diện, chứa tất cả logic phụ thuộc vào khách hàng. Thay vì yêu cầu lớp xử lý thanh toán của bạn chấp nhận ID khách hàng, hãy yêu cầu nó chấp nhận một Customerhoặc ICustomerđối tượng. Bất cứ khi nào nó cần làm một cái gì đó phụ thuộc vào khách hàng, nó gọi phương thức thích hợp.


Lớp khách hàng là một ý tưởng không tồi. Sẽ phải có một loại đối tượng tùy chỉnh cho mỗi Khách hàng, nhưng tôi không rõ liệu đây có phải là bản ghi DB, tệp thuộc tính (thuộc loại nào đó) hay lớp khác không.
Andrew

10

Bạn có thể muốn xem xét việc viết các tính toán tùy chỉnh dưới dạng "trình cắm" cho ứng dụng của mình. Sau đó, bạn sẽ sử dụng tệp cấu hình để cho bạn biết chương trình nên sử dụng plugin tính toán nào cho khách hàng nào. Bằng cách này, ứng dụng chính của bạn sẽ không cần phải được biên dịch lại cho mọi khách hàng mới - nó chỉ cần đọc (hoặc đọc lại) tệp cấu hình và tải các trình cắm mới.


Cảm ơn. Làm thế nào một trình cắm thêm hoạt động trong môi trường Java? Ví dụ: tôi muốn viết công thức "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue" lưu nó làm tài sản cho khách hàng và chèn nó Trong phương pháp thích hợp?
Andrew

@Andrew, Một cách mà một plugin có thể hoạt động là sử dụng sự phản chiếu để tải trong một lớp theo tên. Tệp cấu hình sẽ liệt kê tên của các lớp cho khách hàng. Tất nhiên, bạn phải có một số cách để xác định plugin nào dành cho khách hàng nào (ví dụ: theo tên của nó hoặc một số siêu dữ liệu được lưu trữ ở đâu đó).
Kat

@Kat Cảm ơn bạn, nếu bạn đọc câu hỏi đã được chỉnh sửa của tôi, đó thực sự là cách tôi đang thực hiện. Hạn chế là nhà phát triển phải tạo một lớp mới nhằm hạn chế khả năng mở rộng - Tôi muốn một người hỗ trợ chỉ có thể chỉnh sửa tệp thuộc tính nhưng tôi nghĩ có quá nhiều phức tạp.
Andrew

@Andrew: Bạn có thể viết các công thức của mình vào một tệp thuộc tính, nhưng sau đó bạn cần một số trình phân tích cú pháp biểu thức. Và một ngày nào đó bạn có thể chạy vào một công thức quá phức tạp để (dễ dàng) viết dưới dạng một biểu thức một dòng trong tệp thuộc tính. Nếu bạn thực sự muốn những người không phải lập trình viên có thể làm việc trên những cái này, bạn sẽ cần một số loại trình soạn thảo biểu thức thân thiện với người dùng để họ sử dụng sẽ tạo mã cho ứng dụng của bạn. Điều này có thể được thực hiện (tôi đã nhìn thấy nó), nhưng nó không tầm thường.
Thất vọngWithFormsDesigner

4

Tôi sẽ đi với một quy tắc được thiết lập để mô tả các tính toán. Điều này có thể được tổ chức trong bất kỳ cửa hàng kiên trì và sửa đổi linh hoạt.

Là một thay thế xem xét điều này:

customerOps = [oper1, oper2, ..., operN]; // array with customer specific operations
index = customerOpsIndex(customer);
customerOps[index](parms);

Trường hợp customerOpsIndextính toán chỉ số hoạt động đúng (bạn biết khách hàng nào cần điều trị).


Nếu tôi có thể tạo ra một bộ quy tắc toàn diện, tôi sẽ làm. Nhưng mỗi khách hàng mới có thể có bộ quy tắc riêng. Ngoài ra tôi muốn toàn bộ điều được chứa trong mã, nếu có một mẫu thiết kế để làm điều này. Ví dụ về công tắc {} của tôi thực hiện điều này khá độc đáo, nhưng nó không linh hoạt lắm.
Andrew

Bạn có thể lưu trữ các hoạt động được gọi cho một khách hàng trong một mảng và sử dụng ID khách hàng để lập chỉ mục cho nó.
qwerty_so

Điều đó có vẻ hứa hẹn hơn. :)
Andrew

3

Một cái gì đó như dưới đây:

Lưu ý rằng bạn vẫn có câu lệnh chuyển đổi trong repo. Mặc dù vậy, không có vấn đề gì thực sự xảy ra, tại một số điểm bạn cần ánh xạ id khách hàng theo logic yêu cầu. Bạn có thể nhận được thông minh và di chuyển nó vào một tệp cấu hình, đặt ánh xạ vào từ điển hoặc tải động trong các cụm logic, nhưng về cơ bản nó sẽ sôi sục để chuyển đổi.

public interface ICustomer
{
    int Calculate();
}
public class CustomerLogic101 : ICustomer
{
    public int Calculate() { return 101; }
}
public class CustomerLogic102 : ICustomer
{
    public int Calculate() { return 102; }
}

public class CustomerRepo
{
    public ICustomer GetCustomerById(
        string id)
    {
        var data;//get data from db
        if (data.logicType == "101")
        {
            return new CustomerLogic101();
        }
        if (data.logicType == "102")
        {
            return new CustomerLogic102();
        }
    }
}
public class Calculator
{
    public int CalculateCustomer(string custId)
    {
        CustomerRepo repo = new CustomerRepo();
        var cust = repo.GetCustomerById(custId);
        return cust.Calculate();
    }
}

2

Có vẻ như bạn có ánh xạ từ 1 đến 1 từ khách hàng sang mã tùy chỉnh và vì bạn đang sử dụng ngôn ngữ được biên dịch nên bạn phải xây dựng lại hệ thống của mình mỗi khi bạn có một khách hàng mới

Hãy thử một ngôn ngữ kịch bản nhúng.

Ví dụ: nếu hệ thống của bạn bằng Java, bạn có thể nhúng JRuby và sau đó cho mỗi khách hàng lưu trữ một đoạn mã Ruby tương ứng. Lý tưởng nhất là ở đâu đó dưới sự kiểm soát phiên bản, trong cùng hoặc trong một repo git riêng. Và sau đó đánh giá đoạn mã đó trong ngữ cảnh của ứng dụng Java của bạn. JRuby có thể gọi bất kỳ hàm Java nào và truy cập bất kỳ đối tượng Java nào.

package com.example;

import org.jruby.embed.LocalVariableBehavior;
import org.jruby.embed.ScriptingContainer;

public class Main {

    private ScriptingContainer ruby;

    public static void main(String[] args) {
        new Main().run();
    }

    public void run() {
        ruby = new ScriptingContainer(LocalVariableBehavior.PERSISTENT);
        // Assign the Java objects that you want to share
        ruby.put("main", this);
        // Execute a script (can be of any length, and taken from a file)
        Object result = ruby.runScriptlet("main.hello_world");
        // Use the result as if it were a Java object
        System.out.println(result);
    }

    public String getHelloWorld() {
        return "Hello, worlds!";
    }

}

Đây là một mô hình rất phổ biến. Ví dụ: nhiều trò chơi trên máy tính được viết bằng C ++ nhưng sử dụng các tập lệnh Lua nhúng để xác định hành vi khách hàng của từng đối thủ trong trò chơi.

Mặt khác, nếu bạn có nhiều ánh xạ từ 1 đến khách hàng sang mã tùy chỉnh, chỉ cần sử dụng mẫu "Chiến lược" như đã đề xuất.

Nếu ánh xạ không dựa trên thực hiện ID người dùng, hãy thêm một matchchức năng cho từng đối tượng chiến lược và đưa ra lựa chọn theo thứ tự sử dụng chiến lược nào.

Đây là một số mã giả

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)

2
Tôi sẽ tránh cách tiếp cận này. Xu hướng là đặt tất cả các đoạn mã đó vào một db và sau đó bạn mất kiểm soát nguồn, phiên bản và thử nghiệm.
Ewan

Đủ công bằng. Họ tốt nhất lưu trữ chúng trong một repo git riêng hoặc bất cứ nơi nào. Đã cập nhật câu trả lời của tôi để nói rằng các đoạn trích lý tưởng nên nằm dưới sự kiểm soát phiên bản.
akuhn

Chúng có thể được lưu trữ trong một cái gì đó như một tập tin thuộc tính? Tôi đang cố gắng tránh việc có các thuộc tính Khách hàng cố định tồn tại ở nhiều hơn một địa điểm, nếu không, thật dễ dàng để chuyển thành mì spaghetti không thể nhầm lẫn.
Andrew

2

Tôi sẽ bơi ngược lại hiện tại.

Tôi sẽ thử thực hiện ngôn ngữ biểu hiện của riêng tôi với ANTLR .

Cho đến nay, tất cả các câu trả lời đều mạnh mẽ dựa trên tùy chỉnh mã. Việc triển khai các lớp học cụ thể cho từng khách hàng đối với tôi dường như, tại một thời điểm nào đó trong tương lai, sẽ không có quy mô tốt. Việc bảo trì sẽ tốn kém và đau đớn.

Vì vậy, với Antlr, ý tưởng là xác định ngôn ngữ của riêng bạn. Rằng bạn có thể cho phép người dùng (hoặc nhà phát triển) viết quy tắc kinh doanh bằng ngôn ngữ đó.

Lấy bình luận của bạn làm ví dụ:

Tôi muốn viết công thức "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue"

Với EL của bạn, bạn có thể nêu các câu như:

If paymentID startWith 'R' then (paymentPercentage / paymentTotal) else paymentOther

Sau đó...

lưu nó như một tài sản cho khách hàng, và chèn nó vào phương pháp thích hợp?

Bạn có thể. Đó là một chuỗi, bạn có thể lưu nó dưới dạng thuộc tính hoặc thuộc tính.

Tôi sẽ không nói dối. Nó khá phức tạp và khó khăn. Thậm chí còn khó hơn nếu các quy tắc kinh doanh cũng phức tạp.

Dưới đây là một số câu hỏi SO có thể bạn quan tâm:


Lưu ý: ANTLR cũng tạo mã cho Python và Javascript. Điều đó có thể giúp viết bằng chứng về khái niệm mà không cần quá nhiều chi phí.

Nếu bạn thấy Antlr quá khó, bạn có thể thử với các lib như Expr4J, JEval, Parsii. Những công trình này với mức độ trừu tượng cao hơn.


Đó là một ý tưởng tốt nhưng đau đầu bảo trì cho người tiếp theo xuống dòng. Nhưng tôi sẽ không loại bỏ bất kỳ ý tưởng nào cho đến khi tôi đánh giá và cân nhắc tất cả các biến.
Andrew

1
Nhiều lựa chọn bạn có càng tốt. Tôi chỉ muốn tặng thêm một người
Laiv

Không thể phủ nhận rằng đây là một cách tiếp cận hợp lệ, chỉ cần nhìn vào websphere hoặc các hệ thống doanh nghiệp khác mà 'người dùng cuối có thể tùy chỉnh' Nhưng giống như @akuhn trả lời nhược điểm là bây giờ bạn đang lập trình bằng 2 ngôn ngữ và mất phiên bản của bạn / kiểm soát nguồn / kiểm soát thay đổi
Ewan

@Ewan xin lỗi, tôi sợ tôi không hiểu lý lẽ của bạn. Các mô tả ngôn ngữ và các lớp được tạo tự động có thể là một phần hoặc không phải của dự án. Nhưng trong mọi trường hợp tôi không thấy lý do tại sao tôi có thể mất quản lý phiên bản / mã nguồn. Mặt khác, dường như có 2 ngôn ngữ khác nhau, nhưng đó là tạm thời. Khi ngôn ngữ được thực hiện, bạn chỉ đặt chuỗi. Giống như biểu thức Cron. Về lâu dài có ít thứ để lo lắng hơn.
Laiv

"Nếu thanh toánID bắt đầu với 'R' thì (PaymentPercentage / PaymentTotal) khác thanh toán khác" Bạn lưu trữ thứ này ở đâu? phiên bản nào vậy nó được viết bằng ngôn ngữ gì? bạn đã có bài kiểm tra đơn vị nào cho nó?
Ewan

1

Ít nhất bạn có thể bên ngoài thuật toán để lớp Khách hàng không cần thay đổi khi một khách hàng mới được thêm vào bằng cách sử dụng một mẫu thiết kế được gọi là Mẫu chiến lược (nằm trong Gang of Four).

Từ đoạn trích bạn đưa ra, có thể tranh cãi liệu mô hình chiến lược sẽ ít được bảo trì hơn hay có thể duy trì nhiều hơn, nhưng ít nhất nó sẽ loại bỏ kiến ​​thức của lớp Khách hàng về những gì cần thực hiện (và sẽ loại bỏ trường hợp chuyển đổi của bạn).

Một đối tượng StrategFactory sẽ tạo một con trỏ StrategIntf (hoặc tham chiếu) dựa trên ID khách hàng. Nhà máy có thể trả về một triển khai mặc định cho các khách hàng không đặc biệt.

Lớp Khách hàng chỉ cần yêu cầu Nhà máy đưa ra chiến lược chính xác và sau đó gọi nó.

Đây là giả C ++ rất ngắn gọn để cho bạn thấy ý tôi là gì.

class Customer
{
public:

    void doCalculations()
    {
        CalculationsStrategyIntf& strategy = CalculationsStrategyFactory::instance().getStrategy(*this);
        strategy.doCalculations();
    }
};


class CalculationsStrategyIntf
{
public:
    virtual void doCalculations() = 0;
};

Hạn chế của giải pháp này là, đối với mỗi khách hàng mới cần logic đặc biệt, bạn sẽ cần tạo một triển khai mới của Tính toánStrargetyIntf và cập nhật nhà máy để trả lại cho khách hàng thích hợp. Điều này cũng yêu cầu biên dịch. Nhưng ít nhất bạn sẽ tránh được mã spaghetti ngày càng tăng trong lớp khách hàng.


Có, tôi nghĩ rằng ai đó đã đề cập đến một mẫu tương tự trong một câu trả lời khác, cho mỗi khách hàng để gói logic trong một lớp riêng biệt thực hiện giao diện Khách hàng và sau đó gọi lớp đó từ lớp PaymentProcessor. Điều đó đầy hứa hẹn nhưng điều đó có nghĩa là mỗi khi chúng tôi mang đến một Khách hàng mới, chúng tôi phải viết một lớp mới - không thực sự là vấn đề lớn, nhưng không phải là điều mà một Người hỗ trợ có thể làm. Vẫn là một lựa chọn.
Andrew

-1

Tạo một giao diện với phương thức duy nhất và sử dụng lamdas trong mỗi lớp thực hiện. Hoặc bạn có thể lớp ẩn danh để thực hiện các phương thức cho các khách hàng khác nhau

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.