Design Pattern : chain of responsibility implementation in Java

The source code of design patterns can be viewed and downloaded here :
https://github.com/ebundy/java-examples/tree/master/design-patterns

What is it ?

The chain of responsibility design pattern is a behavioral pattern.
The pattern defines for a request to handle, a chain of objets which, turn-to-turn have the ability to stop the chain processing and to response to the request.

When using it ?

In a chain of responsibility, only a single object can take the responsibility to respond to the request. If we wish that more than one candidate to be able to act on the request, we stay away from the chain of responsibility pattern. The filter pattern meets better this need.

The chain of responsibility pattern is applied in multiple contexts : technical as business.
Handling user events in user interfaces is a common usage.
In this context, the chain allows graphical layers and components to handle user inputs such as mouse clicks, pressed keys, etc…
The idea is that graphical components are in a chain ordered from the most local component related to the user action (a button for example) to the less local component related to the user action (the frame component for example). As soon as a component of the chain handles the request, the chain flow is stopped.

To be original, I propose you to explore the chain of responsibility around another theme : decision tables.
First, we interest in a basic implementation of the pattern.
Then, we will see how to improve it.

Our use case : applying the best discount for our clients

Imagine we own a website selling products for professionals.
Like most online sellers, we propose discounts under conditions.
As we want our clients to come back regularly to place other orders, we have decided to compute and apply the best discount for our clients.
We have the following discount types :
– 5% off for happy hour
– 7.5% off for the first order
– 10% off for order 100€ or more
– 25% off for order 200€ or more
– 30% off for first order and order 200€ or more
– 15% off for 4 orders or more in one month

Without using a chain of responsibility, we have different ways to apply the best discount for our clients. Some are good, other less.
The most important is avoiding a series of conditional blocks ordered in a specific order to handle rules since it is error-prone and hard to maintain.
With this bad practice, when we insert, modify or delete a rule, we must move it at the convenient place according to its ranking in terms of discount.
Besides, when we delete a rule, we must be sure to remove all needed information for the deleted rule without impacting other rules. It may be complicated if some variables are shared between conditional blocs. For example, let me take these 3 discounts:
– 7.5% off for the first order
– 25% off for order 200€ or more
– 30% off for first order and order 200€ or more
A monolithic process could mix these rule implementations to avoid duplication. It would create dependencies between rules and therefore would prevent them from evolving independently.
For example, deleting a outer rule (7.5% off for first order) could create side effects in inner rules (25% off for order more 200€).

Alongside that, it would be more flexible to add or remove dynamically some rules of the chain.
For example, if some discounts can be applied only for some clients, it would be interesting at runtime to create the chain according the client type.

At runtime, the rules order should not be hard-coded. 
Discount percentages could be data stored and updated externally from the application.
So if a discount type sees its percentage modified, we should not need to modify the code, repackage the application and redeploy it.

Before presenting the implementation of the chain of responsibility, we will present a conceptual view of it.
It allows to understand the intent of the pattern. Implementation considerations are handled just after.

Conceptual view of our chain of responsibility :

The pattern is conceptually simple.
It is a linked list of rules where each concrete rule shares a common interface in order to manipulate them independently of their implementation.
The common interface provides a method to execute the rule logic : apply().
The rules logic needs to have contextual data to accomplish their task. So an instance of InputForDiscountRules is provided as parameter when the apply() method is invoked.

Beyond the conceptual view, implementing the chain asks several questions :
– How to assign a successor rule to a rule ?
– How to not repeat the common and boiler plate code of each rule? That is, if a rule doesn’t match, we have to try the next rule if it exists.
– How to create the whole chain ?

The participating structures to implement the decision table

