4.接口隔离原则
定义:
接口隔离原则是针对不同的模块提供专门的接口,而不是建立一个庞大的臃肿的接口供客户端访问。意在接口设计的粒度越小,越灵活,方法越少,越容易扩展,而且尽量只有一个方法。接口隔离原则与单一职责类似,但是出发角度不同。单一职责是讲得的是职责单一,只有一种产生影响的原因。而接口隔离讲得是接口级别,比单一职责范围限定更窄。
规范规约:
- 接口要尽量小
尽量满足单一职责原则
- 接口要高内聚
提高模块内的联系,减少对外交互,尽量少public,如果不可以避免,那么是否可以单独抽取一个对外交换的接口,和其他接口区分。
- 定制服务
可以为特定的场景提供定制接口,保证模块间的耦合性的同时,不影像其他业务。
- 接口设计是有限度的
不是说接口设计粒度越小就更优秀,接口粒度越小,系统灵活性提高,但是随之代理的问题就是,增加了管理的难度。所以接口设计还是要有一限度,这个需要更具经验而来。
举例:
声明一个吃饭工具的接口EatingTool ,接口含有两个抽象方法:cuttFood和pickFood。声明了一个筷子类Chopstick,筷子了实现了接口EatingTool ,并实现了cuttFood和pickFood方法。测试类用筷子吃摊鸡蛋的时候,先用筷子把摊鸡蛋切成小块,然后再夹起来送到嘴里,这个设计和过程都是合乎情理的。
/**
* 吃饭工具接口
*/
public interface EatingTool {
//切割食物
void cuttFood(String food);
//拾取食物
void pickFood(String food);
}
/**
* 筷子类
*/
public class Chopstick implements EatingTool{
@Override
public void cuttFood(String food) {
System.out.println("切割了 :" + food);
}
@Override
public void pickFood(String food) {
System.out.println("夹起了 :" + food);
}
}
public class TestEat {
public static void main(String[] args) {
Chopstick chopstick = new Chopstick();
chopstick.cuttFood("摊鸡蛋");
chopstick.pickFood("摊鸡蛋");
}
}
打印结果:
切割了 :摊鸡蛋
夹起了 :摊鸡蛋
但是有一天,筷子不见了,只剩下刀叉,但是摊鸡蛋还是要吃的,用刀叉也一样,但事实错了。声明一个刀类,也实现了工具接口EatingTool,但是这里就有问题了。餐刀只能用来切割食物,不能拾取食物啊,用餐刀吃饭,气氛有点怪怪的。EatingTool有两个功能,不满足单一职责,怎么办呢? 拆分?EatingTool保留没有具体细节,将切割食物和拾取食物拆分出两个接口。于是又吃到了摊鸡蛋,代码如下:
/**
* 吃饭工具接口
*/
public interface EatingTool {
}
/**
* 切割食物的工具
*/
public interface CuttFoodTool extends EatingTool{
//切割食物
void cuttFood(String food);
}
/**
* 拾取食物
*/
public interface PickFoodTool extends EatingTool{
//拾取食物
void pickFood(String food);
}
/**
* 餐刀
*/
public class Knife implements CuttFoodTool{
@Override
public void cuttFood(String food) {
System.out.println("切割 "+food );
}
}
/**
* 餐叉
*/
public class Fork implements PickFoodTool{
@Override
public void pickFood(String food) {
System.out.println("拾取 "+ food);
}
}
public class TestEat {
public static void main(String[] args) {
Knife knife = new Knife();
knife.cuttFood("摊鸡蛋");
Fork fork = new Fork();
fork.pickFood("摊鸡蛋");
}
}
执行结果:
切割 摊鸡蛋
拾取 摊鸡蛋
不过此时的筷子类也需要适当修改,分别实现CuttFoodTool,PickFoodTool两个接口
/**
* 筷子类
*/
public class Chopstick implements CuttFoodTool,PickFoodTool{
@Override
public void cuttFood(String food) {
System.out.println("切割了 :" + food);
}
@Override
public void pickFood(String food) {
System.out.println("夹起了 :" + food);
}
}
优势:
- 接口的容量越小,越灵活,越容易扩展
- 提高了内聚,一个接口只有一个工作
- 降低模块间的耦合性
实践:
单一接口职责,不单指名义上的接口,其实也包含了实体类,一个接口只服务与一个模块或业务逻辑。接口的设计要尽量避免臃肿和庞大,接口越臃肿庞大受影响的因素越多,影响的范围越大。当发现接口已经被污染时,能修改拆分尽量修改差分,不行的化就采用适配器的方式进行适配。
5.迪米特法则Least Knowledge Principle(LKP)
定义:
迪米特法则规定的是一个对象应该让另一个对象有更少的了解。在软件工程中的体现是一个类应该提供更少的public方法,相对于调用方,就是只是知道public 方法,其他细节不想了解,也无需了解。
迪米特法则意在两个类之间松耦合,耦合体现的方式有依赖,组合, 聚和。迪米特法则,将这种关系描述朋友,而朋友类是指方法的输入输出参数,声明的变量,内部类不算是朋友类。朋友类是产生直接耦合关系的类。
举例:
有些话只对朋友说
比如A类和C类分属在不同的模块,现在A和C需要关联,但是又不想A和C过度耦合,那么就可以增加一个B类,来中间调和一下。也就是朋友类,其实朋友类也就是过渡类。
朋友之间也是有距离的。
过渡类可以原本关联的类松耦合但是对于对于这个朋友也要有限定要求,朋友之间也不是无话不谈,封装的方法也要有内聚性。否则结果会增加系统的复杂性。
班主任让班长上课前点名统计一下人数,老师不必亲自去数班里来了多少人,只是想知道最后的结果,通过班长去统计人数。申明一个老师类Teacher ,告知班长点名的方法inform()。申明一个班长类Monitor,班长有一个公开的点名的方法roolCall,但是具体怎么点名是私有的count()。老师是知道班长要点名,通过monitor参数调用roolCall方法。但是具体点名的方法,班长是没和老师说,老师也不用知道。可能是按人头数的,也可能是班长来的时候已经快上课了,为了节省时间,看了一眼签到表就告诉班主任了。代码如下:
/**
* 班主任
*/
public class Teacher {
//告知班长点名
public void inform(Monitor monitor){
monitor.roolCall();
}
}
/import java.util.List;
/**
* 班长
*/
public class Monitor {
//全班学生
private List<String> sudentList;
public Monitor(List<String> sudentList){
this.sudentList = sudentList;
}
/**
* 统计人数
*/
public void roolCall(){
int count = count();
System.out.println("全班一共来了 " + count + "人");
}
private int count(){
return sudentList.size();
}
}
import java.util.ArrayList;
import java.util.List;
/**
* 迪米特法则测试类
*/
public class TestTM {
public static void main(String[] args) {
//学生陆续进行教室
List<String> list = new ArrayList<>();
list.add("小黄");
list.add("小红");
list.add("小蓝");
list.add("班长");
/**
* 老师让班长点名统计来了多少同学
*/
//老师类
Teacher teacher = new Teacher();
//班长类
Monitor monitor = new Monitor(list);
teacher.inform(monitor);
}
}
打印结果:
全班一共来了 4人
优点:
迪米特法则将通过过度类使两个之间耦合度降低。
实践:
迪米特法制虽然实现了解耦,但是这种解耦也是有限度的解耦。可以通过两个本来没有关系的类通过过度类产生依赖,降低了耦合的强度,但是如果使用不慎,有可能产生大量的过度类,这样就得不偿失了。
迪米特法则是从类与类之间的关系的角度来讲解如何做到高内聚低耦合。迪米特法则是希望类之间的联系越少越好,让类越来越独立。每个类应该只了解和自己相关的部分,一但发生变化,那么了解这一变化的类会越来越少。
6、开闭原则
定义:
开闭原则的定义是一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。开闭原则是设计的基础,需求是会发生变化的,很多情况是在前一个需求已经稳定投产后,后续需求又需求的原有基础上有改动。而开闭原则这是限定了如何来应对这种变化,毕竟谁也无法保证设计的接口或者类始终会满足这种变化。开闭原则对于这种变化原则上是进行扩展,而不是在原有基础上进行修改。不过变化也是多种多样的,规则的粒度也可调整比如可以将变化分类选择不同的策略:
- 逻辑变化:
比如只是涉及一条逻辑修改,并且其他类也是遵守对应规则,那这种情况是允许修改的。反之,如果会造成的影响,这种情况应该是要新增一个方法,来修改这个逻辑
- 模块变化
一个模块变化,会对其他的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的,刚刚的书籍打折处理就是类似的处理模块,该部分的变化甚至会引起界面的变化。
- 可见试图的变化
比如界面需要增加一个显示字段,如果原因返回内容已经存在这个字段,只是页面增加显示还是简单的。但是如果这个字段还要通过连表查询后计算得出,那这种变化就比较恐怖了,扩展就是很有必要的。还有就是在查询时,起初是查询一条,后来必须支持多条查询,这就需要在设计初期是否要考虑这种场景了。
优点:
开闭原则像是欲按,真对是未来可能产生的影响而预先考虑的方案,将风险和影响降低。
实践:
- 1.抽象约束
- 2.元数据(metadata)控制模块行为
- 3.制定项目章程
- 4.封装变化
上述四种方式都是为了尽量满足开闭原则。开闭原则的最根据意义是为了提高代码的可扩展性。提高代码的可扩展性,就是在代码的设计之初预留扩展点,而这一点常常是建立在对业务熟悉或者使用场景熟悉的基础之上。这一点恐怕实在很难掌握,稍有不慎就会产生过度设计或者设计不足。在实际的开发过种,常常是针对比较确定或者短期内就可能会扩展、需求改动代码对于代码构造影响比较大的、还有就是成本不高的扩展点。开闭原则并不是完全杜绝修改,而是以最小的代价修改的代价来完成新功能的迭代。
六大设计原则总结:
学习了完了六大设计原则,感知最深的应该是高内聚,松耦合对于一个系统来说是多么的重要。设计原则都是围绕如何做到这一点。不过原则不是硬性标准,不懂得变通,生搬硬套,那带来的更多的困扰,所以还是需要结合实际来准确运用。
单一职责原则:是对接口、类,行为的限定
里氏替换原则:将父类作为传输条件,提高代码的健壮性
依赖倒置原则:模块之间的关联应该通过接口或者抽象类
接口隔离原则:从接口层(接口,类)来看,接口越单一越意于扩张和维护
迪米特法则原因:两个类通过过渡类来建立耦合关系,过渡类具有高内聚的特点
开闭原则:影响小直接修改,影响大直接扩展。越底层,越要避免修改,进行扩张。影响是从低到高会逐层放大。
参考:《设计模式禅》 秦小波著