六大设计原则

单一职责原则(SPR)

单一职责的原则定义是:应该有且仅有一个原因引起类的变更

单一职责原则要求一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责,他就负责一件事情

单一职责原则的好处

1.类的复杂性降低,实现什么职责都有清晰明确的定义

2.可读性提高,可维护性提高,复杂性降低

3.变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对应的实现类有影响,对其他接口无影响,这对系统的扩展性、维护性都有非常大的帮助

单一职责的使用

单一原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。

对于单一职责原则,接口一定要做到单一职责,类的设计尽可能做到单一职责

里式替换原则(LSP)

继承的优缺点

继承的优点

1.代码共享,减少创造类的工作量,每个子类都有父类的方法和属性

2.提高代码的重用性

3.子类保存父类的共性,同时拥有自己的特性

4.提高代码可扩展性,实现父类的方法就可以“为所欲为”了,市场上大多开源框架的扩展接口都是通过继承父类实现

5.提高产品或项目的开放性

继承的缺点

1.只要继承就必须拥有父类所有的属性和方法

2.降低代码的灵活性,子类必须拥有父类的所有属性和方法,让子类多了些约束

3.增强了耦合性,当父类的常量、变量和方法被修改时,必须要考虑子类的修改,而且在缺乏规范的环境下,可能会需要重构大片的代码

里式替换原则定义

如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型

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

通俗来讲就是只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

里式替换原则的含义

子类必须完全实现父类的方法

以FPS类游戏为例,描述一下里面用到的枪:

枪的主要职责是射击,如何射击在各个具体子类定义
枪支的抽象类

1
2
3
4
public abstract class AbstractGun {      
//枪用来干什么的?杀敌!
public abstract void shoot();
}

手枪、步枪、机枪的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Handgun extends AbstractGun {          
//手枪的特点是携带方便,射程短
@Override
public void shoot() {
System.out.println("手枪射击...");
}
}
public class Rifle extends AbstractGun{
//步枪的特点是射程远,威力大
@Override
public void shoot(){
System.out.println("步枪射击...");
}
}
public class MachineGun extends AbstractGun{
@Override
public void shoot(){
System.out.println("机枪扫射...");
}
}

士兵的实现类

1
2
3
4
5
6
7
8
9
10
11
12
public class Soldier {      
//定义士兵的枪支
private AbstractGun gun;
//给士兵一支枪
public void setGun(AbstractGun _gun){
this.gun = _gun;
}
public void killEnemy(){
System.out.println("士兵开始杀敌人...");
gun.shoot();
}
}

测试类

1
2
3
4
5
6
7
8
9
public class Client {      
public static void main(String[] args) {
//产生三毛这个士兵
Soldier sanMao = new Soldier();
//给三毛一支枪
sanMao.setGun(new Rifle());
sanMao.killEnemy();
}
}

运行结果

1
2
士兵开始杀敌人...
步枪射击...

如果要使用机枪可以直接把sanMao.setGun(new Rifle());改为sanMao.setGun(new MachineGun());即可,在编写程序时士兵类根本不需要知道是哪个子类(枪)被传入

注意 在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则
如果子类不能完整的实现父类方法,或者某些父类方法在子类中发生“畸变”,则建议断开继承关系,采用依赖、聚集、组合等关系代替继承

子类可以有自己的特性

子类可以有自己的方法和属性,LSP可以正着用,但是不能反过来用,在子类出现的地方,父类未必可以使用。还是以FPS游戏为例,步枪可分为AK47、AUG狙击步枪等,把这两把枪引入之后的子类图:

AUG狙击步枪类

1
2
3
4
5
6
7
8
9
public class AUG extends Rifle {      
//狙击枪都携带一个精准的望远镜
public void zoomOut(){
System.out.println("通过望远镜察看敌人...");
}
public void shoot(){
System.out.println("AUG射击...");
}
}

狙击手类

1
2
3
4
5
6
7
8
public class Snipper {              
public void killEnemy(AUG aug){
//首先看看敌人的情况,别杀死敌人,自己也被人干掉
aug.zoomOut();
//开始射击
aug.shoot();
}
}

测试类

1
2
3
4
5
6
7
8
public class Client {       
public static void main(String[] args) {
//产生三毛这个狙击手
Snipper sanMao = new Snipper();
sanMao.setRifle(new AUG());
sanMao.killEnemy();
}
}

运行结果

1
2
通过望远镜察看敌人... 
AUG射击...

在这里如果换成了父类会在运行期抛出java.lang.ClassCastException异常,也就是向下转型不安全

覆盖或实现父类的方法时输入参数可以被放大

里式替换原则要求定制一个契约,就是父类或接口,这种设计方法也叫做Design by Contract(契约设计),与LSP有异曲同工之妙。契约制定了,也就同时制定了前置条件和后置条件,前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈,标准是什么。

子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或者更宽松

覆盖或实现父类的方法时输出结果可以被缩小

父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类

覆写,父类和子类的同名方法的输入参数是相同的,两个方法的范围值S小于等于T,这是覆写的要求,这才是重中之重,子类覆写父类的方法,天经地义。

