Java常用设计模式的实例学习系列-面向对象的六个设计原则-以购物车支付为例

超级链接: Java常用设计模式的实例学习系列-绪论

参考:《HeadFirst设计模式》


1.原始设计

简述

本文以购物车支付场景为例,对面向对象的六个原则进行理解。

本文中的代码是逐步重构的,如果本步骤的代码与上步骤的代码相同,则不再展示。

本文的主要目的是理解六个设计原则,所以对于需求是否合理和代码是否粗糙就请不要计较了。

完整代码可以参考github:https://github.com/hanchao5272/design-pattern

1.2.场景

  • 每个商品都有名称和价钱。
  • 购物车可以添加多个商品。
  • 购物车支付时使用支付宝支付。
  • 可能的需求变化:
    • 增加微信支付。
    • 商品的价格计算不仅仅有普通折扣,还有会员折上折。
    • 购物车不只当前这种,后面会出现更加先进的购物车。
    • … …

1.3.原始代码

直接上代码

商品类:Goods

/**
 * <p>商品</P>
 *
 * @author hanchao
 */
#Setter
#Getter
#AllArgsConstructor
public class Goods {
    /**
     * 商品名称
     */
    private String name;

    /**
     * 商品价格
     */
    private Float price;

    /**
     * 折扣
     */
    private Float discount;
}

购物车类:ShoppingCart

/**
 * <p>购物车</P>
 *
 * @author hanchao
 */
#Slf4j
#NoArgsConstructor
public class ShoppingCart {
    /**
     * 商品列表
     */
    private List<Goods> goodsList = new ArrayList<>();

    /**
     * 添加商品
     */
    public ShoppingCart addGoods(Goods goods) {
        goodsList.add(goods);
        return this;
    }

    /**
     * 显示商品
     */
    public ShoppingCart showGoods() {
        goodsList.forEach(goods -> log.info("购物车中有:{},价格:{}.", goods.getName(), goods.getPrice() * goods.getDiscount()));
        log.info("-------------------------------------");
        log.info("购物车中获取总价格:{}元\n", this.totalCost());
        return this;
    }

    /**
     * 计算总价
     */
    public float totalCost() {
        return goodsList.stream().map(goods -> goods.getPrice() * goods.getDiscount()).reduce(Float::sum).orElse(0f);
    }

    /**
     * 连接支付宝
     */
    public void connect() {
        log.info("开始进行支付宝支付:");
        log.info("1.初始化支付宝客户端...");
        log.info("2.设置请求参数...");
        log.info("3.请求支付宝进行付款,并获取支付结果...");
        log.info("-------------------------------------");
    }

    /**
     * 支付(日志显示2位小数)
     */
    public void pay() {
        float money = this.totalCost();
        connect();
        log.info("4.通过支付宝支付了" + FormatUtil.format(money) + "元.");
    }
}

**工具类:浮点型保留2位小数:FormatUtil **

/**
 * <p>格式化工具类</P>
 *
 * @author hanchao
 */
public class FormatUtil {
    /**
     * 浮点数显示两位小数
     */
    public static String format(float number) {
        return new DecimalFormat("0.00").format(Objects.isNull(number) ? 0 : number);
    }
}

测试代码

    public static void main(String[] args) {
        ShoppingCart shoppingCart = new ShoppingCart();

        shoppingCart.addGoods(new Goods("一双球鞋", 3500f, 1f))
                .addGoods(new Goods("一件外套", 2800.00f, 0.80f))
                .showGoods()
                .pay();
    }

测试结果:

2019-07-18 14:19:03,784  INFO - 购物车中有:一双球鞋,价格:3500.0. 
2019-07-18 14:19:03,786  INFO - 购物车中有:一件外套,价格:2240.0. 
2019-07-18 14:19:03,786  INFO - ------------------------------------- 
2019-07-18 14:19:03,793  INFO - 购物车中获取总价格:5740.0元
 
2019-07-18 14:19:03,793  INFO - 开始进行支付宝支付: 
2019-07-18 14:19:03,793  INFO - 1.初始化支付宝客户端... 
2019-07-18 14:19:03,793  INFO - 2.设置请求参数... 
2019-07-18 14:19:03,793  INFO - 3.请求支付宝进行付款,并获取支付结果... 
2019-07-18 14:19:03,793  INFO - ------------------------------------- 
2019-07-18 14:19:03,794  INFO - 4.通过支付宝支付了5740.00元. 

##2. 六个原则

###2.1. 原则一:单一职责原则

单一职责原则:

  • 就一个类而言,应该仅有一个引起它变化的原因.
  • 简而言之:一个类应该是一组相关性很高的函数、数据的封装。

