《盘点软件设计中的七大原则》

说在前头:本人为大二在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,能力有限,文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正。若在阅读时有任何的问题,也可通过评论提出,本人将根据自身能力对问题进行一定的解答。

前言

在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据 7 条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。

一、开闭原则

定义:当项目的需求需要做出更改或者增加的时候,在不修改源代码的前提下,可以扩展模块的功能,使其功能得到实现。即:一个软件实体,对扩展开放,对修改关闭。

作用:

  • 方便软件的测试 :因为开闭原则是在不修改原来的代码基础上进行功能的修改,因此,当我们测试新修改的功能是否可用时,我们只需测试修改部分的代码即可。
  • 提高代码的复用性:开闭原则在一定的程度上,引导着代码的编写往颗粒度小的方向发展,根据原子和抽象编程可以提高代码的复用性。
  • 提高代码的可维护性:遵守了开闭原则的代码,其稳定性和延续性增强,易于日后的维护。

示例:我们先来创建一个具体的对象类Tom,Tom是一个人类,他拥有人类的各种基本技能(吃饭,走路,跑步,跳跃),如下:

package com.bosen.www;
​
/**
 * 

Tom实体类

* @author Bosen 2021/5/16 21:49 */ public class Tom { public void eat() { System.out.println("吃饭");     } public void walk() { System.out.println("走路");     } public void run() { System.out.println("跑步");     }    public void jump() { System.out.println("跳跃"); } } ​

我们来假设:Tom的家庭经济状况良好,Tom的父母给他报了一个钢琴培训班,因此Tom获得了弹奏钢琴的技能。那我们需要将新的技能点给Tom加上,如果使用最传统的实现方式,我们将在Tom类中新添加playThePiano()方法,以达到给Tom加上新的技能的要求(如下)

package com.bosen.www;
​
/**
 * 

Tom实体类

* @author Bosen 2021/5/16 21:49 */ public class Tom { public void eat() { System.out.println("吃饭");     } public void walk() { System.out.println("走路");     } public void run() { System.out.println("跑步");     } public void jump() { System.out.println("跳跃"); } public void playThePiano() { System.out.println("弹钢琴"); } }

但这种实现方式,我们对源代码进行了修改,明显不符合我们的开闭原则的要求。因此我们需要用拓展的方式让Tom的新技能优雅的添加上。

我们需要新建一个培训班类TrainingCourse,并设置了playThePiano()方法,这样下来我们的Tom只需要通过参加(继承)培训班,就学会了弹钢琴。

package com.bosen.www;
​
/**
 * 

培训班类

* @author Bosen 2021/5/16 22:07 */ public class TrainingCourse { public void playThePiano() { System.out.println("弹钢琴"); } } 

package com.bosen.www;
​
/**
 * 

Tom实体类

* @author Bosen 2021/5/16 21:49 */ public class Tom extends TrainingCourse{ public void eat() { System.out.println("吃饭"); } public void walk() { System.out.println("走路"); } public void run() { System.out.println("跑步"); } public void jump() { System.out.println("跳跃"); } }

这样,我们就无需修改Tom内部的代码,只需要通过继承的方式,即可达到新方法的添加。

二、依赖倒置原则

定义:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象
  • 抽象不应该依赖细节
  • 细节应该依赖抽象

作用:

  • 依赖倒置原则可以降低类间的耦合性。
  • 依赖倒置原则可以提高系统的稳定性
  • 依赖倒置原则可以减少并行开发引起的风险。
  • 依赖倒置原则可以提高代码的可读性和可维护性。

示例:我们获取新闻时事方式有很多种,比如通过看电视获取,看报纸获取等等。我们来试试实现这一个功能。先创建一个TV类和Newspaper类,这两个类都有获取时事的功能getCurrentAffairs(),接下来我们继续请出Tom同学帮我们完成测试,创建Tom类,并实现从电视获取时事和从报纸获取时事的两个方法getCurrentAffairsByTV(),getCurrentAffairsByNewspaper()。代码如下:

package com.bosen.www;
​
/**
 * 

电视类

* @author Bosen 2021/5/16 23:03 */ public class TV { public void getCurrentAffairs() { System.out.println("通过电视获取时事"); } }
package com.bosen.www;
​
/**
 * 

报纸类

* @author Bosen 2021/5/16 23:04 */ public class Newspaper { public void getCurrentAffairs() { System.out.println("通过报纸获取时事"); } }
package com.bosen.www;
​
/**
 * 

Tom实体类

* @author Bosen 2021/5/16 21:49 */ public class Tom { public void getCurrentAffairsByTV() { new TV().getCurrentAffairs(); } ​ public void getCurrentAffairsByNewspaper() { new Newspaper().getCurrentAffairs(); } }

