47

Are there any known design patterns for implementing discount models?

By discount models, I mean the following:

  1. If a customer buys Product X, Product Y and Product Z he gets a discount of 10% or $100.

  2. If a customer buys 100 units of Product X, he gets a discount of 15% or $500

  3. If a customer has brought in the last year more than $100K, he gets a flat 20% discount

  4. If a customer has purchased 2 units of Product X, he gets 1 unit of Product X (or Product Y) free.

  5. ...

Is there a generic pattern that can be applied to handle all of the above scenarios? I am thinking of a few models, but unable to come across a generic one.

4
  • IIRC all the tutorials I've seen with examples involving discounts (there are many) suggest Strategy pattern
    – gnat
    Commented Jan 18, 2012 at 11:49
  • 2
    @Kanini Is this a real-world problem? In such a case is this system in real-time or deferred-time? Are these rules presented as a rule-engine or database values? Is the search for discounts hierarchical based on priority? Strategy pattern would work in most of the cases but your rules must be factored out to make it work
    – Ubermensch
    Commented Jan 18, 2012 at 12:56
  • 4
    Also if someone purchases 2 units of Product X, one Product Y, and one Product Z, would be get both 10% and an extra product X?
    – Ubermensch
    Commented Jan 18, 2012 at 13:04
  • @gnat some links to some of those tutorials please.
    – user16764
    Commented Jan 18, 2012 at 18:22

7 Answers 7

22

If the issue is that you need to apply multiple discounts, under given circumstances, you might want to consider the Chain of Responsibility pattern.

In a nutshell, you pass the information you want to process into the first processor, and it decides from there whether to pass it on to further processors before returning the result.

This way you can change the structure and sequence of the processors without ever changing the calling code.

3
  • Chain of Responsibility is another good one. Might even be coupled with a strategy. In a case where only one discount can be applied, each strategy is chained with another. Each chain computes its discount (if the customer is eligible), compares it to the previous discount, and passes the customer, order, and discount data to the next chain. +1.
    – Thomas Owens
    Commented Jan 18, 2012 at 12:38
  • 2
    Just my opinion, but I find it more likely that "Chain of Responsibility" is over-designing for this case. A simple list of discount models (if needed, with an ordering numer) should do it. The list itself is independent from the customer and his orders, since all customers are to be treated equally. "Chain of responsibilty" is more appropriate when the discount model list changes very frequently at run time in a highly dynamic manner.
    – Doc Brown
    Commented Jan 19, 2012 at 6:51
  • dofactory.com/Patterns/PatternChain.aspx Is broken! Commented Sep 11, 2020 at 6:45
14

Well, I would design a discount model as a pair "Precondition" and "Discount", where "Precondition" is a class with methods

  bool IsFulfilled(Customer c);

or / and

  bool IsFulfilled(Customer c, Order o);

and Discount has a method void ApplyTo(Customer c). This gives you the ability to combine any type of precondition with any type of discount (I think this is a form of "bridge pattern").

If you have a fixed number of preconditions, then you can solve the problem by building specific subtypes (strategy pattern). However, when your preconditions are allowed to be very complex, with logical statements like AND, OR and NOT, you may better implement some kind of rule interpreter for the conditions. The rules can be a plain text string written in a simple "domain specific language".

The same goes for the "Discount" class, you can either have some subtypes for different types of discounts, or a general approach where the discount rules are given in text form, evalued by some interpreter.

1
  • My intuition suggests this could be what he is looking for in the context of the question
    – Ubermensch
    Commented Jan 18, 2012 at 13:08
13

Strategy, Decorator, and State patterns stand out to me as potential starting points. State might be particularly useful for 2 or 3, since 2 is dependent on the state of the order and 3 is dependent on the state of the customer within a time period. Strategy and Decorator stand out for the others, since you can use strategy to implement multiple order-price calculation algorithms and decorator to add new discounts to the order.

However, remember that design patterns are just models. There might not be a single pattern that applies, but rather a systems of patterns. Also consider making modifications to the described models to make them better fit your solution. It's better to have a good design than to force a pattern where it doesn't necessarily help just for the sake of being able to say you have a pattern.