We have 4 :
business classes representing a request for the decision table. InputForDiscountRules is the enclosing business object .
It contains all information needed for concrete rules : the client order that contains the total price and other business contextual information required to determinate if a rule should handle the request or not.
Besides, the order class provides a method for applying the discount. In this way, concrete rules have the possibility to apply the discount when it is applicable.
IDiscountRule interface standing for a rule. It offers two operations : setNextRule() to add the successor rule and apply() to allow the rule to handle the request if it matches to .
an abstract class, AbstractDiscountRule, implementing partially the IDiscountRule interface. It provides a minimal implementation of the rule concept but apply() which must be implemented by concrete classes. It provides applyNextRuleIfExist(), a helper method for concrete classes to go on the chain processing when they don’t respond to the request.
concrete rule classes. Each concrete class derives from AbstractDiscountRule. They implement the apply() method.

The following class diagram illustrates these explanations and more :

A first implementation of our decision table with a chain of responsibility

Order is the class representing an order as input for our rules in order to check rule matching.

package davidhxxx.teach.designpattern.cor.common;
 
public class Order {
 
    private float priceTotal;
 
    public Order(float priceTotal) {
	this.priceTotal = priceTotal;
    }
 
    public float getPriceTotal() {
	return priceTotal;
    }
 
    public void applyDiscountInPrct(float discountInPrct) {
	this.priceTotal = priceTotal * (1F - discountInPrct);
    }
 
}

For most rules, our decision table needs general information related to the client order history (order count, order count for current month, etc…), so we must have a mean to have this data when we want to try to apply a discount rule.
To achieve this, we dispose of a class gathering all information necessary to handle any rules.
Broadly, in our example, we have two ways to feed a instance of this class : eager loading mode which initially loads all needed information and a lazy loading mode which loads data on demand, so when it is useful for the current rule.
This issue being out of our scope, we put aside it and make the most simple : using an eager mode loading.

InputForDiscountRules, the class representing data needed to process rules

package davidhxxx.teach.designpattern.cor.common;
 
public class InputForDiscountRules {
 
    private Order order;
    private boolean isFirstOrder;
    private int orderCountInOneMonth;
    private boolean isHappyHour;
 
    public InputForDiscountRules(Order order, boolean isFirstOrder, int orderCountInOneMonth, boolean isHappyHour) {
	this.order = order;
	this.isFirstOrder = isFirstOrder;
	this.orderCountInOneMonth = orderCountInOneMonth;
	this.isHappyHour = isHappyHour;
    }
 
    public int getOrderCountInOneMonth() {
	return orderCountInOneMonth;
    }
 
    public Order getOrder() {
	return order;
    }
 
    public boolean isFirstOrder() {
	return isFirstOrder;
    }
 
    public boolean isHappyHour() {
	return isHappyHour;
    }
 
}

IDiscountRuleis the rule interface, the base contract for any rules of our chain

package davidhxxx.teach.designpattern.cor.common;
 
public interface IDiscountRule {
 
    public void setNextRule(IDiscountRule nextRule);
    public abstract boolean apply(InputForDiscountRules inputDataForDiscountRules);
 
}

AbstractDiscountRule, the abstract class of Rule with common implementation and helper methods

package davidhxxx.teach.designpattern.cor.common;
 
public abstract class AbstractDiscountRule implements IDiscountRule {
 
    protected IDiscountRule nextRule;
 
    public void setNextRule(IDiscountRule nextRule) {
	this.nextRule = nextRule;
    }
 
    public boolean applyNextRuleIfExist(InputForDiscountRules inputDataForDiscountRules) {
	if (this.nextRule != null) {
	    return this.nextRule.apply(inputDataForDiscountRules);
	}
	return false;
    }
 
}

Our concrete rules :

RuleApplyDiscountFirstOrder : 7.5% off for first order

package davidhxxx.teach.designpattern.cor;
 
import davidhxxx.teach.designpattern.cor.common.AbstractDiscountRule;
import davidhxxx.teach.designpattern.cor.common.InputForDiscountRules;
 
