使用一种可以在客户之间变化的方法对类进行正确的设计


12

我有一个用于处理客户付款的类。除了一个类(用于计算(例如)客户的用户欠款额的)方法外,对于每个客户而言,此类中的一种方法均是相同的。各个客户之间的差异可能很大,并且由于存在许多自定义因素,因此没有简单的方法来捕获诸如属性文件之类的计算逻辑。

我可以编写难看的代码,以根据customerID进行切换:

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

但这需要在我们每次获得新客户时都重新构建班级。有什么更好的方法?

[编辑]“重复”的文章完全不同。我不是在问如何避免使用switch语句,而是在寻求最适合这种情况的现代设计-如果我想编写恐龙代码,可以使用switch语句解决。此处提供的示例是通用的,无济于事,因为它们本质上是在说“嘿,在某些情况下,此开关工作得很好,而在另一些情况下,则效果很好。”


[编辑]我决定采用排名靠前的答案(为实现标准接口的每个客户创建一个单独的“客户”类),原因如下:

  1. 一致性:我可以创建一个接口,以确保所有Customer类都可以接收并返回相同的输出,即使是由其他开发人员创建的

  2. 可维护性:所有代码都使用相同的语言(Java)编写,因此无需其他任何人学习单独的编码语言来维护应该是简陋的功能。

  3. 重用:如果在代码中出现了类似的问题,我可以重用Customer类来容纳许多方法来实现“自定义”逻辑。

  4. 熟悉:我已经知道如何执行此操作,因此我可以快速完成它,然后继续处理其他更紧迫的问题。

缺点:

  1. 每个新客户都需要编译新的Customer类,这可能会增加我们编译和部署更改的方式的复杂性。

  2. 每个新客户都必须由开发人员添加-支持人员不能只是将逻辑添加到属性文件之类的东西中。这不是理想的选择……但是后来我也不确定支持人员将如何写出必要的业务逻辑,尤其是当它很复杂且有很多例外时(很可能)。

  3. 如果我们增加许多新客户,它将无法很好地扩展。这是不期望的,但是如果确实发生了,我们将不得不重新考虑代码的许多其他部分以及这一部分。

对于您感兴趣的人,可以使用Java Reflection按名称调用类:

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
也许您应该详细说明计算的类型。只是计算出的折扣还是不同的工作流程?
qwerty_so 2016年

@ThomasKilian只是一个例子。假设有一个Payment对象,计算结果可能类似于“将Payment.percentage乘以Payment.total,但如果Payment.id以'R'开头则不会。” 那种粒度。每个客户都有自己的规则。
安德鲁


你有没有试过像这样。为每个客户设置不同的公式。
Laiv 2016年

1
仅当您有专用于每个客户的产品时,才可以使用来自各种答案的建议插件。如果您有一个服务器解决方案可以动态检查客户ID,那么它将无法正常工作。那么您的环境如何?
qwerty_so 2016年

Answers:


14

我有一个用于处理客户付款的类。除了一个类(用于计算(例如)客户的用户欠款额的)方法外,对于每个客户而言,此类中的一种方法均是相同的。

我想到两个选择。

选项1:将您的类设为抽象类,其中客户之间不同的方法是抽象方法。然后为每个客户创建一个子类。

选项2:创建一个包含所有与客户相关的逻辑的Customer类或ICustomer接口。与其让您的付款处理类接受一个客户ID,不如让它接受一个CustomerICustomer对象。每当它需要做一些与客户有关的事情时,它都会调用适当的方法。


客户类不是一个坏主意。每个客户都必须有某种自定义对象,但是我不清楚这是数据库记录,属性文件(某种类型)还是其他类。
安德鲁

10

您可能需要考虑将自定义计算作为应用程序的“插件”编写。然后,您将使用配置文件来告诉您程序哪个计算插件应用于哪个客户。这样,您的主应用程序无需为每个新客户重新编译-它只需要读取(或重新读取)配置文件并加载新插件。


谢谢。在Java环境中,插件如何工作?例如,我要编写公式“结果= if(Payment.id.startsWith(“ R”))?Payment.percentage * Payment.total:Payment.otherValue”将其保存为客户的属性,然后插入用适当的方法?
安德鲁

@Andrew,插件可以工作的一种方法是使用反射按名称加载类。配置文件将列出客户的类名称。当然,您必须有某种方法来标识哪个插件是针对哪个客户的(例如,通过其名称或存储在某处的某些元数据)。
凯特

@Kat谢谢,如果您阅读了我编辑过的问题,那实际上就是我的做法。缺点是开发人员必须创建一个限制可伸缩性的新类-我希望支持人员只能编辑属性文件,但我认为这样做太复杂了。
安德鲁

@Andrew:您也许可以在属性文件中编写公式,但是随后您需要某种表达式解析器。而且,您可能有一天会遇到一个公式,该公式太复杂而无法(轻松地)作为一个单行表达式写入属性文件。如果您确实希望非程序员能够处理这些问题,则需要某种用户友好的表达式编辑器供他们使用,以便为您的应用程序生成代码。可以做到这一点(我已经看过),但这并不简单。
FrustratedWithFormsDesigner

4

我将使用规则集来描述计算。它可以保存在任何持久性存储中,并可以动态修改。

作为替代方案,请考虑以下事项:

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

在哪里customerOpsIndex计算出正确的操作指标(您知道哪个客户需要哪种处理方法)。


如果我可以创建一个全面的规则集,那我会。但是每个新客户可能都有自己的一套规则。如果有设计模式,我也希望将整个内容包含在代码中。我的switch {}示例可以很好地做到这一点,但是它并不十分灵活。
安德鲁

