说在前头:本人为大二在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,能力有限,文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正。若在阅读时有任何的问题,也可通过评论提出,本人将根据自身能力对问题进行一定的解答。
前言
在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据 7 条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。
一、开闭原则
定义:当项目的需求需要做出更改或者增加的时候,在不修改源代码的前提下,可以扩展模块的功能,使其功能得到实现。即:一个软件实体,对扩展开放,对修改关闭。
作用:
- 方便软件的测试 :因为开闭原则是在不修改原来的代码基础上进行功能的修改,因此,当我们测试新修改的功能是否可用时,我们只需测试修改部分的代码即可。
- 提高代码的复用性:开闭原则在一定的程度上,引导着代码的编写往颗粒度小的方向发展,根据原子和抽象编程可以提高代码的复用性。
- 提高代码的可维护性:遵守了开闭原则的代码,其稳定性和延续性增强,易于日后的维护。
示例:我们先来创建一个具体的对象类Tom,Tom是一个人类,他拥有人类的各种基本技能(吃饭,走路,跑步,跳跃),如下:
package com.bosen.www;
/**
* <p>Tom实体类</p>
* @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;
/**
* <p>Tom实体类</p>
* @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;
/**
* <p>培训班类</p>
* @author Bosen 2021/5/16 22:07
*/
public class TrainingCourse {
public void playThePiano() {
System.out.println("弹钢琴");
}
}
package com.bosen.www;
/**
* <p>Tom实体类</p>
* @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;
/**
* <p>电视类</p>
* @author Bosen 2021/5/16 23:03
*/
public class TV {
public void getCurrentAffairs() {
System.out.println("通过电视获取时事");
}
}
package com.bosen.www;
/**
* <p>报纸类</p>
* @author Bosen 2021/5/16 23:04
*/
public class Newspaper {
public void getCurrentAffairs() {
System.out.println("通过报纸获取时事");
}
}
package com.bosen.www;
/**
* <p>Tom实体类</p>
* @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;
/**
* <p>发现时事的接口,定义了获取时事的方法</p>
* @author Bosen 2021/5/16 23:10
*/
public interface IWatch {
void getCurrentAffairs();
}
package com.bosen.www;
/**
* <p>报纸类</p>
* @author Bosen 2021/5/16 23:04
*/
public class Newspaper implements IWatch {
@Override
public void getCurrentAffairs() {
System.out.println("通过报纸获取时事");
}
}
package com.bosen.www;
/**
* <p>电视类</p>
* @author Bosen 2021/5/16 23:03
*/
public class TV implements IWatch {
@Override
public void getCurrentAffairs() {
System.out.println("通过电视获取时事");
}
}
package com.bosen.www;
/**
* <p>Tom实体类</p>
* @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;
/**
* <p>培训班类</p>
* @author Bosen 2021/5/16 22:07
*/
public interface TrainingCourse {
void playThePiano();
void mathematicalOlympiad();
}
package com.bosen.www;
/**
* <p>Tom实体类</p>
* @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;
/**
* <p>数学班接口</p>
* @author Bosen 2021/5/17 13:40
*/
public interface MathCourse {
void mathematicalOlympiad();
}
package com.bosen.www;
/**
* <p>钢琴班接口</p>
* @author Bosen 2021/5/17 13:40
*/
public interface PianoCourse {
void playThePiano();
}
package com.bosen.www;
/**
* <p>Tom实体类</p>
* @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;
/**
* <p>明星类</p>
* @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;
/**
* <p>粉丝类</p>
* @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;
/**
* <p>媒体类</p>
* @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;
/**
* <p>经纪人类</p>
* @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;
/**
* <p>测试类</p>
* @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();
}
}
测试程序输出结果
从上述测试的程序我们可以看到,明星无需知道粉丝和媒体的存在,具体的业务交由经纪人全权代理完成即可。类和类之间都相对独立,降低了耦合度,当项目规模增大时,便于维护。
六、里氏替换原则
定义:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
作用:约束子类对父类代码的修改,避免修改父类方法时引入新的错误。
示例:我们都知道正方形是特殊的矩形,正方形可以继承矩形。
又因为正方形的长宽必须相等,所以正方形类需要对长方形类设置宽高的方法中进行重写,此时违反了我们里氏替换原则,从而导致继承泛滥的问题,先让结合具体的代码看看这样做会出现什么样的弊端?
package com.bosen.www.test6;
/**
* <p>矩形类</p>
* @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;
/**
* <p>正方形类,继承矩形类,并重写父类方法</p>
* @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("方法结束!");
}
}
执行结果如下:
将测试的方法改为子类如下:
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("方法结束!");
}
}
执行后为一个死循环,一直重复输出
上面代码的死循环正是因为子类重写了父类的方法导致的,也正是因为这种继承泛滥的问题,才出现了里氏替换原则的约束思想。
七、合成复用原则
定义:要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
作用:
- 维持类的封装性
- 降低新旧类之间的耦合度
- 提高代码复用的灵活性
示例:一辆汽车,它可以分为许多类型,我们假设从类型上分有汽油车和新能源车,颜色上分有黑色和白色。在不运用合成服用原则思想的前提下,当我们要生产一辆黑色的汽油车时,我需要做以下步骤,首先定义一个顶层父类汽车类Car,然后是汽车的类型类(汽油类型GasolineCar、新能源类型NewEnergyCar),最后是颜色类(BlackGasolineCar),实现代码如下:
package com.bosen.www.test7;
/**
* <p>顶层汽车类</p>
* @author Bosen 2021/5/17 20:05
*/
public class Car {
public String i = "车";
}
package com.bosen.www.test7;
/**
* <p>汽油车类,继承Car</p>
* @author Bosen 2021/5/17 20:05
*/
public class GasolineCar extends Car {
public String type = "汽油";
}
package com.bosen.www.test7;
/**
* <p>新能源汽车类,继承Car</p>
* @author Bosen 2021/5/17 20:06
*/
public class NewEnergyCar extends Car {
public String type = "新能源";
}
package com.bosen.www.test7;
/**
* <p>黑色汽油车类</p>
* @author Bosen 2021/5/17 20:06
*/
public class BlackGasolineCar extends GasolineCar {
public String color = "黑色";
}
package com.bosen.www.test7;
/**
* <p>测试类</p>
* @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);
}
}
程序运行结果如下:
这样我们就完成了对黑色汽油车的创建,但这样实现,类和类之继承关系太过复杂,不便于维护。所以我们引入合成复用的思想对代码进行修改,修改如下:
package com.bosen.www.test7;
/**
* <p>汽车类</p>
* @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;
/**
* <p>汽车颜色类</p>
* @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;
/**
* <p>测试类</p>
* @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);
}
}
程序运行结果如下:
经过修改,我们不难发现,运用了合成复用思想的程序代码,类与类之间的关系更抽象化了,对于代码的复用率,和灵活性也随之提高。
总结
学习设计原则是学习设计模式的基础。在实际开发的过程中,我们并不需要要求所有的代码都遵循设计原则,我们还要考虑人力、时间、成本、质量,不能刻意或“强迫”追求完美,但要在适当的场景下遵循设计原则。
👇扫描二维码关注