30 Decorator Pattern How to Optimize Sophisticated Product Pricing Strategies in E Commerce Systems

30 Decorator pattern How to optimize sophisticated product pricing strategies in e-commerce systems #

Hi, I’m Liu Chao.

Before we start today’s lesson, I’d like you to think about a question. Imagine that there is a requirement to design a decoration feature, where users can dynamically choose different decoration options to decorate their houses. For example, basic features like electrical wiring, ceiling, and wall painting, are available, but additional features like window curtain design or ceiling design might not be needed by all users. These additional features should be dynamically added. Furthermore, if there are new decoration options, they should also be dynamically added. If you were responsible for this, how would you design it?

At this point, you might think that typically, adding features to an object can be done by either directly modifying the code within the object or by deriving appropriate sub-classes to extend it. However, the former requires modifying the object’s code every time, which is not an ideal object-oriented design. Even if the latter is achieved by extending through sub-classes, it would be difficult to meet complex requirements for arbitrary combinations of features.

In such cases, using the decorator pattern is probably the most suitable solution. I think you may have some understanding of its advantages, so I’ll summarize them here.

The decorator pattern allows for dynamically adding decoration options to an object. It adds functionality to an object from its external interface, thus providing great flexibility for extension. With this pattern, we can add new features to an object without modifying the original code. In addition, the decorator pattern also enables dynamic composition of objects, allowing us to flexibly match the required features to dynamically composed objects.

Now, let’s take a closer look at the advantages of this pattern through practice.

What is the Decorator Pattern? #

Before we dive into the Decorator Pattern, let me briefly explain what it is. The Decorator Pattern consists of several roles: interface, concrete object, decorator class, and concrete decorator class.

The interface defines the implementation methods of the concrete object. The concrete object defines some initialization operations. For example, in a home decoration scenario, the installation of utilities, ceiling, and wall painting are all initialization operations. The decorator class is an abstract class that initializes the concrete object. Other concrete decorator classes inherit from this abstract class.

Now let’s use the Decorator Pattern to implement the home decoration functionality. The code is as follows:

/**
 * Define a basic decoration interface
 */
public interface IDecorator {
    
    /**
     * Decoration method
     */
    void decorate();
 
}

/**
 * Basic decoration class
 */
public class Decorator implements IDecorator{
 
    /**
     * Basic implementation method
     */
    public void decorate() {
        System.out.println("Installing utilities, ceiling, and painting the walls...");
    }
 
}

/**
 * Base decorator class
 */
public abstract class BaseDecorator implements IDecorator{
    
    private IDecorator decorator;
    
    public BaseDecorator(IDecorator decorator) {
        this.decorator = decorator;
    }
    
    /**
     * Call the decoration method
     */
    public void decorate() {
        if(decorator != null) {
            decorator.decorate();
        }
    }
}

/**
 * Curtain decorator class
 */
public class CurtainDecorator extends BaseDecorator{
 
    public CurtainDecorator(IDecorator decorator) {
        super(decorator);
    }
    
    /**
     * Specific curtain decoration method
     */
    @Override
    public void decorate() {
        System.out.println("Installing curtains...");
        super.decorate();
    }
 
}

public static void main( String[] args )
{
    IDecorator decorator = new Decorator();
    IDecorator curtainDecorator = new CurtainDecorator(decorator);
    curtainDecorator.decorate();
}

Output:

Installing curtains...
Installing utilities, ceiling, and painting the walls...

Through this example, we can understand that if we want to add new decorative functionality to the base class, we only need to implement a subclass based on the abstract class BaseDecorator, call the parent class through the constructor, and override the decoration method to implement the functionality of installing curtains. In the main function, we instantiate the decorator class and call the decoration method to obtain the curtain decoration functionality on top of the basic decoration.

The code structure of the decoration functionality implemented based on the Decorator Pattern is concise and easy to read. The business logic is also very clear. If we need to extend additional decorating functionality, we only need to add a subclass that inherits from the abstract decorator class.

In this example, we have only implemented business extension functionality. Next, I will use the Decorator Pattern to optimize the item pricing strategy in an e-commerce system, allowing for flexible combinations of different promotional activities.

Optimizing the Product Pricing Strategy in E-commerce Systems #

I believe you are familiar with the various types of discounts, vouchers, coupons, and special deductions that are frequently used when purchasing goods. From a development perspective, implementing these features can be quite complex.