public class RuleApplyDiscountFirstOrder extends AbstractDiscountRule {
 
    public boolean apply(InputForDiscountRules inputDataForDiscountRules) {
	if (applyDiscountWhenFirstOrder(inputDataForDiscountRules)) {
	    return true;
	}
	return applyNextRuleIfExist(inputDataForDiscountRules);
 
    }
 
    private boolean applyDiscountWhenFirstOrder(InputForDiscountRules inputDataForDiscountRules) {
	if (inputDataForDiscountRules.isFirstOrder()) {
	    inputDataForDiscountRules.getOrder().applyDiscountInPrct(0.075F);
	    return true;
	}
	return false;
    }
 
}

RuleApplyDiscountHappyHour 5% off for Happy Hour

package davidhxxx.teach.designpattern.cor;
 
import davidhxxx.teach.designpattern.cor.common.AbstractDiscountRule;
import davidhxxx.teach.designpattern.cor.common.InputForDiscountRules;
 
public class RuleApplyDiscountHappyHour extends AbstractDiscountRule  {
 
    public boolean apply(InputForDiscountRules inputDataForDiscountRules) {
	if (applyDiscountWhenHappyHour(inputDataForDiscountRules)) {
	    return true;
	}
	return applyNextRuleIfExist(inputDataForDiscountRules);
    }
 
    private boolean applyDiscountWhenHappyHour(InputForDiscountRules inputDataForDiscountRules) {
	if (inputDataForDiscountRules.isHappyHour()) {
	    inputDataForDiscountRules.getOrder().applyDiscountInPrct(0.05F);
	    return true;
	}
	return false;
    }
 
}

RuleApplyDiscountOrderMore100Euros :10% off for order more 100€

package davidhxxx.teach.designpattern.cor;
 
import davidhxxx.teach.designpattern.cor.common.AbstractDiscountRule;
import davidhxxx.teach.designpattern.cor.common.InputForDiscountRules;
import davidhxxx.teach.designpattern.cor.common.Order;
 
public class RuleApplyDiscountOrderMore100Euros extends AbstractDiscountRule  {
 
    public boolean apply(InputForDiscountRules inputDataForDiscountRules) {
	if (applyDiscountWhenOrderMore100Euros(inputDataForDiscountRules)) {
	    return true;
	}
	return applyNextRuleIfExist(inputDataForDiscountRules);
 
    }
 
    private boolean applyDiscountWhenOrderMore100Euros(InputForDiscountRules inputDataForDiscountRules) {
	Order order = inputDataForDiscountRules.getOrder();
 
	if (order.getPriceTotal() > 100) {
	    order.applyDiscountInPrct(0.1F);
	    return true;
	}
	return false;
    }
 
}

RuleApplyDiscountOrderMore200Euros : 25% off for order more 200€

package davidhxxx.teach.designpattern.cor;
 
import davidhxxx.teach.designpattern.cor.common.AbstractDiscountRule;
import davidhxxx.teach.designpattern.cor.common.InputForDiscountRules;
import davidhxxx.teach.designpattern.cor.common.Order;
 
public class RuleApplyDiscountOrderMore200Euros extends AbstractDiscountRule  {
 
    public boolean apply(InputForDiscountRules inputDataForDiscountRules) {
	if (applyDiscountWhenOrderMore200Euros(inputDataForDiscountRules)) {
	    return true;
	}
	return applyNextRuleIfExist(inputDataForDiscountRules);
 
    }
 
    private boolean applyDiscountWhenOrderMore200Euros(InputForDiscountRules inputDataForDiscountRules) {
	Order order = inputDataForDiscountRules.getOrder();
 
	if (order.getPriceTotal() > 200) {
	    order.applyDiscountInPrct(0.25F);
	    return true;
	}
	return false;
    }
 
}

RuleApplyDiscountFirstOrderAndMore200Euros :30% off for first order and order more 200€

package davidhxxx.teach.designpattern.cor;
 