问题

回顾章节1.3.ShoppingCart类,发现它违背了单一职责原则:购物逻辑与支付逻辑放在了同一个类中。

随着需求的不断发展,支付逻辑与购物逻辑都在不断增多,代码越来越复杂,这种设计毫无扩展性与灵活性。

解决

将支付宝相关逻辑抽离出来,放到单独的类中处理。

支付类:AliPayClient

/**
 * <p>支付宝类</P>
 *
 * @author hanchao
 */
#Slf4j
public class AliPayClient {
    /**
     * 连接支付宝
     */
    private void connect() {
        log.info("开始进行支付宝支付:");
        log.info("1.初始化支付宝客户端...");
        log.info("2.设置请求参数...");
        log.info("3.请求支付宝进行付款,并获取支付结果...");
        log.info("-------------------------------------");
    }

    /**
     * 支付(日志显示2位小数)
     */
    public void pay(float money) {
        connect();
        log.info("4.通过支付宝支付了" + FormatUtil.format(money) + "元.");
    }
}

购物车类:ShoppingCart

/**
 * <p>购物车</P>
 *
 * @author hanchao
 */
#Slf4j
#NoArgsConstructor
public class ShoppingCart {
  	//....
    
    /**
     * 通过支付宝支付
     */
    public void pay() {
        new AliPayClient().pay(totalCost());
    }
}

总结:

经过上述优化,AliPayClient只负责支付相关逻辑,ShoppingCart只负责购物相关逻辑。当一方需求发生变化时,只需要修改一个类,不会影响另一个类。


2.2.原则二:开闭原则

开闭原则

  • 对扩展开发,对修复关闭。
  • 在面向对象设计中,不允许更改的是系统的抽象层,而允许扩展的是系统的实现层。
  • 换言之,定义一个尽可能不易发生变化的抽象设计层,允许尽可能多的行为在实现层被实现。
  • 解决问题关键在于抽象化
  • 这里的抽象指的是interface或者abstract class
  • 抽象的过程,实质上是在概括归纳总结它的本质。
  • 通过抽象,还能够统一规范实现类的需要实现的方法。
  • 同样是抽象,强调的是对类本身的处理。

问题

回顾章节2.2.的代码,发现它违背了开闭原则:支付客户端和购物车都是具体实现类。

解决

将支付客户端和购物车抽象化。

支付宝接口:IAliPayClient

/**
 * <p>支付宝的抽象类</P>
 *
 * @author hanchao
 */
public interface IAliPayClient {
    /**
     * 连接
     */
    void connect();

    /**
     * 支付
     */
    void pay(Float money);
}

支付宝实现类:AliPayClient

/**
 * <p>支付宝</P>
 *
 * @author hanchao
 */
#Slf4j
public class AliPayClient implements IAliPayClient {
    /**
     * 连接
     */
    @Override
    public void connect() {
        //...
    }

    /**
     * 支付
     */
    @Override
    public void pay(Float money) {
       //...
    }
}

购物车接口:IShoppingCart

/**
 * <p>购物车的抽象类</P>
 *
 * @author hanchao
 */
public interface IShoppingCart {

    /**
     * 添加商品
     */
    IShoppingCart addGoods(Goods goods);

    /**
     * 显示商品
     */
    IShoppingCart showGoods();

    /**
     * 计算总价
     */
    Float totalCost();

    /**
     * 通过支付宝支付
     */
    void pay();
}

购物车实现类:

/**
 * <p>普通购物车</P>
 *
 * @author hanchao
 */
#Slf4j
#NoArgsConstructor
public class ShoppingCart implements IShoppingCart {
  	//....

    /**
     * 添加商品
     */
    @Override
    public IShoppingCart addGoods(Goods goods) {
        //...
    }

    /**
     * 显示商品
     */
    @Override
    public IShoppingCart showGoods() {
        //...
    }

    /**
     * 计算总价
     */
    @Override
    public Float totalCost() {
        //...
    }

    /**
     * 通过支付宝支付
     */
    @Override
    public void pay() {
        IAliPayClient aliPayClient = new AliPayClient();
        aliPayClient.pay(totalCost());
    }
}

测试代码

    public static void main(String[] args) {
      	//注意这里是接口
        IShoppingCart shoppingCart = new ShoppingCart();

        shoppingCart.addGoods(new Goods("一双球鞋", 3500f, 1f))
                .addGoods(new Goods("一件外套", 2800.00f, 0.80f))
                .showGoods()
                //设置微信支付方式
                .pay();
    }

总结:

经过上述优化,如果需求发生变化,比方说新出现了一种超级购物车实现SupperShoppingCart,这样在ShoppingDemo类中,只需要修改IShoppingCart shoppingCart = new SupperShoppingCart();即可,无需修改后续的添加商品,显示购物车内容和结算等代码逻辑。


2.3.原则三:依赖倒置原则

依赖倒置原则

  • 又称之为面向接口编程
  • 要依赖于抽象,不要依赖于具体。
  • 要针对接口编程,不针对实现编程。
  • 以抽象方式耦合是依赖倒转原则的关键。
  • 同样是抽象,强调的是对依赖关系的处理。

问题

回顾章节2.2.ShoppingCart#pay()方法,发现它违背了违背依赖倒置原则:当新增微信支付时,因为写死了用支付宝支付,所以还是要修改支付代码。

解决

将支付宝支付和微信支付抽象成更高层次的支付接口;将购物车类对支付类的依赖设置为可替换的。

支付接口:PayClient

/**
 * <p>支付的抽象类</P>
 *
 * @author hanchao
 */
public interface PayClient {
    /**
     * 连接
     */
    void connect();
    /**
     * 支付
     */
    void pay(Float money);
}

支付实现类:支付宝:AliPayClient

/**
 * <p>支付宝</P>
 *
 * @author hanchao
 */
#Slf4j
public class AliPayClient implements PayClient {
  	//...
}

支付实现类:微信:WeChatPayClient

/**
 * <p>微信支付</P>
 *
 * @author hanchao
 */
#Slf4j
public class WeChatPayClient implements PayClient {
    /**
     * 连接
     */
    @Override
    public void connect() {
        //do nothing
    }

    /**
     * 支付
     */
    @Override
    public void pay(Float money) {
        log.info("开始进行微信支付:");
        log.info("1.设置请求参数...");
        log.info("2.发送HTTP请求微信付款地址进行支付...");
        log.info("3.进行微信支付回调,并获取支付结果...");
        log.info("-------------------------------------");
        log.info("4.通过微信支付了" + money + "元.");
    }
}

购物车接口:IShoppingCart

/**
 * <p>购物车的抽象类</P>
 *
 * @author hanchao
 */
public interface IShoppingCart {

    IShoppingCart setPayClient(PayClient payClient);
  
  	//....
}

购物车实现类:ShoppingCart

/**
 * <p>购物车</P>
 *
 * @author hanchao
 */
#Slf4j
#NoArgsConstructor
public class ShoppingCart implements IShoppingCart {
  	//....

    /**
     * 支付方式
     */
    private PayClient payClient = new AliPayClient();

    @Override
    public IShoppingCart setPayClient(PayClient payClient) {
        this.payClient = payClient;
        return this;
    }

   	//....

    /**
     * 通过支付宝支付
     */
    @Override
    public void pay() {
        payClient.pay(totalCost());
    }
}

测试代码

    public static void main(String[] args) {
        IShoppingCart shoppingCart = new ShoppingCart();

        shoppingCart.addGoods(new Goods("一双球鞋", 3500f, 1f))
                .addGoods(new Goods("一件外套", 2800.00f, 0.80f))
                .showGoods()
                //设置微信支付方式,而无需修改ShoppingCart
                .setPayClient(new WeChatPayClient())
                .pay();
    }

总结

经过上述优化,即使需求发生变化,比如新增一种支付方式银联支付UnionPayClient,则无需修改ShoppingCart的代码,只需调用shoppingCart..setPayClient(new UnionPayClient())即可。


2.4.原则四:里式替换原则

里式替换原则

  • 子类型必须能够替换它们的基类型。
  • 一个软件实体如果使用的是一个基类,那么当把这个基类替换成继承该基类的子类,程序的行为不会发生任何变化。
  • 通俗来说:只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或者异常,但反过来就未必能行,有子类出现的地方,父类未必就能适应。

问题

回顾章节2.3.WeChatPayClient#pay()方法,发现它违背了里式替换原则:微信支付作为支付抽象类的子类,并不能完全实现父类规定的保留2位小数功能。

解决

修改子类型的重载方法,完全实现父类型的功能。

支付实现类:微信:WeChatPayClient

/**
 * <p>微信支付</P>
 *
 * @author hanchao
 */
#Slf4j
public class WeChatPayClient implements PayClient {
    //...

    /**
     * 支付
     */
    @Override
    public void pay(Float money) {
        //...
        log.info("4.通过微信支付了" + FormatUtil.format(money) + "元.");
    }
}

总结

经过上述优化,微信支付完全实现了父类规定的接口功能,这样能够避免问题的出现。