您可以将要为客户调用的操作存储在数组中,并使用客户ID为其建立索引。
qwerty_so

看起来更有希望。:)
安德鲁

3

类似于以下内容:

请注意,您在仓库中仍然有switch语句。但是,这并没有真正的解决方法,在某些时候,您需要将客户ID映射到所需的逻辑。您可以变得聪明起来,将其移到配置文件中,将映射放入字典中或动态加载到逻辑程序集中,但是从本质上讲,它可以转换。

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

听起来您从客户到自定义代码具有一对一的映射关系,并且由于您使用的是编译语言,因此每次获得新客户时都必须重新构建系统

尝试使用嵌入式脚本语言。

例如,如果您的系统使用Java,则可以嵌入JRuby,然后为每个客户存储相应的Ruby代码段。理想情况下,在相同或单独的git repo中受版本控制的某个位置。然后在Java应用程序的上下文中评估该代码段。JRuby可以调用任何Java函数并访问任何Java对象。

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

}

这是非常常见的模式。例如,许多计算机游戏都是用C ++编写的,但是使用嵌入式Lua脚本来定义游戏中每个对手的顾客行为。

另一方面,如果您有从客户到自定义代码的多对一映射,则只需使用已经建议的“策略”模式即可。

如果映射不是基于用户ID make,则match向每个策略对象添加一个函数,并有序选择要使用的策略。

这是一些伪代码

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

2
我会避免这种方法。趋势是将所有这些脚本片段放入db中,然后您将失去源代码控制,版本控制和测试。
伊万

很公平。他们最好将它们存储在单独的git repo中或任何地方。更新了我的回答,说代码段最好应该在版本控制下。
akuhn '16

它们可以存储在诸如属性文件之类的文件中吗?我试图避免在多个位置存在固定的Customer属性,否则很容易变成无法维护的意大利面。
安德鲁

2

我要逆流而行。

我将尝试使用ANTLR实现自己的表达语言。

到目前为止,所有答案都强烈基于代码定制。在我看来,为每个客户实施具体的类在将来的某个时刻将无法很好地扩展。维护将是昂贵且痛苦的。

因此,使用Antlr,其想法是定义自己的语言。您可以允许用户(或开发人员)以这种语言编写业务规则。

以您的评论为例:

我想写公式“结果= if(Payment.id.startsWith(“ R”))?Payment.percentage * Payment.total:Payment.otherValue”

使用EL,您应该可以陈述以下语句:

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

然后...

将其另存为客户的属性,然后将其插入适当的方法中?

你可以。这是一个字符串,您可以将其另存为属性或属性。

我不会撒谎 这是相当复杂和困难的。如果业务规则也很复杂,那就更难了。

这里有一些您可能会感兴趣的问题:


注意: ANTLR也会为Python和Javascript生成代码。这可能有助于编写概念证明而没有太多开销。

如果您发现Antlr太难了,可以尝试使用Expr4J,Jeval和Parsii之类的库。这些具有更高的抽象水平。


这是一个好主意,但对下一个家伙来说却是个维护难题。但是在评估并权衡所有变量之前,我不会放弃任何想法。
安德鲁

1
您拥有的许多选择都更好。我只想再给一个
Laiv 2016年

不能否认这是一种有效的方法,只看一下“最终用户可以自定义”的Websphere或其他现成的企业系统,但就像@akuhn的回答一样,缺点是现在您正在使用两种语言进行编程,并且失去了版本控制功能/ source control / change control
Ewan

@Ewan抱歉,我担心我不明白您的论点。语言描述符和自动生成的类可以是项目的一部分,也可以不是项目的一部分。但是无论如何,我不明白为什么我会失去版本控制/源代码管理。另一方面,似乎有2种不同的语言,但这只是暂时的。语言完成后,您只需设置字符串。像Cron表达式一样。从长远来看,不必担心。
2016年

“如果paymentID以'R'开始,则(paymentPercentage / paymentTotal)否则为paymentOther“将此存储在哪里?它是什么版本?用什么语言写的?您为此准备了什么单元测试?
伊万

1

您至少可以对算法进行外部化处理,以便使用称为“策略模式”的设计模式(位于四人组中)添加新客户时,无需更改Customer类。

从您提供的摘录中,策略模式是不太可维护还是更具可维护性是有争议的,但是它至少会消除Customer类对需要完成的工作的了解(并消除您的转换案例)。

StrategyFactory对象将基于CustomerID创建StrategyIntf指针(或引用)。工厂可以为非特殊客户返回默认实现。

客户类只需要向工厂询问正确的策略,然后调用它即可。

这是非常简短的伪C ++,向您展示我的意思。

class Customer
{
public:

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


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

该解决方案的缺点在于,对于每个需要特殊逻辑的新客户,您将需要创建CalculationsStrategyIntf的新实现并更新工厂以将其返回给合适的客户。这也需要编译。但是您至少要避免在客户类别中不断增长的意大利面条代码。


是的,我认为有人在另一个答案中提到了类似的模式,即每个客户将逻辑封装在实现Customer接口的单独类中,然后从PaymentProcessor类调用该类。这是有希望的,但这意味着每次我们吸引新客户时,我们都必须编写新的类-并不是什么大不了的事,但是支持人员不能做的事情。仍然是一个选择。
安德鲁

-1

用单个方法创建一个接口,并在每个实现类中使用lamda。或者,您可以匿名类为不同的客户端实现方法

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.