import davidhxxx.teach.designpattern.cor.common.AbstractDiscountRule;
import davidhxxx.teach.designpattern.cor.common.InputForDiscountRules;
import davidhxxx.teach.designpattern.cor.common.Order;
 
public class RuleApplyDiscountFirstOrderAndMore200Euros extends AbstractDiscountRule {
 
    public boolean apply(InputForDiscountRules inputDataForDiscountRules) {
	if (applyDiscountWhenOrderMore200Euros(inputDataForDiscountRules)) {
	    return true;
	}
	return applyNextRuleIfExist(inputDataForDiscountRules);
 
    }
 
    private boolean applyDiscountWhenOrderMore200Euros(InputForDiscountRules inputDataForDiscountRules) {
	Order order = inputDataForDiscountRules.getOrder();
 
	if (inputDataForDiscountRules.isFirstOrder() && order.getPriceTotal() > 200) {
	    order.applyDiscountInPrct(0.30F);
	    return true;
	}
	return false;
    }
 
}

RuleApplyDiscountMore4OrdersInOneMonth :15% off for more 4 orders in one month

package davidhxxx.teach.designpattern.cor;
 
import davidhxxx.teach.designpattern.cor.common.AbstractDiscountRule;
import davidhxxx.teach.designpattern.cor.common.InputForDiscountRules;
 
public class RuleApplyDiscountMore4OrdersInOneMonth extends AbstractDiscountRule  {
 
    public boolean apply(InputForDiscountRules inputDataForDiscountRules) {
	if (applyDiscountWhenMore4OrdersInOneMonth(inputDataForDiscountRules)) {
	    return true;
	}
	return applyNextRuleIfExist(inputDataForDiscountRules);
 
    }
 
    private boolean applyDiscountWhenMore4OrdersInOneMonth(InputForDiscountRules inputDataForDiscountRules) {
 
	if (inputDataForDiscountRules.getOrderCountInOneMonth() > 4) {
	    inputDataForDiscountRules.getOrder().applyDiscountInPrct(0.15F);
	    return true;
	}
	return false;
    }
 
}

It misses a last thing to make our pattern operational : creating the chain.
I propose to do it in the context of a unit test. It allows us to add assertions and to check that our chain of responsibility works as expected.

CorTest

package davidhxxx.teach.designpattern.cor.client;
 
import org.junit.Test;
 
import davidhxxx.teach.designpattern.cor.RuleApplyDiscountFirstOrder;
import davidhxxx.teach.designpattern.cor.RuleApplyDiscountFirstOrderAndMore200Euros;
import davidhxxx.teach.designpattern.cor.RuleApplyDiscountHappyHour;
import davidhxxx.teach.designpattern.cor.RuleApplyDiscountOrderMore100Euros;
import davidhxxx.teach.designpattern.cor.RuleApplyDiscountOrderMore200Euros;
import davidhxxx.teach.designpattern.cor.common.IDiscountRule;
import davidhxxx.teach.designpattern.cor.common.InputForDiscountRules;
import davidhxxx.teach.designpattern.cor.common.Order;
import junit.framework.Assert;
 
public class CorTest {
 
