设计模式
本文整理自 史上最全设计模式导学目录(完整版)
文章目录
- 设计模式(Design Pattern)
- 一、六大原则
- 1.单一职责原则(Single-Resposibility Principle)
- 2. 开闭原则(Open-Closed principle)
- 3.里式替换原则(Liskov-Substituion Principle)
- 4.依赖倒置原则(Dependecy-Inversion Principle)
- 5.接口隔离原则(Interface-Segregation Principle)
- 6.良性依赖原则
- 二、分类
- 三、创建型
- 四、结构型
- 五、行为型
设计模式(Design Pattern)
设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。
一、六大原则
1.单一职责原则(Single-Resposibility Principle)
高内聚
如果一个类承担的职责过多,那么这些职责就会相互依赖,一个职责的变化可能会影响另一个职责的履行。
2. 开闭原则(Open-Closed principle)
需求变更时,不必改动源代码,就能扩展它的行为,通常通过抽象类或接口来实现。
3.里式替换原则(Liskov-Substituion Principle)
子类型必须能够替换掉它们的基类型。正是子类型的可替换性,才使得使用基类型模块无需修改就可扩充。
4.依赖倒置原则(Dependecy-Inversion Principle)
抽象不应依赖于细节,细节应该依赖于抽象。
先有抽象程度比较高的实体,再有才是更加细节化的实体。
5.接口隔离原则(Interface-Segregation Principle)
多个专用接口优于一个单一的通用接口,本质上也是高内聚。
6.良性依赖原则
不会在实际中造成危害的依赖关系,都是良性依赖。
二、分类
设计模式共23种,分为三大类:
- 创建型模式(5):工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式(7):适配器模式、装饰器模式、外观模式、代理模式、桥接模式、组合模式、享元模式。
- 行为型模式(11):观察者模式、策略模式、模板方法模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
三、创建型
1.工厂模式(factory)
1.1 定义
定义一个用于创建对象的接口,让子类决定将哪一个类实例化。
1.2例子
需求
日志记录器的设计:
该记录器可以通过多种途径保存系统的运行日志,如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,Sunny公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置过程较为复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。
如何封装记录器的初始化过程并保证多种记录器切换的灵活性是Sunny公司开发人员面临的一个难题。
设计
实现
public interface Logger {
void writeLog();
}
public class FileLogger implements Logger {
public FileLogger() {
System.out.println("FileLogger");
}
@Override
public void writeLog() {
System.out.println("FileLogger.writeLog()");
}
}
public class DatabaseLogger implements Logger {
public DatabaseLogger() {
System.out.println("DatabaseLogger");
}
@Override
public void writeLog() {
System.out.println("DatabaseLogger.writeLog()");
}
}
//日志记录器工厂
public interface LoggerFactory {
//静态工厂方法
Logger createLogger();
}
public class FileLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
FileLogger logger = new FileLogger();
return logger;
}
}
public class DatabaseLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
Logger logger = new DatabaseLogger();
return logger;
}
}
测试
public class Test {
/**
* LoggerFactory的具体类型,可以写在配置文件中,灵活使用
* @param args
*/
public static void main(String[] args) {
LoggerFactory factory=new DatabaseLoggerFactory();
Logger logger=factory.createLogger();
logger.writeLog();
}
}
1.3优缺点
优点:
- 具体实现被隐藏,用户无需关心创建细节
- 具体工厂类都具有同一抽象父类
- 使用工厂方法模式的另一个优点是在系统中加入新产品时,而只要添加一个具体工厂和具体产品就可以了,这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
缺点:
- 在添加新产品较多时,新增类较多,复杂性较高
- 抽象层较多,增加理解难度
2.抽象工厂模式(Abstract Factory)
2.1 定义
提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
在抽象工厂模式中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,抽象工厂模式的这种性质称为“开闭原则”的倾斜性。
2.2例子
需求
Sunny软件公司欲开发一套界面皮肤库,可以对Java桌面软件进行界面美化。为了保护版权,该皮肤库源代码不打算公开,而只向用户提供已打包为jar文件的class字节码文件。用户在使用时可以通过菜单来选择皮肤,不同的皮肤将提供视觉效果不同的按钮、文本框、组合框等界面元素,其结构示意图如图1所示:
该皮肤库需要具备良好的灵活性和可扩展性,用户可以*选择不同的皮肤,开发人员可以在不修改既有代码的基础上增加新的皮肤。
设计
实现
public interface Button {
void display();
}
public class SpringButton implements Button {
public void display() {
System.out.println("SpringButton.display()");
}
}
public class SummerButton implements Button {
public void display() {
System.out.println("SummerButton.display()");
}
}
public interface TextField {
void display();
}
public class SpringTextField implements TextField {
public void display() {
System.out.println("SpringTextField.display()");
}
}
public class SummerTextField implements TextField {
public void display() {
System.out.println("SummerTextField.display()");
}
}
//抽象工厂
public interface AbstractFactory {
Button createButton();
TextField createTextField();
}
public class SpringFactory implements AbstractFactory {
@Override
public Button createButton() {
return new SpringButton();
}
@Override
public TextField createTextField() {
return new SpringTextField();
}
}
public class SummerFactory implements AbstractFactory {
@Override
public Button createButton() {
return new SpringButton();
}
@Override
public TextField createTextField() {
return new SpringTextField();
}
}
测试
public class Test {
/**
* AbstractFactory的具体类型,可以写在配置文件中,灵活使用
* @param args
*/
public static void main(String[] args) {
AbstractFactory factory = new SpringFactory();
Button button = factory.createButton();
button.display();
TextField textField = factory.createTextField();
textField.display();
}
}
2.3优缺点
优点:
- 具体实现被隐藏,用户无需关心创建细节
- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。
- 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。
缺点:
- 增加新的产品等级结构很麻烦
在抽象工厂模式中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,抽象工厂模式的这种性质称为“开闭原则”的倾斜性。
3.单例模式(Singleton)
四、结构型
1.适配器模式(Adapter)
1.1 定义
将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
1.2对象适配器
需求
Sunny软件公司在很久以前曾开发了一个算法库,里面包含了一些常用的算法,例如排序算法和查找算法,在进行各类软件开发时经常需要重用该算法库中的算法。在为某学校开发教务管理系统时,开发人员发现需要对学生成绩进行排序和查找,该系统的设计人员已经开发了一个成绩操作接口ScoreOperation,在该接口中声明了排序方法sort(int[]) 和查找方法search(int[], int),为了提高排序和查找的效率,开发人员决定重用算法库中的快速排序算法类QuickSort和二分查找算法类BinarySearch,其中QuickSort的quickSort(int[])方法实现了快速排序,BinarySearch 的binarySearch (int[], int)方法实现了二分查找。
由于某些原因,现在Sunny公司开发人员已经找不到该算法库的源代码,无法直接通过复制和粘贴操作来重用其中的代码;部分开发人员已经针对ScoreOperation接口编程,如果再要求对该接口进行修改或要求大家直接使用QuickSort类和BinarySearch类将导致大量代码需要修改。
Sunny软件公司开发人员面对这个没有源码的算法库,遇到一个幸福而又烦恼的问题:如何在既不修改现有接口又不需要任何算法库代码的基础上能够实现算法库的重用?
设计
实现
//抽象成绩操作类:目标接口
public interface ScoreOperation {
public int[] sort(int array[]); //成绩排序
public int search(int array[],int key); //成绩查找
}
// 适配器
public class OperationAdapter implements ScoreOperation {
BinarySearch binarySearch;
QuickSort quickSort;
public OperationAdapter() {
binarySearch = new BinarySearch();
quickSort = new QuickSort();
}
@Override
public int[] sort(int[] array) {
return quickSort.quickSort(array);
}
@Override
public int search(int[] array, int key) {
return binarySearch.binarySearch(array, key);
}
}
//快速排序类:适配者
public class QuickSort {
public int[] quickSort(int array[]) {
sort(array,0,array.length-1);
return array;
}
public void sort(int array[],int p, int r) {
int q=0;
if(p<r) {
q=partition(array,p,r);
sort(array,p,q-1);
sort(array,q+1,r);
}
}
public int partition(int[] a, int p, int r) {
int x=a[r];
int j=p-1;
for (int i=p;i<=r-1;i++) {
if (a[i]<=x) {
j++;
swap(a,j,i);
}
}
swap(a,j+1,r);
return j+1;
}
public void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
//二分查找类:适配者
public class BinarySearch {
public int binarySearch(int array[],int key) {
int low = 0;
int high = array.length -1;
while(low <= high) {
int mid = (low + high) / 2;
int midVal = array[mid];
if(midVal < key) {
low = mid +1;
}
else if (midVal > key) {
high = mid -1;
}
else {
return 1; //找到元素返回1
}
}
return -1; //未找到元素返回-1
}
}
测试
public class Test {
public static void main(String[] args) {
ScoreOperation operation = new OperationAdapter();
int[] scores = {84, 76, 50, 69, 90, 91, 88, 96}; //定义成绩数组
for (int i : scores) {
System.out.print(i + ",");
}
System.out.println();
scores = operation.sort(scores);
for (int i : scores) {
System.out.print(i + ",");
}
}
}
1.3缺省适配器
当不需要实现一个接口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可以选择性地覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中的所有方法的情况,又称为单接口适配器模式。
设计
实现
public interface ServiceInterface {
void test1();
void test2();
void test3();
void test4();
}
public abstract class AbstractServiceClass implements ServiceInterface {
public void test1() {
}
public void test2() {
}
public void test3() {
}
public void test4() {
}
}
public class ConcreteServiceClass extends AbstractServiceClass {
public void test1() {
}
}
1.4优缺点
优点:
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
- 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
- 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
缺点:
- 不能多继承,一次最多只能适配一个适配者类,不能同时适配多个适配者
- 类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。
2.装饰器模式(Decorator)
2.1 定义
动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。
2.2例子
需求
图形界面构件库的设计
Sunny软件公司基于面向对象技术开发了一套图形界面构件库VisualComponent,该构件库提供了大量基本构件,如窗体、文本框、列表框等,由于在使用该构件库时,用户经常要求定制一些特效显示效果,如带滚动条的窗体、带黑色边框的文本框、既带滚动条又带黑色边框的列表框等等,因此经常需要对该构件库进行扩展以增强其功能,如图
如何提高图形界面构件库性的可扩展性并降低其维护成本是Sunny公司开发人员必须面对的一个问题。
设计
实现
public interface Component {
public void display();
}
public class TextBox implements Component{
@Override
public void display() {
System.out.println("TextBox.display()");
}
}
public class Window implements Component {
public void display() {
System.out.println("Window.display()");
}
}
//构件装饰类:抽象装饰类
public class ComponentDecorator implements Component {
private Component component; //维持对抽象构件类型对象的引用
public ComponentDecorator(Component component) //注入抽象构件类型的对象
{
this.component = component;
}
public void display() {
component.display();
}
}
//黑色边框装饰类:具体装饰类
public class BlackBorderDecorator extends ComponentDecorator {
public BlackBorderDecorator(Component component) {
super(component);
}
public void display() {
this.setBlackBorder();
super.display();
}
public void setBlackBorder() {
System.out.println("为构件增加黑色边框!");
}
}
//滚动条装饰类:具体装饰类
public class ScrollBarDecorator extends ComponentDecorator {
public ScrollBarDecorator(Component component) {
super(component);
}
public void display() {
this.setScrollBar();
super.display();
}
public void setScrollBar() {
System.out.println("为构件增加滚动条!");
}
}
测试
public class Test {
public static void main(String[] args) {
Component component = new Window(); //定义具体构件
Component componentSB = new ScrollBarDecorator(component); //定义装饰后的构件
componentSB.display();
}
}
2.3优缺点
优点:
- 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
- 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
- 增加新的功能很方便,无须修改已有系统,符合“开闭原则”。
缺点:
- 比继承更加灵活,也更容易出错
在以下情况下可以考虑使用装饰模式:
-
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
-
当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。
不能采用继承的情况主要有两类:
- 第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;
- 第二类是因为类已定义为不能被继承(如Java语言中的final类)。
3.外观模式(Facade)
3.1 定义
为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
3.2例子
需求
文件加密模块设计
某软件公司欲开发一个可应用于多个软件的文件加密模块,该模块可以对文件中的数据进行加密并将加密之后的数据存储在一个新文件中,具体的流程包括三个部分,分别是读取源文件、加密、保存加密之后的文件,其中,读取文件和保存文件使用流来实现,加密操作通过求模运算实现。这三个操作相对独立,为了实现代码的独立重用,让设计更符合单一职责原则,这三个操作的业务代码封装在三个不同的类中。
如果在应用实例“文件加密模块”中需要更换一个加密类,不再使用原有的基于求模运算的加密类CipherMachine,而改为基于移位运算的新加密类NewCipherMachine
设计
实现
public class FileReader {
public String Read(String fileNameSrc) {
return "";
}
}
public class FileWriter {
public void Write(String encryptStr, String fileNameDes) {
}
}
public class CipherMachine {
// 数据加密
public String Encrypt(String plainText) {
return "";
}
}
public class NewCipherMachine {
// 数据加密
public String Encrypt(String plainText) {
return "";
}
}
public abstract class AbstractEncryptFacade {
public abstract void fileEncrypt(String fileNameSrc, String fileNameDes);
}
public class EncryptFacade extends AbstractEncryptFacade{
//维持对其他对象的引用
private FileReader reader;
private CipherMachine cipher;
private FileWriter writer;
public EncryptFacade()
{
reader = new FileReader();
cipher = new CipherMachine();
writer = new FileWriter();
}
//调用其他对象的业务方法
public void fileEncrypt(String fileNameSrc, String fileNameDes)
{
String plainStr = reader.Read(fileNameSrc);
String encryptStr = cipher.Encrypt(plainStr);
writer.Write(encryptStr, fileNameDes);
}
}
public class NewEncryptFacade extends AbstractEncryptFacade {
private FileReader reader;
private NewCipherMachine cipher;
private FileWriter writer;
public NewEncryptFacade() {
reader = new FileReader();
cipher = new NewCipherMachine();
writer = new FileWriter();
}
public void fileEncrypt(String fileNameSrc, String fileNameDes) {
String plainStr = reader.Read(fileNameSrc);
String encryptStr = cipher.Encrypt(plainStr);
writer.Write(encryptStr, fileNameDes);
}
}
测试
public class Test {
public static void main(String[] args) {
AbstractEncryptFacade ef = new EncryptFacade();
ef.fileEncrypt("src.txt", "des.txt");
AbstractEncryptFacade ef2 = new NewEncryptFacade();
ef2.fileEncrypt("src.txt", "des.txt");
}
}
3.3优缺点
优点:
- 子系统与客户端间解耦
- 一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。
缺点:
- 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则。
适用场景:
(1) 当要为访问一系列复杂的子系统提供一个简单入口时可以使用外观模式。
(2) 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
(3) 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
4.代理模式(Proxy)
4.1 定义
给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。
常见的代理形式包括远程代理、保护代理、虚拟代理、缓冲代理、智能引用代理
(1) 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又称为大使(Ambassador)。
(2) 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
(3) 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
(4) 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
(5) 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。
4.2例子
需求
某软件公司承接了某信息咨询公司的收费商务信息查询系统的开发任务,该系统的基本需求如下:
(1) 在进行商务信息查询之前用户需要通过身份验证,只有合法用户才能够使用该查询系统;
(2) 在进行商务信息查询时系统需要记录查询日志,以便根据查询次数收取查询费用。
该软件公司开发人员已完成了商务信息查询模块的开发任务,现希望能够以一种松耦合的方式向原有系统增加身份验证和日志记录功能,客户端代码可以无区别地对待原始的商务信息查询模块和增加新功能之后的商务信息查询模块,而且可能在将来还要在该信息查询模块中增加一些新的功能。
设计
实现
public interface Searcher {
String doSearch(String userId, String keyword);
}
public class RealSearcher implements Searcher{
@Override
public String doSearch(String userId, String keyword) {
return "001";
}
}
//扩展功能 模拟实现登录验证
public class AccessValidator {
public boolean Validate(String userId) {
return true;
}
}
//扩展功能 模拟实现日志记录
public class Logger {
public void Log(String userId) {
System.out.println("用户" + userId + "查询次数加1!");
}
}
// 代理类
public class ProxcySearcher implements Searcher {
private RealSearcher searcher = new RealSearcher(); //维持一个对真实主题的引用
private AccessValidator validator;
private Logger logger;
@Override
public String doSearch(String userId, String keyword) {
//如果身份验证成功,则执行查询
if (this.validate(userId)) {
System.out.println(userId+"用户验证通过");
String result = searcher.doSearch(userId, keyword); //调用真实主题对象的查询方法
this.log(userId); //记录查询日志
return result; //返回查询结果
} else {
return null;
}
}
//创建访问验证对象并调用其Validate()方法实现身份验证
public boolean validate(String userId) {
validator = new AccessValidator();
return validator.Validate(userId);
}
//创建日志记录对象并调用其Log()方法实现日志记录
public void log(String userId) {
logger = new Logger();
logger.Log(userId);
}
}
测试
public class Test {
public static void main(String[] args) {
Searcher searcher = new ProxcySearcher();
String result = searcher.doSearch("杨过", "玉女心经");
}
}
4.3优缺点
优点:
- 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
- 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。
缺点:
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理。
五、行为型
1.观察者模式(Observer)
1.1 定义
定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
观察者模式的别名包括发布-订阅(P ublish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。
1.2例子
需求
微博消息推送
设计
实现
public abstract class Subject {
//定义一个观察者集合用于存储所有观察者对象
protected List<Observer> observers = new ArrayList();
//注册方法,用于向观察者集合中增加一个观察者
public void attach(Observer observer) {
observers.add(observer);
}
//注销方法,用于在观察者集合中删除一个观察者
public void detach(Observer observer) {
observers.remove(observer);
}
//声明抽象通知方法
public abstract void notifyMethod(String message);
}
public class ConcreteSubject extends Subject {
//实现通知方法
public void notifyMethod(String message) {
//遍历观察者集合,调用每一个观察者的响应方法
for (Object obs : observers) {
((Observer) obs).update(message);
}
}
}
//抽象观察者
public interface Observer {
//声明响应方法
public void update(String message);
}
// 具体观察者
public class ConcreteObserver implements Observer {
// 微博用户名
private String name;
public ConcreteObserver(String name) {
this.name = name;
}
//实现响应方法
public void update(String message) {
System.out.println("to " + name + ":" + message);
}
}
测试
public class Test {
public static void main(String[] args) {
Subject subject = new ConcreteSubject();
// 创建观察者
ConcreteObserver observer1 = new ConcreteObserver("dyn");
ConcreteObserver observer2 = new ConcreteObserver("wxy");
ConcreteObserver observer3 = new ConcreteObserver("jzy");
// 将观察者放入观察者集合,订阅消息
subject.attach(observer1);
subject.attach(observer2);
subject.attach(observer3);
// 推送消息
subject.notifyMethod("Jolin微博更新了");
}
}
console
to dyn:Jolin微博更新了
to wxy:Jolin微博更新了
to jzy:Jolin微博更新了
1.3优缺点
优点:
- 观察者模式支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。
- 观察者模式满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。
- 使用工厂方法模式的另一个优点是在系统中加入新产品时,而只要添加一个具体工厂和具体产品就可以了,这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
缺点:
- 观察者过多时,全部通知到比较耗时
- 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
上一篇: MATLAB—信号与系统中的应用
下一篇: linux中的 【信号】