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);
Andrew
  • 251

8 Answers8

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.

Sophie Swett
  • 1,349
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

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

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();
    }
}
Ewan
  • 83,178
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, ...)
akuhn
  • 727
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.

Laiv
  • 14,990
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.

Matthew James Briggs
  • 1,747
  • 3
  • 15
  • 23
-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