    @Test
    public void assertFirstAcceptedRuleAreAppliedInOrder() throws Exception {
 
	// fixture
	IDiscountRule firstRule = createChainOfRules();
 
	// CASE : more 200 euros applied and first order
	Order order = new Order(400F);
	InputForDiscountRules inputForDiscountRules = new InputForDiscountRules(order, true, 6, true);
	// action
	firstRule.apply(inputForDiscountRules);
	// assertion
	Assert.assertEquals(280F, order.getPriceTotal());
 
	// CASE : more 200 euros applied
	order = new Order(400F);
	inputForDiscountRules = new InputForDiscountRules(order, false, 6, true);
	// action
	firstRule.apply(inputForDiscountRules);
	// assertion
	Assert.assertEquals(300F, order.getPriceTotal());
 
	// CASE : more 100 euros applied
	order = new Order(200F);
	inputForDiscountRules = new InputForDiscountRules(order, false, 1, true);
	// action
	firstRule.apply(inputForDiscountRules);
	// assertion
	Assert.assertEquals(180F, order.getPriceTotal());
 
	// CASE : happy hour applied
	order = new Order(100F);
	inputForDiscountRules = new InputForDiscountRules(order, false, 1, true);
	// action
	firstRule.apply(inputForDiscountRules);
	// assertion
	Assert.assertEquals(95F, order.getPriceTotal());
 
	// CASE : no reduction applied
	order = new Order(100F);
	inputForDiscountRules = new InputForDiscountRules(order, false, 4, false);
	// action
	firstRule.apply(inputForDiscountRules);
	// assertion
	Assert.assertEquals(100F, order.getPriceTotal());
    }
 
    private IDiscountRule createChainOfRules() {
	IDiscountRule ruleFirstOrder = new RuleApplyDiscountFirstOrder();
	IDiscountRule ruleHappyHour = new RuleApplyDiscountHappyHour();
	IDiscountRule ruleOrderMore100Euros = new RuleApplyDiscountOrderMore100Euros();
	IDiscountRule ruleOrderMore200Euros = new RuleApplyDiscountOrderMore200Euros();
 
	// first node of the chain
	IDiscountRule firstRule = new RuleApplyDiscountFirstOrderAndMore200Euros();
 
	firstRule.setNextRule(ruleOrderMore200Euros);
	ruleOrderMore200Euros.setNextRule(ruleOrderMore100Euros);
	ruleOrderMore100Euros.setNextRule(ruleFirstOrder);
	ruleFirstOrder.setNextRule(ruleHappyHour);
	return firstRule;
    }
 
}

Improving the creation of our chain and its dispatching process

Our code works but on closer inspection, we notice three substantial drawbacks in our implementation :

– the chain creation is cumbersome.
– a rule cannot update its level of responsibility (here, it is bind to the discount applied) without side effects.
– each concrete rule must dispatch to the next rule if it does not apply the discount. If we forget to write this call to the next rule, we could accidentally stop the chain and so make a mistake in the discount applied for our clients.

The two first drawbacks can be corrected. For the first point, we can delegate the rules chain creation and its complexity to an dedicated class.
The second point (the ability to dynamically sort our chain) could be addressed thanks to a public method in our rule instance to get the discount rate and therefore to sort rules.
However, about the last drawback, we could improve it with a classic chain of responsibility pattern but it would not be done in a perfect way.
Indeed, in this pattern, each concrete rule has two responsibilities : acting if it takes its responsibility and if it’s not the case, it must dispatch the request to the next rule.
Don’t forget : patterns has advantages and drawbacks. According to the usage of them, drawbacks are more or less important.
In order to relieve the dispatching process from concrete rules, we could push up it in the hierarchy : in the abstract class AbstractDiscountRule.
Our IDiscountRule interface has two apply methods now. One method for the logic rule : apply() as previously. It’s an abstract method implemented by the concrete class but in this version, it has single responsibility : handling the request and returns true if it rules has matched and false else. Contrary to the first version, the apply() implementation must not call the next element of the chain.
In deed, it’s applyChain(), a new method which processes the rule chain. In reality, applyChain() does more. It orchestrates the apply() and the call to the next rule.
So, the chain client must now call applyChain() to process the chain and not apply() otherwise only the first rule would be processed.
This update allows to relieve concrete class implementations to handle the chain processing (forward to the next rule), but as side effects it increases responsibilities of IDiscountRule and AbstractDiscountRule and therefore their API.

As discussed, calling apply() or applyChain() is totally different. So, clients of this API should be careful when using it.
That’s why methods naming is very important, as well as the javadoc.

