美文网首页
设计模式六大原则

设计模式六大原则

作者: 潘长河 | 来源:发表于2021-01-22 08:54 被阅读0次

    单一职责

    应该有且仅有一个原因引起类的变更。

    例如,设计一个视频播放系统,要求:如果是VIP用户就播放完整影片,否则只允许试看5分钟。

    class VideoService {
    
        public void play(Long videoId, Long userId) {
            if (haveAuth(userId)) {
                System.out.println("播放完整影片...");
            }else {
                System.out.println("试看5分钟...");
            }
        }
    
        public boolean haveAuth(Long userId){
            // 判断用户是否是VIP
            return true;
        }
    }
    

    这个类的设计明显不符合单一职责,它做了两件事情:1.视频的播放、2.权限的判断。

    如果非会员从原来的试看5分钟提升至10分钟,play的逻辑是不是要修改?
    谁说只有VIP才可以观看完整影片,如果允许用户付费解锁单个影片呢?haveAuth是不是要修改?

    这就已经有两个原因会引起类的变更了,你能百分百保证其中一个逻辑的变更不会影响到另外一个逻辑的运行吗?这通常很难保证,需求变更时,改动的代码应该越少越好。

    将职责进行拆分,AuthService负责权限,VideoService负责视频的播放:

    class VideoService {
        AuthService authService = new AuthService();
    
        public void play(Long videoId, Long userId) {
            if (authService.haveAuth(userId)) {
                System.out.println("播放完整影片...");
            }else {
                System.out.println("试看5分钟...");
            }
        }
    }
    
    class AuthService{
        
        public boolean haveAuth(Long userId){
            // 用户是否是VIP? or  用户是否购买单个影片?
            return true;
        }
    }
    

    现在,VideoService职责很清晰,有权限就播放完整影片,没权限就试看,至于用户是VIP还是购买了单个影片,它完全不关心。

    单一职责的优点:

    • 类的复杂性降低,实现什么职责都有清晰明确的定义。
    • 可读性提高。
    • 可维护性提高。
    • 变更引起的风险降低。

    事实上,VideoService的职责还可以进行拆分,它本应该只负责视频的播放,至于是完整影片还是5分钟,这其实也属于业务逻辑。但是在实际的开发中,拆分到这一层差不多就可以了,职责拆分的过细,会导致类文件的激增,类间的结构会变得复杂,开发成本会上升。
    一个类的职责到底有哪些?这通常很难有一个明确的定义,需要结合实际情况来设计。

    单一职责的核心就是拆分,职责的拆分,它不仅适用于类,也适用于方法,每一个方法的功能都应该是相互独立,职责清晰的,这不仅提高了程序的可读性和可维护性,方法的复用性也会提高。一个功能过于“丰富”的方法,通常很难被复用。


    里氏替换原则

    所有引用基类的地方必须能透明地使用其子类的对象。

    子类可以扩展父类的功能,但不能改变父类原有的功能。

    例如系统在发送短信时,需要将发送记录保存到数据库中,如果子类重写了父类的方法,修改了业务逻辑,日志记录功能将会丢失,这是非常危险的。

    class SmsService{
    
        public void sendMessage(String target) {
            System.out.println("发送短信前记录日志...");
            System.out.println("发送短信...");
        }
    }
    
    class MySmsService extends SmsService {
    
        @Override
        public void sendMessage(String target) {
            System.out.println("子类发送短信...");
        }
    }
    

    一种较好的解决办法是,将公共的代码提取到基类,然后预留一个钩子函数交给子类扩展:

    class SmsService{
    
        // 发送短信,final,禁止子类重写
        public final void sendMessage(String target) {
            System.out.println("发送短信前记录日志...");
            System.out.println("发送短信...");
            hook();
        }
    
        protected void hook(){
            // 钩子函数,父类什么也不做,子类扩展
        }
    }
    
    class MySmsService extends SmsService {
    
        @Override
        protected void hook() {
            System.out.println("短信发送后的增强...");
        }
    }
    

    继承的优点:

    1. 代码共享。
    2. 提高代码的复用性。
    3. 提高类的开放性,可以被子类增强。

    继承的缺点:

    1. 较强的侵入性,子类必须拥有父类的所有方法和属性。
    2. 增加了耦合,父类的特性改变,子类也必须跟着改变。

    继承有它的优点,自然也有它的缺点,如果子类不能完整的实现父类的方法,或者父类的方法在子类的实现中“变味儿”了,则建议断开继承关系,采用依赖、组合等关系来代替。


    依赖倒置原则

    1. 高层模块不依赖低层模块,而是互相依赖其抽象。
    2. 抽象不依赖细节,细节依赖于抽象。

    通常一个业务逻辑会由N个原子逻辑组成,原子逻辑是不可分割的,也就是低层模块,由N个原子逻辑组成的业务逻辑就是高层模块。
    例如:用户下订单是一个高层模块,下单需要发起支付,扣减库存等操作就是低层模块,订单服务应该依赖支付、库存服务的接口,而不是实现细节。说白了就是:面向接口编程

    如下示例,是一个订单服务:

    // 订单服务
    class OrderService {
        AliPay pay = new AliPay();
    
        // 下单
        public void order() {
            // 支付
            pay.pay();
            // 发货......
        }
    
    }
    
    class AliPay {
        void pay() {
            System.out.println("使用支付宝支付...");
        }
    }
    

    没有将支付抽象出来,订单服务直接依赖AliPay实现,导致程序难以扩展。
    如果要接入微信支付呢?所有涉及到支付的地方都需要修改,风险太大。
    有支付就有退款,你没办法强制要求所有的支付服务都实现退款功能,说白了系统没有将支付服务规范起来。

    基于依赖倒置原则,程序可以优化如下:

    // 订单服务
    class OrderService {
        private Pay pay;
    
        public OrderService(Pay pay) {
            this.pay = pay;
        }
    
        // 下单
        public void order() {
            // 支付
            pay.pay();
            // 发货......
        }
    
    }
    
    interface Pay {
        // 支付
        void pay();
    
        // 退款
        void refund();
    }
    
    class AliPay implements Pay{
    
        @Override
        public void pay() {
            System.out.println("支付宝支付...");
        }
    
        @Override
        public void refund() {
            System.out.println("退款到支付宝账户...");
        }
    }
    
    class WeChat implements Pay{
    
        @Override
        public void pay() {
            System.out.println("微信支付...");
        }
    
        @Override
        public void refund() {
            System.out.println("退款到微信账户...");
        }
    }
    

    定义Pay接口,将支付服务规范化,涉及到支付业务的只依赖接口,不依赖具体实现。程序的扩展性得到提高,更换第三方支付业务,只需要实现Pay接口,通过构造器传入即可,其他代码都不用动。

    当支付服务的规则有变更时,修改接口,所有的实现类都必须做相应的调整,否则编译会不通过,这就是实现依赖于抽象,低层依赖于高层,依赖倒置了。

    为了满足依赖倒置原则,开发的时候尽量遵循如下规则:

    1. 类尽量有抽象父类或接口。
    2. 变量、形参尽量是抽象类或接口。
    3. 尽量不要从具体类再派生出子类。
    4. 尽量不要重写基类方法,会影响依赖的稳定性。
    5. 结合里氏替换原则使用,可以用子类对象透明的代替基类对象。

    接口隔离原则

    1. 客户端不应该依赖它不需要的接口。
    2. 类间的依赖关系应该建立在最小的接口上。

    接口尽量细化,建立单一接口,避免定义过度“臃肿而庞大”的接口。

    如下示例,设计一个个人博客系统,普通用户只能查看博客,而管理员则可以增删改查。

    interface IBlogService {
    
        void add();
    
        void delete();
    
        void update();
    
        void select();
    }
    
    class UserBlogService implements IBlogService{
    
        @Override
        public void add() {
            // 普通用户不能新增
        }
    
        @Override
        public void delete() {
            // 普通用户不能删除
        }
    
        @Override
        public void update() {
            // 普通用户不能修改
        }
    
        @Override
        public void select() {
            System.out.println("普通用户只能查看博客...");
        }
    }
    
    class AdminBlogService implements IBlogService{
    
        @Override
        public void add() {
            System.out.println("管理员新增博客...");
        }
    
        @Override
        public void delete() {
            System.out.println("管理员删除博客...");
        }
    
        @Override
        public void update() {
            System.out.println("管理员更新博客...");
        }
    
        @Override
        public void select() {
            System.out.println("管理员查看博客...");
        }
    }
    

    显然,普通用户UserBlogService被迫实现了它不需要实现的接口,IBlogService应该被拆分为两个接口,一个定义查看,一个定义管理维护。

    // 只可查看
    interface IReadBlogService {
    
        void select();
    }
    
    // 支持增删改查
    interface IAdminBlogService extends IReadBlogService{
        
        void add();
        
        void delete();
        
        void update();
    }
    
    // 普通用户只能查看
    class UserBlogService implements IReadBlogService{
        @Override
        public void select() {
            System.out.println("普通用户只能查看博客...");
        }
    }
    
    // 管理员不仅能看,还能增删改
    class AdminBlogService implements IAdminBlogService {
    
        @Override
        public void add() {
            System.out.println("管理员新增博客...");
        }
    
        @Override
        public void delete() {
            System.out.println("管理员删除博客...");
        }
    
        @Override
        public void update() {
            System.out.println("管理员更新博客...");
        }
    
        @Override
        public void select() {
            System.out.println("管理员查看博客...");
        }
    }
    

    要做到极致的接口隔离原则,就是一个接口只有一个方法,但是开发中我们绝不会这么去做,粒度太小了,导致接口数量剧增,难以开发。但是接口粒度太大灵活性会降低,所以如何定义好一个接口的粒度需要开发人员长期的实践和经验积累。


    迪米特法则

    也被称为:最少知道原则一个类应该对它依赖的类了解的越少越好。

    迪米特法则的核心是:只与朋友交流。它的目的是减少类间的依赖,提高内聚,降低耦合。
    朋友类的定义:成员变量,方法的入参、出参属于朋友类,出现在方法体内部的则不是朋友类。

    一个类应该对其所依赖的类知道的越少越好,你的内部实现不管多复杂我并不关心,我只管调用你的public方法。

    示例,创建一个缓存容器,缓存有三种淘汰策略,分别是:FIFO、LFU、LRU。

    // 缓存接口
    interface Cache{
    
        void something();
    }
    
    class FIFOCache implements Cache{
    
        @Override
        public void something() {
            System.out.println("先进先出的缓存...");
        }
    }
    
    class LFUCache implements Cache{
    
        @Override
        public void something() {
            System.out.println("最少使用缓存...");
        }
    }
    
    class LRUCache implements Cache{
    
        @Override
        public void something() {
            System.out.println("最久未使用缓存...");
        }
    }
    

    定义缓存枚举和缓存工厂,来获取缓存容器对象:

    enum CacheType{
        FIFO,
        LFU,
        LRU
    }
    
    class CacheFactory{
        public static Cache create(CacheType type) {
            switch (type) {
                case FIFO:
                    return new FIFOCache();
                case LFU:
                    return new LFUCache();
                case LRU:
                    return new LRUCache();
                default:
                    return null;
            }
        }
    }
    

    客户端使用:

    public class Client {
        public static void main(String[] args) {
            Cache cache = CacheFactory.create(CacheType.FIFO);
        }
    }
    

    客户端除了依赖于CacheFactory,还依赖于CacheType。这不符合迪米特法则,客户端应该只和CacheFactory打交道就够了。

    因此CacheFactory可以做如下优化:

    class CacheFactory{
        
        public static Cache createFIFO(){
            return new FIFOCache();
        }
        
        public static Cache createLFU(){
            return new LFUCache();
        }
        
        public static Cache createLRU(){
            return new LRUCache();
        }
    }
    

    客户端调用:

    public class Client {
        public static void main(String[] args) {
            Cache fifo = CacheFactory.createFIFO();
            Cache lfu = CacheFactory.createLFU();
            Cache lru = CacheFactory.createLRU();
        }
    }
    

    这样客户端就只依赖CacheFactory了,方法名见名知意,降低了耦合,缺点是加了新的淘汰策略的容器需要修改CacheFactory


    开闭原则

    一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
    大白话就是:对于一个程序而言,应该通过扩展来实现需求变化,而不是修改现有的代码。

    一个软件产品,开发完成上线后会进入漫长的维护期,期间还会遇到各种需求变化,需要对软件进行维护升级,开发者在设计程序的时候要尽量适应变化,以提高软件的稳定性和扩展性。

    面对变化,最直接的方式就是修改现有代码,但是它的风险是很大的,你很难保证依赖这一块代码的其他功能不会受到影响,因此测试人员必须对所有可能受到影响的功能重新测试一遍,成本太大了。

    另一种方式,就是通过派生类来做扩展,在原有的功能之上做增强,以满足客户的新需求。

    例如设计一个商城系统,支持添加商品,计算总额并输出订单详情:

    @Getter
    class Goods {
        private String name;//商品名
        private BigDecimal price;//价格
        private BigDecimal actuallyPaid;//实付
    
        public Goods(String name, BigDecimal price) {
            this.name = name;
            this.price = price;
            this.actuallyPaid = price;
        }
    
        public void setActuallyPaid(BigDecimal actuallyPaid) {
            this.actuallyPaid = actuallyPaid;
        }
    }
    
    // 订单
    class Order{
        private List<Goods> list = new ArrayList<>();
        private BigDecimal totalMoney;//总金额
    
        // 添加商品
        public void addGoods(Goods goods){
            list.add(goods);
        }
    
        // 支付
        public void pay() {
            BigDecimal sum = BigDecimal.ZERO;
            for (Goods goods : list) {
                sum = sum.add(goods.getPrice());
            }
            totalMoney = sum;
        }
    
        // 输出订单详情
        public void print() {
            System.out.println("商品\t原价\t实付");
            for (Goods goods : list) {
                System.out.println(goods.getName()+"\t"+goods.getPrice()+"\t"+goods.getActuallyPaid());
            }
            System.out.println("总价:" + totalMoney);
        }
    }
    

    客户端使用:

    public class Client {
        public static void main(String[] args) {
            Order order = new Order();
            order.addGoods(new Goods("苹果",BigDecimal.valueOf(100)));
            order.addGoods(new Goods("香蕉",BigDecimal.valueOf(200)));
            order.addGoods(new Goods("橘子", BigDecimal.valueOf(150)));
            order.pay();
            order.print();
        }
    }
    
    商品  原价  实付
    苹果  100 100
    香蕉  200 200
    橘子  150 150
    总价:450
    

    程序很简单,那如果遇到双11,推出了【满300减40】的优惠活动,程序要如何处理?直接修改Order::pay()逻辑吗?那如果双11过去了,恢复到原价购买呢?再把支付逻辑改回来吗?累不累???

    优化一下,将Order抽象出来,定义为接口:

    interface Order {
    
        // 添加商品
        void addGoods(Goods goods);
    
        // 支付
        void pay();
    
        // 输出订单信息
        void print();
    }
    

    编写一个抽象订单类,实现公共的业务逻辑:

    abstract class AbstractOrder implements Order {
        protected List<Goods> list = new ArrayList<>();
        protected BigDecimal totalMoney;
    
        @Override
        public final void addGoods(Goods goods) {
            list.add(goods);
        }
    
        @Override
        public final void print() {
            System.out.println("商品\t原价\t实付");
            for (Goods goods : list) {
                System.out.println(goods.getName() + "\t" + goods.getPrice() + "\t" + goods.getActuallyPaid());
            }
            System.out.println("总价:" + totalMoney);
        }
    }
    

    平价订单实现:

    // 平价订单
    class ParOrder extends AbstractOrder {
    
        @Override
        public void pay() {
            BigDecimal sum = BigDecimal.ZERO;
            for (Goods goods : list) {
                sum = sum.add(goods.getPrice());
            }
            totalMoney = sum;
        }
    }
    

    折扣订单实现:

    // 折扣订单 满300减40,上不封顶
    class DiscountOrder extends AbstractOrder {
    
        @Override
        public void pay() {
            BigDecimal originalPrice = BigDecimal.ZERO;
            for (Goods goods : list) {
                originalPrice = originalPrice.add(goods.getPrice());
            }
            totalMoney = originalPrice.subtract(BigDecimal.valueOf(Math.floorDiv(originalPrice.intValue(), 300) * 40));
            for (Goods goods : list) {
                // 计算单个商品的实付,方便退款
                goods.setActuallyPaid(
                        goods.getPrice()
                                .divide(originalPrice,2,BigDecimal.ROUND_HALF_UP)
                                .multiply(totalMoney));
            }
        }
    }
    

    客户端使用:

    public class Client {
        public static void main(String[] args) {
            Order order = new DiscountOrder();//折扣订单
            order.addGoods(new Goods("苹果", BigDecimal.valueOf(100)));
            order.addGoods(new Goods("香蕉", BigDecimal.valueOf(200)));
            order.addGoods(new Goods("橘子", BigDecimal.valueOf(100)));
            order.pay();
            order.print();
        }
    }
    商品  原价  实付
    苹果  100 90.00
    香蕉  200 180.00
    橘子  100 90.00
    总价:360
    

    只要更换订单实现即可,其他地方都不用动,以后如果有新的折扣活动,继续增加实现类即可。

    开闭原则是一个比较“虚”的原则,它没有告诉我们应该如何去实现。事实上,它是软件设计的一个目标,只要开发者遵循了前面五大设计原则,善用23种设计模式,就是在遵循开闭原则,设计出来的软件一定是对扩展开放,对修改关闭的。

    开闭原则的核心是:封装程序中变与不变的地方。用接口或抽象来封装规则,描述契约,这是基本稳定不变的,通过派生子类来实现细节,扩展功能,这是易变的,尽量依赖抽象,避免依赖实现。

    相关文章

      网友评论

          本文标题:设计模式六大原则

          本文链接:https://www.haomeiwen.com/subject/qsfszktx.html