设计模式的七大原则

345 阅读13分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

1、开闭原则(Open Close Principle)

书面表达:开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

个人理解:简单来说就是我在新公司原有的程序上开发新的需求,一般我们Java业务工作就是CRUD

image.png 这是最简单的链路,基本上没什么技术可言。但我们业界有一句话就是不要重复造轮子,所以这里调用的Mapper方法、Service方法都有可能被多条链路在使用,假设你现在想要开发的需求需要修改到这个公用的方法(假设是MethodA),那么你必须将这个MethodA连接的所有链路都要理清,甚至重新测试(隔壁测试岗位的已经在下单40米长刀了),最难受的情况就是牵一发而动全身,到时候别的开发、测试、产品、项目经理一起站你后面

image.png

如果这条链路只有一个业务用到,那么你修改一下也无所谓,反正现在是开发新需求嘛。但是在多个链路共用就得好好考虑,在这种情况下,copy一个MethodA_copy出来,新需求修改成MethodA_copy_new,而原来的链路不变,新需求的链路单独从MethodA_copy_new经过,既不会影响原来的功能,又可以解决新的开发需求。

image.png

如果直接修改,导致原来的功能也被修改

image.png

基于开闭原则可以采用的方法

image.png

但这种方法的缺陷也是比较明显的,就是会产生重复代码,所以需要下面的单一职责原则来减少重复代码,当然没办法去掉所有重复代码,根据《重构-改善既有代码的设计》这本书,基于开闭原则之下,有少部分的重复代码是可以接受的。

2、单一职责原则(Single Responsibility Principle)

书面表达:一个类或者一个方法只负责一项职责,尽量做到类的只有一个行为原因引起变化;

个人理解:单一接口原则其实就是比较简单了,就是理清每个接口每个类属于自己的职责,但各位程序员扪心自问一下自己有没有写过那种山一样的代码,或者说遇到过,反正我是有遇到过一个方法里写着上千行的代码……我就纳闷了,公司又不是看代码量给钱的。

image.png

遇到这种代码心里只有一个声音:快跑!千万不要回头拍照!当然了,微信钱包+支付宝钱包拦住了我。

很多人的流水账式代码,逻辑操作你们懂的,少至一两行,多至……很多很多行

image.png

那么这里之间的逻辑操作我们可以拆分,将同样或者类似的逻辑封装起来

原来的代码:

image.png

后来的代码:

image.png

image.png

两种方式对比起来有什么改变:

(1)后来的两个代码加起来总行数比原来更多了?是的没错,但是好处在什么地方,假设你浏览到method这个方法,原来你需要将这段逻辑一行一行读下去,最不济也要略读。那么用接口单一原则扩展出来之后呢,浏览method方法的doSomeOperations这里时,一行代码就可以知道干了什么,当然了,对编写doSomeOperations这个方法的程序员有一定要求,命名规范+注释。

(2)这里的OneUtil只是用来举例用的,在实际开发中不仅可以抽到Util中、也可以抽到Convert中,甚至是抽到同一个类/接口做成private权限的方法也可以,根据《重构-改善既有代码的设计》这本书,一个方法的行数最好用你的显示器刚好装下--方便程序员阅读,对机器来说,只要字节码正确就好,但是我们程序员毕竟是肉眼看的代码。

3、里氏代换原则(Liskov Substitution Principle)

书面表达:里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

个人理解:基类就是父类,派生类就是子类,在Java里面,理想的子类继承父类是,子类一定比父类强大;

class GrandFather{
    public void alive() {
        // 活着
    }
}

class Father extends GrandFather{
    @Override
    public void alive() {
        super.alive();
    }

    public void eat() {
        // 吃饭
    }
}

class Son extends Father{
    @Override
    public void alive() {
        super.alive();
    }

    @Override
    public void eat() {
        super.eat();
    }
    
    public void run() {
        // 跑步
    }
}

里式替换原则只在两个类有继承关系存在时才会显现出来,基于理想情况下(子类一定比父类强大:子类必须扩展新的方法,尽量不重写父类具体的方法--抽象方法则必须重写),子类替换父类,不会影响原来的功能。

原来:

public static void method() {
    // 前置逻辑
    GrandFather man = new GrandFather();
    man.alive();
    // 后置逻辑
}

后来:这两个都不能影响alive的逻辑

public static void method() {
    // 前置逻辑
    GrandFather man = new Father();
    man.alive();
    // 后置逻辑
}
public static void method() {
    // 前置逻辑
    GrandFather man = new Son();
    man.alive();
    // 后置逻辑
}

当然了,子类在继承父类时,可以扩展(应该说必须扩展,不然继承父类有啥意义呢)。当然,话是撂在这里了,但我们实际开发……违反里式替换原则也是经常发生的事。