P.s.上面的示例比较low,将就着看吧。


2.5.接口隔离原则

接口隔离原则

  • 又称之为最小接口原则
  • 使用多个专一功能的接口比使用一个的总接口总要好。
  • 一个类对另外一个类的依赖性应当是建立在最小接口上的。

问题

回顾章节2.3.WeChatPayClient类,发现它违背了接口隔离原则:PayClient的接口有两个方法,但是connect()对于微信支付是无用的。

解决

拆解大接口,形成小接口。

支付接口:PayClient

/**
 * <p>支付的抽象类</P>
 *
 * @author hanchao
 */
public interface PayClient {
    /**
     * 支付
     */
    void pay(float money);
}

可连接接口:Connectable

/**
 * <p>可连接的</P>
 *
 * @author hanchao
 */
public interface Connectable {
    /**
     * 连接
     */
    void connect();
}

支付实现类:支付宝:AliPayClient

/**
 * <p>支付宝</P>
 *
 * @author hanchao
 */
#Slf4j
public class AliPayClient implements PayClient, Connectable {
  //...
}

支付实现类:微信:WeChatPayClient

/**
 * <p>微信支付</P>
 *
 * @author hanchao
 */
#Slf4j
public class WeChatPayClient implements PayClient {
    /**
     * 支付
     */
    @Override
    public void pay(float money) {
        log.info("开始进行微信支付:");
        log.info("1.设置请求参数...");
        log.info("2.发送HTTP请求微信付款地址进行支付...");
        log.info("3.进行微信支付回调,并获取支付结果...");
        log.info("-------------------------------------");
        log.info("4.通过微信支付了" + FormatUtil.format(money) + "元.");
    }
}

总结

经过上述优化,微信支付只需要实现PayClient即可,无需去重写不需要的方法。


2.6.原则六:迪米特原则

迪米特原则

  • 又称之为最少知识原则
  • 对象与对象之间应该使用尽可能少的方法来关联,避免千丝万缕的关系。
  • 通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少。

需求变化

商品的价格计算不仅仅有普通折扣,还有会员折上折

问题

回顾章节1.3.ShoppingCart#showGoods()和ShoppingCart#totalCost()方法,发现它违背了迪米特原则:购物车不应该关心商品的计算逻辑。

这种设计导致购物车过多参与价格的计算,耦合性太高,当价格计算规则发生变化时,这些方法都需要修改。

解决

购物车不应该知道商品价格的计算规则,他只要能够通过某个方法直接获取商品的最终价格接口。

商品类:Goods

/**
 * <p>商品</P>
 *
 * @author hanchao
 */
#Setter
#Getter
#AllArgsConstructor
public class Goods {
    /**
     * 商品名称
     */
    private String name;

    /**
     * 商品价格
     */
    private float price;

    /**
     * 折扣
     */
    private float discount;

    /**
     * 会员折扣
     */
    private float vipDiscount;
  
    /**
     * 计算最终价格(把价格计算放在自己的类中,不然别的类知道)
     */
    public float getFinalPrice() {
        return price * discount * vipDiscount;
    }
}

购物车实现类:ShoppingCart

/**
 * <p>购物车</P>
 * <p>
 * 最后一个原则:开闭原则:对扩展开放,对修改关闭。前面做的事情。
 *
 * @author hanchao
 */
#Slf4j
#NoArgsConstructor
public class ShoppingCart implements IShoppingCart {
    //...

    /**
     * 显示商品
     */
    @Override
    public IShoppingCart showGoods() {
        goodsList.forEach(goods -> log.info("购物车中有:{},价格:{}.", goods.getName(), goods.getFinalPrice()));
        log.info("-------------------------------------");
        log.info("购物车中获取总价格:{}元\n", this.totalCost());
        return this;
    }

    /**
     * 计算总价
     */
    @Override
    public float totalCost() {
        return goodsList.stream().map(Goods::getFinalPrice).reduce(Float::sum).orElse(0f);
    }

    //...
}

总结

经过上述优化,当商品的计算方式再次发生变化,其实对购物车类并无影响。

总结

面向对象的六个设计原则的总体目的:降低耦合性,提高系统可维护性、可扩展性、灵活性、容错性等。

面向对象的六个设计原则没有先后顺序,也没有主次,本文只是为了叙述方便所以采取了文中的引入顺序。

在实际开发中很难用到全部的六个原则,要集合实际情况量力而行,不要过分追求设计原则,而忘了最初目的,造成舍本逐末。

在软件开发过程中,唯一不变的就是变化,所以我们只能尽量做到代码的优化,但是却不能保证代码永远满足需求。


参考:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值