什么是设计模式?
设计模式(Design Pattern) ,这个术语最初并不是出现在软件设计中,而是被用于建筑领域的设计中,后来随着软件工程的发展,设计模式思想被引入到软件工程中。直到今天,狭义的设计模式还是人们广为流传的 23 种经典设计模式。
设计模式(Design pattern)代表了计算机软件工程最佳的实践,是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。
在当今大型工程软件体系的发展背景下,不管是设计模式也好,其他工程模式也罢,都是为了解决问题而发明的有效方法。除了常见的的23种设计模式以外,还有 MVVM、MVC、Combinator 等,都是前辈们经过多年的摸爬滚打总结出来的,其有效性不容置疑。对于传统简单的程序开发,写一个简单的算法可能比引入某种设计模式更容易实现,但对于大型项目开发或框架设计,用设计模式来组织代码才是主要方向。
数据结构、算法、设计模式,只有牢固掌握这三项基本技能,才有资格称为软件工程师。
合理使用设计模式
曾有人这样说:
设计模式是为了封装变化,让各个模块可以独立变化。精准地使用设计模式的前提是你能够精准的预测需求变更的走向。
我们都知道大部分人是做不到的,所以大部分人就算精通设计模式也多少会做错点什么东西。所以这其实不怪设计模式。所以说如何避免过度设计,这就要求你深入的理解你的程序所在的领域的知识,了解用户使用你的软件是为了解决什么问题,这样你预测用户的需求才会比以前更加准确,从而避免了你使用设计模式来封装一些根本不会发生的变化,也避免了你忽视了未来会发生的变化从而发现你使用的模式根本不能适应需求的新走向。
所以,在你满足了【知道所有设计模式为什么要被发明出来】的前提之后,剩下的其实都跟编程没关系,而跟你的领域知识和领域经验有关系。
我认为说的很对,合理使用设计模式应该基于对工程的完全理解。
设计模式的七大原则
俗话说得好,没有规矩不成方圆,设计模式也要遵循一些准则。一般地,我们在设计软件工程的时候需要遵循七项基本原则,分别是:
- 单一职责原则(Single Responsibility)
- 接口隔离原则(Interface Segregation)
- 依赖倒置原则(Dependence Inversion)
- 里氏替换原则(Liskov Substitution)
- 开闭原则(Open Close)
- 迪米特法则(Demeter)
- 合成复用原则(Composite Reuse)
设计模式包含了面向对象的精髓,懂了设计模式就懂了面向对象分析(OOA)和设计(OOD)的精要。
1. 单一职责原则
对于类来说,一个类应该只负责一项职责。
- 降低类的复杂度。
- 提高类的可读性,可维护性。
- 降低变更引起的风险。
- 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级别违反单一职责原则;只有类中的方法足够少,可以在方法级别违反单一职责原则。
2. 接口隔离原则
一个类不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
3. 依赖倒置原则 (面向接口编程)
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象。
- 抽象不应该依赖细节,细节应该依赖抽象。
- 依赖倒转(倒置)的中心思想是面向接口编程。
- 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在 Java 中,抽象指的是接口或抽象类,细节就是具体的实现类。
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
4. 里氏替换原则 (正确使用继承)
里氏替换原则,主张使用“抽象(Abstraction)”和“多态(Polymorphism)”将设计中的静态结构改为动态结构,维持设计的封闭性。“抽象”是语言提供的功能。“多态”由继承语义实现。
里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程式中代替其基类(超类)对象。”
- 如果对每个类型为 T1 的对象 object1 ,都有类型为 T2 的对象 object2 ,使得以 T1 定义的所有程序 P 在所有的对象 object1 都代换成 object2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象。
- 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。
- 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过 聚合、组合、依赖 来解决问题。
5. 开闭原则 (编程中最基础、最重要的设计原则)
- 一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节。
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
- 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。
违反开闭原则的示例代码
GraphicEditor 类违反了开闭原则。
// 主方法
public class Ocp {
public static void main(String[] args) {
//使用看看存在的问题
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
}
}
//这是一个用于绘图的类 [使用方]
class GraphicEditor {
//接收Shape对象,然后根据type,来绘制不同的图形
public void drawShape(Shape s) {
if (s.m_type == 1)
drawRectangle(s);
else if (s.m_type == 2)
drawCircle(s);
else if (s.m_type == 3)
drawTriangle(s);
}
//绘制矩形
public void drawRectangle(Shape r) {
System.out.println(" 绘制矩形 ");
}
//绘制圆形
public void drawCircle(Shape r) {
System.out.println(" 绘制圆形 ");
}
//绘制三角形
public void drawTriangle(Shape r) {
System.out.println(" 绘制三角形 ");
}
}
//Shape类,基类
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}
//新增画三角形
class Triangle extends Shape {
Triangle() {
super.m_type = 3;
}
}
改进示例代码
// 主方法
public class Ocp {
public static void main(String[] args) {
//使用看看存在的问题
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
graphicEditor.drawShape(new OtherGraphic());
}
}
//这是一个用于绘图的类 [使用方]
class GraphicEditor {
//接收Shape对象,调用draw方法
public void drawShape(Shape s) {
s.draw();
}
}
//Shape类,基类
abstract class Shape {
int m_type;
public abstract void draw(); //抽象方法
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 绘制矩形 ");
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 绘制圆形 ");
}
}
//新增画三角形
class Triangle extends Shape {
Triangle() {
super.m_type = 3;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 绘制三角形 ");
}
}
//新增一个图形
class OtherGraphic extends Shape {
OtherGraphic() {
super.m_type = 4;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 绘制其它图形 ");
}
}
6. 迪米特法则 (降低类之间的耦合)
- 一个对象应该对其他对象保持最少的了解。
- 类与类关系越密切,耦合度越大。
- 迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息。
- 迪米特法则还有个更简单的定义:只与直接的朋友通信。
什么是直接的朋友?
每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系, 我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合 等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
违反迪米特法则的示例代码
//主方法
public class Demeter1 {
public static void main(String[] args) {
//创建了一个 SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id 和 学校总部的员工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
//学校总部员工类
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//学院的员工类
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理学院员工的管理类
class CollegeManager {
//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<>();
//这里我们增加了10个员工到 list
for (int i = 0; i < 10; i++) {
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
}
//学校管理类
//分析 SchoolManager 类的直接朋友类有 Employee、CollegeManager
//CollegeEmployee 不是 直接朋友 而是一个陌生类,这样违背了 迪米特法则
class SchoolManager {
//返回学校总部的员工
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<>();
//这里我们增加了5个员工到 list
for (int i = 0; i < 5; i++) {
Employee emp = new Employee();
emp.setId("学校总部员工id= " + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工信息(id)
void printAllEmployee(CollegeManager sub) {
//分析问题
//1. 这里的 CollegeEmployee 不是 SchoolManager的直接朋友
//2. CollegeEmployee 是以局部变量方式出现在 SchoolManager
//3. 违反了 迪米特法则
//获取到学院员工
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
//获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
改进示例代码
// 主方法
public class Demeter1 {
public static void main(String[] args) {
System.out.println("~~~使用迪米特法则的改进~~~");
//创建了一个 SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id 和 学校总部的员工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
//学校总部员工类
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//学院的员工类
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理学院员工的管理类
class CollegeManager {
//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<>();
//这里我们增加了10个员工到 list
for (int i = 0; i < 10; i++) {
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
//输出学院员工的信息
public void printEmployee() {
//获取到学院员工
List<CollegeEmployee> list1 = getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
}
}
//学校管理类
//分析 SchoolManager 类的直接朋友类有 Employee、CollegeManager
class SchoolManager {
//返回学校总部的员工
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<>();
//这里我们增加了5个员工到 list
for (int i = 0; i < 5; i++) {
Employee emp = new Employee();
emp.setId("学校总部员工id= " + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工信息(id)
void printAllEmployee(CollegeManager sub) {
//分析问题
//1. 将输出学院的员工方法,封装到CollegeManager
sub.printEmployee();
//获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
7. 合成复用原则
原则是尽量使用组合/聚合的方式,而不是使用继承。
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则和里氏替换原则相辅相成,两者都是开闭原则的具体实现规范。
为什么不推荐优先使用继承?
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。而组合和聚合复用维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 继承限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。而组合和聚合复用可以在运行时动态进行,新对象可以动态地引用与已有对象类型相同的对象,也就是说,在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。
设计模式的分类
设计模式有两种分类方法,即 模式的目的 和 模式的作用范围。
-
根据模式是用来完成什么工作,这种方式可分为 创建型模式、结构型模式 和 行为型模式 3 种。
- 创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。主要有单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。
- 结构型模式:用于描述如何将类或对象按某种布局组成更大的结构。主要有代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
- 行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。主要有模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。
-
根据模式是主要用于类上还是主要用于对象上,这种方式可分为 类模式 和 对象模式 两种。
- 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。工厂方法、(类)适配器、模板方法、解释器属于该模式。
- 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。除了以上 4 种,其他的都是对象模式。
总结
这 7 种设计原则是软件设计模式必须尽量遵循的原则,各种原则要求的侧重点不同。
- 开闭原则,要对扩展开放,对修改关闭
- 依赖倒置原则,要面向接口编程
- 接口隔离原则,接口要精简设计
- 单一职责原则,实现类要职责单一
- 里氏替换原则,不要破坏继承体系
- 迪米特法则,要降低类之间的耦合度
- 合成复用原则,优先使用组合或者聚合关系复用,少用继承关系复用