image.png

4、依赖倒转原则(Dependence Inversion Principle)

书面表达:这个原则是开闭原则的基础,依赖倒置原则是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

个人理解:关键点就是依赖抽象,不依赖具体(细节),实际开发中用到比较多的就是Service层了,我们敲代码基本上都是先敲 IService 接口,里面放着没有方法体的抽象方法(JDk8之后可以有方法体不是这里的重点),再敲个 ServiceImple 实现 IService接口,重写里面所有的方法;依赖倒转原则其实就是面向过程写法到面向对象写法转化

image.png

面向过程的写法:

class Test{
    public static void main(String[] args) {
        new Person().operate(new QQEmail(), "手持方天画戟战三英之吕奉先");
    }
}

class QQEmail {
    public String sendMsg(){
        return "这是QQ邮箱发送的消息";
    }
}

class Person {
    public void operate(QQEmail qqEmail, String userName){
        System.out.println(userName + "发送的消息:" + qqEmail.sendMsg());
    }
}

image.png

那么这时候我们想用网易的163邮箱发送怎么办

(1)添加163邮箱

(2)修改Person客户程序

(3)修改调用程序

class Test{
    public static void main(String[] args) {
//        new Person().operate(new QQEmail(), "手持方天画戟战三英之吕奉先");
        new Person().operate(new WYEmail(), "七进七出救阿斗之常山赵子龙");
    }
}

class QQEmail {
    public String sendMsg(){
        return "这是QQ邮箱发送的消息";
    }
}
class WYEmail {
    public String sendMsg(){
        return "这是网易163邮箱发送的消息";
    }
}

class Person {
//    public void operate(QQEmail qqEmail, String userName){}
    public void operate(WYEmail wyEmail, String userName){
        System.out.println(userName + "发送的消息:" + wyEmail.sendMsg());
    }
}

image.png

那么改为面向对象写法呢

class Test{
    public static void main(String[] args) {
        new Person().operate(new QQEmail(), "温酒斩华雄之关云长");
    }
}

interface Email{
    String sendMsg();
}

class QQEmail implements Email{
    public String sendMsg(){
        return "这是QQ邮箱发送的消息";
    }
}

class Person {
    public void operate(Email email, String userName){
        System.out.println(userName + "发送的消息:" + email.sendMsg());
    }
}

如果想增加一个网易163邮箱 (1)添加163邮箱

(2)修改调用程序

class Test{
    public static void main(String[] args) {
        new Person().operate(new QQEmail(), "人生得意须尽欢之诗仙李太白");
        new Person().operate(new WYEmail(), "人生得意须尽欢之诗仙李太白");
    }
}

class WYEmail implements Email{
    public String sendMsg(){
        return "这是网易163邮箱发送的消息";
    }
}

image.png

从这个例子来看,用了依赖导致原则之后的面向对象,好像也没方便到哪里去……

image.png

就仅仅减少了一步,这是因为这里代码量不多,对于实际开发中,代码量远远比这里多,而且对于Person类来说不需要再修改,也符合开闭原则。

5、接口隔离原则(Interface Segregation Principle)

书面表达:这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。

个人理解:基本上每个系统都会有用户这玩意儿,比如User/UserInfo等,有点B Number的都知道数据库存关于用户的也会有对应的表,那么问题来了,用户相关的东西是一堆堆的,姓名、性别、身份证号、身高、体重、账号密码等等,当然了不一样的系统还是会有区别,在后端代码基本上都有一个UserService接口来操作,随着系统的不断开发,就会越来越臃肿,而接口隔离原则就是使用多个隔离的接口代替臃肿的接口。

interface UserService{
    // 账号
    void operateAccount();
    // 密码
    void operatePassword();
    // 姓名
    void operateUserName();
    // 性别
    void operateGender();
    // 身份证号
    void operateIdentity();
    // 地址
    void operateAddress();
    // 邮箱
    void operateEmail();
    // ......假设还有很多操作
}

这时候问题就很明显了,我想要找一个实现类去实现这个接口……好家伙,我得将里面所有的方法重写,那么我再找另一个类去实现,又得全部重写

image.png

那么这时候我刻意分成两个接口(也可以说是运用了单一接口原则),将账号安全的放一个接口,将一些常用信息放另一个接口,这样实现类实现接口可以选择性实现

interface UserAccountService{
    // 账号
    void operateAccount();
    // 密码
    void operatePassword();
    // 邮箱
    void operateEmail();
}

interface UserInfoService{
    // 姓名
    void operateUserName();
    // 性别
    void operateGender();
    // 身份证号
    void operateIdentity();
    // 地址
    void operateAddress();
}

6、迪米特法则,又称最少知道原则(Demeter Principle)

