12

I have a class used to process customer payments. All but one of the methods of this class are the same for every customer, except for one that calculates (for example) how much the customer's user owes. This can vary greatly from customer to customer and there is no easy way to capture the logic of the calculations in something like a properties file, as there can be any number of custom factors.

I could write ugly code that switches based on 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
}

but this requires rebuilding the class every time we get a new customer. What is the better way?

[Edit] The "duplicate" article is completely different. I'm not asking how to avoid a switch statement, I'm asking for the modern design that best applies to this case -- which I could solve with a switch statement if I wanted to write dinosaur code. The examples provided there are generic, and not helpful, since they essentially say "Hey, the switch works pretty good in some cases, not in some others."


[Edit] I decided to go with the top-ranked answer (create a separate "Customer" class for each customer that implements a standard interface) for the following reasons:

  1. Consistency: I can create an interface that ensures all Customer classes receive and return the same output, even if created by another developer

  2. Maintainability: All code is written in the same language (Java) so there is no need for anyone else to learn a separate coding language in order to maintain what should be a dead-simple feature.

  3. Reuse: In case a similar problem crops up in the code, I can reuse the Customer class to hold any number of methods to implement "custom" logic.

  4. Familiarity: I already know how to do this, so I can get it done quickly and move on to other, more pressing issues.

Drawbacks:

  1. Each new customer requires a compile of the new Customer class, which may add some complexity to how we compile and deploy changes.

  2. Each new customer has to be added by a developer -- a support person can't just add the logic to something like a properties file. This is not ideal ... but then I also wasn't sure how a Support person would be able to write out the necessary business logic, especially if it is complex with many exceptions (as is likely).

  3. It won't scale well if we add many, many new customers. This is not expected, but if it does happen we'll have to rethink many other parts of the code as well as this one.

For those of you interested, you can use Java Reflection to call a class by name:

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);
13
  • 1
    Maybe you should elaborate on the type of calculations. Is it just a rebate being calculated or is it a different work flow?
    – user188153
    Dec 21, 2016 at 22:00
  • @ThomasKilian It's just an example. Imagine a Payment object, and the calculations can be something like "multiply the Payment.percentage by the Payment.total -- but not if Payment.id starts with 'R'". That kind of granularity. Every customer has their own rules.
    – Andrew
    Dec 21, 2016 at 22:20
  • Have you tried something like this. Setting different formulas for each customer.
    – Laiv
    Dec 21, 2016 at 22:49
  • 1
    The suggested plug-ins from various answers will only work if you have a dedicated-per-customer product. If you have a server solution where you check the customer id dynamically, it won't work. So what is your environment?
    – user188153
    Dec 22, 2016 at 10:57

8 Answers 8

14

I have a class used to process customer payments. All but one of the methods of this class are the same for every customer, except for one that calculates (for example) how much the customer's user owes.

Two options come to my mind.

Option 1: Make your class an abstract class, where the method which varies between customers is an abstract method. Then create a subclass for each customer.

Option 2: Create a Customer class, or an ICustomer interface, containing all of the customer-dependent logic. Instead of having your payment processing class accept a customer ID, have it accept a Customer or ICustomer object. Whenever it needs to do something customer-dependent, it calls the appropriate method.

1
  • The Customer class is not a bad idea. There's going to have to be some kind of custom object for each Customer, but I wasn't clear whether this should be a DB record, a properties file (of some kind), or another class.
    – Andrew
    Dec 22, 2016 at 0:24
10

You might want to look into writing the custom calculations as "plug-ins" for your application. Then, you'd use a configuration file to tell you program which calculation plugin should be used for which customer. This way, your main application wouldn't need to be recompiled for every new customer - it only needs to read (or re-read) a configuration file and load new plug-ins.

4
  • Thanks. How would a plug-in work in say a Java environment? For example, I want to write the formula "result = if (Payment.id.startsWith("R")) ? Payment.percentage * Payment.total : Payment.otherValue" save that as a property for the customer, and insert it in the appropriate method?
    – Andrew
    Dec 22, 2016 at 19:47
  • @Andrew, One way a plugin can work is to use reflection to load in a class by name. The config file would list the names of classes for customers. Of course, you'd have to have some way to identify which plugin is for which customer (eg, by its name or some metadata stored somewhere).
    – Kat
    Dec 30, 2016 at 17:44
  • @Kat Thank you, if you read my edited question that's actually how I'm doing it. The drawback is that a developer has to create a new class which limits scalability -- I'd prefer a support person could just edit a properties file but I think there's too much complexity.
    – Andrew
    Dec 30, 2016 at 19:26
  • @Andrew: You might be able to write your formulas in a properties file, but then you'd need some sort of expression parser. And you may one day run into a formula that is too complex to (easily) write as a one-line expression in a properties file. If you really want non-programmers to be able to work on these, you'll need some sort of user-friendly expression editor for them to use which will generate code for your application. This can be done (I've seen it), but it's not trivial. Jan 3, 2017 at 22:04
4

I'd go with a rule set to describe the calculations. This can be held in any persistence store and modified dynamically.

As an alternative consider this:

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

Where customerOpsIndex calculated the right operation index (you know which customer needs which treatment).

3
  • If I could create a comprehensive rule set I would. But each new customer might have their own set of rules. Also I would prefer the entire thing be contained in the code, if there is a design pattern to do this. My switch{} example does this quite nicely, but it's not very flexible.
    – Andrew
    Dec 21, 2016 at 22:23
  • You could store the operations to be called for a customer in an array and use the customer ID to index it.
    – user188153
    Dec 21, 2016 at 22:44
  • That looks more promising. :)
    – Andrew
    Dec 21, 2016 at 22:57
