【代码优雅】JAVA通过设计模式消除ifelse和重复代码

通过设计模式消除ifelse和重复代码

使用该方法前,先来了解一下工厂模式以及模板方法模式

工厂模式(创建型模式)

工厂模式分为3种

  • 简单工厂模式(Simple Factory Pattern)
  • 工厂方法模式(Factory Method Pattern)
  • 抽象工厂模式(Abstract Factory Pattern)
简单工厂模式(Simple Factory Pattern)

模拟场景是,有一家汽车厂(AutoFactory)要生产汽车,主要生产巴士(Bus)和轿车(Car),使用代码模拟如下:

public interface Auto {
    //汽车具有被驾驶功能
    void drive();
}

然后设计两种汽车:轿车和巴士

class Bus implements Auto{
    @Override
    public void drive() {
        // 巴士驾驶方式
    }
}
class Car implements Auto{
    @Override
    public void drive() {
        // 轿车驾驶方式
    }
}

开始建造"工厂"

public class AutoFactory{
    // 生产汽车
    public Auto produceCar(String type){
        if("car".equals(type)){
            return new Car();
        }else if("bus".equals(type)){
            return new Car();
        }
        return null;
    }
}

若需要生产一台轿车,则需通过工厂创建

AutoFactory autoFactory = new AutoFactory();
Auto car = factory.produce("car");
car.drive();

简单工厂模式实现了生成产品类的代码跟具体的产品实现分离
在工厂类中你可以添加所需的生成产品的逻辑代码
但是问题来了,这不符合“开放-封闭”原则的,也就是说对扩展开放,对修改关闭,如果你要加一个新的汽车类型还需要修改produce方法,为解决这个问题,从而引入了工厂方法模式(Factory Method Pattern)。

工厂方法模式(Factory Method Pattern)

工厂为了扩大市场,现在要开始生产卡车(Truck)了,于是我们设计一辆卡车:

class Truck implements Auto{
    @Override
    public void drive() {
        // 卡车驾驶方式
    }
}

按照简单工厂的逻辑,需要修改Produce方法(也就是说要改造已有的工厂),这样会影响已有生产,那怎么解决呢?办法是再建新的工厂:

首先设计一个工厂原型(工厂接口):

public interface IAutoFactory{
    // 生产汽车
    public Auto produce(String type)
}

然后将原来的工厂简单改造符合设计好的工厂原型(实现接口即可):

public class AutoFactory implements IAutoFatory{
    // 生产汽车
    public Auto produceCar(String type){
        if("car".equals(type)){
            return new Car();
        }else if("bus".equals(type)){
            return new Car();
        }
        return null;
    }
}

接下来为了生产卡车,我们要为卡车单独建厂

public class TruckAutofFactory implements IAutofactory{
    //生产卡车
    @Override
    public Auto produce(){
        return new Truck();
    }
}

开始生产卡车:

IAutoFactory factory = new TruckAutofFactory();
Auto car = factory.produce();
car.drive();

这里的抽象工厂中,我们为了减少改造成本,在简单工厂基础上做最小修改,理论上produce参数可以没有,然后为小轿车、大巴车和卡车分别建立工厂,分别生产。这样如果有了新的类型的车,可以不改动之前的代码,新建一个“工厂”即可,做到“开放封闭原则”。

虽然看似类变多了,逻辑复杂了,但是这种改造带来的好处也是显而易见的:不变动老的代码,通过新建工厂类完成新功能的添加,老功能不变,最大限度的避免动了老代码的逻辑导致引入新的bug。

工厂方法的结构图如下:

抽象工厂模式(Abstract Factory Pattern)

我们继续针对汽车工厂说明,由于接下来工厂需要继续扩大规模,开始涉足汽车配件,上层决定涉足汽车大灯业务,针对已有车型生产前大灯。但是如果按照工厂方法模式,需要再继续新建一批工厂,针对每种汽车再建N个工厂,考虑到成本和简单性,针对对已有汽车工厂改造。