书面表达:最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

个人理解:强调只和朋友说话,不和陌生人说话,强调的是降低耦合(永远的目的)

image.png

image.png

作为一个开发仔,招聘我的是这家公司的老板,但实际上从笔试到面试到发工资,跟我直接对接的都是人事部门,而不是老板,老板是间接招聘我的,根据迪米特法则就是,我一个开发仔不能跟老板有任何接触。

class Test{
    public static void main(String[] args) {
        PitifulDeveloper pitifulDeveloper = new PitifulDeveloper();
        pitifulDeveloper.work();
        // 跟人事部门直接接触
        System.out.println("开发仔获得报酬:"+pitifulDeveloper.getSalary(new PersonnelDept()));
    }
}

// 老板
class Boss{
    // 真正发工资
    public String payroll(){
        return "3000¥";
    }
}

// 人事部门
class PersonnelDept{
    // 直接发工资
    public String payroll(){
        // 真正发工资
        return new Boss().payroll();
    }
}

// 开发
class PitifulDeveloper{
    // 工作
    public void work(){
        System.out.println("开发仔敲代码");
    }
    // 取得报酬
    public String getSalary(PersonnelDept personnelDept){
        return personnelDept.payroll();
    }
}

image.png

7、合成复用原则(Composite Reuse Principle)

书面表达:合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。

个人理解:众所周知,java四大特性包括:抽象、封装、继承、多态,众所又周知,Java尽可能不用继承……因为Java只支持单继承而不可多继承。

(1)继承破坏了封装,因为将父类的细节暴露给了子类;(子类能用父亲的一切非privete方法)

(2)继承会提高程序的耦合性,父类对自身修改会影响到子类;(子类被迫用父类修改过的方法)

(3)简单来说继承就是透明的,而组合是黑盒子,调用一方不需要知道被调用一方的修改;

image.png

其实这个还蛮容易理解的,在实际开发一般用的也是合成/聚合,最经典的就是MVC开发了 经典spring/springboot

@Controller
class UserController{
    @Resource
    private UserService userService;

    public void method(){
        userService.method();
    }
}

@Service
class UserService{
    public void method(){
        System.out.println("知天易,逆天难");
    }
}

担心有些小伙伴还没学框架,那么用其他的例子

继承方式:

class UserFather{
    public String chant(){
        return "爱你孤身走暗巷,爱你不跪的模样,爱你对峙过绝望不肯哭一场";
    }
}

class UserSon extends UserFather{

    public String chant(){
        return new UserFather().chant();
    }
}

class Test{
    public static void main(String[] args) {
        System.out.println("小明在唱:"+new UserSon().chant());
    }
}

image.png

本来小学生小明在喜欢唱《孤勇者》,但是他爸爸除了孤勇者之外还喜欢唱喜羊羊,虽然小明觉得自己已经很成熟了,不能喜欢喜羊羊了,但是既然继承了父亲,那自己只能被迫会唱。

public String chantOther(){
    return "别看我只是一只羊~";
}

那么用组合的方式:组合方式可以通过方法传参、构造器、setter方法等;用了组合的方式可以确定小明只会唱孤勇者了,爸爸即使会唱其他任何儿歌都不会影响到小明了。

class UserFather{
    public String chant(){
        return "爱你孤身走暗巷,爱你不跪的模样,爱你对峙过绝望不肯哭一场~";
    }
    public String chantOther(){
        return "别看我只是一只羊~";
    }
}

class UserSon{
    public String chant(UserFather father){
        return father.chant();
    }
}

class Test{
    public static void main(String[] args) {
        System.out.println("小明在唱:"+new UserSon().chant(new UserFather()));
    }
}

当然这里其实看不出什么降低耦合,如果抽象成接口就可以看出来了

class UserMother implements Person{
    public String chant(){
        return "别看我只是一只羊~";
    }
}

class UserFather implements Person{
    public String chant(){
        return "爱你孤身走暗巷,爱你不跪的模样,爱你对峙过绝望不肯哭一场~";
    }
    public String chantOther(){
        return "别看我只是一只羊~";
    }
}

interface Person{
    String chant();
}

class UserSon{
    public String chant(Person person){
        return person.chant();
    }
}

class Test{
    public static void main(String[] args) {
        System.out.println("小明在唱:"+new UserSon().chant(new UserFather()));
        System.out.println("小明在唱:"+new UserSon().chant(new UserMother()));
    }
}

可以看到小明无论接收父亲还是母亲都不用修改自身代码了。这是利用了依赖倒转原则。

总结:一边查资料一边码字码了大半天,不一定是特别好的文章,但主要的目的是用自己语言理解、记住这七大原则。接收各位读者的意见与建议,但是不接受喷。希望各位看过之后有收获~