5
  • That's not really what the state pattern is intended to do though, is it?
    – pdr
    Commented Jan 18, 2012 at 12:14
  • @pdr I don't see why not. But it depends on your implementation. If your customer object tracks customer-dependent discounts, then there might be an operation to return the discounts the customer is eligible for. As the customer buys things, the attributes change and this method implementation changes via the state pattern.
    – Thomas Owens
    Commented Jan 18, 2012 at 12:21
  • 1
    Hmm, I see what you mean. I think it depends if the customer is a semi-permanent object in the application, or just something that lives in the database and needs updating. It's not clear from the question, so fair enough. +1
    – pdr
    Commented Jan 18, 2012 at 12:25
  • 4
    From my experiences, these kinds of discounting business rules get modified all the time by fickle marketing/sales departments. There is a big need to make them data driven and user modifiable rather than entirely code driven. How would this affect the choice of model?
    – jfrankcarr
    Commented Jan 18, 2012 at 14:56
  • @jfrankcarr In my mind, it wouldn't. I'd populate the values for sets of items that lead to a discount, the percentages off, and so forth from some kind of configuration. Sort of dynamically building my state machine transitions and attributes of my decorators and strategies.
    – Thomas Owens
    Commented Jan 18, 2012 at 15:14
5
  • Probably need a IDiscount interface as all the different discounts are the same thing, and we will want to deal with them conceptually as generic discounts.

  • The "this customer's order" class probably needs a collection of Discounts. List? Hash? Linked List? Don't care, yet. Discounts apply to the purchase, not the customer!

  • Keep Discount instance building separate from Customer, Shopping Cart, History, etc. It will change a lot - as @jfrankcarr pointed out.

  • Probably a different class for each discount as the algorithm and parameters for each discount vary wildly and unpredictably.

  • I see a lot of event handling as discount calculating responds to shopping cart changes, and vice versa.

Design Pattern Application

  • I see a strategy pattern. IDiscount is the interface for implementing different discount algorithms.
  • I see a factory. Certainly not a full-blown abstract factory pattern, but a single class at this point in the analysis. Reasonably, there must be a single place where there is sufficient context to decide what discounts apply and to then create them. One class. If the rules for applying discounts later explodes due to a Marketing Department mushroom party, any additional Discount Construction logic must still coalesce in this base factory class, I think.

  • I can see Chain of Responsibility. This is not mutually exclusive to the factory idea. Instead of iterating a discount collection, each discount calls the next guy. The "customer's order" class does not hold a collection of discounts in this case.

  • The "hmmmm...." factor in Chain of Responsibility, I think, is that each Discount has a reference to the next. The implication is that order matters. Which is not the case. Also, the concept CoR embodies is that one object cannot handle the request so it is passed "up to the next higher authority." Our model is different. The only request is to calculate. Every discount does this. The output or effect may be null but every discount calculates. I instinctively lean toward a higher real-world fidelity.

Assumptions

  • Discounts are based on current shopping cart and/or purchase history
  • Zero or more discounts may apply. There are no mutually exclusive discounts
  • Proper calculation is not dependent on the order in which discounts are applied.

What changes, what stays the same?

  • Discounts are very different. Different number and kind of parameters to make up each rule.

  • Arguments for qualifying discounts change as the shopping cart changes.

  • The number of discounts available changes

  • The discounts this customer qualifies for changes as his shopping cart changes.

  • Shopping history does not change in the context of this purchase

  • Total cost changes dynamically as a function of purchase lines and discounts applied.

  • After initial application a discount's output may change, as purchase quantity changes for example.

2
  • Great and complete answer... BUT I only have a concern and is that the assumptions section should not be there, at least not to lead the answer. The whole idea is that the pattern helps to exactly give confort and forget about the "assumptions", the generic rule doesn't need to know of how the calculations are made, and what any detailed implementation uses from the context (Customer, Cart items, Day Time, Season, so on). Really help full though
    – le0diaz
    Commented Nov 27, 2017 at 15:28
  • The up front bullets and 'Assumptions' section are just my "show your work" reasoning about the problem itself which of course has a bering on the discount model design. For example my assumptions about discount execution order and interdependence lead me to de-emphasize Chain of Responsibility. Note that I am thinking about the pattern's intent, not the complexity as @docbrown does. I'm an exuberant proponent for reflecting intent in design.
    – radarbob
    Commented Aug 8, 2018 at 18:00