3

Something like the below :

notice you still have the switch statement in the repo. There's no real getting around this though, at some point you need to map a customer id to the required logic. You can get clever and move it to a config file, put the mapping in a dictionary or dynamically load in the logic assemblies, but it essentially boils down to switch.

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

Sounds like you have a 1 to 1 mapping from customers to custom code and because you're using a compiled language you have to rebuild your system each time you get a new customer

Try an embedded scripting language.

For example, if your system is in Java you could embed JRuby and then for each customer store a corresponding snippet of Ruby code. Ideally somewhere under version control, either in the same or in a separate git repo. And then evaluate that snippet in the context of your Java application. JRuby can call any Java function and access any Java object.

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

}

This is a very common pattern. For example, many computer games are written in C++ but use embedded Lua scripts to define the customer behavior of each opponent in the game.

On the other hand, if you have a many to 1 mapping from customers to custom code, just use the "Strategy" pattern as already suggested.

If the mapping is not based on user ID make, add a match function to each strategy object and make an ordered choice of which strategy to use.

Here is some pseudo code

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)
3
  • 2
    I would avoid this approach. The tendency is to put all those script snippets in a db and then you lose source control, versioning and testing.
    – Ewan
    Dec 22, 2016 at 10:00
  • Fair enough. They best store them in a separate git repo or wherever. Updated my answer to say that snippets should ideally be under version control.
    – akuhn
    Dec 22, 2016 at 10:10
  • Could they be stored in something like a properties file? I'm trying to avoid having fixed Customer properties exist in more than one location, otherwise it's easy to devolve into unmaintainable spaghetti.
    – Andrew
    Dec 22, 2016 at 20:42
2

I'm going to swim against the current.

I would try implementing my own expression language with ANTLR.

So far, all the answers are strongly based on code customization. Implementing concrete classes for each customer seems to me that, at some point in the future, is not going to scale well. The maintenance is going to be expensive and painful.

So, with Antlr, the idea is to define your own language. That you can allow users (or devs) to write business rules in such language.

Taking your comment as example:

I want to write the formula "result = if (Payment.id.startsWith("R")) ? Payment.percentage * Payment.total : Payment.otherValue"

With your EL, should be possible for you to state sentences like:

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

Then...

save that as a property for the customer, and insert it in the appropriate method?

You could. It's a string, you could save it as property or attribute.

I won't lie. It's quite complicated and hard. It's even harder if the business rules are complicated too.

Here some SO questions that may be of interest to you:


Note: ANTLR generates code for Python and Javascript too. That may help to write proofs of concept without too much overhead.

If you find Antlr to be too hard, you could try with libs like Expr4J, JEval, Parsii. These works with a higher level of abstraction.

6
  • It's a good idea but a maintenance headache for the next guy down the line. But I'm not going to discard any idea until I've evaluated and weighed all the variables.
    – Andrew
    Dec 22, 2016 at 23:32
  • 1
    The many options you have the better. I just wanted to give one more
    – Laiv
    Dec 22, 2016 at 23:37
  • Theres no denying that this is a valid approach, just look at websphere or other off the shelf enterprise systems that 'the end users can customise' But like @akuhn s answer the downside is that now you are programming in 2 languages and lose your versioning/source control/change control
    – Ewan
    Dec 23, 2016 at 9:21
  • @Ewan sorry, I'm afraid of I didn't understood your arguments. The language descriptiors and the classes autogenerated can be part or not of the project. But in any case I don't see why I could lose versioning/source code management. On another hand, might seem that there are 2 different languages, but that's temporary. Once the language is done, you only set strings. Like Cron expressions. At long run there's way less thing to be worried about.
    – Laiv
    Dec 23, 2016 at 9:35
  • "If paymentID startWith 'R' then (paymentPercentage / paymentTotal) else paymentOther" where do you store this? what version is it? what language is it written in? what unit tests have you got for it?
    – Ewan
    Dec 23, 2016 at 9:39
1

You can at least externalize the algorithm so that the Customer class does not need to change when a new customer is added by using a design pattern called the Strategy Pattern (it's in the Gang of Four).

From the snippet you gave, it's arguable whether the strategy pattern would be less maintainable or more maintainable, but it would at least eliminate the Customer class's knowledge of what needs to be done (and would eliminate your switch case).

A StrategyFactory object would create a StrategyIntf pointer (or reference) based on the CustomerID. The Factory could return a default implementation for customers that are not special.

The Customer class need only ask the Factory for the correct strategy and then call it.

This is very brief pseudo C++ to show you what I mean.

class Customer
{
public:

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


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

The drawback to this solution is that, for each new customer that needs special logic, you would need to create a new implementation of the CalculationsStrategyIntf and update the factory to return it for the appropriate customer(s). This also requires compiling. But you would at least avoid ever-growing spaghetti code in the customer class.

1
  • Yes, I think someone mentioned a similar pattern in another answer, for each customer to encapsulate the logic in a separate class that implements Customer interface and then call that class from the PaymentProcessor class. It's promising but it means every time we bring on a new Customer we have to write a new class -- not really a big deal, but not something a Support person can do. Still it's an option.
    – Andrew
    Dec 23, 2016 at 17:31
-1

Create an interface with single method and use lamdas in each implementation class. Or you can you anonymous class to implement the methods for different clients

Not the answer you're looking for? Browse other questions tagged or ask your own question.