书接上回,本篇继续讲一下设计模式六大原则(有些书认为是7大原则)
原则定义
1>高层次的模块不要依赖低层次的模块,二者都应该依赖于其抽象
2>抽象不应该依赖于具体,而是具体应该依赖于抽象
看懂上面的定义得提前理解几个词:高层次模块,低层次模块, 抽象,具体
高层次模块:也叫上层代码,一般可以认为调用方。以系统三层结构(表现层,业务层,持久层)为例子,表现层相对于业务层来说,就是高层,业务层相对于持久层来说就是低层。
低层次模块:也叫为下层,一般可以认为被调用方。以系统三层结构(表现层,业务层,持久层)为例子,业务层相对于表现层来说,就是低层,持久层相对于业务层来说就是低层。
抽象:设计模式的中的抽象可以理解为约束/规范,代码中表现就是:接口/抽象类
具体:设计模式中的具体就是抽象具现化,也即:接口实现类/抽象类的子类
上面总结一句话就是:面向接口编程,再精简一点就是:面向多态编程
案例分析
需求:实现多终端(pc端/app端)签到案例
/**
* App端签到
*/
public class AppClient {
public void signIn(){
System.out.println("app端签到.....");
}
}
/**
* pc端签到
*/
public class PCClient {
public void signIn(){
System.out.println("pc端签到.....");
}
}
/**
* 签到用户
*/
public class User {
//pc
public void signInPC(PCClient client){
client.signIn();
}
//app
public void signInApp(AppClient client){
client.signIn();
}
}
测试:
public class App {
public static void main(String[] args) {
//签到用户
User user = new User();
user.signInPC(new PCClient()); //pc签到
user.signInApp(new AppClient()); //app签到
}
}
结果:
pc端签到.....
app端签到......
解析
上面案例中App是高层次类,User是低层次类,
App,User, AppClient PCClient 都是具体,没有抽象
App VS User
App是高层次类, User为低层次类
User VS AppClient/PCClient
User是高层次类, AppClient/PCClient是低层次类
当前存在的问题:
1>App类需要调用User类签到方法实现不同签到,如果User类缺少某些签到方法,App类也缺少某些签到方法。此时的App类严重依赖User类。这不符合依赖倒置原则中高层次模块不要依赖低层次模块
2>User类是具体类,需求变动时(比如:添加了小程序签到),User类就必须额外添加小程序签到方法。User类就必须变动了。这就违反了开闭原则【对拓展开发,对修改关闭】。
改版:
将签到逻辑抽象成行为,让User类依赖不再是具体的AppClient/PCClient而是抽象接口
/**
* 签到行为(行为约束/规则)
*/
public interface ISign {
void signIn();
}
/**
* App端签到
*/
public class AppClient implements ISign{
public void signIn(){
System.out.println("app端签到.....");
}
}
/**
* pc端签到
*/
public class PCClient implements ISign{
public void signIn(){
System.out.println("pc端签到.....");
}
}
/**
* 签到用户
*/
public class User {
//pc/app
public void signIn(ISign client){
client.signIn();
}
}
测试:
public class App {
public static void main(String[] args) {
//签到用户
User user = new User();
ISign sginPC = new PCClient();
ISign sginApp = new AppClient();
user.signIn(sginPC); //pc签到
user.signIn(sginApp); //app签到
}
}
解析:
1>改版后的App类对User类依赖降低了,App类需要什么签到功能只需要new出不同的签到客户端对象即可。User类只需要提供固定签到方法即可。这符合高层次的模块不要依赖低层次的模块
另外User类对 AppClient/PCClient类的依赖由原先的具体类转成抽象接口,结构更稳啦,因为接口定制规则已经固定了,具体类再怎么变化都在规则之内。
2>如果需求变化,需要加入新的签到功能,只需按照ISign接口约定签到规则创建新的签到客户端类即可。User类完全不需要变动。因为:具体(User类)依赖于抽象(ISign类),接口的稳定性高于具体实现类。
思考
这样设计意义在哪?
1> 系统稳定性,维护性,拓展性考虑
一个好的系统架构必须是一个稳定系统架构,设计系统架构时遵守依赖倒置原则是必须的,这怎么理解?回到传统版的代码设计,如果User类需要涉及到各种终端的对接,如果单纯去添加User类的signInXx方法,势必引入不可以预知的变动。比如:如果User有子类,子类乱重写,现有的终端类失效(甚至要去除)。
传统版中User类对接各种终端具体实例,这种设计在系统后期维护与拓展上也会带来挑战。原因:各种终端实例对签到这个逻辑没有具体约定,实现完全是凭程序员个人修养。比如: 客户端签到方法设计为 signPC signApp signXxx等等。那怎么解决这些乱象,设计出优雅的架构呢?很简单:遵循依赖倒置原则,制定好规范,按照规范设计代码。
具体实现是多变的,而抽象是稳定,以抽象为基础搭建起来的架构自然比以具体实现为基础搭建起来的架构要稳定的多。
2>减少耦合
改版后的设计,可以发现,高层次模块(User)对低层次模块(PCClient、AppClient)调用依赖中间的ISign接口,使用接口隔开高低层的直接接触,高层次模块只需要按实现约定的规则(ISgin)调用,不需要关注低层次模块的具体实现, 低层次模块按照约定规则实现与传值即可。这减少2个层面的代码存在过多耦合,有利于后续系统拓展与优化。
实现方式
开发中依赖倒置原则实现方式常见有3种:
1>接口传递
/**
* 签到用户
*/
public class User {
//pc/app
public void signIn(ISign client){
client.signIn();
}
}
2>构造器方式
/**
* 签到用户
*/
public class User2 {
private ISign sign;
public User2(ISign sign){
this.sign = sign;
}
//pc/app
public void signIn(){
sign.signIn();
}
}
3>setter方式
/**
* 签到用户
*/
public class User3 {
private ISign sign;
public void setSign(ISign sign){
this.sign = sign;
}
//pc/app
public void signIn(){
sign.signIn();
}
}
运用
我们还是从JDK里面找例子,集合中的排序方法:
Comparator 接口规定了排序的规则,两两比较,由最终返回值决定谁大谁小,谁先谁后。
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
2个排序规则:
//正排, 匿名内部类
Comparator<Integer> cp1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
};
//倒排,匿名内部类
Comparator<Integer> cp2 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
};
比较:
public class App {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
//正排
Comparator<Integer> cp1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
};
//倒排
Comparator<Integer> cp2 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
};
list.sort(cp1);
System.out.println(list); //[1,2,3,4]
list.sort(cp2);
System.out.println(list);//[4,3,2,1]
}
}
解析
Comparator 接口定义规定中,高层模块(list集合sort方法)按照规则调用底层模块(Comparator 接口正排倒排实现)。
总结
依赖倒置原则使用过程中要明确的:
1>低层模块尽量依赖有抽象类或接口, 也即低层模块代码需要按规范实现,不能天马行空。
2>高层模块依赖低层模块的抽象规范(接口)而不是具体实现,有利于程序扩展和优化
一句话概括: 面向接口编程~