一.OOAD是什么?
面向过程:注重过程。
面向对象:注重结果。
1.OO:Object-Orientation,面向对象,是一套集编程思想,方法,原则,模式,解决方案等为一体的编程模式。OO的思想贯穿于整个软件开发的过程,比如需求分析,设计,编程,测试,升级等.
2.OOA:Object-Oriented Analysis,面向对象分析方法,在确定需求或者业务的时候,按照面向对象的思想来分析业务。
分析阶段主要解决以下问题:
a.建立针对业务问题域的清晰视图
b.列出系统必须要完成的核心任务
c.针对问题域建立公共词汇表
d.列出针对此问题域的最佳解决方案
此阶段要解决的核心问题是"what to do?
OOA具体分析事物时,大致遵循五个基本步骤:
a.确定类和对象
b.确定结构(类与类之间关系,系统整体局部关系)
c.确定主题(事物总体概貌模型)
d.确定属性
e.确定方法(行为)
3.OOD:Object-Oriented Design,面向对象设计是OO方法中的一个中间过渡环节,其主要工作是对OOA分析的结果作进一步的规范化整理,以便能够被OOP直接接受。
设计阶段主要解决以下问题:
a.如何解决具体的业务问题
b.引入系统工作所需的各方面的支持元素
c.定义系统的实现策略
此阶段要解决的核心问题是"How to do?"
4.OOP: Object-Orientation Programming,面向对象程序设计,是按照OO的方法学来开发程序的编程方式。
OOP旨在达到软件工程三个主要目标:重用性、灵活性和扩展性。
OIP:面向接口编程;
二.面向对象和面向过程的区别
面向过程是一种自顶向下的编程,分析得到每个步骤然后分别去实现(函数),最后按步骤调用函数实现系统功能;
面向对象以“对象”为中心的编程思想。
它们分析解决问题的思路是完全不同的,拿五子棋游戏举例:
面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用分别的函数来实现,问题就解决了。
而面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为 1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。
第一类对象【棋子】负责接受用户输入,并告知第二类对象【棋盘】棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象【规则系统】来对棋局进行判定。
可以明显地看出,面向对象是【以功能来划分问题】,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在了很多步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。
功能上的统一【保证】了面向对象设计的【可扩展性】。比如我要加入【悔棋】功能,如果要改动面向过程的设计,那么从输入到判断到显示这一连串的步骤都要改动,甚至步骤之间的循序都要进行大规模调整。如果是面向对象的话,只用改动棋盘对象就行了,棋盘系统保存了黑白双方的棋谱,简单回溯就可以了,而显示和规则判断则不用顾及。
再比如我要把这个【五子棋】游戏【改为围棋游戏】,如果你是面向过程设计,那么五子棋的规则就分布在了你的程序的每一个角落,要改动还不如重写。但是如果你当初就是面向对象的设计,那么你只用改动规则对象就可以了,五子棋和围棋的区别不就是规则吗?(当然棋盘大小好像也不一样,但是你会觉得这是一个难题吗?直接在棋盘对象中进行一番小改动就可以了。)而下棋的大致步骤从面向对象的角度来看没有任何变化。
三、面向对象基本概念(程序员面试宝典 41页开始)
1.对象 和 类
类是对具有相同属性相同行为的同一类事物的抽象描述;
类是对象的抽象,对象是类的实例。
2.抽象
归纳总结的过程。
忽略掉一个对象或实体的细节而只关注其本质特征的过程。
3.封装
将现实世界里面存在的某个客体的属性和行为绑定在一起,并放置在一个类内。
1.数据隐藏;
class Student{
private int age;
public voi setAge(int age){
}
}
2.代码抽取;
3.框架【SSM,反射】
优点:隐藏客体属性实现数据保护;提高软件的可维护性;降低复杂度
4.继承
is a 的关系, extends 类与类只能单继承,接口之间也可以多重继承,
a.子类可以继承父类的属性、方法
b.子类可以有自己的特性存在。
继承性是面向对象程序设计语言不同于其它语言的最重要的特点,是其他语言所没有的。有单继承,也有多重继承。
优点:提高软件的重用性。
5.多态
一种形式,多种状态
& 逻辑运算符 位运算
引用.方法();
相同类域的不同对象调用相同方法时的不同表现形式。
分为编译时多态、运行时多态。
a.子类继承父类,类实现接口
b.子类重写父类的同名方法,类实现接口中的抽象方法
c.父类的引用指向子类的对象,接口类型的引用指向具体实现类的对象
使代码更加的灵活。
四、类与类之间的关系
1.继承关系:is a
2.实现关系(接口)
3.依赖关系:类A对象中并不需要存在有类B类型的对象属性,而是在使用时,直接在方法中实例化类B(new对象)来调用相关的业务方法来完成整个业务操作。
4.关联关系:体现的是两个类之间语义级别的一种强依赖关系,这种关系比依赖更强,不存在依赖关系的偶然性,也不是临时的,一般是长期的,而且双方相互平等。一般使用成员变量来实现,可以单向、也可以双向。(例如个人与公司)
关联关系又可总体分为聚合关系和组合关系:
5.聚合关系也称"has-a"关系
聚合(Aggregation):---Has a---------人拥有电脑
6.组合(Composition): ---Contains a---
人是由脚、身体、头等部分组成的
聚合关系表示事物的整体/部分关系的较弱情况,组合关系表示事物的整体/部分关系的较强的情况.
总结:继承、实现、依赖关系很容易理解,关联、聚合、组合往往难以区分,关键要看具体的应用场景。
重聚合、组合,轻继承。
五、如何选择抽象类和接口
抽象类表示is a的关系;
接口表示like a 的关系;(飞机和鸟都可以飞;猫和狗都是宠物,可以遛弯、与人亲近)
接口具有多继承的优点,避免了类的多继承的缺陷(二义性),历史上接口很大程度上是为了解决多继承的种种问题而设计出来的。
1.语法区别
Interface:
属性:public static final
方法:只能是抽象方法public abstract
注意:可以没有任何方法
被实现类实现。
Class:abstract来修饰的
属性:正常属性
方法:可以是普通方法,也可以是抽象方法。
注意:可以没有任何抽象方法
被子类继承。
2.设计角度
一个类可以实现多个接口但是一个类只能继承一个父类。 接口和抽象类的设计,都是在有意识的规避java不能多继承的操作
Java多继承会产生二义性;
接口是抽象类的极致的抽象。
3.思想角度
a.类进行抽象的时候是否关注属性
b.尽量让问题域独立存在,根据操作选择合适的抽象类或接口.
结论:到底如何去选择,要看我们对问题域的理解。
例如:一个关于门的设计问题
门都有俩个基本的行为 open close
将来系统中会出现很多种门(不同的类)
MyDoor1 MyDoor2 MyDoor3 MyDoor4
这些类都具备open close的行为,但是具体实现又不一样.
如何选择:
1.抽象出一个抽象类,类中俩个抽象方法open close alarm
2.抽象出一个接口,接口中俩个抽象方法open close
将来多了一个新的功能:警报器
1.抽象出一个抽象类,类中俩个抽象方法open close
MyDoor1 extends Door
2.抽象出一个接口,接口中俩个抽象方法alarm
六、面向对象设计原则
好的软件系统设计至少应该具备以下三点:
可扩展性(Extensibility)
灵活性(Flexibility)
可插入性(Pluggability)
为了达到以上三点,常用需要使用一些设计原则,这些面向对象的设计原则,大致可分为七种:(要求能说出来至少四种)
1.单一职责原则(Single Responsibility Principle,SRP)
类的职责要单一,不能将太多职责放在同一个类中.
生活中:高效的完成某项工作;降低对某人的依赖性。
项目中:高度的模块化,将问题局限某部分,问题易排查修改。
2.开闭原则(Open-Closed Principle, OCP)
对扩展开放,对修改关闭。软件设计应该尽可能的使架构稳定而且又容易满足不同的需求,提高代码可重用性。理论上希望能达到不修改源代码的前提下扩展功能。
3.里氏代换原则(Liskov Substitution Principle,LSP)
在系统中,一个可以接受基类对象的地方必然可以接受一个子类对象。
使得所有基类的接口语义是稳定的,有利于系统扩展。
4.依赖倒转原则(Dependency Inversion Principle,DIP)
要针对抽象编程,而不是针对具体类编程.
(面向接口编程,更容易维护、扩展)
5.接口隔离原则(Interface Segregation Principle,ISP)
使用多个专门的接口来取代一个统一的接口.
6.合成复用原则((Composition/Aggregate Reuse Principle,CARP)
系统中尽量使用组合和聚合的关系,而尽量少用甚至不用继承关系。
7.迪米特法则(Law of Demeter, LoD)
一个软件实体应当尽量少的与其他实体发生相互作用.
又叫做最少知道原则。也就是说一个对象应当对其他对象要尽可能少的了解,不要和陌生人说话。
耦合度越高,稳定性越差,该法则目的就是降低耦合。代码优化时需要重点关注的就是【解耦】。
高内聚、低耦合:
耦合性:也称块间联系。指软件系统结构中【各模块间】相互【联系紧密程度】的一种度量。
模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息。
内聚性:又称块内联系。指【模块的功能强度】的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。所谓高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。
对于低耦合,粗浅的理解是:一个完整的系统,模块与模块之间,尽可能的使其独立存在。也就是说,让每个模块,尽可能的独立完成某个特定的子功能。模块与模块之间的接口,尽量的少而简单。如果某两个模块间的关系比较复杂的话,最好首先考虑进一步的模块划分。这样有利于修改和组合。
七、面向对象设计模式
设计模式(Design pattern)是一套【被反复使用】、【多数人知晓】的、【经过分类编目】的、【代码设计经验的总结】。(对软件设计问题的可重用的解决方案)
使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
设计模式于己于他人于系统是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。
项目中合理的运用设计模式可以完美的解决很多问题,【每种模式】描述一种在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。
作用:
1.重用设计比重用代码更有意义,可以充分利用已有的软件开发经验
2.为设计提供公共词汇,方便交流和书写开发文档
3.降低错误可能性
4.节省时间
一、设计模式的分类
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
二、Java的23中设计模式
1、工厂方法模式(Factory Method)
工厂模式的作用是用来【创建复杂对象】。对象的实例化往往借助new关键字来实现,但有些对象比较复杂,在new之前需要额外的操作,这就使得new的过程相对复杂(可能多行代码)。另外系统中可能需要多次创建对象,所以如果直接new来实现功能就会比较繁琐,此时可以选择工厂模式。
工厂模式本质 上就是将对象的new过程封装到 某个类的方法中,简化用户的调用过程,实现代码重用。
工厂方法模式分为三种:普通工厂模式 多个工厂方法模式 静态工厂方法模式
1.1、普通工厂模式,就是建立一个工厂类,对实现了同一接口的产品类进行实例的创建
例子:女娲造人,不同材料造出不同人
1.2、多个工厂方法模式
是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
// 将上面的代码做下修改,改动下Factory类就行
// 这个就不用根据用户传的字符串类创建对象了
1.3、静态工厂方法模式
将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
// 使用static修饰工厂内部生产方法
// 使得可以通过工厂类名直接调用方法生产出对象
2、抽象工厂模式(Abstract Factory)
工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了开闭原则,所以,从设计角度考虑,这有一定的问题,如何解决?
抽象工厂模式可以解决: 创建多个工厂类,每个工厂类只负责一类对象的创建,这样一旦需要增加新对象的创建,直接增加新的工厂类就可以了,不需要修改之前的代码。
例子:
女娲造人,使用多个炼丹炉,每个炉子专门生产一种人
该模式是依赖倒转原则和开闭原则的经典体现。
3、单例模式(Singleton)
单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。
Singleton1 饿汉模式
Singleton2 懒汉模式
Singleton3 同步方法解决多线程(效率不高)
Singleton4 同步代码块解决(提高效率)
new Clazz();a.开辟内存 b.构造器初始化
c.返回地址值
棋子b.c不确定先后次序
如何保证,实例化对象时,全部【初始化完成后,再返回对象地址】,给用户使用。
Singleton5 类加载解决线程(效率也很高)
getDeclaredConstructor(Class<?>... parameterTypes)
作弊手段可以帮助我们获取多个对象
a.反射 字节码对象|镜像 Singleton6
解决方案: 修改构造器,使之第一次调用成功,以后调用失败
b.序列化 反序列化
序列化: 将对象写入流
反序列化: 从流中读取出来对象
private Object readResolve() {
return getInstance();
}
如果重写了该方法 反序列化时,就调用这个方法获取返回的对象;如果不重写,则从流中获取对象。
例子:
//1.简单的单例类 饿汉模式
public class Singleton {
/* 持有私有静态实例,防止被引用*/
private static Singleton instance = new Singleton();
/* 私有构造方法,防止被实例化 */
private Singleton() {
}
/* 静态工程方法,返回Singleton实例 */
public static Singleton getInstance() {
return instance;
}
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
private Object readResolve() {
return instance;
}
}
这个类是可以实现单例模式的,但是存在不少问题,比如在类中不管用户是否要使用该类的对象,就先创建好了一个实例放在内存中。
//2.简单的单例类 懒汉模式
public class Singleton {
/* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */
private static Singleton instance = null;
/* 私有构造方法,防止被实例化 */
private Singleton() {
}
/* 静态工程方法,创建实例 */
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
private Object readResolve() {
return instance;
}
}
//3.解决线程同步问题
这个类可以满足基本要求,但是,像这样【毫无线程安全保护】的类,如果我们把它放入多线程的环境下,肯定就会出现问题了,如何解决?我们首先会想到对getInstance方法加synchronized关键字,如下:
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
//4.解决效率低下问题
但是,synchronized作为修饰符在方法上使用,在性能上会有所下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。我们改成下面这个:
public static Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
似乎解决了之前提到的问题,将synchronized关键字加在了方法内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。但是,这样的情况,还是有可能有问题的。
看下面的情况:在Java指令中【创建对象】和【赋值操作】是分开进行的,也就是说instance = new Singleton();语句并非是一个原子操作,在 JVM 中这句代码大概做了下面 3 件事情:
1.给 new的对象 分配内存
2.调用 Singleton 的构造函数来初始化成员变量
3.将引用instance指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的【第二步和第三步的顺序是不能保证】的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,另外一个线程B抢夺到了CPU的执行权,这时instance已经是非null了(但却没有初始化),所以线程B会直接返回 instance,然后使用,结果就会出现问题了(因为对象还没有初始化)。
//5.上述问题的解决方案
还有另外一种解决方案: 【使用内部类来维护单例的实现】,JVM内部的机制能够保证当一个类被加载的时候,这个【类的加载】过程是【线程互斥】的(就是加载完毕后别的线程才能使用)。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。例如:
public class Singleton {
/* 私有构造方法,防止被实例化 */
private Singleton() {
}
/* 此处使用一个内部类来维护单例 */
private static class SingletonFactory {
private static Singleton instance = new Singleton();
}
/* 获取实例 */
public static Singleton getInstance() {
return SingletonFactory.instance;
}
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
private Object readResolve() {
return getInstance();
}
}
但是如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。
//6.特殊情况讨论【不按常理出牌,反射实现】
同时,我们还可以使用反射去创建这个类的对象,即使它的构造器是私有的,我们也是可以调用到的。那么这个时候我们就需要再次修改代码去访问别人反射调用构造器。
例子:
//在构造器中控制一下,构造器只允许调用一次,之后在调用就抛出异常
public class Singleton {
private static boolean flag;
/* 私有构造方法,防止被实例化 */
private Singleton() {
if(!flag){
flag = false;
}else{
throw new RuntimeException("不能多次创建单例对象");
}
}
/* 此处使用一个内部类来维护单例 */
private static class SingletonFactory {
private static Singleton instance = new Singleton();
}
/* 获取实例 */
public static Singleton getInstance() {
return SingletonFactory.instance;
}
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
private Object readResolve() {
return getInstance();
}
}
//7.第二种特殊情况,序列化实现
反射的问题处理完了之后,这里还有一个问题,就是如果把单例对象进行序列化然后再反序列化,那么内存中就会出现俩个一样的单例对象,只是内存地址不同。这种情况我们可以使用readResolve方法来防止。
private Object readResolve(){.....}
ObjectInputStream 会检查对象的class是否定义了readResolve方法。如果定义了,将由readResolve方法指定返回的对象。返回对象的类型一定要是兼容的,否则会抛出ClassCastException 。
例子:
public abstract class Singleton8 implements Serializable{
private static final long serialVersionUID = 7863921642928237696L;
/* 此处使用一个内部类来维护单例 */
private static class SingletonFactory {
@SuppressWarnings("serial")
private static Singleton8 instance = new Singleton8(){};
}
//测试方式,把单例对象序列化后再反序列化从而获得一个新的对象 就相当于复制了一个单例对象
public Singleton8 copy() throws Exception{
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(Singleton8.getInstance());
InputStream is = new ByteArrayInputStream(os.toByteArray());
ObjectInputStream ois = new ObjectInputStream(is);
Singleton8 obj = (Singleton8) ois.readObject();
return obj;
}
/* 获取实例 */
public static Singleton8 getInstance() {
return SingletonFactory.instance;
}
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
/* 把这个方法注释前和注释后来运行测试代码观察结果 */
private Object readResolve() {
return getInstance();
}
}
4、建造者模式(Builder)
工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来【创建复合对象】,所谓复合对象就是指某个类具有不同的属性(成员对象)。
建造者模式主要用于【分步骤构建一个复杂的对象】,在这其中“分步骤”是一个稳定的算法,而复杂对象的各个部分则经常变化。我们在该模式中引入了一个导演类,专门负责规范复杂对象创建的步骤,这就简化了建造过程。建造者只需要关心如何建造各个局部对象即可,而不用关心如何组装。
建造者模式主要用来解决【局部对象】的需求变化。 这样可以【对对象构造的过程进行更加精细的控制】。
例子:
//CPU接口
public interface CPU {
}
//Inter的cpu
class IntelCPU implements CPU{
}
//AMD的cpu
class AMDCPU implements CPU{
}
//内存接口
public interface Memory {
}
//金士顿内存
class KingstonMemory implements Memory{
}
//三星内存
class SamsungMemory implements Memory{
}
//主板内存
public interface Mainboard {
}
//华硕主板
class AsusMainboard implements Mainboard{
}
//技嘉主板
class GaMainboard implements Mainboard{
}
//计算机
public class Computer {
private CPU cpu;
private Memory memory;
private Mainboard mainboard;
get/set
}
//计算机的builder的接口
public interface ComputerBuilder {
public void buildCPU();
public void buildMemory();
public void buildMainboard();
public Computer getComputer();
}
//联想电脑的builder
public class LenoveComputerBuilder implements ComputerBuilder {
private Computer lenoveComputer;
public LenoveComputerBuilder(){
lenoveComputer = new Computer();
}
public void buildCPU() {
lenoveComputer.setCpu(new IntelCPU());
}
public void buildMemory() {
lenoveComputer.setMemory(new KingstonMemory());
}
public void buildMainboard() {
lenoveComputer.setMainboard(new AsusMainboard());
}
public Computer getComputer() {
return lenoveComputer;
}
}
//惠普电脑的builder
public class HPComputerBuilder implements ComputerBuilder {
private Computer HPComputer;
public HPComputerBuilder(){
HPComputer = new Computer();
}
public void buildCPU() {
HPComputer.setCpu(new AMDCPU());
}
public void buildMemory() {
HPComputer.setMemory(new SamsungMemory());
}
public void buildMainboard() {
HPComputer.setMainboard(new GaMainboard());
}
public Computer getComputer() {
return HPComputer;
}
}
//Director类(导演)
//指导如何具体的创造电脑
public class Director {
private ComputerBuilder builder;
public Director(ComputerBuilder builder) {
this.builder = builder;
}
//用户自定义的自造顺序 具体指导各种builder如何创建电脑
public void construct() {
builder.buildCPU();
builder.buildMemory();
builder.buildMainboard();
}
}
//测试类
public class Test {
public static void main(String[] args) {
Computer lenoveComputer = null;
ComputerBuilder lenoveComputerBuilder = new LenoveComputerBuilder();
Director director = new Director(lenoveComputerBuilder);
director.construct();
lenoveComputer = lenoveComputerBuilder.getComputer();
System.out.println(lenoveComputer);
}
}
从上述实例可以看出,建造者模式将很多功能集成到一个类里,这个类可以创造出比较复杂的东西。所以与工厂模式的区别就是:【工厂模式关注的是创建单个产品】,而建造者模式则【关注创建复杂对象的各个部件】。因此,是选择工厂模式还是建造者模式,依实际情况而定。
例如一个Car类是由发动机、底盘、软件系统三个对象组成,那么我们在建造者模式中就要先分别创造出这三个部分然后再把他们组装成一个Car对象。
5、原型模式
5.1 原型模式的适用场景
在需要【重复的创建大量相似对象】时,我们考虑使用原型模式。
比如需要在一个循环体内创建对象(该对象的创建过程比较复杂或循环次数很多),使用原型模式不仅可以【简化创建过程】,而且可以使系统整体【性能提高】很多。
思考:我们通常如何创建相似对象?
class A;
A a = new A();
A b = a;
以上代码就已经可以满足需求了,为何要使用原型模式去复制?
5.2 原型模式优点
原型模式借助Object类中的clone方法实现创建。
使用原型模式创建对象比new对象 效率更高,因为clone是一个本地方法,直接操作内存中二进制流,特别是复制大对象时,性能差别非常明显。
简化对象的创建【不调用构造函数】,使得创建对象时就像我们编辑文档时复制粘贴那样简单。
注意:
使用原型模式复制对象不会调用类的构造方法,所以构造函数不执行;
如果要使用clone方法,【必须实现Cloneable接口】。
5.3 浅拷贝和深拷贝
浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型变量,指向的还是原对象所指向的。浅拷贝我们通过重写clone方法即可实现。
深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。深拷贝借助序列化来实现功能。
引用数据类型: 数组、类对象
如何理解深拷贝、浅拷贝?
孙悟空拔一根猴毛可以克隆一个自己,
如果浅拷贝,两个悟空一根金箍棒;
深拷贝,两个悟空两根金箍棒。
创建型模式总结:
工厂方法模式: 用于创建复杂对象
抽象工厂模式: 用于创建一组相关或相互依赖的复杂对象
单例模式: 用于得到内存中的唯一对象
建造者模式: 用于创建模块化的更加复杂的对象
原型模式: 用于得到一个对象的拷贝
所有的创建类模式本质上都是对对象的创建过程进行封装。使客户端可以直接得到对象,而不用去关心如何创建对象。
上面是五种是创建型模式,接下来看一下7种结构型模式 :
从软件系统的功能出发,专门设计出特定的结构,方便实现系统的功能。
适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式
6、适配器模式Adapter
生活中有很多适配器的应用场景,笔记本电源适配器。
适配器模式将某个类的接口转换成客户端期望的另一个接口,使得原本由于接口不兼容而不能在一起工作的类可以在一起工作。
应用场景:
系统(Client)需要使用现有的类(Adaptee),但这些类的接口不符合系统的接口(Target.request());
旧的系统开发的类已经实现了一些功能,但客户端(当前状态)却只能以另外接口(新接口)的形式访问,但我们不希望手动更该原有类;
使用第三方组件,组件接口的定义和自己定义不同,而又不想修改自己的接口,但一定要使用第三方接口去实现功能;
适配器模式可以分为三类:
类的适配器模式(采用继承实现)
对象适配器模式(采用对象组合方式实现)
默认适配器模式(采用抽象类实现)
当我们想实现一个接口但又不想实现接口中所有的方法,只想去实现一部分方法时,可以使用默认适配器模式。
具体方法是在接口和具体实现类中间添加一个抽象类,而用抽象类去空实现目标接口的所有方法。
GUI这个章节应该是见过不少的监听器接口的适配器类:XxxxAdapter,即使适配器模式的具体体现。
模式优点:
通过适配器,客户端可以调用统一接口,使程序员编程更简单、更直接、更紧凑;
更好的复用性,复用了现有的类,解决了现有类和复用环境要求不一致的问题;
将目标类Target和适配者Adaptee解耦,通过引入适配器Adapter重用Adaptee,无需修改原有代码;
7、装饰器模式(Decorator)
动作:核心动作,装饰动作
生活中的例子:追女孩子约会 领成绩单回家给老爸看
概念:
顾名思义,装饰模式就是给一个对象增加一些新的功能,而且是【动态】的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。
这里的动态指的是用户可以根据自己的需求把之前定好的功能任意组合。
应用场景:
1、需要扩展一个类的功能。
2、动态的为一个对象增加功能,而且还能动态撤销。
缺点:产生过多相似的对象,不易排错!
8、代理模式
生活里面的例子:你打官司,有律师代理;国外买东西,代购;让你的朋友帮你追女孩,代理去拍卖会,挟天子以令诸侯。
这个设计模式十分重要,今后很多框架里面都会用到。代码结构和装饰模式很相似,但是它们的功能是不同的。代理模式是控制、限制对象的功能;装饰模式是个对象增加新功能。
注意:
装饰模式和代理模式在很多情况下,大部分代码都是类似的,但是这俩种设计的意图是不一样的,装饰模式是增强被包装对象的功能,代理模式是控制被代理对象的行为。
编码过程中,也常用到,比如我们操作数据库时:
开启事务
数据操作DML
提交事务
如果有异常发生则回滚事务
class StudentManager{
public:
void save(Student *s);
void update(Student *s);
void delete(Student *s){
//开启事务(此处若实现这个功能,不合适,不符合OOD原则,单一职责原则,所以我可以找一个代理对象帮我实现,有点类似装饰模式吧!)
操作数据,将数据删除
//提交事务
}
};
代理模式的应用场景:
如果已有的方法在使用的时候需要对原有的方法进行改进,此时有两种办法:
1、修改原有的方法来适应。这样违反了“对扩展开放,对修改关闭”的原则。
2、就是采用一个代理类调用原有的方法,且对产生的结果进行控制。这种方法就是代理模式。
使用代理模式,可以将功能划分的更加清晰,有助于后期维护!
代理模式的分类:
静态代理模式
由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
动态代理模式
在程序运行时,运用反射机制动态创建而成。
静态代理模式具体类图、源码。
观察静态代理模式代码可以发现每一个代理类只能为一个接口服务,那么倘若我们有多个接口,是不是要写多个Proxy类与之对应。这样一来程序开发中必然会产生过多的代理。
而且,所有的代理操作除了调用的方法不一样之外,其他的操作都一样,则此时肯定是重复代码。解决这一问题最好的做法是可以通过一个代理类完成全部的代理功能,那就引入了我们的动态代理了。
重点学习动态代理模式:
动态代理:JDK默认支持的,第三方提供的(功能更强大)。
1.java.lang.reflect包下面的Proxy类和InvocationHandler接口提供了生成动态代理类的能力。
具体操作为:使用Proxy.newProxyInstance(classLoader, interfaces, h)方法,动态获取代理对象proxy;然后proxy.目标方法()最终实现想要的功能;
前提:用户需要自定义MyHandler implements InvocationHandler;重写invoke()方法。
但是JDK提供的动态代理,有一个缺点:目标类必须实现某个接口,否则不能生成动态代理类。利用第三方CGLib可以解决该问题。
2.CGLib代理
CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为目标对象创建一个子类对象,并在子类对象中拦截所有父类方法的调用,然后在方法调用前后调用后都可以加入自己想要执行的代码。
需要这种方法只是需要俩个第三方jar包: cglib-3.2.1.jar和asm-5.0.4.jar
同时很多框架已经把这些jar包整合到一起了,比如spring框架的spring-core-3.2.4.RELEASE.jar,这一个jar包就包括上述俩个jar包的大多数功能。
应用步骤1:先添加第三方jar包,在项目中添加新目录,然后将两个jar包拷贝进去,最后选中jar包右键 Build Path添加即可。
步骤2:新建一个类class MyCglibProxyFactory implements MethodInterceptor;
实现获取代理对象的方法:public Object getInstance(Class<?> c);
步骤3:重写Object intercept(Object obj, Method method, Object[] args,MethodProxy proxy);该方法作用是:当代理类调用目标方法时,代理类拦截下来,按照此方法去实现功能。
代理模式在各种框架里面被广泛应用,比如Hibernate和Spring,AOP(面向界面编程)都有动态代理的经典应用。
9、门面模式Facade
也称为外观模式;
生活中的例子:便民服务中心
功能:
为子系统提供一个集中化和简化的沟通渠道(而非引入新的行为,医院接待员不能看病)。
为子系统的一组接口提供一个一致的界面;外观模式定义了一个高层接口,使得子系统的接口更容易使用。
应用场景:
实际应用中,我们在涉及一些老旧的代码或者即便不是老旧code,但涉及多个子系统时,我们为了更方便的使用之前的方法,除了重写全部代码,还可能采用这样一种策略:重新进行类的设计,将原来分散在源码中的类/结构及方法重新组合,形成新的、统一的接口,供上层应用使用,同时也隐藏了子系统或者子模块中功能实现的复杂性。
优点:
简单易用
松散耦合(客户端和子系统耦合度降低,子系统内部功能更容易扩展维护)
更好的划分了访问层次(方法有对外有对内,隐藏内部细节)
10、桥接模式
分析举例:诺基亚、摩托罗拉手机上安装游戏和通讯录软件;画出结构图分析。
在这个系统中,手机有不同类型,软件也有不同类型,整个系统要适应两个方面的变化。
桥接模式解决问题的思路是:将变化部分抽象出来,使变化部分和主类分离开来,从而将多个维度的变化彻底分离,最后提供一个管理类来组合不同维度上的变化。
直白点讲就是将 两个变化的部分比如手机和软件都进行抽象得到抽象类或接口,再由它们派生出各式各样的具体手机与软件,最后让具体的手机和软件自由组合,得到我们想要的手机软件。
桥接模式的核心:将抽象和具体实现向分离,使它们独立发生变化。
JDBC桥DriverManager一样,JDBC进行连接数据库的时候,在各个数据库之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是JDBC提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了。
11、组合模式Composite
给一个公司做管理系统,先给总公司做好,人力资源部和财务部用起来很好用。现在需要系统升级,把系统修改后给分公司、办事处去用,他们也都有人力资源部和财务部,要求所有分公司和办事处要有和总公司一样的功能。
问题的核心是希望总公司组织结构(人力资源部,财务部)的管理功能,能够复用给分公司。这其实就是【整体和局部】可以【被一致对待】的问题。此时可以使用组合模式。
【整体】的方法 也存在于 【局部】中,但是方法的实现是不同的。
【文件系统】也是通过组合模式实现的。
12、享元模式Flyweight
享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。
在程序设计中,有时需要生成大量的类对象,如果发现这些类对象除了几个参数外基本上都是相同的,我们就可以使用享元模式避免这种资源的开销。具体做法是:重新设计类,去除不同的参数(外部状态),在方法调用时将它们以参数的形式传递进来。
在享元对象内部并且不会随着环境改变而改变的共享部分,称为享元对象的内部状态(类内部数据成员);而随着环境改变而改变,不可以共享的状态就是外部状态(类外部数据成员)。
接项目,赚外快,去给私营业主做网站(信息发布、产品展示、博客留言、论坛功能),做好以后租用虚拟空间,使用数据库,运行代码就好了。
然后熟人介绍,又有很多类似的项目介绍给我,我按照相同的办法处理,问题出现了(我又要租用虚拟空间,使用数据库,复制相同的代码简单修改,然后运行起来,貌似挺简单的,但是后期的维护难以想象…)。
享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。
例子:
//数据库连接池
public class ConnectionPool {
private Vector<Connection> pool;
/*公有属性*/
private String url = "jdbc:mysql://localhost:3306/test";
private String username = "root";
private String password = "root";
private String driverClassName = "com.mysql.jdbc.Driver";
private int poolSize = 100;
//这里对instance可以使用一个单例模式
private static ConnectionPool instance = null;
Connection conn = null;
/*构造方法,做一些初始化工作*/
private ConnectionPool() {
pool = new Vector<Connection>(poolSize);
for (int i = 0; i < poolSize; i++) {
try {
Class.forName(driverClassName);
conn = DriverManager.getConnection(url, username, password);
pool.add(conn);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/* 把连接对象返回到连接池 */
public synchronized void release() {
pool.add(conn);
}
/* 返回连接池中的一个数据库连接 */
public synchronized Connection getConnection() {
if (pool.size() > 0) {
Connection conn = pool.get(0);
pool.remove(conn);
return conn;
} else {
return null;
}
}
}
再看剩余的11种行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式
13、策略模式
在软件开发的过程中,实现一个功能有多种算法或者策略,我么可以根据环境或条件的不同去选择不同的算法或策略来完成该功能。
策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。策略模式的重心【不是如何实现算法】,而是如何【组织调用】这些算法,使得程序更灵活,扩展性维护性更好。
比如你旅行需要选择出行方式:步行、公交、火车、高铁、飞机。
刘备去江东娶媳妇,三个锦囊。
刘备在江夏 孙权在南京 曹操打江南
刘备孙权合伙 赤壁之战,打跑了曹操
刘备借机占了荆州 借
周瑜 美人计 诸葛亮 准备了三个锦囊(具体算法) 给赵云
孙尚香
第一个锦囊 刘备,城里面,大肆采买物品,放出声去娶媳妇,拜访了乔国丈
贺喜吴国太 50 20
周瑜 年轻 编草鞋卖鞋的 沉湎其中 乐不思蜀
第二个锦囊 编瞎话:曹操率领了几十万大军要打来了, 骗人,到江边祭祖
孙权派人去追,500人+赵云
第三个锦囊 实话实说:挑拨离间,激怒孙尚香, 闯关
到边界以后,诸葛亮等着
二气周瑜:
唯有读书可以改变气质!
14、模板方法模式
该模式在一个类中实现一个操作的【骨架算法】,而将算法的一些【步骤延迟到子类中】。这个模式使得子类可以在【不改变算法结构】的前提下,只需要【重定义各个步骤】,就可以【实现不同的功能】。
比如老师和学生早上起来后都要准备去学校。他们做的事情(算法框架)是一样的:穿衣服起床、吃早饭、带上东西,但实现起来细节完全不同。
15、观察者模式
1.基本概念
也称为依赖、发布–订阅模式。
公众号: 被观察者 维护一个集合,放入所有的观察者
微信用户: 观察者
生活实例,关注微信公众号,更新后自动通知所有人。
观察者模式中有【主题(Subject)】和【观察者(Observer)】.
观察者模式定义了对象之间的【一对多】的【依赖关系】,当"一"的状态发生变化时,它所依赖的"多"的一方都会收到通知并且自动更新.
2.气象观察站的实例
/* Subject被观察者类 接口
* WeatherData 具体被观察者类 气象站
*
* Observer 观察者接口
* StatisticsDisplay 统计显示面板(具体观察者 实现上述两个接口)
*
* Test测试类
*/
3.观察者模式优点
使主题和观察者之间【耦合度降得很低】。为什么呢?
因为关于观察者的一切,主题(被观察者)只知道观察者实现了Observer接口,并不需要观察者具体的类是谁,做了什么或者其他细节.这样的话,由于松耦合,改变主题或者观察者其中一方,并不会影响另一方,只要他们之间的接口仍被遵守,就可以自由地改变它.
16、迭代子模式(Iterator)
定义:
顾名思义,迭代器模式提供一种方法【顺序访问】一个聚合对象中的各种元素,而又【不暴露该对象的内部】表示。
迭代器模式应用的场景及意义
(1)访问一个聚合对象的内容而无需暴露它的内部表示
(2)支持对聚合对象的多种遍历
(3)为遍历不同的聚合结构提供一个统一的接口
迭代器模式的优缺点:
迭代器模式的优点有:
简化了遍历方式,对于对象集合的遍历,还是比较麻烦的,对于数组或者有序列表,我们尚可以通过游标来取得,但用户需要在对集合了解很清楚的前提下,自行遍历对象,但是对于hash表来说,用户遍历起来就比较麻烦了。而引入了迭代器方法后,用户用起来就简单的多了。
可以提供多种遍历方式,比如说对有序列表,我们可以根据需要提供正序遍历,倒序遍历两种迭代器,用户用起来只需要得到我们实现好的迭代器,就可以方便的对集合进行遍历了。
封装性良好,用户只需要得到迭代器就可以遍历,而对于遍历算法则不用去关心。
迭代器模式的缺点:
对于比较简单的遍历(像数组或者有序列表),使用迭代器方式遍历较为繁琐,大家可能都有感觉,像ArrayList,我们宁可愿意使用for循环和get方法来遍历集合。
总的来说:
【迭代器模式是与集合共生共死】的,一般来说,我们只要实现一个集合,就需要同时提供这个集合的迭代器,就像java中的Collection,List、Set、Map等,这些集合都有自己的迭代器。假如我们要实现一个这样的新的容器,当然也需要引入迭代器模式,给我们的容器实现一个迭代器。
迭代器模式总结:
迭代器模式(Iterator)就是分离了聚合对象的遍历行为,抽象出一个迭代器来负责这样既可以做到不暴露集合的内部结构,又可让外部代码透明的访问集合内部数据。
17、责任链模式
责任链模式,有多个对象,每个对象持有对下一个对象的引用,这样就会形成一条链,请求在这条链上传递,直到某一对象决定处理该请求。但是发出者并不清楚到底最终哪个对象会处理该请求,所以,责任链模式可以实现,在隐瞒客户端的情况下,对系统进行动态的调整。
有点类似黑箱操作。
优点是很【灵活】,内部传递规则可随意改动,每个人可以动态指定继任者;
也可以从任意一个节点开始处理。
如果不用该模式的话,我们需要和各个层次对象发生【耦合关系】,反映在代码上,一个类里面包含很多if...else...语句,耦合度太高。
实际应用:员工涨工资。
20、状态模式
核心思想就是:当对象的状态改变时,会影响其行为的执行效果!
你要执行一个动作,该动作的执行结果会依赖当前的状态。
你在开车,往前走一公里。(当前状态: 你在哪里,你朝向哪里)
Car{
State s;
void forward(){
//s.forward(this);
//if(s == 东){}elseif(南){}else if(西){}else{}
}
}
使用该模式之前,行为的执行会改变状态,执行的效果依赖当前的状态,【状态的转换】很复杂。
使用之后,行为直接操作,不用关心状态,因为内部已经【封装了状态转换规则】,后期也很容易扩展。
小车的例子。
22、中介者模式(Mediator)
中介者模式也是用来【降低类和类之间的耦合】的,因为如果类和类之间有依赖关系的话,不利于功能的拓展和维护,因为只要修改一个对象,其它关联的对象都得进行修改。如果使用中介者模式,【只需关心和Mediator类】的关系,具体类类之间的关系及调度交给Mediator就行。
适用场景:
在面向对象编程中,一个类必然会与其他的类发生依赖关系,完全独立的类是没有意义的。
一个类同时依赖多个类的情况也相当普遍,既然存在这样的情况,说明,一对多的依赖关系有它的合理性,适当的使用中介者模式可以使原本凌乱的对象关系清晰,但是如果滥用,则可能会带来反的效果。
一般来说,只有对于那种房东和租户类之间是网状结构的关系,才会考虑使用中介者模式。可以将网状结构变为星状结构,使房东和租户类之间的关系变的清晰一些。
优点:
避免类与类的过度耦合,使各个类可以单独使用;
中介者模式将对象一对多关联转变成一对一关联关系;
可以将对象的行为与协作进行抽象,能够比较灵活的处理对象间相互的作用。
16、迭代子模式(Iterator)
顾名思义,迭代器模式就是顺序访问聚集中的对象,一般来说,集合中非常常见,如果对集合类比较熟悉的话,理解本模式会十分轻松。
例子:
public interface Collection {
public Iterator iterator();
/*取得集合元素*/
public Object get(int i);
/*取得集合大小*/
public int size();
}
public interface Iterator {
//前移 上一个元素
public Object previous();
//后移 下一个元素
public Object next();
public boolean hasNext();
//取得第一个元素
public Object first();
}
public class MyCollection implements Collection {
//假设这个集合内部是由数组实现
public String string[] = {"A","B","C","D","E"};
public Iterator iterator() {
return new MyIterator(this);
}
public Object get(int i) {
return string[i];
}
public int size() {
return string.length;
}
}
//这个地方其实一般会设计为内部类
public class MyIterator implements Iterator {
private Collection collection;
private int pos = -1;
public MyIterator(Collection collection){
this.collection = collection;
}
public Object previous() {
if(pos > 0){
pos--;
}
return collection.get(pos);
}
public Object next() {
if(pos<collection.size()-1){
pos++;
}
return collection.get(pos);
}
public boolean hasNext() {
if(pos<collection.size()-1){
return true;
}else{
return false;
}
}
public Object first() {
pos = 0;
return collection.get(pos);
}
}
//测试类
public class Test {
public static void main(String[] args) {
Collection collection = new MyCollection();
Iterator it = collection.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}
}
18、命令模式(Command)
命令模式很好理解,举个例子,司令员下令让士兵去干件事情,从整个事情的角度来考虑,司令员的作用是,发出口令,口令经过传递,传到了士兵耳朵里,士兵去执行。这个过程好在,三者(司令、命令、士兵)相互解耦,任何一方都不用去依赖其他人的具体实现,只需要做好自己的事儿就行,司令员要的是结果,不会去关注到底士兵是怎么实现的。
例子:
//命令执行的接口
public interface Command {
public void exe();
}
//具体实现的命令
public class MyCommand implements Command {
private Receiver receiver;
public MyCommand(Receiver receiver) {
this.receiver = receiver;
}
public void exe() {
receiver.action();
}
}
//被调用者(士兵)
public class Receiver {
public void action(){
System.out.println("command received!");
}
}
//调用者(司令员)
public class Invoker {
private Command command;
public Invoker(Command command) {
this.command = command;
}
public void action(){
command.exe();
}
}
//测试类
public class Test {
public static void main(String[] args) {
Receiver receiver = new Receiver();
Command cmd = new MyCommand(receiver);
Invoker invoker = new Invoker(cmd);
invoker.action();
}
}
这个很好理解,命令模式的目的就是达到命令的发出者和执行者之间解耦,实现请求和执行分开。
对于大多数请求-响应模式的功能,比较适合使用命令模式。
19、备忘录模式(Memento)
也可以叫备份模式,主要目的是保存一个对象的某个状态,以便在适当的时候恢复对象:假设有原始类A,A中有各种属性,A可以决定需要备份的属性,备忘录类B是用来存储A的一些内部状态,类C呢,就是一个用来存储备忘录的,且只能存储,不能修改等操作。
例子:
//原始类,里面有需要保存的属性value
public class Original {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Original(String value) {
this.value = value;
}
//创建备忘录对象用来存储属性值
public Memento createMemento(){
return new Memento(value);
}
//还原属性值
public void restoreMemento(Memento memento){
this.value = memento.getValue();
}
}
//备忘录类,用来保存value值
public class Memento {
private String value;
public Memento(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
//存储备忘录的类,持有Memento类的实例
public class Storage {
private Memento memento;
public Storage(Memento memento) {
this.memento = memento;
}
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
//测试类
public class Test {
public static void main(String[] args) {
// 创建原始类
Original origi = new Original("egg");
// 创建备忘录
Storage storage = new Storage(origi.createMemento());
// 修改原始类的状态
System.out.println("初始化状态为:" + origi.getValue());
origi.setValue("niu");
System.out.println("修改后的状态为:" + origi.getValue());
// 回复原始类的状态
origi.restoreMemento(storage.getMemento());
System.out.println("恢复后的状态为:" + origi.getValue());
}
}
21、访问者模式(Visitor)
访问者模式把数据结构和作用于结构上的操作解耦合,使得对数据操作可相对自由地演化。访问者模式适用于数据结构相对稳定,算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。其缺点就是增加新的数据结构很困难。
简单来说,访问者模式就是一种分离对象数据结构与行为的方法,通过这种分离,可达到为一个被访问者动态添加新的操作而无需做其它的修改的效果。
例子:
//访问者接口
public interface Visitor {
public void visit(Subject sub);
}
//访问者的一个具体实现
public class MyVisitor implements Visitor {
public void visit(Subject sub) {
System.out.println(“visit the subject:”+sub.getSubject());
}
}
//被访问者接口
public interface Subject {
public void accept(Visitor visitor);
public String getSubject();
}
//被访问者的一个具体实现
public class MySubject implements Subject {
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String getSubject() {
return "love";
}
}
//测试类
public class Test {
public static void main(String[] args) {
Visitor visitor = new MyVisitor();
Subject sub = new MySubject();
sub.accept(visitor);
}
}
该模式适用场景:如果我们想为一个现有的类增加新功能,不得不考虑几个事情:1、新功能会不会与现有功能出现兼容性问题?2、以后会不会再需要添加?3、如果类不允许修改代码怎么办?面对这些问题,最好的解决方法就是使用访问者模式,访问者模式适用于数据结构相对稳定的系统,把数据结构和算法解耦
23、解释器模式(Interpreter)
例子:解释器接口(这里的是专门解析数学运算表达式)
public interface Expression {
public int interpret(Context context);
}
//加法
public class Plus implements Expression {
public int interpret(Context context) {
return context.getNum1()+context.getNum2();
}
}
//减法
public class Minus implements Expression {
public int interpret(Context context) {
return context.getNum1()-context.getNum2();
}
}
//Context类是一个上下文环境类 持有运行中所需的数据
public class Context {
private int num1;
private int num2;
public Context(int num1, int num2) {
this.num1 = num1;
this.num2 = num2;
}
public int getNum1() {
return num1;
}
public void setNum1(int num1) {
this.num1 = num1;
}
public int getNum2() {
return num2;
}
public void setNum2(int num2) {
this.num2 = num2;
}
}
//测试类
public class Test {
public static void main(String[] args) {
// 计算9+2-8的值
int result = new Minus().interpret(new Context(
new Plus().interpret(new Context(9, 2)), 8));
//相当于:new Minus().interpret(new Context(11, 8));
System.out.println(result);
}
}
在以下情况下可以使用解释器模式:
有一个简单的语法规则,比如一个sql语句,如果我们需要根据sql语句进行其他语言的转换,就可以使用解释器模式来对语句进行解释。
一些重复发生的问题,比如加减乘除四则运算,但是公式每次都不同,有时是a+b-c*d,有时是a*b+c-d,等等等等个,公式千变万化,但是都是由加减乘除四个非终结符来连接的,这时我们就可以使用解释器模式。