从如上代码我们可以发现,Tom对于TV和Newspaper类的依赖严重,内嵌进了Tom类内部,这样设计会使得代码不便于维护。根据依赖倒置原则,我们可以这样设计,修改如下:

package com.bosen.www;
​
/**
 * 

发现时事的接口,定义了获取时事的方法

* @author Bosen 2021/5/16 23:10 */ public interface IWatch { void getCurrentAffairs(); }
package com.bosen.www;
​
/**
 * 

报纸类

* @author Bosen 2021/5/16 23:04 */ public class Newspaper implements IWatch { @Override public void getCurrentAffairs() { System.out.println("通过报纸获取时事"); } }
package com.bosen.www;
​
/**
 * 

电视类

* @author Bosen 2021/5/16 23:03 */ public class TV implements IWatch { @Override public void getCurrentAffairs() { System.out.println("通过电视获取时事"); } }

package com.bosen.www;
​
/**
 * 

Tom实体类

* @author Bosen 2021/5/16 21:49 */ public class Tom { ​ private IWatch watch; ​ public Tom(IWatch watch) { this.watch = watch; } ​ public void getCurrentAffairs() { watch.getCurrentAffairs(); } }
 

可以看到,我们在原有的基础上,增加了一个接口类,TV和Newspaper类实现了该接口,并且,Tom也无需关心具体获取时事的方法,由外部传入对应的类,Tom即可成功获取时事。这样的实现方式,不单止抽象了类之间的依赖关系,也提高了代码的维护性。

三、单一职责

定义:一个对象不应该承担过多的责任,当一个类或对象承担了过多的指责或者方法,容易消弱对象或类对其他职责的能力。并且,当客户端调用此类时,容易加入许多不必要的代码段,造成代码冗余。

作用:单一职责主要降低了代码的复杂度,提高了代码的可读性,随着代码的可读性加强以及复杂度的降低,代码的可维护性也跟着提升,与此同时,当需求变更时,需要修改某一功能的实现,只需修改该功能的代码即可,无需变更负责其他职责的代码。

四、接口隔离原则

定义:要求将庞大的接口拆分成细粒度小的接口,使接口只包含调用端感兴趣或者需要的接口。

作用:将臃肿庞大的接口拆分成细粒度更小的接口,可以预防外来的需求变更,提高代码可维护性,降低了系统的耦合性。

单一职责和接口隔离的区别:从大体上看,接口隔离原则与单一职责原则非常相像,但从细节上看又有着许多不同。单一职责主要关注的是职责的隔离,接口隔离关注的是接口依赖的隔离。单一职责主要约束类,针对程序的实现和细节,接口隔离注重整个项目的抽象体系的构建。

接下来我们使用具体代码来模仿一下:创建一个培训班接口TrainingCourse,定义钢琴课(playThePiano)和奥数课(mathematicalOlympiad),Tom去参加该培训班(实现接口),代码如下:

package com.bosen.www;
​
/**
 * 

培训班类

* @author Bosen 2021/5/16 22:07 */ public interface TrainingCourse { void playThePiano(); ​ void mathematicalOlympiad(); }
package com.bosen.www;
​
/**
 * 

Tom实体类

* @author Bosen 2021/5/16 21:49 */ public class Tom implements TrainingCourse { @Override public void playThePiano() { System.out.println("弹钢琴"); } @Override public void mathematicalOlympiad() { System.out.println("奥数"); } }

通过上面的代码我们可以看出一点不合理的地方,就是我们Tom同学如果只想要去学习钢琴课的时候,Tom同学必须也要报奥数班,如果Tom不报奥数班(不实现mathematicalOlympiad方法),培训班就会告诉你一定要将奥数班也报了才可以正式上课(因为,Java的接口定义,实现该接口的类必须实现接口下的所有方法,否则编译无法通过),这种捆绑消费行为明显是不合理的。

接下来我们使用接口隔离的实现,对上面的代码进行修改。将培训班拆分成数学班(MathCourse)和钢琴班(PianoCourse),数学班教奥数(mathematicalOlympiad),钢琴班教弹钢琴(playThePiano),代码如下:

package com.bosen.www;
​
/**
 * 

数学班接口

* @author Bosen 2021/5/17 13:40 */ public interface MathCourse { void mathematicalOlympiad(); }
package com.bosen.www;
​
/**
 * 

钢琴班接口

* @author Bosen 2021/5/17 13:40 */ public interface PianoCourse { void playThePiano(); }
package com.bosen.www;
​
/**
 * 

Tom实体类

* @author Bosen 2021/5/16 21:49 */ public class Tom implements PianoCourse { @Override public void playThePiano() { System.out.println("弹钢琴"); } }

这样一来,我们的Tom同学就可以根据自己自身的需求,报自己感兴趣的培训班即可,无需被捆绑消费。

五、迪米特原则(最少知道原则)