For example, during the annual “Singles’ Day” shopping extravaganza, developers often have to design combinations of vouchers + limited-time discounts or vouchers + coupons to provide multiple discounts. At other times, for various reasons, merchants may offer special deduction coupons to customers. This can be combined with other discounts to create different combinations.

The fastest and most common way to implement these combination discounts is through using a large number of if-else statements. However, this approach involves a lot of logic, making it difficult for other developers to understand the business requirements. Furthermore, any changes to the discount strategy or price combinations would require modifying the code logic.

Here, the decorator pattern, which was mentioned earlier, is very suitable. Its independent and freely combinable nature, as well as its ability to easily extend functionality, can effectively overcome the drawbacks of using if-else statements. Now, let’s use the decorator pattern to implement an optimized pricing strategy for products.

First, let’s create classes for orders and product attributes. In this example, to keep things concise, I have only included a few key fields. The important relationships between these attributes are as follows: the main order contains multiple detailed orders, the detailed orders record product information, and the product information includes promotion type information. A product can have multiple promotion types (this example only discusses single promotions and combination promotions).

/**
 * Main order
 */
public class Order {
    
    private int id; // order ID
    private String orderNo; // order number
    private BigDecimal totalPayMoney; // total payment amount
    private List<OrderDetail> list; // detailed order list
}

/**
 * Detailed order
 */
public class OrderDetail {
    private int id; // detailed order ID
    private int orderId;// main order ID
    private Merchandise merchandise; // product details
    private BigDecimal payMoney; // payment price
}

/**
 * Product
 */
public class Merchandise {
    
    private String sku; // product SKU
    private String name; // product name
    private BigDecimal price; // product price
    private Map<PromotionType, SupportPromotions> supportPromotions; // supported promotion types
}

/**
 * Promotion type
 */
public class SupportPromotions implements Cloneable{
 
    private int id; // promotion ID for the product
    private PromotionType promotionType; // promotion type 1\coupon 2\red packet
    private int priority; // priority
    private UserCoupon userCoupon; // coupon received by the user for the product
    private UserRedPacket userRedPacket; // red packet received by the user for the product
    
    // Override the clone method
    public SupportPromotions clone(){
        SupportPromotions supportPromotions = null;
        try{
            supportPromotions = (SupportPromotions)super.clone();
        }catch(CloneNotSupportedException e){
            e.printStackTrace();
        }
        return supportPromotions;
    }
}

/**
 * Coupon
 */
public class UserCoupon {
    
    private int id; // coupon ID
    private int userId; // user ID receiving the coupon
    private String sku; // product SKU
    private BigDecimal coupon; // coupon amount
}

/**
 * Red packet
 */
public class UserRedPacket {
 
    private int id; // red packet ID
    private int userId; // user receiving the red packet
    private String sku; // product SKU
    private BigDecimal redPacket; // red packet amount
}

Next, let’s create an interface and a base class for calculating the payment amount:

/**
 * Interface for calculating the payment amount
 */
public interface IBaseCount {
    
    BigDecimal countPayMoney(OrderDetail orderDetail);
}

/**
 * Base class for payment calculation
 */
public class BaseCount implements IBaseCount{
 
    public BigDecimal countPayMoney(OrderDetail orderDetail) {
        orderDetail.setPayMoney(orderDetail.getMerchandise().getPrice());
        System.out.println("The original price of the product is: " + orderDetail.getPayMoney());
        
        return orderDetail.getPayMoney();
    }
}

Then, let’s create an abstract class for calculating the payment amount, which calls the base class:

/**
 * Abstract class for calculating the payment amount
 */
public abstract class BaseCountDecorator implements IBaseCount{
    
    private IBaseCount count;
public BaseCountDecorator(IBaseCount count) {
    this.count = count;
}

public BigDecimal countPayMoney(OrderDetail orderDetail) {
    BigDecimal payTotalMoney = new BigDecimal(0);
    if(count!=null) {
        payTotalMoney = count.countPayMoney(orderDetail);
    }
    return payTotalMoney;
}
}

Then, we implement the desired decorator classes (coupon calculation class, red packet calculation class) by inheriting the abstract class:

/**
 * Calculate the amount after using a coupon
 */
public class CouponDecorator extends BaseCountDecorator {

    public CouponDecorator(IBaseCount count) {
        super(count);
    }

