设计模式的六大原则
一.设计模式
设计模式,即Design Patterns,是指在软件设计中,被反复使用的一种代码设计经验。使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性。
设计模式这个术语是上个世纪90年代由Erich Gamma、Richard Helm、Raplh Johnson和Jonhn Vlissides四个人总结提炼出来的,并且写了一本Design Patterns的书。这四人也被称为四人帮(GoF)。
GoF把23个常用模式分为创建型模式、结构型模式和行为型模式三类,后面会一一说明。
二.六大原则
为什么要使用设计模式?根本原因还是软件开发要实现可维护、可扩展,就必须尽量复用代码,并且降低代码的耦合度。设计模式主要是基于OOP编程提炼的,它基于以下几个原则:
-
单一职责原则 (SRP):
一个类所承担的功能或职责必须是同一类或者相近的,不要让一个类承担过多的职责。换一句话说,一个类的职责应该单一。(接口的职责必须单一。类的设计尽量做到只有一个原因引起类的变化,指的是设计类而不是实现类,实现类做到单一职责的话无形中会增加很多类,会使原本简单的事情变得更加复杂。)
-
开闭原则 (OCP):
一个软件实体应该对扩展开发,对修改关闭。其含义是说一个软件实体应该通过扩展实现变化,而不是通过修改内部已有的代码来实现变化。
软件实体是啥?
- 项目或软件产品中按照一定逻辑划分的模块
- 抽象和类
- 方法
开闭原则指导我们,当软件需要变化时应”尽量通过”扩展的方式来修改,而不是通过变化已有代码来实现,这里说的是尽量,并不是绝对不可以修改原始类,当我们嗅到“腐化”气味时应尽量早重构。而不是通过继承等方式添加新的实现,这会导致类的膨胀及历史代码的遗留。
通常我们可以用接口或抽象类来约束一组可变化的行为。主要包含3个层次:
- :通过接口或抽象类约束扩展,对扩展边界定义,不允许实现类出现接口或抽象类以外的public 方法。
- :参数类型、引用变量尽量使用接口或抽象对类,而不是实现类。
- :抽象层尽量保持稳定。
当然在实际应用过程中,往往修改源代码和扩展是同时存在的。
-
里氏替换原则 (LSP)
所有引用基类的地方必须能透明地使用其子类对象。
通俗一点解释是这样的:只要任何有父类出现的地方(如形参),都可以替换为子类,而且替换为子类也不会产生异常和错误。对于使用者(方法)本身不需要关心到底是父类还是子类。(但是,有子类出现的地方,替换为父类就不一定可以了。)
一个例子:
//窗口类
public class Window(){
public void show(View child){
child.draw();
}
}
public abstract class View(){
public abstract void draw();
public void measure(int widht,int height){
//测量视图大小
}
}
public class Button extends View{
public void draw(){
//绘制按钮
}
}
public class TextView extends View{
public void draw(){
//绘制文本
}
}
里氏替换原则为良好的继承定义了一个规范:
1、子类必须完全实现父类的方法。我们在做系统设计的时候经常定义一个接口或抽象类,然后编码实现,调用类直接传入接口或抽象类,其实这里已经使用了里氏原则。
如果子类不能全实现父类的方法,或者父类的某些方法在子类中已经发生畸变,建议断开继承关系,采用依赖、聚合、组合等关系代替。
2、子类有自己的个性。即有子类出现的地方,父类未必可以。
3、覆盖或实子类重载父类的方法时,传入的参数必须比父类更宽松(相同或范围大)。(否则会出现本来虚调用父类方法的地方调用了子类的方法)
4、覆盖或实子类重载父类的方法时,返回的结果必须范围更小(和父类类型相同或是父类返回类型的子类)。
-
迪米特原则 (LOD)
一个对象应该对一个被调用的类(被耦合)有最少的了解。调用者只需要知道它需要调用的方法即可。类与类之间的关系越密切,当一个类发生改变时,对另一个类影响也越大。
迪米特原则包含的含义:
- 只和朋友交流:每个类必然会和其他类有耦合关系,两个对象间的耦合就成为朋友关系(聚合、组合、依赖等)。朋友类得定义是这样的:出现在成员变量、方法的输入、输出参数中的称为成员朋友类,而出现在方法体内部的类不属于朋友类。
- 朋友之间也是有距离的:不要对外公布太多的public方法和非静态的public变量,尽量内敛。
迪米特原则的核心观念是:解耦。即:高内聚、低耦合。
-
接口隔离原则 (ISP)
关于接口有一点要说明:类也是接口
有两种定义方法:一种为类不应该依赖它不需要的接口;另一种为类之间的依赖关系应该建立在最小的接口上。接口隔离的原则是将非常庞大、臃肿的接口分割成更小更具体的接口。
有的时候会感觉单一职责和接口隔离隔离原则很像,单一职责更关注的是功能的单一,是业务层次上的划分。而接口隔离原则更关心的是接口的数量要少。
比如说一个接口有一组10个功能,提供给若干个模块使用,每个模块按照按照不同的权限只能使用接口中的一部分功能。按照单一职责原则是允许的,但是按照接口隔离原则是不允许的,接口隔离原则要求:”尽量使用多个专用接口”,意思为有几个模块就提供几个接口,而不是建立一个庞大的接口供所有模块使用。
->但是接口设计时有限度的,当然也不要分了隔离接口而将全部接口都细化,这样就变为过度设计了
最佳方法:
1. 一个接口只服务于一个模块或业务流程
2. 压缩接口对外提供的public方法
-
依赖倒置原则 (DIP)
依赖倒置主要是实现解耦,使得高层次的模块不依赖于低层次模块的具体实现细节。
几个关键点:
a.高层模块不应该依赖底层模块(具体实现),二者都应该依赖其抽象(抽象类或接口)。模块之间的依赖通过抽象产生,实现类之间不发生直接依赖关系,依赖关系通过接口或抽象类产生。(高层模块就是调用端,底层模块就是具体实现类.)
b.抽象不应该依赖细节
c.细节应该依赖于抽象,用java语言解释就是:就是各个模块之间相互传递的参数声明为抽象类型,而不是声明为具体的实现类;
->依赖导致的本质是通过抽象使各个类或者模块的实现彼此独立,不相互影响,实现模块间的松耦合。每个类(底层模块)尽量有接口和抽象类,或者接口抽象类二者皆有。变量的声明尽量是抽象或者接口,尽量不覆盖父类的方法
示例:母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。代码如下:
//窗口类
class Book{
public String getContent(){
return "很久很久以前有一个阿拉伯的故事……";
}
}
class Mother{
public void narrate(Book book){
System.out.println("妈妈开始讲故事");
System.out.println(book.getContent());
}
}
public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.narrate(new Book());
}
}
运行结果:妈妈开始讲故事 很久很久以前有一个阿拉伯的故事……
运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:
class Newspaper{
public String getContent(){
return "林书豪38+7领导尼克斯击败湖人……";
}
}
这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。
所以我们引入一个抽象的接口IReader.(读物,带字的东西都可以属于读物).
interface IReader{
public String getContent();
}
Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为:
class Newspaper implements IReader {
public String getContent(){
return "林书豪17+9助尼克斯击败老鹰……";
}
}
class Book implements IReader{
public String getContent(){
return "很久很久以前有一个阿拉伯的故事……";
}
}
class Mother{
public void narrate(IReader reader){
System.out.println("妈妈开始讲故事");
System.out.println(reader.getContent());
}
}
public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.narrate(new Book());
mother.narrate(new Newspaper());
}
}
这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。这只是一个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。
依赖倒置原则在Java中的表现就是:模块间通过抽象发生,实现类之间不发生直接依赖关系,其依赖关系是通过接口或者抽象类产生的。如果类与类直接依赖细节,那么就会直接耦合,那么当修改时,就会同时修改依赖者代码,这样限制了可扩展性。