• 设计模式(06)——设计原则(1)


    设计原则

    设计原则,是设计模式的内功心法,基本所有的设计模式都是基于设计原则进行的具体化,如果说设计模式是如何操作的话,那么设计原则就是为何这么做的基石,因此,只要我们能充分理解设计原则,那么在此基础上,对设计模式就能更好的理解,甚至能自己设计出一种设计模式来。

    单一职责原则

    定义

    一个类或模块,只需要完成一个范围的功能,而不要搞得大而全。

    场景

    例如我们设计一个社交网站,现在要存储用户信息,类设计如下:

    public class UserInfo {
        private String name;
        private String like;
    
        private String location;
    }
    

    现在,我们想想该类的设计是否符合单一职责原则?


    答案是可能符合,也可能不符合。那么判断依据是什么呢?


    原因就是类或模块职责的判断是根据业务来定的,并没有一个普遍认同的规则。例如,如果该需要要的网站还提供卖东西的功能,那么用户的地址信息,就是一个十分关键的信息,且该块功能就需要抽离出来作为一个单独的模块,此时,地址信息放在这里就不合适了,违反了单一职责原则。


    但如果地址信息,只是一个值对象,也就是说其只是一个展示属性,那么放在这里就是合适的。


    综上所述,可以看到单一职责原则并不是设计出来就一成不变的,其需要结合业务发展的具体情况来判断。因此我们在设计之初,可以考虑一个大而全的类,但随着业务的发展需要,需要持续不断的进行优化(也就是持续重构的思想)。

    用处

    单一职责,因为类的设计比较小而精,因此可以极大提高代码的可维护性和可读性。


    此外因为每个类或模块只涉及自己的功能部分,因此,也做到了高内聚。

    其他

    但类的设计也不是越单一越好,因为如果拆分的过细的话,可能上层一个类需要修改,会导致下层所有依赖其的类都要修改,又影响了代码的可维护性,因此还是要根据业务需要来合理评估,重点是感觉要对。

    开闭原则

    定义

    字面意思,一个类的设计,应该要对拓展开放,对修改关闭。因此这里的重点就是以下定义该如何判断:

    • 什么样的代码修改是拓展;
    • 什么样的代码修改是修改;
    • 修改代码就一定是违反了该原则吗

    场景

        public static void main(String[] args) {
            Demo demo = new Demo();
            demo.consume(1);
        }
    
        // 根据传递过来的级别来进行不同的会员逻辑判断
        public void consume(int type) {
            if (type == 1) {
                Console.log("您好,1级会员!");
            }
            if (type == 2) {
                Console.log("您好,2级会员!");
            }
        }
    

    现在,又提出一个新的需求,还需要根据对应的会员等级进行对应的金额扣除,如果是上述的设计方式,那么修改的方式则是下面这样:

        public void consume(int type, int price) {
            if (type == 1) {
                Console.log("您好,1级会员,扣除金额{}", price);
            }
            if (type == 2) {
                Console.log("您好,2级会员,扣除金额{}", price);
            }
        }
    

    很明显,这样的方式有问题,如果还要再传递一个字段,例如优惠比例,那么依照该方案,则还需要修改接口定义,这就意味着调用方都需要修改,测试用例也需要对应的修改。

    那么如果按照开闭原则的话,该如何设计呢?


    首先我们对代码进行下重构

    // 将所有相关属性封装起来
    public class Vip {
        private int type;
        private int price;
        private int radio;
    }
    

    针对每种处理方式,根据他们的公有行为抽象出一个抽象层:

    public interface VipHandler {
        void consume(Vip vip);
    }
    


    每种特殊处理方式实现对应的抽象:

    public class FirstVipHandler implements VipHandler {
        @Override
        public void consume(Vip vip) {
            if (vip.getType() == 1) {
                Console.log("您好,1级会员,扣除金额{}", vip.getPrice() * vip.getRadio());
            }
        }
    }
    
    public class SecondVipHandler implements VipHandler {
        @Override
        public void consume(Vip vip) {
            if (vip.getType() == 2) {
                Console.log("您好,2级会员,扣除金额{}", vip.getPrice() * vip.getRadio());
            }
        }
    }
    


    通过这样的处理方式,在每次接到新的任务后,就不需要重新修改原有的逻辑方法,可以直接进行拓展即可:

        // 根据传递过来的级别来进行不同的会员逻辑判断
        public void consume(Vip vip, VipHandler vipHandler) {
            vipHandler.consume(vip);
        }
    
    

    其他

    可以看到即使是上述的方式来拓展代码,仍旧会修改原有代码,那么这种方式是违反了开闭原则吗?


    在这里,我们判断其并符合了开闭原则,因为我们判断是修改还是拓展,并不能只是简单的根据看是否修改了原有代码,真正核心的关键问题应该是:

    • 改动没有破坏原有代码的正常运行;
    • 改动没有破坏原有单元测试

    **
    在上述的修改后,我们如果加一种特殊的情况,并没有修改到原先的处理逻辑类,这也就意味着原先的代码不会引入一些可能的 bug,针对原始代码的测试用例也还是可以照常的进行编写,而不用再根据新的改动而进行改动。

    用途

    开闭原则的关键点是代码的可拓展性,即如何快速的拥抱变化,当每次新的任务来后,不必修改原始代码,而直接在原有的基础上进行拓展即可。


    关闭修改是保持原有代码的稳定性

    里氏替换原则

    定义

    子类对象可以代替父类对象出现的任何地方,并保证原来程序逻辑行为不被破坏。

    因为要保证子类对象不能破坏原有程序逻辑行为,因此该方式跟多态的区别是:
    如果子类进行了重写,并在重写的逻辑中加入了跟父类对应方法不同的逻辑,那么该方式可以称之为多态,但就不符合里氏替换原则了。

    用途

    该原则最重要的作用是指导子类的设计,保证在替换父类的时候,不改变原有程序的逻辑行为。


    在这里,重点是逻辑行为的不改变,这就意味着,我们可以对实现的细节进行修改,只要保证业务含义不变,那么就是符合里氏替换原则的。


    因此,针对这种情况,有一种用途是可以 改进 原有的实现,例如原先采用的排序算法比较低效,那么可以设计一个子类,然后重写对应排序算法,保证逻辑不发生变化。重点是,我们做的是改进,不管如何改,排序的业务含义是不变的。


    实现方式,则是按照协议进行编程,关键是子类重写过程中,要保证不破坏原有函数声明的输入、输出、异常以及注释中罗列的任何特殊情况声明。


    接口隔离原则

    定义

    首先,我们要对接口进行定义,明确其特殊含义,对于接口来说,我们将其分为两种类型的表现形式。

    • 一种语法定义,其代表了一组方法的集合;
    • 向外暴露的单个API接口或函数实现


    下面,我们分别对其进行介绍。

    场景:一组API聚合

    public interface UserInfoService {
        boolean login();
        void getUserInfoByPhone();
        
        void deleteUserByPhone();
    }
    

    看上述这个接口定义是否符合接口隔离原则???


    其实这跟单一职责原则一样,也是要看业务发展的。例如,如果该接口是提供给后台管理系统来使用的,那么没有问题,作为一个后台系统的 admin 权限人员当然可以有很多操作的能力。


    但如果该接口是给第三方用户来使用的话,就不是很合适了。因为删除操作是一个高权限能力,作为用户来说,一般是没有权限做的,那么在设计时,对应实现类就不应该实现全部接口内定义的方法,这就是接口隔离原则中所说的,不强制依赖接口中的所有方法。
    **
    而是,根据具体的定义,将其进行拆分,对应权限的实现类实现对应的权限行为。

    场景:单个API接口或函数实现

    public class Statistics {
        private int max;
        private int min;
        private int sum;
        //......
    
        public Statistics count(List<Integer> data){
            Statistics statistics = new Statistics();
            // 计算 Statistics 中的每个值
            return statistics;
        }
    }
    

    首先,我们来看上述方法定义是否符合接口隔离原则???


    在这里,我们还是要结合具体的业务场景才能做出结论,如果使用该函数的调用者,在大部分场景下都需要用到其中的大部分字段,那么该设计就是可以的。


    但是如果每次只用到其中的几个,那么该设计就不合理了,其会浪费大量的无效计算能力,影响性能。在该场景下,就需要进行拆分。

        public int max(List<Integer> data) {
            return data.stream().max(Statistics::compare).get();
        }
    
        public int min(List<Integer> data) {
            return data.stream().min(Statistics::compare).get();
        }
    

    依赖翻转原则

    对于依赖翻转原则来说,有很多看着很像的定义,我们分别对其进行介绍,看看其都是什么含义,他们之间又有什么关联。

    控制翻转(IOC)

    控制翻转,针对是在原有的程序设计流程中,整个程序的运行流程是直接交由程序员来控制的,但是如果使用控制翻转的思想,则是在一个架子中,已经定义好了执行的流程,而只是预先定义好了拓展点,后续程序员所能修改的只有拓展点,开发人员在拓展点里添加相关业务逻辑即可。

    public abstract class VipProcess {
        public abstract boolean isVip();
    
        public void consume() {
            if (isVip()) {
                Console.log("vip hello");
            } else {
                Console.log("get out");
            }
        }
    }
    
    
    public class Vip extends VipProcess {
        @Override
        public boolean isVip() {
            return true;
        }
    
        public static void main(String[] args) {
            VipProcess vip = new Vip();
            vip.consume();
        }
    }
    
    
    public class CommonPeople extends VipProcess {
        @Override
        public boolean isVip() {
            return false;
        }
    
        public static void main(String[] args) {
            VipProcess vip = new CommonPeople();
            vip.consume();
        }
    }
    

    上述方式,就是通过模板方法来实现的控制翻转,提供一个拓展的 isVip() 逻辑来交给程序员来实现,而框架根据实际实现的方法返回来决定下面的程序流转。

    依赖注入(DI)

    依赖注入更好理解,一句话概括:不用程序员显式通过 new 来创建对象,而是通过构造函数,函数传递的方式来传递对象。


    即A类如果需要依赖B类,不是通过在 A 中 new 一个 B 出来,而是在外面创建好 B 后,传递给 A。通过这样的方式,可以在需求改变中,灵活的替换传递参数(B 实现 C 接口的话)。


    而更进一步,现在一些框架都提供了 DI 的功能,只需要简单的配置一下相关类对象,所需参数等,框架就会自动接管对象的创建流程已经生命周期等。(AutoWired)

    依赖翻转(DIP)

    定义:高层模块不依赖于底层模块,而是通过一个抽象层来解耦。


    该定义其实在我们的平常业务开发中,不怎么会用到,因为我们平常就是高层依赖着底层,例如 Controller 依赖 Service,Service 依赖 Repository,该原则的重点还是指导框架层面的开发。


    例如 Tomcat ,我们知道 Tomcat 的运行是我们将程序写完后,打成 war 包扔到对应目录就可以启动了,而 Tomcat 和应用程序就是通过一个共同的抽象 **Servlet **来关联的。


    Tomcat 不直接依赖于底层实现:Web 程序,而是跟 Web 都依赖于 Servlet,而 Servlet 不依赖于具体的Tomcat 实现的 Web 的具体细节。


    通过该实现方式,在编码中,可以灵活的进行替换,比如我们还是一个 web 程序,但是运行的容器不使用 tomcat 了,也可以无缝的进行切换,只要保证要替换的容器,还是依赖于 Servlet 规范即可。

  • 相关阅读:
    [Effective JavaScript 笔记]第54条:将undefined看做“没有值”
    [Effective JavaScript 笔记]第53条:保持一致的约定
    UDP打洞原理介绍
    Uboot启动分析之Start.S
    MMU
    linux_shell
    SSH2配置
    线程同步
    C#线程基础
    客户端服务器通讯常用的一种方法——Marshal类
  • 原文地址:https://www.cnblogs.com/JRookie/p/14069357.html
Copyright © 2020-2023  润新知