Below a updated class diagram illustrating the changes :

cor improved

Now, let’s go for updating classes.

IDiscountRule

package davidhxxx.teach.designpattern.cor.improved.common;
 
public interface IDiscountRule extends IDiscountPrct {
 
    void setNextRule(IDiscountRule nextRule);
 
    /**
     * It's the business rule.
     * must not be called by the client. It's called by applyRoot
     * 
     * @param inputDataForDiscountRules
     * @return
     */
    boolean apply(InputForDiscountRules inputDataForDiscountRules);
 
    /**
     * It's the chain rule processing
     * must be called by the client. It's the outer apply to process the whole rule chain
     * 
     * @param inputDataForDiscountRules
     * @return
     */
    boolean applyChain(InputForDiscountRules inputDataForDiscountRules);
 
 
}

In order to ease and secure our new API, AbstractDiscountRule marks as final the applyChain() method. In this way, classes inheriting from AbstractDiscountRule could not break the chain processing.

Our applyChain() is very simple. It does exactly what each concrete rule class did previously : call the concrete rule, return true if handled. Otherwise, call the next rule. But now, it’s factorized in the abstract class and it allows concrete classes to focus on their business only. So, it’s better.
We can see that the interfaces extends a new interface : IDiscountPrct. We will discuss about it just after.

AbstractDiscountRule

package davidhxxx.teach.designpattern.cor.improved.common;
 
public abstract class AbstractDiscountRule implements IDiscountRule {
 
    protected IDiscountRule nextRule;
 
    public void setNextRule(IDiscountRule nextRule) {
	this.nextRule = nextRule;
    }
 
    public final boolean applyChain(InputForDiscountRules inputDataForDiscountRules) {
	if (apply(inputDataForDiscountRules)) {
	    return true;
	}
 
	return applyNextRuleIfExist(inputDataForDiscountRules);
    }
 
    private boolean applyNextRuleIfExist(InputForDiscountRules inputDataForDiscountRules) {
	if (this.nextRule != null) {
	    return this.nextRule.applyChain(inputDataForDiscountRules);
	}
	return false;
    }
 
}

About the issue of the chain creation and to have the ability to sort automatically the elements of our decision chain, it will be handled thanks to the introduction of a new class, RuleChainFactory, which the main function is creating the rule chain ordered according our need : by decreasing discount applied.
As says before, in order to be able to sort our rules, rules must provide a public access to the applied discount by the rule. But Abstract rules  (IDiscountRule, AbstractDiscountRule) should be reusable in any treatments of our domain where an instance of InputForDiscountRules is the input for rule checking. For example, if we have new requirements and we need to sort our rules by discount price and no longer discount percent, getPrctDiscount() would be irrelevant.

So  IDiscountRule contract will not provide directly a access to the applied  percentage discount. Instead of it, we introduce a new Java interface IDiscountPrct that concrete rule classes implement.
It’s via this interface we provide the public access to the applied discount.

IDiscountPrct

package davidhxxx.teach.designpattern.cor.improved.common;
public interface IDiscountPrct {
   float getPrctDiscount();
}

RuleChainFactory

package davidhxxx.teach.designpattern.cor.improved.common;
 
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
 
public class RuleChainFactory {
 
    private RuleChainFactory() {
    }
 
    private static class Holder {
	private static RuleChainFactory instance = new RuleChainFactory();
    }
 
    public static RuleChainFactory getInstance() {
	return Holder.instance;
    }
 
    public IDiscountRule createDiscountChainOrderedByDecreasingDiscountApplied(IDiscountRule... rules) {
 
	if (rules.length < 2) {
	    throw new IllegalArgumentException("a chain must contain at least two rules");
	}
 
	List listOrdered = Arrays.asList(rules);
	Collections.sort(listOrdered, new DiscountRulesOrderedByInsterestClientComp());
 
	IDiscountRule prevRule = listOrdered.get(0);
	for (int i = 1; i < listOrdered.size(); i++) {
	    IDiscountRule currentRule = listOrdered.get(i);
	    prevRule.setNextRule(currentRule);
	    prevRule = currentRule;
	}
 
	return listOrdered.get(0);
    }
 
}