首先“设计”大灯原型:

// 大灯
public interface Light(){
    // 开灯
    public void turnOn();
}

再“设计”小轿车、大巴车和卡车大灯:

public carLight() implements Light{
     @Override
    public void turnOn(){
        // 轿车大灯
    }
}
public busLight() implements Light{
     @Override
    public void turnOn(){
        // 巴士大灯
    }
}
public trustLight() implements Light{
     @Override
    public void turnOn(){
        // 卡车大灯
    }
}

接下来我们重新“设计”原有的汽车工厂(修改工厂接口或者抽象工厂类)

public interface IAutoFactory{
    // 生产汽车
    public Auto produce(String type);
    // 生产大灯
    public Light produceLight();
}

好的,改造工厂,首先改造卡车工厂:

public class TruckAutofFactory implements IAutofactory{
    //生产卡车
    @Override
    public Auto produce(){
        return new Truck();
    }
    // 生产车灯
    @Override
    public Auto produce2(){
        return new trustLight();
    }
}

就可以使用TruckAutofFactory生产卡车了

模板方法模式(行为型模式)

模板方法模式定义了一个算法的步骤,并允许子类别为一个或多个步骤提供其实践方式。让子类别在不改变算法架构的情况下,重新定义算法中的某些步骤。

模板方法(Template Method)模式包含以下主要角色:

  • 抽象类:负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。

    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。

    • 基本方法:是模板方法的组成部分,可以分为三种:

      • 抽象方法:一个抽象方法由抽象类声明、由其具体子类实现

      • 具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。

      • 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。

        一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型

  • 具体子类:实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

使用场景通常是:一些方法通用,却在每一个子类都重新写了这一方法。(即再向上抽取,提取公共代码)

案例:银行办理业务为例子。去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。

实现:

public abstract class AbstractClass {

    public final void takeNumber(){
        // 取号
    }

    public final void queueUp(){
        // 排队
    }

    /* 办理具体业务 根据字类的需求来进行实现 */
    public abstract  void handleBusiness();

    public final void score(){
        // 评分
    }
}

模拟不同的顾客

public class AUser extends AbstractClass{
    @Override
    public void handleBusiness() {
        this.takeNumber();
        this.queueUp();
        // todo贷款逻辑
        this.score();
    }
}

public class BUser extends AbstractClass{
    @Override
    public void handleBusiness() {
        this.takeNumber();
        this.queueUp();
        // todo存钱逻辑
        this.score();
    }
}

客户端:

 AbstractClass aUser = new AUser();
 aUser.handleBusiness();

结合两种设计模式进行消除代码重复

假设要开发一个购物车下单的功能,针对不同用户进行不同处理:【这个时候第一反应是想到模板方法模式】

  • 普通用户需要收取运费,运费是商品价格的10%,无商品折扣;
  • VIP用户同样需要收取商品价格10%的快递费,但购买两件以上相同商品时,第三件开始享受一定折扣;
  • 内部用户可以免运费,无商品折扣。

目标是实现三种类型的购物车业务逻辑,把入参Map对象(Key是商品ID,Value是商品数量),转换为出参购物车类型Cart。

先实现针对普通用户的购物车处理逻辑:

//购物车
@Data
public class Cart {
    //商品清单
    private List<Item> items = new ArrayList<>();
    //总优惠
    private BigDecimal totalDiscount;
    //商品总价
    private BigDecimal totalItemPrice;
    //总运费
    private BigDecimal totalDeliveryPrice;
    //应付总价
    private BigDecimal payPrice;
}
//购物车中的商品
@Data
public class Item {
    //商品ID
    private long id;
    //商品数量
    private int quantity;
    //商品单价
    private BigDecimal price;
    //商品优惠
    private BigDecimal couponPrice;
    //商品运费
    private BigDecimal deliveryPrice;
}
//普通用户购物车处理
public class NormalUserCart {
    public Cart process(long userId, Map<Long, Integer> items){
        Cart cart = new Cart();

        //把Map的购物车转换为Item列表
        List<Item> itemList = new ArrayList<>();
        items.entrySet().stream().forEach(entry -> {
            Item item = new Item();
            item.setId(entry.getKey());
            item.setPrice(Db.getItemPrice(entry.getKey()));
            item.setQuantity(entry.getValue());
            itemList.add(item);
        });
        cart.setItems(itemList);
        
        //处理运费和商品优惠
        itemList.stream().forEach(item -> {
            //运费为商品总价的10%
            item.setDeliveryPrice(item.getPrice()
                                  .multiply(BigDecimal.valueOf(item.getQuantity()))
                                  .multiply(new BigDecimal("0.1")));
            //无优惠
            item.setCouponPrice(BigDecimal.ZERO);
        });
        
        //计算商品总价
        cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
        
        //计算运费总价
        cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        
        //计算总优惠
        cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        
        //应付总价=商品总价+运费总价-总优惠
        cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
        return cart;
    }
}

然后实现针对VIP用户的购物车逻辑。与普通用户购物车逻辑的不同在于,VIP用户能享受同类商品多买的折扣。所以,这部分代码只需要额外处理多买折扣部分:

public class VipUserCart {


    public Cart process(long userId, Map<Long, Integer> items) {
        ...


        itemList.stream().forEach(item -> {
            //运费为商品总价的10%
            item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
            //购买两件以上相同商品,第三件开始享受一定折扣
            if (item.getQuantity() > 2) {
                item.setCouponPrice(item.getPrice()
                        .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
                       .multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
            } else {
                item.setCouponPrice(BigDecimal.ZERO);
            }
        });

        ...
        return cart;
    }
}

最后是免运费、无折扣的内部用户,同样只是处理商品折扣和运费时的逻辑差异:

public class InternalUserCart {


    public Cart process(long userId, Map<Long, Integer> items) {
        ...

        itemList.stream().forEach(item -> {
            //免运费
            item.setDeliveryPrice(BigDecimal.ZERO);
            //无优惠
            item.setCouponPrice(BigDecimal.ZERO);
        });

        ...
        return cart;
    }
}

对比代码可发现,三种逻辑理论有大部分的代码是重复的

有了三个购物车后,我们就需要根据不同的用户类型使用不同的购物车了。如下代码所示,使用三个if实现不同类型用户调用不同购物车的process方法:

@GetMapping("wrong")
public Cart wrong(@RequestParam("userId") int userId) {
    //根据用户ID获得用户类型
    String userCategory = Db.getUserCategory(userId);
    //普通用户处理逻辑
    if (userCategory.equals("Normal")) {
        NormalUserCart normalUserCart = new NormalUserCart();
        return normalUserCart.process(userId, items);
    }
    //VIP用户处理逻辑
    if (userCategory.equals("Vip")) {
        VipUserCart vipUserCart = new VipUserCart();
        return vipUserCart.process(userId, items);
    }
    //内部用户处理逻辑
    if (userCategory.equals("Internal")) {
        InternalUserCart internalUserCart = new InternalUserCart();
        return internalUserCart.process(userId, items);
    }

    return null;
}

电商的营销玩法是多样的,以后势必还会有更多用户类型,需要更多的购物车。我们就只能不断增加更多的购物车类,一遍一遍地写重复的购物车逻辑、写更多的if逻辑吗?

我们的原则是相同的代码只在一处地方出现

如果我们熟记抽象类和抽象方法的定义的话,这时或许就会想到,是否可以把重复的逻辑定义在抽象类中,三个购物车只要分别实现不同的那份逻辑

这个就是模板方法模式。我们在父类中实现了购物车处理的流程模板,然后把需要特殊处理的地方留空白也就是留抽象方法定义,让子类去实现其中的逻辑。由于父类的逻辑不完整无法单独工作,因此需要定义为抽象类

public abstract class AbstractCart {
    //处理购物车的大量重复逻辑在父类实现
    public Cart process(long userId, Map<Long, Integer> items) {

        Cart cart = new Cart();

        List<Item> itemList = new ArrayList<>();
        items.entrySet().stream().forEach(entry -> {
            Item item = new Item();
            item.setId(entry.getKey());
            item.setPrice(Db.getItemPrice(entry.getKey()));
            item.setQuantity(entry.getValue());
            itemList.add(item);
        });
        cart.setItems(itemList);
        //让子类处理每一个商品的优惠
        itemList.stream().forEach(item -> {
            // todo
            processCouponPrice(userId, item);
            // todo
            processDeliveryPrice(userId, item);
        });
        //计算商品总价
        cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算总运费
cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算总折扣
cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算应付价格
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
        return cart;
    }