    public BigDecimal countPayMoney(OrderDetail orderDetail) {
        BigDecimal payTotalMoney = new BigDecimal(0);
        payTotalMoney = super.countPayMoney(orderDetail);
        payTotalMoney = countCouponPayMoney(orderDetail);
        return payTotalMoney;
    }

    private BigDecimal countCouponPayMoney(OrderDetail orderDetail) {

        BigDecimal coupon =  orderDetail.getMerchandise().getSupportPromotions().get(PromotionType.COUPON).getUserCoupon().getCoupon();
        System.out.println(" Coupon amount: " + coupon);

        orderDetail.setPayMoney(orderDetail.getPayMoney().subtract(coupon));
        return orderDetail.getPayMoney();
    }
}

/**
 * Calculate the amount after using a red packet
 */
public class RedPacketDecorator extends BaseCountDecorator {

    public RedPacketDecorator(IBaseCount count) {
        super(count);
    }

    public BigDecimal countPayMoney(OrderDetail orderDetail) {
        BigDecimal payTotalMoney = new BigDecimal(0);
        payTotalMoney = super.countPayMoney(orderDetail);
        payTotalMoney = countCouponPayMoney(orderDetail);
        return payTotalMoney;
    }

    private BigDecimal countCouponPayMoney(OrderDetail orderDetail) {

        BigDecimal redPacket = orderDetail.getMerchandise().getSupportPromotions().get(PromotionType.REDPACKED).getUserRedPacket().getRedPacket();
        System.out.println(" Red packet discount: " + redPacket);

        orderDetail.setPayMoney(orderDetail.getPayMoney().subtract(redPacket));
        return orderDetail.getPayMoney();
    }
}

Finally, we combine the promotion types of the merchandise through a factory class:

/**
 * Calculate the payment price after promotion
 */
public class PromotionFactory {

    public static BigDecimal getPayMoney(OrderDetail orderDetail) {

        // Get the promotion types set for the merchandise
        Map<PromotionType, SupportPromotions> supportPromotionslist = orderDetail.getMerchandise().getSupportPromotions();

        // Initialize the calculation class
        IBaseCount baseCount = new BaseCount();
        if(supportPromotionslist!=null && supportPromotionslist.size()>0) {
            for(PromotionType promotionType: supportPromotionslist.keySet()) {
                // Combine the promotion types using decorators
                baseCount = protmotion(supportPromotionslist.get(promotionType), baseCount);
            }
        }
        return baseCount.countPayMoney(orderDetail);
    }

    /**
     * Combine the promotion types
     */
    private static IBaseCount protmotion(SupportPromotions supportPromotions, IBaseCount baseCount) {
        if(supportPromotions.getPromotionType()==PromotionType.COUPON) {
            baseCount = new CouponDecorator(baseCount);
        } else if(supportPromotions.getPromotionType()==PromotionType.REDPACKED) {
            baseCount = new RedPacketDecorator(baseCount);
        }
        return baseCount;
    }

}

public class Main {

    public static void main( String[] args ) throws InterruptedException, IOException {

        Order order = new Order();
        init(order);

        for(OrderDetail orderDetail: order.getList()) {
            BigDecimal payMoney = PromotionFactory.getPayMoney(orderDetail);
            orderDetail.setPayMoney(payMoney);
            System.out.println(" Final payment amount: " + orderDetail.getPayMoney());
        }
    }

}

Running the code yields the result:

Original price of the merchandise: 20
Coupon amount: 3
Red packet discount: 10
Final payment amount: 7

You can download and run the above source code from Github. From this example, we can see that the pricing promotion strategy designed using the decorator pattern has independent classes for calculating various promotion types, and various promotion types can be freely combined using a factory class.

Summary #

The decorator pattern introduced in this lecture is mainly used to optimize the complexity of business. It not only simplifies our business code, but also optimizes the structural design of business code, making the entire business logic clear, easy to read and understand.

Typically, the decorator pattern is used to extend the functionality of a class and support dynamic addition and removal of class functionality. In the decorator pattern, the decorator class and the decorated class only care about their own business and do not interfere with each other, truly achieving decoupling.

Thought-provoking Question #

The Chain of Responsibility pattern, the Strategy pattern, and the Decorator pattern have many similarities. Usually, these design patterns are used not only in business scenarios but also in architectural design. Have you ever encountered the usage of these design patterns in source code? Feel free to share with everyone.