We perform the sort of discount rules with a Java comparator : DiscountRulesOrderedByInsterestClientComp.

DiscountRulesOrderedByInsterestClientComp

package davidhxxx.teach.designpattern.cor.improved.common;
 
import java.util.Comparator;
 
public class DiscountRulesOrderedByInsterestClientComp implements Comparator {
 
    public int compare(IDiscountPrct o1, IDiscountPrct o2) {
 
	if (o1.getPrctDiscount() > o2.getPrctDiscount()) {
	    return -1;
	}
 
	else if (o1.getPrctDiscount() < o2.getPrctDiscount()) {
	    return 1;
	}
 
	return 0;
    }
 
}

The separation of concerns in dedicated classes allows to get concrete rules oriented on their rule. Now, our rules are data-oriented and rule oriented and not chain process-oritented any longer. Another benefit of the separation : if we want to test unitary a concrete rule, it becomes straight : just check the rule behavior, not the chain dispatching.

RuleApplyDiscountFirstOrder

package davidhxxx.teach.designpattern.cor.improved;
 
import davidhxxx.teach.designpattern.cor.improved.common.AbstractDiscountRule;
import davidhxxx.teach.designpattern.cor.improved.common.InputForDiscountRules;
 
public class RuleApplyDiscountFirstOrder extends AbstractDiscountRule  {
 
    public boolean apply(InputForDiscountRules inputDataForDiscountRules) {
	if (inputDataForDiscountRules.isFirstOrder()) {
	    inputDataForDiscountRules.getOrder().applyDiscountInPrct(getPrctDiscount());
	    return true;
	}
	return false;
    }
 
    public float getPrctDiscount() {
	return 0.075F;
    }
 
}

We will not show all concrete rules here. It’s the same thing.
To get the runnable code of design patterns, you can download it from github.

The updated unit test shows how it’s easy to create the chain now.

ImprovedCorTest

package davidhxxx.teach.designpattern.cor.improve.client;
 
import org.junit.Test;
 
import davidhxxx.teach.designpattern.cor.improved.RuleApplyDiscountFirstOrder;
import davidhxxx.teach.designpattern.cor.improved.RuleApplyDiscountFirstOrderAndMore200Euros;
import davidhxxx.teach.designpattern.cor.improved.RuleApplyDiscountHappyHour;
import davidhxxx.teach.designpattern.cor.improved.RuleApplyDiscountOrderMore100Euros;
import davidhxxx.teach.designpattern.cor.improved.RuleApplyDiscountOrderMore200Euros;
import davidhxxx.teach.designpattern.cor.improved.common.IDiscountRule;
import davidhxxx.teach.designpattern.cor.improved.common.InputForDiscountRules;
import davidhxxx.teach.designpattern.cor.improved.common.Order;
import davidhxxx.teach.designpattern.cor.improved.common.RuleChainFactory;
import junit.framework.Assert;
 
public class ImprovedCorTest {
 