    //处理商品优惠的逻辑留给子类实现
    protected abstract void processCouponPrice(long userId, Item item);
    //处理配送费的逻辑留给子类实现
    protected abstract void processDeliveryPrice(long userId, Item item);
}

有了这个抽象类,三个子类的实现就非常简单了。

// 普通用户的购物车NormalUserCart,实现的是0优惠和10%运费的逻辑:
@Service(value = "NormalUserCart")
public class NormalUserCart extends AbstractCart {

    @Override
    protected void processCouponPrice(long userId, Item item) {
        item.setCouponPrice(BigDecimal.ZERO);
    }

    @Override
    protected void processDeliveryPrice(long userId, Item item) {
        item.setDeliveryPrice(item.getPrice()
                .multiply(BigDecimal.valueOf(item.getQuantity()))
                .multiply(new BigDecimal("0.1")));
    }
}

// VIP用户的购物车VipUserCart,直接继承了NormalUserCart,只需要修改多买优惠策略:
@Service(value = "VipUserCart")
public class VipUserCart extends NormalUserCart {

    @Override
    protected void processCouponPrice(long userId, Item item) {
        if (item.getQuantity() > 2) {
            item.setCouponPrice(item.getPrice()
                    .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
                    .multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
        } else {
            item.setCouponPrice(BigDecimal.ZERO);
        }
    }
}

// 内部用户购物车InternalUserCart是最简单的,直接设置0运费和0折扣即可:
@Service(value = "InternalUserCart")
public class InternalUserCart extends AbstractCart {
    @Override
    protected void processCouponPrice(long userId, Item item) {
        item.setCouponPrice(BigDecimal.ZERO);
    }

    @Override
    protected void processDeliveryPrice(long userId, Item item) {
        item.setDeliveryPrice(BigDecimal.ZERO);
    }
}

抽象类和三个子类的实现关系图,如下所示:

接下来,我们再看看如何能避免三个if逻辑。

定义三个购物车子类时,我们在@Service注解中对Bean进行了命名。既然三个购物车都叫XXXUserCart,那我们就可以把用户类型字符串拼接UserCart构成购物车Bean的名称,然后利用Spring的IoC容器,通过Bean的名称直接获取到AbstractCart,调用其process方法即可实现通用。

这 就是工厂模式,只不过是借助Spring容器实现罢了:

@GetMapping("right")
public Cart right(@RequestParam("userId") int userId) {
    String userCategory = Db.getUserCategory(userId);
    AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart");
    return cart.process(userId, items);
}

试想, 之后如果有了新的用户类型、新的用户逻辑,是不是完全不用对代码做任何修改,只要新增一个XXXUserCart类继承AbstractCart,实现特殊的优惠和运费处理逻辑就可以了?

这样一来,我们就利用工厂模式+模板方法模式,不仅消除了重复代码,还避免了修改既有代码的风险。这就是设计模式中的开闭原则:对修改关闭,对扩展开放。

ps:学习朱晔老师课程总结

posted @ 2022-12-07 09:41  simonlee_java  阅读(727)  评论(0)    收藏  举报