重载,则要求方法的输入参数类型或数量不相同,在里氏替换原则要求下,就是子类的输入参数宽于或等于父类的输入参数,也就是说你写的这个方法是不会被调用的。

LSP的最佳使用

在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的“个性”被抹杀——委屈了点;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准。

依赖倒置原则(DIP)

依赖倒置原则定义

1.高层模块不应该依赖低层模块,两者都应该依赖其抽象

2.抽象不应该依赖细节

3.细节应该依赖抽象


不可分割的原子逻辑就是低层模块,原子逻辑再组装的就是高层模块
抽象就是指接口或抽象类,两者都是不能直接被实例化
细节就是实现类,实现接口或继承抽象类而生成的类就是细节,其特点是可以直接实例化,可以直接new一个对象。


依赖倒置原则在Java中的表现

1.模块间的依赖通过抽象发生,实现类之间不发生之间的依赖关系,其依赖关系是通过接口或抽象类产生的

2.接口或抽象类不依赖于实现类

3.实现类依赖接口或抽象类

总而言之就是“面向接口编程”——OOD的精髓之一

依赖倒置原则的优点

1.减少类间的耦合性

2.提高系统的稳定性

3.降低并行开发引起的风险

4.提高代码的可读性和可维护性

依赖的三种写法

1.构造函数传递依赖对象

2.Setter方法传递依赖对象

3.接口声明依赖对象

接口隔离原则

接口

实例接口,在java中声明一个类,然后用new关键字产生一个实例,它是对一个类型的事物的描述,也是一种接口

类接口,java中用interface关键字定义的接口

接口隔离原则的定义

1.客户端不应该依赖它不需要的接口

2.类间的依赖关系应该建立在最小的接口上

建立单一接口,不要建立臃肿庞大的接口,通俗来讲就是接口尽量细化,同时接口中的方法尽量少

接口隔离原则的4层含义

1.接口要尽量小
不出现臃肿的接口,在拆分之前首先得保证它遵守单一职责原则

2.接口要高内聚
高内聚就是提高接口、类、模块的处理能力,减少对外的交互。应用到接口隔离原则就是在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统开发越有利,变更的风险也就越少

3.定制服务
一个系统或系统内的模块之间必然会有耦合,有耦合就要有相互访问的接口(并不一定就是Java中定义的Interface,也可能是一个类或单纯的数据交换),我们设计时就需要为各个访问者(即客户端)定制服务
定制服务就是单独为一个个体提供优良的服务。我们在做系统设计时也需要考虑对系统之间或模块之间的接口采用定制服务。采用定制服务就必然有一个要求:只提供访问者需要的方法

4.接口设计是有限度的
接口设计粒度越小,系统越灵活

接口隔离原则最佳使用

一个接口只服务于一个子模块或业务逻辑

通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法

已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理

了解环境,拒绝盲从。每个项目或产品都有特定的环境因素,别看到大师是这样做的你就照抄。千万别,环境不同,接口拆分的标准就不同。深入了解业务逻辑,最好的接口设计就出自你的手中!

迪米特法则(LoD)

也被称为最少知识原则(LKP)

也被称为最少知识原则

一个对象应该对其他对象有最少的了解。
通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。

迪米特法则的4层含义

1.只和朋友交流

两个对象之间的耦合就会成为朋友关系
朋友类的定义:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类

2.朋友间也是有距离的

一个类公开的public属性或方法越多,修改时涉及到的面越广,变更引起的风险扩散也就越大。因此为了保持朋友类间的距离,在设计时需要反复衡量,是否可以修改为private、package-private(包类型,不加访问权限时默认为包类型)、protected等访问权限,是否可以加上final关键字等。
迪米特法则要求类尽量不要对外公布太多的public方法和非静态的public变量,尽量多使用private、package-private、protected等访问权限

3.是自己的就是自己的

如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。

4.谨慎使用Serializable

迪米特法则的最佳使用

迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率在可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。在采用迪米特法则时,需要反复权衡,既做到让结构清晰,又做到高内聚低耦合

开闭原则

开闭原则的定义

一个软件实体如类、模块、函数应该对外扩展开放,对修改类关闭
也就是说,一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。

软件实体

项目或软件产品中按照一定的逻辑规则划分的模块
抽象和类
方法


开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段


开闭原则的好处

1.开闭原则对测试的影响

2.开闭原则可以提高复用性

3.开闭原则可以提高可维护性

4.面向对象开发的要求

如何使用开闭原则

1.抽象约束
抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放。

2.元数据(metadata)控制模块行为
元数据就是用来描述环境和数据的数据,通俗来说就是配置参数

3.制定项目章程
章程中指定了所有人员必须遵守的约定,对项目来说,约定优于配置。

4.封装变化
对变化的封装包含两层含义:第一,将相同的变化封装到一个接口或抽象类中。第二,将不同变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一接口或抽象类中。
封装变化也就是封装可能发生的变化

-------------本文结束❤️感谢您的阅读-------------
ボ wechat
扫描二维码,可获得菜鸡一枚
打赏测试
0%