    @Test
    public void assertFirstAcceptedRuleAreAppliedInOrder() throws Exception {
 
	// fixture
	IDiscountRule firstRule = createChainOfRules();
 
	// CASE : more 200 euros applied and first order
	Order order = new Order(400F);
	InputForDiscountRules inputForDiscountRules = new InputForDiscountRules(order, true, 6, true);
	// action
	firstRule.applyChain(inputForDiscountRules);
	// assertion
	Assert.assertEquals(280F, order.getPriceTotal());
 
	// CASE : more 200 euros applied
	order = new Order(400F);
	inputForDiscountRules = new InputForDiscountRules(order, false, 6, true);
	// action
	firstRule.applyChain(inputForDiscountRules);
	// assertion
	Assert.assertEquals(300F, order.getPriceTotal());
 
	// CASE : more 100 euros applied
	order = new Order(200F);
	inputForDiscountRules = new InputForDiscountRules(order, false, 1, true);
	// action
	firstRule.applyChain(inputForDiscountRules);
	// assertion
	Assert.assertEquals(180F, order.getPriceTotal());
 
	// CASE : happy hour applied
	order = new Order(100F);
	inputForDiscountRules = new InputForDiscountRules(order, false, 1, true);
	// action
	firstRule.applyChain(inputForDiscountRules);
	// assertion
	Assert.assertEquals(95F, order.getPriceTotal());
 
	// CASE : no reduction applied
	order = new Order(100F);
	inputForDiscountRules = new InputForDiscountRules(order, false, 4, false);
	// action
	firstRule.applyChain(inputForDiscountRules);
	// assertion
	Assert.assertEquals(100F, order.getPriceTotal());
    }
 
    private IDiscountRule createChainOfRules() {
 
	IDiscountRule firstRule = RuleChainFactory.getInstance().createDiscountChainOrderedByDecreasingDiscountApplied(
		new RuleApplyDiscountFirstOrder(),
		new RuleApplyDiscountHappyHour(),
		new RuleApplyDiscountOrderMore100Euros(),
		new RuleApplyDiscountOrderMore200Euros(),
		new RuleApplyDiscountFirstOrderAndMore200Euros());
 
	return firstRule;
    }
 
}

Using another design pattern to address our need

For a precise need, there are multiple ways to implement it.
So, if the chain of responsibility addresses fully our need of decision table, other patterns may handle differently this need.
If you want to see how a mediator pattern can organize our decision table execution, you can read mediator implementation in Java which starts from this decision table business requirement (in progress).
It’s interesting to see how two patterns may apply in a good fashion to a same need but also how a pattern may have a little more advantages than another according what we need.

Ce contenu a été publié dans Non classé. Vous pouvez le mettre en favoris avec ce permalien.

4 réponses à Design Pattern : chain of responsibility implementation in Java

  1. Michel dit :

    Hello,

    It is a very nice blog post, congratulations.

    Do you think I can use this design patter to the following discount ?

    If the customer buys 2 units of product X he can buy product Y with 50% discount.

    • davidhxxx dit :

      Hello Michel,

      Thank you.
      You could indeed compute the discount as in a real domain, the products and their quantity of an order should be in the Order class.
      In my example, these don’t appear in the Order class as I have simplified by declaring only the total field.
      So, you could create a specific discount rule that uses them to compute the discount. Now, the chain of responsibility makes sense only if you have a set of discount rules and you want to apply one of them for your order if it matches with your condition. If you have only one rule, using the whole pattern is not relevant as it is not a chain (of rules) any longer but one rule.

  2. Michel dit :

    Thank you David for your answer.
    I need to give two discounts and it can happen in the same order, the discount rules are:

    If the customer buys 2 units of product X he can buy product Y with 50% discount
    The product Z has 10% discount this week

    I am looking for the best design pattern, what do you think ?

    • davidhxxx dit :

      Michel,

      If the discounts are cumulative in a same order (that is you want to apply all of them if all are applicable), the COR pattern is not helpful. You could simply use the iterator pattern with a List of rules and take advantage of the interface and implementations mechanisms.
      With Java, as you know, iterator for List is provided out of the box with Collection classes.

      So finally you could :
      1) Create your list of rules (2 rules for now) with an ArrayList for example
      2) When you want to compute the discounts on one order : iterate on the list of rules to apply discounts on the order.
      That’s all.

      Of course introducing an interface to represent a rule as I used in my example is a good thing. It allows to create as many rule subclasses as required rules. With this design, you could easily add or remove rules without side effect between them.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *