目录
面向对象的七种设计原则
建造者模式
命令模式
享元模式
模板方法模式
责任链模式
建造者模式
原型模式
观察者模式
策略模式与命令模式区别
桥接模式
组合模式
适配器模式
装饰者模式
外观模式
状态模式
状态模式与策略模式区别
[迭代器模式]基本用不到
[备忘模式]很简单、就是有个管理员保存一些对象
[访问者模式] 不常用,不好用,不信你擦擦
中介模式
[解释器模式]
代理模式
简单工厂模式
工厂模式
抽象工厂
委托设计模式
适配器模式 与(装饰者、代理模式)区别
装饰器模式和代理模式区别
源码地址
https://github.com/javanan/DataStructure
阿里云优惠券与阿里云上云教程
面向对象设计七大原则
1、 开闭原则
核心思想:对扩展开放,对修改关闭。
即在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,这样兼容性更好
2、 里氏替换原则
在任何父类出现的地方都可以用他的子类来替代(子类应当可以替换父类,并出现在父类能够出现的任何地方)
3、 单一职责原则
核心:解耦和增强内聚性(高内聚,低耦合)
类被修改的几率很大,因此应该专注于单一的功能。如果你把多个功能放在同一个类中,功能之间就形成了关联,
改变其中一个功能,有可能中止另一个功能,这时就需要新一轮的测试来避免可能出现的问题。
4、 接口隔离原则
核心思想:不应该强迫客户程序依赖他们不需要使用的方法。
接口分离原则的意思就是:一个接口不需要提供太多的行为,一个接口应该只提供一种对外的功能,不应该把所有的操作都封装到一个接口当中.
比如使用Retrofit时,我们需要定义ApiService,有时为了方便维护,把所有方法写到一个接口中,这就会再一个模块调用会出现不需要的使用的方法,这就违背了接口隔离原则
在开发过程中也要注意取舍
分离接口的两种实现方法:
1.使用委托分离接口。(Separation through Delegation)
2.使用多重继承分离接口。(Separation through Multiple Inheritance)
5、 依赖倒置原则
核心:要依赖于抽象,不要依赖于具体的实现
实现方式:
1.通过构造函数传递依赖对象
2.通过setter方法传递依赖对象
3.接口声明实现依赖对象
6、 迪米特原则
又叫最少知识原则
核心思想: 一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。
(类间解耦,低耦合)意思就是降低各个对象之间的耦合,提高系统的可维护性;
而不理会模块的内部工作原理,可以使各个模块的耦合成都降到最低,促进软件的复用
个人理解,一个类的每个方法,自己干自己的事,里面尽量不要引入其他的类和对象
解决方法:
在模块之间只通过接口来通信
只和朋友交流。迪米特还有一个英文解释叫做“Only talk to your immedate friends”,只和直接 的朋友通信,什么叫做直接的朋友呢?每个对象都必然会和其他对象有耦合关系,两个对象之间的耦合就 成为朋友关系,这种关系有很多比如组合、聚合、依赖等等
/**
*
* 首先来看 Teacher 有几个朋友,就一个 GroupLeader 类,这个 就是朋友类,朋友类是怎么定义的呢?
出现在成员变量、方法的输入输出参数中的类被称为成员朋友类, 迪米特法则说是一个类只和朋友类交流,
但是 commond 方法中我们与 Girl 类有了交流,声明了一个 List动态数组,也就是与一个陌生的类 Girl 有了交流,这个不好,
*/
public class Teacher {
//老师对学生发布命令, 清一下女生
public void commond(GroupLeader groupLeader){
List listGirls = new ArrayList() ;
//初始化女生
for(int i=0;i<20;i++){
listGirls.add(new Girl());
}
//告诉体育委员开始执行清查任务
groupLeader.countGirls(listGirls);
}
// 修改后
public class Teacher {
//老师对学生发布命令, 清一下女生
public void commond(GroupLeader groupLeader){
//告诉体育委员开始执行清查任务
groupLeader.countGirls();
}
}
不要出现 getA().getB().getC().getD() 这种情况(在一种极端的情况下是允许出现这种访问:每一个点号后面的返回类型都相同)
迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高,其要求 的结果就是产生了大量的中转或跳转类,类只能和朋友交流,朋友少了你业务跑不起来,朋友多了,你项 目管理就复杂,大家在使用的时候做相互权衡吧
7、组合/聚合复用原则
聚合复用
class Classes{
privateStudent student;
publicClasses(Student student){
this.student=student;
}
}
合成复用
class House{
private Room room;
public House(){
room=new Room();
}
public void createHouse(){
room.createRoom();
}
}
为什么使用合成/聚合复用,而不使用继承复用?
由于合成或聚合可以将已有对象纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能。这样做的好处有
(1) 新对象存取成分对象的唯一方法是通过成分对象的接口。
(2) 这种复用是黑箱复用,因为成分对象的内部细节是新对象看不见的。
(3) 这种复用支持包装。
(4) 这种复用所需的依赖较少。
(5) 每一个新的类可以将焦点集中到一个任务上。
(6) 这种复用可以再运行时间内动态进行,新对象可以动态地引用与成分对象类型相同的对象。
一般而言,如果一个角色得到了更多的责任,那么可以使用合成/聚合关系将新的责任委派到合适的对象。当然,这种复用也有缺点。最主要的缺点就是通过这种复用建造的系统会有较多的对象需要管理。
2、继承复用
继承复用通过扩展一个已有对象的实现来得到新的功能,基类明显的捕获共同的属性和方法,而子类通过增加新的属性和方法来扩展超类的实现。继承是类型的复用。
继承复用的优点。
(1) 新的实现较为容易,因为超类的大部分功能可以通过继承关系自动进入子类。
(2) 修改或扩展继承而来的实现较为容易。
继承复用的缺点。
(1) 继承复用破坏包装,因为继承将超类的实现细节暴露给了子类。因为超类的内部细节常常对子类是透明的,因此这种复用是透明的复用,又叫“白箱”复用。
(2) 如果超类的实现改变了,那么子类的实现也不得不发生改变。因此,当一个基类发生了改变时,这种改变会传导到一级又一级的子类,使得设计师不得不相应的改变这些子类,以适应超类的变化。
(3) 从超类继承而来的实现是静态的,不可能在运行时间内发生变化,因此没有足够的灵活性。
建造者模式
class Car (
val model: String?,
val year: Int
) {
private constructor(builder: Builder) : this(builder.model, builder.year)
class Builder {
var model: String? = null
var year: Int = -1
fun build() = Car(this)
}
companion object {
//核心 fun T.apply(f: T.() -> Unit): T { f(); return this }
//调用者本身扩展一个apply方法, apply 允许可以传递一个无参数的函数无返回值的函数,然后扩展到调用者身上,并在函数体调用这个函数,并放回自身
// 为什么要Builder.() -> Unit扩展,因为这样里面就可以调用当前对象的其他方法,也就是能使用this
fun build(block: Builder.() -> Unit) = Builder().apply(block).build()
}
}
// usage 常规写法
val car = Car.build({
model = "aa"
year = 2018
})
// usage 简化写,好多初学者看到kotlin项目会一脸懵,这是什么意思,其实就是如果只有一个闭包参数就可以省略(),直接写{},
val car = Car.build{
model = "aa"
year = 2018
}
命令模式
Command(命令角色):接口或者抽象类,定义要执行的命令。
ConcreteCommand(具体命令角色):命令角色的具体实现,通常会持有接收者,并调用接收者来处理命令。
Invoker(调用者角色):负责调用命令对象执行请求,通常会持有命令对象(可以持有多个命令对象)。Invoker是Client真正触发命令并要求命令执行相应操作的地方(使用命令对象的入口)。
Receiver(接收者角色):是真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
Client(客户端角色):Client可以创建具体的命令对象,并且设置命令对象的接收者。
Client
public void test() {
Receiver receiver = new Receiver();//创建命令接收者
Command command = new ShutdownCommand(receiver);//创建一个命令的具体实现对象,并指定命令接收者
Invoker invoker = new Invoker(command);//创建一个命令调用者,并指定具体命令
invoker.action();//发起调用命令请求
}
命令模式同时也支持命令的撤销(Undo)操作和恢复(Redo)操作,比如我们平时关机时,也是可以撤销关机的。至于恢复操作,需要我们记下执行过的命令,在需要的时候重新执行一遍。
-
优点
调用者与接受者之间的解藕。
易于扩展,扩展命令只需新增具体命令类即可,符合开放封闭原则。 -
缺点
过多的命令会造成过多的类,
- Android 中Thread
实际上Thread的使用就是一个简单的命令模式,先看下Thread的使用:
new Thread(new Runnable() {
@Override
public void run() {
//doSomeThing
}
}).start();
Thread的start()方法即命令的调用者,
@Override
public void run() {
if (target != null) {
target.run();
}
}
同时Thread的内部会调用Runnable的run(),这里Thread又充当了具体的命令角色,
最后的Runnable则是接受者了,负责最后的功能处理。
- Android 中Handler
另一个比较典型的常用到命令模式就是Handler了,这里就不贴代码了,简单分析下各个类的角色:
接受者:Handler,执行消息的处理操作。
调用者:Looper,调用消息的的处理方法。
命令角色:Message,消息类。
享元模式
-
介绍
享元模式属于结构型模式。
享元模式是池技术的重要实现方式,它可以减少重复对象的创建,使用缓存来共享对象,从而降低内存的使用。
细粒度的对象其状态可以分为两种:内部状态和外部状态。
内部状态:对象可共享出来的信息,存储在享元对象内部并且不会随环境的改变而改变。
外部状态:对象依赖的一个标记是随环境改变而改变的,并且不可共享。
Java 中享元模式,我们接触到最多的还是Java中的String。如果字符串常量池中有此字符则直接返回,否则先在字符串常量池中创建字符串。
String例子
String s0 = "abc";
String s1 = "abc";
System.out.println("s0 == s1 " + s0 == s1);
3.Message类本身就组织了一个栈结构的缓冲池。并使用obtain()方法和recycler()方法来取出和放入
/*package*/ Message next;
private static final Object sPoolSync = new Object();
private static Message sPool;
private static int sPoolSize = 0;
private static final int MAX_POOL_SIZE = 50;
private static boolean gCheckRecycle = true;
在解释这段代码前,需要先明确两点:sPool声明为private static Message sPool;
next声明为/*package*/ Message next;。即前者为该类所有示例共享,后者则每个实例都有。
public static Message obtain() {
synchronized (sPoolSync) {
/**请注意!我们可以看到Message中有一个next字段指向下一个Message,这里就明白了,Message消息池中
没有使用Map这样的容器,而是使用的链表!
*/
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = -1;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
//将自身链入头部,所以next 指向之前的链表头
next = sPool;
//链表的头指向自身
sPool = this;
//sPoolSize 多了一个
sPoolSize++;
}
}
}
spool 和 next 在执行 recycleUnchecked 被赋值 也就是当前对象执行完 我们去回收他,
Message3 next=Message2 spool=Message3 Message2 next = Message spool = Message2 Message next= null spool = Message
- 开发时 多使用 Message.obtain().sendToMessage()形式发送消息可以提高性能,不要再使用Message()
模板方法
- 父类通过实现一套自己的算法流程,方法内部实现一些小的算法
AbstractClass(抽象类):,定义了一整套算法框架。
ConcreteClass(具体实现类):具体实现类,根据需要去实现抽象类中的方法
- Android 中
继续以送快递为例,快递员送快递基本就是一套固定的流程:收到快递,准备派送->联系收货人->确定结果。
public abstract class Postman {//抽象快递员类
//派送流程
public final void post() {//这里申明为final,不希望子类覆盖这个方法,防止更改流程的执行顺序
prepare();//准备派送
call();//联系收货人
if (isSign())//是否签收
sign();//签收
else refuse();//拒签
}
protected void prepare() {//准备操作,固定流程,父类实现
System.out.println("快递已达到,准备派送");
}
protected abstract void call();//联系收货人,联系人不一样,所以为抽象方法,子类实现
protected boolean isSign() {//是否签收,这个是钩子方法,用来控制流程的走向
return true;
}
protected void sign() {//签收,这个是固定流程,父类实现
System.out.println("客户已签收,上报系统");
}
protected void refuse() {//拒签,空实现,这个也是钩子方法,子类可以跟进实际来决定是否去实现这个方法
}
}
- 模板模式在Android 中广泛应用,不如Activity 生命周期、View 的绘制在 super 底层draw中都封装好了方法,AsyncTask
责任链模式
责任链模式是对一个事件的处理方法,所有能对事件进行处理的对象按顺序形成一个链表.事件经过链表中每个处理对象轮流处理.如果有返回值.则返回也是顺着这条链表反向返回.这个链表是先进后出模式.
Android中的源码分析
Android中的事件分发机制就是类似于责任链模式,关于事件分发机制
根View 将事件传递给 子View 子View 再传递给子View ;子View 开始处理,没出处理就返回上一级去处理,递归是回溯调用,是责任链实现的一种方式
另外,OKhttp中对请求的处理也是用到了责任链模式,有兴趣的可以去看下OKhttp的源码。后面有时间也会对OKhttp的源码进行分析。
- 定义责任链的抽象
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
interface Chain {
Request request();
Response proceed(Request request) throws IOException;
/**
* Returns the connection the request will be executed on. This is only available in the chains
* of network interceptors; for application interceptors this is always null.
*/
@Nullable Connection connection();
}
}
OkHttp 中责任链实现
interface Intercept {
fun intercept(chain: Chain):Response?
}
class Chain(val index: Int, val intercepts: List,val request: Request) {
fun procced(index: Int, intercepts: List, request: Request):Response? {
if (index < intercepts.size) {
val intercept = intercepts.get(index)
val next = Chain(index+1,intercepts,request)
val response = intercept.intercept(next)
return response
}
return null
}
fun procced():Response?{
return procced(index,intercepts,request)
}
}
class Response
class Request(var url:String)
fun main() {
val intercepts = arrayListOf()
intercepts.add(object:Intercept{
override fun intercept(chain: Chain): Response? {
chain.request.url = "123"
// 这里不会立即返回,需要等最后一个拦截器执行完,这是一个递归的操作,也可以直接 return null 或者想要的数据
return chain.procced()
}
})
//添加很多拦截器
val request = Request("hahahah")
val chain = Chain(0, intercepts, request)
chain.procced(0, intercepts, request)
}
其中可以优化一下
abstract class Intercept {
open fun intercept(chain: Chain):Response?{
return chain.procced()
}
}
class MyIntercept: Intercept() {
override fun intercept(chain: Chain): Response? {
return super.intercept(chain)
}
}
这种方式就可以在super前后做修改
建造者模式
建造者模式属于创建型模式。
建造者模式主要用来创建复杂的对象,用户可以不用关心其建造过程和细节。优点
封装性良好,隐藏内部构建细节。
易于解耦,将产品本身与产品创建过程进行解耦,可以使用相同的创建过程来得到不同的产品。也就说细节依赖抽象。
易于扩展,具体的建造者类之间相互独立,增加新的具体建造者无需修改原有类库的代码。
易于精确控制对象的创建,由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响。
7.缺点
产生多余的Build对象以及Dirextor类。
建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
原型模式
就是再已经存在对象,创建一个新的一模一样的对象
java 原型需要实现Cloneable方法
参考 - Java 克隆
观察者模式
- 观察者抽象
public interface Observer {//抽象观察者
public void update(String message);//更新方法
}
- 创建具体观察者实现抽象观察者中的方法,这里创建两个类,一个男孩类和一个女孩类,定义他们收到通知后的反应:
public class Boy implements Observer {
private String name;//名字
public Boy(String name) {
this.name = name;
}
@Override
public void update(String message) {//男孩的具体反应
System.out.println(name + ",收到了信息:" + message+"屁颠颠的去取快递.");
}
}
public class Girl implements Observer {
private String name;//名字
public Girl(String name) {
this.name = name;
}
@Override
public void update(String message) {//女孩的具体反应
System.out.println(name + ",收到了信息:" + message+"让男朋友去取快递~");
}
}
- 创建抽象主题即抽象被观察者,定义添加,删除,通知等方法:
public interface Observable {//抽象被观察者
void add(Observer observer);//添加观察者
void remove(Observer observer);//删除观察者
void notify(String message);//通知观察者
}
- 创建具体主题即具体被观察者,也就是快递员,派送快递时根据快递信息来通知收件人让其来取件:
public class Postman implements Observable{//快递员
private List personList = new ArrayList();//保存收件人(观察者)的信息
@Override
public void add(Observer observer) {//添加收件人
personList.add(observer);
}
@Override
public void remove(Observer observer) {//移除收件人
personList.remove(observer);
}
@Override
public void notify(String message) {//逐一通知收件人(观察者)
for (Observer observer : personList) {
observer.update(message);
}
}
}
- 客户端测试
public void test(){
Observable postman=new Postman();
Observer boy1=new Boy("路飞");
Observer boy2=new Boy("乔巴");
Observer girl1=new Girl("娜美");
postman.add(boy1);
postman.add(boy2);
postman.add(girl1);
postman.notify("快递到了,请下楼领取.");
}
- 应用场景
当一个对象的改变需要通知其它对象改变时,而且它不知道具体有多少个对象有待改变时。
当一个对象必须通知其它对象,而它又不能假定其它对象是谁
跨系统的消息交换场景,如消息队列、事件总线的处理机制。
策略模式与命令模式区别
策略模式 定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。该模式使得算法可独立于它们的客户变化。
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
我个人觉得,策略模式和命令模式的其中一个最大区别,
是在于:策略模式对付的问题域通常是一个,就是说,多个策略只是处理同一个问题,比如排序问题,使用不同的排序算法
而命令模式对付的是多个问题域,就是很多不同的命令来做不同的事情。 比如关机 开启 命令,开机和关机 是两种不同的事情
再比如,关机时,有程序没退出(可以采用一个一个退出的算法退出),也可以(采用直接杀死所有程序的方式)
比如 让机器人去跑步 和 吃饭 是命令模式,因为干的不一样的事,跑步 的路径选着 是策略模式
- 如果是播放不同类型音乐,应该是命令模式,接收到不同的命令后去播放不同类型的音乐,同时还需要一个接收者知道播放的进度
更多的情况命令模式比策略模式多了一个接收者
桥接设计模式
- 介绍
桥接模式属于结构型模式。
举个生活中的例子,一条数据线,一头USB接口的可以连接电脑、充电宝等等,另一头可以连接不同品牌的手机,通过这条数据线,两头不同的东西就可以连接起来,这就是桥接模式
- 应用场景
一个类存在两个或以上的独立维度的变化,且这些维度都需要进行拓展。
不希望使用继承或因为多层次继承导致类的个数急剧增加时。
如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,可以通过桥接模式使他们在抽象层建立一个关联关系。
- 优点
分离了抽象与实现。让抽象部分和实现部分独立开来,分别定义接口,这有助于对系统进行分层,从而产生更好的结构化的系统。
良好的扩展性。抽象部分和实现部分都可以分别独立扩展,不会相互影响。
- 缺点
增加了系统的复杂性。
不容易设计,抽象与实现的分离要设计得好比较有难度
组合模式
将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
-
介绍
组合模式属于结构型模式。
组合模式有时叫做部分—整体模式,主要是描述部分与整体的关系。
组合模式实际上就是个树形结构,一棵树的节点如果没有分支,就是叶子节点;如果存在分支,则是树枝节点。
我们平时遇到的最典型的组合结构就是文件和文件夹了,具体的文件就是叶子节点,而文件夹下还可以存在文件和文件夹,所以文件夹一般是树枝节点
- 例子
透明的组合模式
public class Content extends PageElement {//具体内容
public Content(String name) {
super(name);
}
@Override
public void addPageElement(PageElement pageElement) {
throw new UnsupportedOperationException("不支持此操作");
}
@Override
public void rmPageElement(PageElement pageElement) {
throw new UnsupportedOperationException("不支持此操作");
}
@Override
public void clear() {
throw new UnsupportedOperationException("不支持此操作");
}
@Override
public void print(String placeholder) {
System.out.println(placeholder + "──" + getName());
}
}
public class Column extends PageElement {//栏目
public Column(String name) {
super(name);
}
@Override
public void addPageElement(PageElement pageElement) {
mPageElements.add(pageElement);
}
@Override
public void rmPageElement(PageElement pageElement) {
mPageElements.remove(pageElement);
}
@Override
public void clear() {
mPageElements.clear();
}
/**
* @param placeholder 占位符
*/
@Override
public void print(String placeholder) {
//利用递归来打印文件夹结构
System.out.println(placeholder + "└──" + getName());
Iterator i = mPageElements.iterator();
while (i.hasNext()) {
PageElement pageElement = i.next();
pageElement.print(placeholder + " ");
}
}
}
public void test() {
//创建网站根页面 root
PageElement root = new Column("网站页面");
//网站页面添加两个栏目:音乐,视屏;以及一个广告内容。
PageElement music = new Column("音乐");
PageElement video = new Column("视屏");
PageElement ad = new Content("广告");
root.addPageElement(music);
root.addPageElement(video);
root.addPageElement(ad);
//音乐栏目添加两个子栏目:国语,粤语
PageElement chineseMusic = new Column("国语");
PageElement cantoneseMusic = new Column("粤语");
music.addPageElement(chineseMusic);
music.addPageElement(cantoneseMusic);
//国语,粤语栏目添加具体内容
chineseMusic.addPageElement(new Content("十年.mp3"));
cantoneseMusic.addPageElement(new Content("明年今日.mp3"));
//视频栏目添加具体内容
video.addPageElement(new Content("唐伯虎点秋香.avi"));
//打印整个页面的内容
root.print("");
}
安全的组合模式
public abstract class PageElement {//页面
private String name;
public PageElement(String name) {
this.name = name;
}
//抽象组件角色去掉增删等接口
public abstract void print(String placeholder);
public String getName() {
return name;
}
}
public class Content extends PageElement {//具体内容,只专注自己的职责
public Content(String name) {
super(name);
}
@Override
public void print(String placeholder) {
System.out.println(placeholder + "──" + getName());
}
}
public class Column extends PageElement {//栏目
private List mPageElements = new ArrayList<>();//用来保存页面元素
public Column(String name) {
super(name);
}
public void addPageElement(PageElement pageElement) {
mPageElements.add(pageElement);
}
public void rmPageElement(PageElement pageElement) {
mPageElements.remove(pageElement);
}
public void clear() {
mPageElements.clear();
}
@Override
public void print(String placeholder) {
System.out.println(placeholder + "└──" + getName());
Iterator i = mPageElements.iterator();
while (i.hasNext()) {
PageElement pageElement = i.next();
pageElement.print(placeholder + " ");
}
}
}
public void test() {//客户端测试方法
//依赖具体的实现类Column
Column root = new Column("网站页面");
Column music = new Column("音乐");
Column video = new Column("视屏");
PageElement ad = new Content("广告");
root.addPageElement(music);
root.addPageElement(video);
root.addPageElement(ad);
Column chineseMusic = new Column("国语");
Column cantoneseMusic = new Column("粤语");
music.addPageElement(chineseMusic);
music.addPageElement(cantoneseMusic);
chineseMusic.addPageElement(new Content("十年.mp3"));
cantoneseMusic.addPageElement(new Content("明年今日.mp3"));
video.addPageElement(new Content("唐伯虎点秋香.avi"));
root.print("");
}
安全的组合模式将职责区分开来放在不同的接口中,这样一来,设计上就比较安全,也遵循了单一职责原则和接口隔离原则,但是也让客户端必须依赖于具体的实现;透明的组合模式,以违反单一职责原则和接口隔离原则来换取透明性,但遵循依赖倒置原则,客户端可以直接依赖于抽象组件即可,将叶子和树枝一视同仁,也就是说,一个元素究竟是枝干节点还是叶子节点,对客户端是透明的。
一方面,我们写代码时应该遵循各种设计原则,但实际上,有些设计模式原则在使用时会发生冲突,这就需要我们根据实际情况去衡量做出取舍,适合自己的才是最好的。
适配器模式
- 对象适配器模式与类适配器模式比较
类适配器采用了继承的方式来实现;而对象适配器是通过传递对象来实现,这是一种组合的方式。
类适配器由于采用了继承,可以重写父类的方法;对象适配器则不能修改对象本身的方法等。
适配器通过继承都获得了父类的方法,客户端使用时都会把这些方法暴露出去,增加了一定的使用成本;对象适配器则不会。
类适配器只能适配他的父类,这个父类的其他子类都不能适配到;而对象适配器可以适配不同的对象,只要这个对象的类型是同样的。
类适配器不需要额外的引用;对象适配器需要额外的引用来保存对象。
总的来说,使用对象适配器比较好。当然具体问题具体分析。
- 应用场景
当想使用一个已经存在的类,但它的接口不符合需求时。
当想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作。
- 优点
提高了类的复用性,适配器能让一个类有更广泛的用途。
提高了灵活性,更换适配器就能达到不同的效果。不用时也可以随时删掉适配器,对原系统没影响。
符合开放封闭原则,不用修改原有代码。没有任何关系的类通过增加适配器就能联系在一起。
- 缺点
过多的使用适配器,会让系统非常零乱,不易整体进行把握。明明调用A接口,却被适配成B接口
装饰者模式
- 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。
Component(抽象组件):接口或者抽象类,被装饰的最原始的对象。具体组件与抽象装饰角色的父类。
ConcreteComponent(具体组件):实现抽象组件的接口。
Decorator(抽象装饰角色):一般是抽象类,抽象组件的子类,同时持有一个被装饰者的引用,用来调用被装饰者的方法;同时可以给被装饰者增加新的职责。
ConcreteDecorator(具体装饰类):抽象装饰角色的具体实现。
装饰者模式属于结构型模式。
装饰者模式在生活中应用实际上也非常广泛,一如一间房,放上厨具,它就是厨房;放上床,就是卧室。
通常我们扩展类的功能是通过继承的方式来实现,但是装饰者模式是通过组合的方式来实现,这是继承的替代方案之一。
- 应用场景
需要扩展一个类的功能,或给一个类增加附加功能时
需要动态的给一个对象增加功能,这些功能可以再动态的撤销
当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
- 优点
采用组合的方式,可以动态的扩展功能,同时也可以在运行时选择不同的装饰器,来实现不同的功能。
有效避免了使用继承的方式扩展对象功能而带来的灵活性差,子类无限制扩张的问题。
被装饰者与装饰者解偶,被装饰者可以不知道装饰者的存在,同时新增功能时原有代码也无需改变,符合开放封闭原则。
- 缺点
装饰层过多的话,维护起来比较困难。
如果要修改抽象组件这个基类的话,后面的一些子类可能也需跟着修改,较容易出错
Android中的源码分析
我们都知道Activity、Service、Application等都是一个Context,这里面实际上就是通过装饰者模式来实现的。下面以startActivity()这个方法来简单分析一下。
Context类
Context实际上是个抽象类,里面定义了大量的抽象方法,其中就包含了startActivity()方法:
Context类在这里就充当了抽象组件的角色,ContextImpl类则是具体的组件,而ContextWrapper就是具体的装饰角色,通过扩展ContextWrapper增加不同的功能,就形成了Activity、Service等子类。
- 总结:代码结构上更像一个链表的结构, 装饰器持有被装饰的引用,同时也有被装饰的同样方法,方法内部在对被装饰者调用
public abstract class Room {
public abstract void fitment();//装修方法
}
public class NewRoom extends Room {//继承Room
@Override
public void fitment() {
System.out.println("这是一间新房:装上电");
}
}
public abstract class RoomDecorator extends Room {//继承Room,拥有父类相同的方法
private Room mRoom;//持有被装饰者的引用,这里是需要装修的房间
public RoomDecorator(Room room) {
this.mRoom = room;
}
@Override
public void fitment() {
mRoom.fitment();//调用被装饰者的方法
}
}
public class Bedroom extends RoomDecorator {//卧室类,继承自RoomDecorator
public Bedroom(Room room) {
super(room);
}
@Override
public void fitment() {
super.fitment();
addBedding();
}
private void addBedding() {
System.out.println("装修成卧室:添加卧具");
}
}
public class Kitchen extends RoomDecorator {//厨房类,继承自RoomDecorator
public Kitchen(Room room) {
super(room);
}
@Override
public void fitment() {
super.fitment();
addKitchenware();
}
private void addKitchenware() {
System.out.println("装修成厨房:添加厨具");
}
}
public void test() {
Room newRoom = new NewRoom();//有一间新房间
RoomDecorator bedroom = new Bedroom(newRoom);
bedroom.fitment();//装修成卧室
RoomDecorator kitchen = new Kitchen(newRoom);
kitchen.fitment();//装修成厨房
}
外观模式
- 要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。外观模式提供一个高层次的接口,使得子系统更易于使用。
- 介绍
外观模式属于结构型模式。
外观模式也叫门面模式。
通常我们对API进行封装,都会用到外观模式,只是我们可能不知道而已。外观模式通过一个外观类使得整个系统的结构只有一个统一的高层接口,这样能降低用户的使用成本。
一搬 我们封装SDK的时候,通常对外提供统一的Mananger 去调用,提供统一入口, 比如EventBus,Glide等等
降低了客户端与子系统类的耦合度,实现了子系统与客户之间的松耦合关系。
外观类对子系统的接口封装,使得系统更易于使用。
提高灵活性,不管子系统如何变化,只要不影响门面对象,就可以自由修改。
- 缺点
增加新的子系统可能需要修改外观类的源代码,违背了“开闭原则”。一搬我们SDK可以进行jar替换更新,对于一个系统
所有子系统的功能都通过一个接口来提供,这个接口可能会变得很复杂。
状态模式
对于可以切换状态的状态模式不满足“开闭原则”的要求。
1.概述
在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理。最直接的解决方案是将这些所有可能发生的情况全都考虑到。然后使用if... ellse语句来做状态判断来进行不同情况的处理。但是对复杂状态的判断就显得“力不从心了”。随着增加新的状态或者修改一个状体(if else(或switch case)语句的增多或者修改)可能会引起很大的修改,而程序的可读性,扩展性也会变得很弱。维护也会很麻烦。那么我就考虑只修改自身状态的模式。
例子1:按钮来控制一个电梯的状态,一个电梯开们,关门,停,运行。每一种状态改变,都有可能要根据其他状态来更新处理。例如,开门状体,你不能在运行的时候开门,而是在电梯定下后才能开门。
例子2:我们给一部手机打电话,就可能出现这几种情况:用户开机,用户关机,用户欠费停机,用户消户等。 所以当我们拨打这个号码的时候:系统就要判断,该用户是否在开机且不忙状态,又或者是关机,欠费等状态。但不管是那种状态我们都应给出对应的处理操作。
2.问题
对象如何在每一种状态下表现出不同的行为?
3.解决方案
状态模式:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
在很多情况下,一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态,这样的对象叫做有状态的(stateful)对象,这样的对象状态是从事先定义好的一系列值中取出的。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。
4.适用性
在下面的两种情况下均可使用State模式:
if else(或switch case)语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常 , 有多个操作包含这一相同的条件结构。 State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
5.结构
6.模式的组成
环境类(Context): 定义客户感兴趣的接口。维护一个ConcreteState子类的实例,这个实例定义当前状态。
抽象状态类(State): 定义一个接口以封装与Context的一个特定状态相关的行为。
具体状态类(ConcreteState): 每一子类实现一个与Context的一个状态相关的行为。
7.效果
State模式有下面一些效果:
状态模式的优点:
1 ) 它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来: State模式将所有与一个特定的状态相关的行为都放入一个对象中。因为所有与状态相关的代码都存在于某一个State子类中, 所以通过定义新的子类可以很容易的增加新的状态和转换。另一个方法是使用数据值定义内部状态并且让 Context操作来显式地检查这些数据。但这样将会使整个Context的实现中遍布看起来很相似的条件if else语句或switch case语句。增加一个新的状态可能需要改变若干个操作, 这就使得维护变得复杂了。State模式避免了这个问题, 但可能会引入另一个问题, 因为该模式将不同状态的行为分布在多个State子类中。这就增加了子类的数目,相对于单个类的实现来说不够紧凑。但是如果有许多状态时这样的分布实际上更好一些, 否则需要使用巨大的条件语句。正如很长的过程一样,巨大的条件语句是不受欢迎的。它们形成一大整块并且使得代码不够清晰,这又使得它们难以修改和扩展。 State模式提供了一个更好的方法来组织与特定状态相关的代码。决定状态转移的逻辑不在单块的 i f或s w i t c h语句中, 而是分布在State子类之间。将每一个状态转换和动作封装到一个类中,就把着眼点从执行状态提高到整个对象的状态。这将使代码结构化并使其意图更加清晰。
它使得状态转换显式化: 当一个对象仅以内部数据值来定义当前状态时 , 其状态仅表现为对一些变量的赋值,这不够明确。为不同的状态引入独立的对象使得转换变得更加明确。而且, State对象可保证Context不会发生内部状态不一致的情况,因为从 Context的角度看,状态转换是原子的—只需重新绑定一个变量(即Context的State对象变量),而无需为多个变量赋值
State对象可被共享 如果State对象没有实例变量—即它们表示的状态完全以它们的类型来编码—那么各Context对象可以共享一个State对象。当状态以这种方式被共享时, 它们必然是没有内部状态, 只有行为的轻量级对象。
状态模式的缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
8.实现
我们用电梯的例子来说明:
简单地实现代码:
abstract class ILift {
//电梯的四个状态
const OPENING_STATE = 1; //门敞状态
const CLOSING_STATE = 2; //门闭状态
const RUNNING_STATE = 3; //运行状态
const STOPPING_STATE = 4; //停止状态;
//设置电梯的状态
public abstract function setState($state);
//首先电梯门开启动作
public abstract function open();
//电梯门有开启,那当然也就有关闭了
public abstract function close();
//电梯要能上能下,跑起来
public abstract function run();
//电梯还要能停下来,停不下来那就扯淡了
public abstract function stop();
}
/**
* 电梯的实现类
*/
class Lift extends ILift {
private $state;
public function setState($state) {
$this->state = $state;
}
//电梯门关闭
public function close() {
//电梯在什么状态下才能关闭
switch($this->state){
case ILift::OPENING_STATE: //如果是则可以关门,同时修改电梯状态
$this->setState(ILift::CLOSING_STATE);
break;
case ILift::CLOSING_STATE: //如果电梯就是关门状态,则什么都不做
//do nothing;
return ;
break;
case ILift::RUNNING_STATE: //如果是正在运行,门本来就是关闭的,也说明都不做
//do nothing;
return ;
break;
case ILift::STOPPING_STATE: //如果是停止状态,本也是关闭的,什么也不做
//do nothing;
return ;
break;
}
echo 'Lift colse
';
}
//电梯门开启
public function open() {
//电梯在什么状态才能开启
switch($this->state){
case ILift::OPENING_STATE: //如果已经在门敞状态,则什么都不做
//do nothing;
return ;
break;
case ILift::CLOSING_STATE: //如是电梯时关闭状态,则可以开启
$this->setState(ILift::OPENING_STATE);
break;
case ILift::RUNNING_STATE: //正在运行状态,则不能开门,什么都不做
//do nothing;
return ;
break;
case ILift::STOPPING_STATE: //停止状态,淡然要开门了
$this->setState(ILift::OPENING_STATE);
break;
}
echo 'Lift open
';
}
///电梯开始跑起来
public function run() {
switch($this->state){
case ILift::OPENING_STATE: //如果已经在门敞状态,则不你能运行,什么都不做
//do nothing;
return ;
break;
case ILift::CLOSING_STATE: //如是电梯时关闭状态,则可以运行
$this->setState(ILift::RUNNING_STATE);
break;
case ILift::RUNNING_STATE: //正在运行状态,则什么都不做
//do nothing;
return ;
break;
case ILift::STOPPING_STATE: //停止状态,可以运行
$this->setState(ILift::RUNNING_STATE);
}
echo 'Lift run
';
}
//电梯停止
public function stop() {
switch($this->state){
case ILift::OPENING_STATE: //如果已经在门敞状态,那肯定要先停下来的,什么都不做
//do nothing;
return ;
break;
case ILift::CLOSING_STATE: //如是电梯时关闭状态,则当然可以停止了
$this->setState(ILift::CLOSING_STATE);
break;
case ILift::RUNNING_STATE: //正在运行状态,有运行当然那也就有停止了
$this->setState(ILift::CLOSING_STATE);
break;
case ILift::STOPPING_STATE: //停止状态,什么都不做
//do nothing;
return ;
break;
}
echo 'Lift stop
';
}
}
$lift = new Lift();
//电梯的初始条件应该是停止状态
$lift->setState(ILift::STOPPING_STATE);
//首先是电梯门开启,人进去
$lift->open();
//然后电梯门关闭
$lift->close();
//再然后,电梯跑起来,向上或者向下
$lift->run();
//最后到达目的地,电梯挺下来
$lift->stop();
显然我们已经完成了我们的基本业务操作,但是,我们在程序中使用了大量的switch…case这样的判断(if…else也是一样),首先是程序的可阅读性很差,其次扩展非常不方便。一旦我们有新的状态加入的话,例如新加通电和断点状态。我们势必要在每个业务方法里边增加相应的case语句。也就是四个函数open,close,run,stop都需要修改相应case语句。
状态模式:把不同状态的操作分散到不同的状态对象里去完成。看看状态类的uml类图:
/**
*
* 定义一个电梯的接口
*/
abstract class LiftState{
//定义一个环境角色,也就是封装状态的变换引起的功能变化
protected $_context;
public function setContext(Context $context){
$this->_context = $context;
}
//首先电梯门开启动作
public abstract function open();
//电梯门有开启,那当然也就有关闭了
public abstract function close();
//电梯要能上能下,跑起来
public abstract function run();
//电梯还要能停下来,停不下来那就扯淡了
public abstract function stop();
}
/**
* 环境类:定义客户感兴趣的接口。维护一个ConcreteState子类的实例,这个实例定义当前状态。
*/
class Context {
//定义出所有的电梯状态
static $openningState = null;
static $closeingState = null;
static $runningState = null;
static $stoppingState = null;
public function __construct() {
self::$openningState = new OpenningState();
self::$closeingState = new ClosingState();
self::$runningState = new RunningState();
self::$stoppingState = new StoppingState();
}
//定一个当前电梯状态
private $_liftState;
public function getLiftState() {
return $this->_liftState;
}
public function setLiftState($liftState) {
$this->_liftState = $liftState;
//把当前的环境通知到各个实现类中
$this->_liftState->setContext($this);
}
public function open(){
$this->_liftState->open();
}
public function close(){
$this->_liftState->close();
}
public function run(){
$this->_liftState->run();
}
public function stop(){
$this->_liftState->stop();
}
}
/**
* 在电梯门开启的状态下能做什么事情
*/
class OpenningState extends LiftState {
/**
* 开启当然可以关闭了,我就想测试一下电梯门开关功能
*
*/
public function close() {
//状态修改
$this->_context->setLiftState(Context::$closeingState);
//动作委托为CloseState来执行
$this->_context->getLiftState()->close();
}
//打开电梯门
public function open() {
echo 'lift open...', '
';
}
//门开着电梯就想跑,这电梯,吓死你!
public function run() {
//do nothing;
}
//开门还不停止?
public function stop() {
//do nothing;
}
}
/**
* 电梯门关闭以后,电梯可以做哪些事情
*/
class ClosingState extends LiftState {
//电梯门关闭,这是关闭状态要实现的动作
public function close() {
echo 'lift close...', '
';
}
//电梯门关了再打开,逗你玩呢,那这个允许呀
public function open() {
$this->_context->setLiftState(Context::$openningState); //置为门敞状态
$this->_context->getLiftState()->open();
}
//电梯门关了就跑,这是再正常不过了
public function run() {
$this->_context->setLiftState(Context::$runningState); //设置为运行状态;
$this->_context->getLiftState()->run();
}
//电梯门关着,我就不按楼层
public function stop() {
$this->_context->setLiftState(Context::$stoppingState); //设置为停止状态;
$this->_context->getLiftState()->stop();
}
}
/**
* 电梯在运行状态下能做哪些动作
*/
class RunningState extends LiftState {
//电梯门关闭?这是肯定了
public function close() {
//do nothing
}
//运行的时候开电梯门?你疯了!电梯不会给你开的
public function open() {
//do nothing
}
//这是在运行状态下要实现的方法
public function run() {
echo 'lift run...', '
';
}
//这个事绝对是合理的,光运行不停止还有谁敢做这个电梯?!估计只有上帝了
public function stop() {
$this->_context->setLiftState(Context::$stoppingState); //环境设置为停止状态;
$this->_context->getLiftState()->stop();
}
}
/**
* 在停止状态下能做什么事情
*/
class StoppingState extends LiftState {
//停止状态关门?电梯门本来就是关着的!
public function close() {
//do nothing;
}
//停止状态,开门,那是要的!
public function open() {
$this->_context->setLiftState(Context::$openningState);
$this->_context->getLiftState()->open();
}
//停止状态再跑起来,正常的很
public function run() {
$this->_context->setLiftState(Context::$runningState);
$this->_context->getLiftState()->run();
}
//停止状态是怎么发生的呢?当然是停止方法执行了
public function stop() {
echo 'lift stop...', '
';
}
}
/**
* 模拟电梯的动作
*/
class Client {
public static function main() {
$context = new Context();
$context->setLiftState(new ClosingState());
$context->open();
$context->close();
$context->run();
$context->stop();
}
}
Client::main();
9.与其他相关模式
1)职责链模式,
职责链模式和状态模式都可以解决If分支语句过多,
从定义来看,状态模式是一个对象的内在状态发生改变(一个对象,相对比较稳定,处理完一个对象下一个对象的处理一般都已确定),
而职责链模式是多个对象之间的改变(多个对象之间的话,就会出现某个对象不存在的现在,就像我们举例的公司请假流程,经理可能不在公司情况),这也说明他们两个模式处理的情况不同。
这两个设计模式最大的区别就是状态模式是让各个状态对象自己知道其下一个处理的对象是谁。
而职责链模式中的各个对象并不指定其下一个处理的对象到底是谁,只有在客户端才设定。
用我们通俗的编程语言来说,就是
状态模式:
相当于If else if else;
设计路线:各个State类的内部实现(相当于If,else If内的条件)
执行时通过State调用Context方法来执行。
职责链模式:
相当于Swich case
设计路线:客户设定,每个子类(case)的参数是下一个子类(case)。
使用时,向链的第一个子类的执行方法传递参数就可以。
就像对设计模式的总结,有的人采用的是状态模式,从头到尾,提前一定定义好下一个处理的对象是谁,而我采用的是职责链模式,随时都有可能调整链的顺序。
10.总结与分析
状态模式的主要优点在于封装了转换规则,并枚举可能的状态,它将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为,还可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数;其缺点在于使用状态模式会增加系统类和对象的个数,且状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,对于可以切换状态的状态模式不满足“开闭原则”的要求。
回复关键字:
1、回复 “10” 查看 最有价值的10个spring boot开源项目
2、回复 “国旗” 获取国旗头像教程**
3、回复 “Ubuntu” 获取****100 个最佳 Ubuntu 应用 和 linux神器
4、回复 “idea” 获取**最新idea破解教程 和 装逼神奇
5、回复 “ssh” 获取史上最好的 ssh工具 支持mac
6、回复 “代金券” 免费获取腾讯云和阿里云代金券
推荐阅读:
MySQL优化-一篇文章就够了(转发加收藏吧)
Spring Boot最核心的27个注解,你了解多少?
程序员一般可以从什么平台接私活?
看完这14张思维导图,你的python才算入门
手把手讲解 OkHttp硬核知识点(1)
Python 爬取微信公众号文章和评论 (有源码)
Java 开发人员常用的服务配置(Nginx、Tomcat、JVM、Mysql、Redis)
腾讯电话面试总结—Linux运维工程师
python爬虫:(嘿嘿嘿)爬你喜欢的照片
面试官问我:一个 TCP 连接可以发多少个 HTTP 请求?我竟然回答不上来..
教你迅雷&百度非会员也能享受不限速的特权
Chrome开发者工具(DevTools)使用技巧
100个最有价值的开源项目--微信开发系列
IDEA 2019 最新激活教程
一台Linux服务器可以负载多少个连接?(底部有福利)
我的官网
我的官网http://guan2ye.com
我的CSDN地址http://blog.csdn.net/chenjianandiyi
我的地址http://www.jianshu.com/u/9b5d1921ce34
我的githubhttps://github.com/javanan
我的码云地址https://gitee.com/jamen/
阿里云优惠券与阿里云上云教程
** 个人微信公众号: dou_zhe_wan **
欢迎关注
免责声明:
1.本公众号所转载文章均来自公开网络。
2.如果出处标注有误或侵犯到原著作者权益,请联系删除。
3.转载本公众号中的文章请注明原文链接和作者,否则产生的任何版权纠纷均与本公众号无关。