定义:一个对象对其他对象保持最少的了解,尽量降低类与类之间的耦合,强调只与相关类交流。相关类指的是出现在成员变量、方法的输入、输出参数中的类。

作用:降低类之间的耦合度,提高了模块之间的独立性。提高了类的可复用性和扩展性。

示例:我们先创建四个对象(明星类Idol,经纪人类Agent,粉丝类Fans,媒体类Media)。我们通过这四个对象实现两个功能(明星与粉丝见面会、明星与媒体公司业务洽谈),由于明星更专注于专业领域的工作,开见面会,和媒体洽谈的事务都会交由经纪人完成。在这个场景中,明星与经纪人是朋友,而与粉丝和媒体是陌生人,明星不需要关注开粉丝见面会的具体流程,也不需要关注媒体商谈的过程,这些都交由经纪人完成即可,经纪人完成后告诉明星结果。这种设计完全符合我们的迪米特原则。下面我们通过代码来实现:

package com.bosen.www.test5;
​
/**
 * 

明星类

* @author Bosen 2021/5/17 13:52 */ public class Idol { private String name; ​ public Idol(String name) { this.name = name; } ​ public String getName() { return name; } }
package com.bosen.www.test5;
​
/**
 * 

粉丝类

* @author Bosen 2021/5/17 13:55 */ public class Fans { private String name; ​ public Fans(String name) { this.name = name; } ​ public String getName() { return name; } }
package com.bosen.www.test5;
​
/**
 * 

媒体类

* @author Bosen 2021/5/17 13:55 */ public class Media { private String name; ​ public Media(String name) { this.name = name; } ​ public String getName() { return name; } }
package com.bosen.www.test5;
​
/**
 * 

经纪人类

* @author Bosen 2021/5/17 13:54 */ public class Agent { private Idol idol; // 与明星打交道 private Fans fans; // 与粉丝打交道 private Media media; // 与媒体打交道 ​ public Agent(Idol idol, Fans fans, Media media) { this.idol = idol; this.fans = fans; this.media = media; } ​ /* * 组织明星与粉丝的见面会 */ public void meeting() { System.out.println( this.idol.getName() + "与" + this.fans.getName() + "见面了!" ); } ​ /* * 组织明星与媒体进行商务洽谈 */ public void business() { System.out.println( this.idol.getName() + "与" + this.media.getName() + "进行洽谈!" ); } }
package com.bosen.www.test5;
​
/**
 * 

测试类

* @author Bosen 2021/5/17 16:49 */ public class Test { public static void main(String[] args) { Idol idol = new Idol("明星"); Fans fans = new Fans("粉丝"); Media media = new Media("媒体"); ​ Agent agent = new Agent(idol, fans, media); ​ // 组织明星与粉丝的见面会 agent.meeting(); // 组织明星与媒体进行商务洽谈 agent.business(); } }

测试程序输出结果

《盘点软件设计中的七大原则》_第1张图片

从上述测试的程序我们可以看到,明星无需知道粉丝和媒体的存在,具体的业务交由经纪人全权代理完成即可。类和类之间都相对独立,降低了耦合度,当项目规模增大时,便于维护。

六、里氏替换原则

定义:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

作用:约束子类对父类代码的修改,避免修改父类方法时引入新的错误。

示例:我们都知道正方形是特殊的矩形,正方形可以继承矩形。

又因为正方形的长宽必须相等,所以正方形类需要对长方形类设置宽高的方法中进行重写,此时违反了我们里氏替换原则,从而导致继承泛滥的问题,先让结合具体的代码看看这样做会出现什么样的弊端?

package com.bosen.www.test6;
​
/**
 * 

矩形类

* @author Bosen 2021/5/17 17:22 */ public class Rectangle { private int height; private int width; ​ public int getHeight() { return height; } ​ public void setHeight(int height) { this.height = height; } ​ public int getWidth() { return width; } ​ public void setWidth(int width) { this.width = width; } }
package com.bosen.www.test6;
​
/**
 * 

正方形类,继承矩形类,并重写父类方法

* @author Bosen 2021/5/17 17:22 */ public class Square extends Rectangle { ​ private int length; ​ public void setLength(int length) { this.length = length; } ​ public int getLength() { return length; } ​ @Override public void setWidth(int width) { super.setWidth(length); } ​ @Override public int getWidth() { return getLength(); } ​ @Override public void setHeight(int height) { super.setHeight(length); } ​ @Override public int getHeight() { return getLength(); } }

假设我们规定,矩形的宽应该大于高,当我们用户设置的数值是高大于宽时,我们需要将矩形的宽高进行调整,因此定义一个方法resize(),使宽一直增大,直到宽大于高,后程序才停止。代码如下

package com.bosen.www.test6;
​
public class Test {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setHeight(20);
        rectangle.setWidth(15);
​
        resize(rectangle);
    }
    
    /*
     * 调整宽高,使宽大于等于高
     */
    public static void resize(Rectangle rectangle) {
        while (rectangle.getHeight() >= rectangle.getWidth()) {
            rectangle.setWidth(rectangle.getWidth() + 1);
            System.out.println(
                    "width:"+rectangle.getWidth()+",height:"+rectangle.getHeight()
            );
        }
        System.out.println("方法结束!");
    }
}

执行结果如下:

《盘点软件设计中的七大原则》_第2张图片

将测试的方法改为子类如下:

package com.bosen.www.test6;
​
public class Test {
    public static void main(String[] args) {
        Square square = new Square();
        square.setLength(20);
​
        resize(square);
    }
    
    /*
     * 调整宽高,使宽大于等于高
     */
    public static void resize(Rectangle rectangle) {
        while (rectangle.getHeight() >= rectangle.getWidth()) {
            rectangle.setWidth(rectangle.getWidth() + 1);
            System.out.println(
                    "width:"+rectangle.getWidth()+",height:"+rectangle.getHeight()
            );
        }
        System.out.println("方法结束!");
    }
}

执行后为一个死循环,一直重复输出

《盘点软件设计中的七大原则》_第3张图片

上面代码的死循环正是因为子类重写了父类的方法导致的,也正是因为这种继承泛滥的问题,才出现了里氏替换原则的约束思想。

七、合成复用原则

定义:要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

作用:

  • 维持类的封装性
  • 降低新旧类之间的耦合度
  • 提高代码复用的灵活性    

示例:一辆汽车,它可以分为许多类型,我们假设从类型上分有汽油车和新能源车,颜色上分有黑色和白色。在不运用合成服用原则思想的前提下,当我们要生产一辆黑色的汽油车时,我需要做以下步骤,首先定义一个顶层父类汽车类Car,然后是汽车的类型类(汽油类型GasolineCar、新能源类型NewEnergyCar),最后是颜色类(BlackGasolineCar),实现代码如下:

package com.bosen.www.test7;
​
/**
 * 

顶层汽车类

* @author Bosen 2021/5/17 20:05 */ public class Car { public String i = "车"; }

package com.bosen.www.test7;
​
/**
 * 

汽油车类,继承Car

* @author Bosen 2021/5/17 20:05 */ public class GasolineCar extends Car { public String type = "汽油"; }
package com.bosen.www.test7;
​
/**
 * 

新能源汽车类,继承Car

* @author Bosen 2021/5/17 20:06 */ public class NewEnergyCar extends Car { public String type = "新能源"; }
package com.bosen.www.test7;
​
/**
 * 

黑色汽油车类

* @author Bosen 2021/5/17 20:06 */ public class BlackGasolineCar extends GasolineCar { public String color = "黑色"; }
package com.bosen.www.test7;
​
/**
 * 

测试类

* @author Bosen 2021/5/17 20:10 */ public class Test { public static void main(String[] args) { BlackGasolineCar car = new BlackGasolineCar(); System.out.println(car.color+car.type+car.i); } }

程序运行结果如下:

《盘点软件设计中的七大原则》_第4张图片

这样我们就完成了对黑色汽油车的创建,但这样实现,类和类之继承关系太过复杂,不便于维护。所以我们引入合成复用的思想对代码进行修改,修改如下:

package com.bosen.www.test7;
​
/**
 * 

汽车类

* @author Bosen 2021/5/17 20:05 */ public class Car { private String color; private String type; ​ @Override public String toString() { return color + type; } ​ public void setColor(String color) { this.color = color; } ​ public void setType(String type) { this.type = type; } }
package com.bosen.www.test7;
​
/**
 * 

汽车颜色类

* @author Bosen 2021/5/17 20:20 */ public class Color { public static String BLACK = "黑色"; public static String WHITE = "白色"; }
package com.bosen.www.test7;
​
public class Type {
    public static String GASOLINE = "汽油车";
    public static String NEW_ENERGY = "新能源车";
}
package com.bosen.www.test7;
​
/**
 * 

测试类

* @author Bosen 2021/5/17 20:10 */ public class Test { public static void main(String[] args) { Car car = new Car(); car.setColor(Color.BLACK); car.setType(Type.GASOLINE); ​ System.out.println(car); } }

程序运行结果如下:

《盘点软件设计中的七大原则》_第5张图片

经过修改,我们不难发现,运用了合成复用思想的程序代码,类与类之间的关系更抽象化了,对于代码的复用率,和灵活性也随之提高。


总结

学习设计原则是学习设计模式的基础。在实际开发的过程中,我们并不需要要求所有的代码都遵循设计原则,我们还要考虑人力、时间、成本、质量,不能刻意或“强迫”追求完美,但要在适当的场景下遵循设计原则。

   扫描二维码关注

你可能感兴趣的:(设计模式,java)