1

Logically, a discount model can be anything, so you can't assume you can program all cases in advance. Nor can anyone answering your question be completely sure what you actually need. However, assuming you get the usual kinds of discounts found in the real world...

A big question is whether the discounts will be programmed, or if you want users to enter them. As mentioned above, you can't never have them be programmed, but usually the goal is to try to make it more data entry like for common cases, rather than programming them all. This applies to some extent even if programmers are used to create all the discounts.

Martin Fowler mentions "Individual Instance Method" in "Analysis patterns: reusable object models" as part of how to implement "Posting Rules" for accounting systems, but the rules seem fairly similar to yours. I'd give more detail but it's a copyrighted work and

For a user interface, you either need to come up with use-cases that are fairly simple, or else build an interpreter and query builder. Possibly both, one for simple cases and one more advanced. If you do write an interpreter, this is likely a fairly good case for using the Interpreter pattern, since it's relatively simple to code compared to a parser generator, and the slower parse time probably won't really matter. (If you like using parser generators don't let me stop you).

Don't try to do everything with an interpreter though - at some point you are just programming in your own crummy language, so you might as well use a real one. If your interpreted language supports functions (it probably should support calling them - defining them is dubious) those can be coded in a real language. Don't go further down this road than you have to.

No matter what you do, eventually someone will want the discount to be based on whether they purchased within 30 business days of a promotion - where business days count only if there was no holiday in the region defined by either the store's postal code or the client's postal code. So don't try to design the perfect system in advance - assume you will sometimes need to write code for new kinds of discounts and design accordingly.

0

Is there is any point is asking if there is a useful pattern for this? What type of pattern is required - structural or behavioral?

Ideally, if i were to write a software for this, all it will take is an algorithm. A simple function that calculates total discount as follows:

cart.calculateDiscount(productVector);

You don't quite need anything more than this!

To clarify: I understand that there will be many rules - most basic of such representation should be in the form of a rulebase (set of data attributes and resultant discount against it) and the function such as above would iterate it to compute it. If rules gets added or removed you shouldn't end up changing the code- just change the rule base.

Pattern will be required only if we need different objects needs to access API's of each other or communicate to put a task in place.

PS: Think about it - when firewall processes packets and passes or rejects packets (or modifies it) - what design pattern does it use? The answer is NONE of the above described!

1
  • Of course you need more than that!!!. The idea is that the algorithm is not tighly coupled with the code implementation. If you check the scenarios is highly probable that more scenarios will come up, also you did somehow mentioned it, he doesn't really what any other 'Rule' will depend on. Its naive to think that a rule will only depend on the product list, but in reality depends on customer, time, season and so on. Dont know what Firewall implementation you checked, but the ones I've check DO HAVE many structural/design patterns
    – le0diaz
    Commented Nov 27, 2017 at 15:22
0

I was asked a similar question in an interview. The exact requirement was

Design a shopping cart with products and coupons and calculate the net price after applying coupons on products. Coupons can be of different types with certain conditions.

  1. N% off that is 10% off for all the individual
  2. P% off on next item
  3. D% off on next item of Type T.

Sequentially apply all the coupons on the cart and get the Total amount.

I could come up with following solution. I used CoR pattern. With little changes to Coupon implementation point no. 1,2 and 4 can be achieved. For point no. 3, Customer's history object can be made available to the Cart and
can be passed to Coupon/Discount chain.

import java.util.ArrayList;
import java.util.List;

interface Cart {
    List<CartItem> getItems();
    // void setItems(Set<CartItem> cartItems);
}
class ShoppingCart implements Cart {
    private List<CartItem> items = new ArrayList<>();
    boolean addItem(CartItem item) {
        return items.add(item);
    }
    boolean removeItem(CartItem item) {
        items.remove(item);
        return true;
    }
    public List<CartItem> getItems() {
        return items;
    }

    public double totalCartValue() {
        double total = 0;
        for (CartItem item : items) {
            total += item.getPrice();
        }
        return total;
    }
}
interface CartItem  {
    public String getName();
    public double getPrice();
    public void setPrice(double price);
}
abstract class Product implements CartItem {
    String name;
    double price;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
}
class Card extends Product {
    public Card(String name, double price) {
        this.name = name;
        this.price = price;
    }
}
class Tshirt extends Product {
    public Tshirt(String name, double price) {
        this.name = name;
        this.price = price;
    }
}
class BackPack extends Product {
    public BackPack(String name, double price) {
        this.name = name;
        this.price = price;
    }
}
interface Coupon {
    void apply(Cart cart);
    void setSuccessor(Coupon successor);
}
class CouponAll implements Coupon {
    private static final float DISCOUNT = 0.25f;
    Coupon successor;
    public void setSuccessor(Coupon successor) {
        this.successor = successor;
    }
    public void apply(Cart cart) {
        for (CartItem item : cart.getItems()) {
            item.setPrice(item.getPrice() - item.getPrice() * DISCOUNT);
        }
        if (successor != null) {
            successor.apply(cart);
        }
    }
}
class CouponNext implements Coupon {
    private static final float DISCOUNT = 0.10f;
    private int discountItemIndex;
    Coupon successor;
    CouponNext(int discountItemIndex)   {
        this.discountItemIndex = discountItemIndex;
    }
    public void setSuccessor(Coupon successor) {
        this.successor = successor;
    }
    public void apply(Cart cart) {
        if(discountItemIndex < cart.getItems().size())  {
            CartItem cartItem = cart.getItems().get(discountItemIndex);
            cartItem.setPrice(cartItem.getPrice() - cartItem.getPrice() * DISCOUNT);
        }
        if (successor != null) {
            successor.apply(cart);
        }
    }
}
class CouponNextBackPack implements Coupon {
    private static final float DISCOUNT = 0.15f;
    private int discountItemStartIndex;
    Coupon successor;
    Class<?> productType;
    CouponNextBackPack(int discountItemStartIndex ,Class<?> productType)    {
        this.discountItemStartIndex = discountItemStartIndex;
        this.productType = productType;
    }
    public void setSuccessor(Coupon successor) {
        this.successor = successor;
    }
    public void apply(Cart cart) {
        if (discountItemStartIndex < cart.getItems().size()) {
            for (int i = discountItemStartIndex; i <  cart.getItems().size(); ++i) {
                CartItem cartItem = cart.getItems().get(i);
                if(productType.isInstance(cartItem))    {
                    cartItem.setPrice(cartItem.getPrice() - cartItem.getPrice() * DISCOUNT);
                    break;
                }
            }
        }
        if (successor != null) {
            successor.apply(cart);
        }
    }
}
public class DriverClassCoR {
    public static void main(String[] args) {
        Product card1 = new Card("Card", 12.99);
        Product card2 = new Card("Card", 12.99);
        Product card3 = new Card("Card", 12.99);
        Product tshirt1 = new Tshirt("Tshirt", 24.99);
        Product tshirt2 = new Tshirt("Tshirt", 24.99);
        Product backPack1 = new BackPack("BackPack", 34.99);
        ShoppingCart cart = new ShoppingCart();
        cart.addItem(card1);        cart.addItem(card2);        cart.addItem(card3);
        Coupon couponAll = new CouponAll();
        cart.addItem(tshirt1);
        Coupon couponNext = new CouponNext(cart.getItems().size());
        couponAll.setSuccessor(couponNext);
        Coupon couponNextBackPack =null;
        couponNextBackPack = new CouponNextBackPack(cart.getItems().size(),  BackPack.class);
        couponNext.setSuccessor(couponNextBackPack);
        cart.addItem(tshirt2);
        cart.addItem(backPack1);
        System.out.println("Total car value before discounts \t" +cart.totalCartValue());
        couponAll.apply(cart);
        //System.out.println(backPack1.getClass().isInstance(backPack1));
        System.out.println("Total car value after discounts \t" +cart.totalCartValue());
    }

}

Hope this helps.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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