谈谈有关设计模式的思想精髓:变继承关系为组合关系如何创建对象(单例状态装饰者模式)
Posted 鸽一门
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了谈谈有关设计模式的思想精髓:变继承关系为组合关系如何创建对象(单例状态装饰者模式)相关的知识,希望对你有一定的参考价值。
说起设计模式,最广为人知的就是“四人帮”编写的设计模式,其书的副标题为软件中可重用的元素,此书本来是一篇博士论文,它将很多通用的设计思想总结并命名成设计模式,希望开发人员之间通过专有的模式名称交流,可惜最为熟悉的是被滥用最多的Singleton单例模式,但是此书中有许多值得学习的思想。
从当今角度看此书,其中一部分模式是教我们如何对现实的事物去建模,这是“设计”。而有一部分则是受到了“语言限制”。由于此书是用C++编写的,后期Java在此基础上编写,两者都有语言带来的限制,因此衍生了一些为消除语言限制的模式,例如“Visit Pattern”。随着设计模式的发展,除了此书中代码实现的模式,即软件的代码组织结构之外,还增加了一些软件架构层面上的模式,例如并发模式、架构模式。
此篇博文不去详细介绍模式的定义、代码实现等基础内容,而是从设计思想层面来探索,进而结合部分设计模式详细讲解,涉及到的知识点如下:
- 变继承关系为组合关系
- 如何创建对象
- 单例模式
- 状态模式
- 装饰者模式
一.再谈Singleton单例模式
通常谈到设计模式,大家第一就会想到Singleton单例模式,但是它其实是被滥用最多的,从它本身的设计而言,并不希望它是最常用的模式,但由于其简单易理解,所以被开发人员滥用,模式使用还是应当结合实际。
1. Singleton优缺点
- 确保全局至多只有一个对象。
- 主要用于:构造缓慢的对象;需要统一管理的资源。
- 缺点:很多全局状态;线程安全性。
2. Singleton的创建
如何来保证这个类在全局当中只有一个对象,一般是通过一个变量去保存,判断此变量是否为空,为空则创建,否则直接获取其引用,可大致分为以下三个方法:
- 双重锁模式 Double Checked Locking
当考虑到多线程情况时,需要用到双重锁模式,也就是Check两次,第一次检查全局变量是否为null,若为空不可直接创建,因为其他线程可能也在创建,所以需要锁住对象,再Check一次,还是null再次创建。
但是!并不建议这种双重锁模式,最好的方法还是利用系统特性,例如Java语言中的静态变量。
-
作为Java类的静态变量
静态变量属于类,只此一份,将对象存储在静态变量中。缺点:在程序初始化的时候就要创建这个变量,一旦运行之后就无法控制其创建,还可能增加初始化时间。 -
使用框架提供的能力
基于以上缺点,还有一种方法就是使用框架提供的能力,通常系统中在一个依赖注入框架中,它们有提供Singleton对象的能力。
二. 状态模式-----变继承关系为组合关系
以上Singleton单例模式并不是设计模式中的重点,只是因为其简单易理解所以被广泛应用,从此点开始重点在于设计模式的思想精髓,先介绍变继承关系为组合关系。
1. 继承关系再认识
(1)描述“is-a”关系
回顾一下继承关系的定义,不仅可以复用对基类的成员变量或者函数,还可以增加或修改基类的行为。这个定义放到实际应用中是比较含糊的。这种定偏向于结构化,子类可以增加或修改基类的行为,这种特性对基类的结构是有非常高的要求。
什么是什么,一般不是从结构的角度来判断,大部分是从功能的角度来判断,例如现实生活中交通工具的定义?可以载人或载物,因此汽车、三轮等等就是交通工具,而不是说有发动机或轮子的就是交通工具。因此有了这种惯性思维后,以继承关系去建模会遇到很多局限性。
(2)不要用继承关系来实现复用
事实上希望类中的变量限制范围是public 或是 private,实际中protected也被广泛应用,即基类的成员变量或函数只得子类使用,以这种方式来实现复用更为简单?其实不然,采取的方法便是使用设计模式来实现复用,后续会详细讲解。
2. 如果Employee升级成了Manager?
这是google面试过的一个问题,在学习Java语言继承特性时,有一个常见的代码示例,即Employee(基类)、Manager(派生类),如果Employee升级成了Manager?,代码方面应该怎么做?
(只贴重点类代码,所以代码在文章末尾链接处)
public class Employee
public static List<Employee> allEmployees;
private final String name;
private final int salary;
public Employee(String name, int salary)
this.name = name;
this.salary = salary;
public Employee(String name)
this(name, 0);
public void doWork()
public void getPaid(BankEndPoint bank)
bank.payment(name, salary);
// Package private for logic in the package to control
// when employees are loaded.
static void loadAllEmployees()
// Loads all employees from database.
@Override
public String toString()
return "Employee [name=" + name
+ ", salary=" + salary + "]";
public String getName()
return name;
public int getSalary()
return salary;
public class Manager extends Employee
private final List<Employee> reporters;
public Manager(String name, int salary,
List<Employee> reporters)
super(name, salary);
this.reporters = Collections.unmodifiableList(
new ArrayList<>(reporters));
@Override
public void getPaid(BankEndPoint bank)
super.getPaid(bank);
getStocks();
@Override
public void doWork()
Employee worker = selectReporter();
worker.doWork();
@Override
public String toString()
return "Manager [name=" + getName()
+ ", salary=" + getSalary() + "]";
private Employee selectReporter()
loadReporters();
return null;
private void getStocks()
private void loadReporters()
reporters.clear();
reporters.add(new Employee("John", 10000));
reporters.add(new Employee("Mary", 20000));
以上就是常见的“Employee为Manager父类”的代码示例,代码并不复杂,查看即可理解。也许你认为Employee和Manager的继承关系并无差错,不错,Manager is an Employee,从构成的角度也符合。但是,思考以上提出的问题,一旦**Employee升级成了Manager **怎么办?
例如以下代码Tester 类中对象employee1 ,它创建之后如何升级成一个Manager ?没有办法!但是在实际处理中这是一个常见的需求,而且在Employee中将成员变量name、salary设置成final 了,更不用谈升级了,除非重新去创建,把原来的引用换成新的引用。
public class Tester
public static void main(String[] args)
Employee employee1 = new Employee("Mary", 20000);
employee1 = new Manager("Mary",40000);
你也许认为Employee升级成了Manager后很多成员变量、行为改变了,所以不再使用以前的对象, 一定要新建一个换掉原来引用。在这个场景下此答案可以接受,但它并不是问题本身想问的。
**一个对象要改变,需要改变其状态。**从实际来考虑,当Employee升级成了Manager后,人还是这个人,只是职能发生了改变,以此来建模。
3. 状态模式解决问题
查看上图,Employee中的doWork()
函数保留,只是其中的role变化了,从一开始Engineer的工作方式改变为Manager的工作方式,从代码的角度解释就是说:
- 当role是Engineer时,调用属于Engineer的
doWork()
。 - 当role是Manager时,调用属于Manager的
doWork()
。
由此你可以再次意思到“is-a”关系的定义含糊,导致滥用继承关系,以前一直以来的固化思维“Employee是Manager的父类”,其实是滥用,实际上应该去理解真实事物到底是什么样关系,建模的结果如上所示。
下面是代码修改过程:
(1)Role接口
首先定义一个Role接口,其中只有一个方法doWork()
,不同的角色实现不同的工作内容。
public interface Role
void doWork();
(2)Engineer实现Role接口
public class Engineer implements Role
@Override
public void doWork()
System.out.println("Doing engineer work.");
@Override
public String toString()
return "Engineer";
(3)Manager实现Role接口
public class Manager implements Role
private final List<Employee> reporters;
public Manager(List<Employee> reporters)
this.reporters = Collections.unmodifiableList(
new ArrayList<>(reporters));
@Override
public void doWork()
System.out.println("Dispatching work");
Employee worker = selectReporter();
worker.doWork();
@Override
public String toString()
return "Manager";
......
(4)Employee添加成员变量Role
public class Employee
private final String name;
private final int salary;
private Role role; //成员变量
public Employee(String name, int salary, Role role)
this.name = name;
this.salary = salary;
this.role = role; //构造方法中需要传入
public void doWork()
role.doWork(); //不同角色处理不同工作
.....
(5)测试
我们在测试类中创建了两个Employee 雇员,后来将其中一个雇员升级为Manager,再次打印调用它们工作函数后的结果。
public class Tester
public static void main(String[] args)
Employee employee1 = new Employee("John", 10000,
new Engineer());
Employee employee2 = new Employee("Mary", 20000,
new Engineer());
.....
employee2.setRole(new Manager(Arrays.asList(employee1)));
.....
employee1.doWork();
.....
employee2.doWork();
结果截图
由此图可知,Mary由一开始的Employee升级成了Manager,此问题完美被解决,主要解决思路采用了“状态模式”,变继承关系为组合关系,同时意识到了“is-a”继承关系的滥用问题,应当根据实际事务需求具体分析!
三. 装饰器模式------变继承关系重点内容为组合关系
1. 提出问题
Java中有一个接口Runnable,其中方法就是run()
,它其实就是一个任务,将需要做的事情封装在run()
方法中,此类便成为一个task,可被调度。此部分对于学习过Java的开发人员是再基础不过的,那么思考一个问题:
假设现在有一个类LoggingRunnable用来记录任务开始结束的时间。将任务包装在Tansaction里,在开始时beginTansaction,才会开始执行run()
方法,结束时commit,有异常是roll back,这就是TansactionalRunnable。那么如何实现LoggingRunnable、TansactionalRunnable?
2. 有缺陷的实现方法
此部分将介绍一个最容易想到“使用继承”思想的实现方法,注意此方法是有缺陷的,正确方法将在后续介绍。
(1)CodingTask
首先是一个简单的CodingTask类,用于模拟工作任务,其run()
中线程睡眠5秒用来模拟正在工作。
public class CodingTask implements Runnable
@Override
public void run()
System.out.println("Writing code.");
try
Thread.sleep(5000);
catch (InterruptedException e)
throw new RuntimeException(e);
(2)LoggingRunnable
LoggingRunnable用来记录任务开始结束的时间,所以在其run()
中记录时间开始及结束,那么真正的任务执行函数被抽象出来为doRun()
,按照大家最熟知的操作,将它设为抽象方法,同时将此类设为抽象类,这样继承该类时需要实现该抽象方法,实现doRun()
中真正需要做的事。
以上“继承”思想在平时开发中最为常见,但并非适用于每个场景,关于此缺陷后续会讲解。代码如下:
public abstact class LoggingRunnable implements Runnable
protected abstract void doRun();
@Override
public void run()
long startTime = System.currentTimeMillis();
System.out.println("Task started at "
+ startTime);
doRun();
System.out.println("Task finished. Elapsed time: "
+ (System.currentTimeMillis() - startTime));
(3)修改CodingTask
在完成以上LoggingRunnable抽象类设计后,需知CodingTask不再是实现Runnable,而是继承LoggingRunnable,实现doRun
。
public class CodingTask implements LoggingRunnable
......
@Override
public void doRun()
.....
(4)Tester测试类
public class Tester
public static void main(String[] args)
new CodingTask().run();
结果
分析
结果也没什么问题,为何一直声称此种实现方式不太好呢?
注意CodingTask修改之前和之后的对比,职责未变,始终是用来代表工作的执行,但是修改之后,CodingTask默认加了一个前提,那就是每次调度CodingTask执行都会打印日志。
(5)TansactionalRunnable
不止如此,我们还有一个类TansactionalRunnable,它是用来管理Tansactiona,同样那它应该也有一个抽象的doRun()
方法,继续按照之前“继承”的思想去设计此类,后续你将发现问题。
public class TransactionalRunnable implements Runnable
protected abstract void doRun();
@Override
public void run()
boolean shouldRollback = false;
try
beginTransaction();
doRun();
catch (Exception e)
shouldRollback = true;
throw e;
finally
if (shouldRollback)
rollback();
else
commit();
private void commit()
System.out.println("commit");
private void rollback()
System.out.println("rollback");
private void beginTransaction()
System.out.println("beginTransaction");
(6)再度修改CodingTask
现在需要管理CodingTask,即完成时commit,有异常时roll back,实现这些只需要继承TransactionalRunnable 即可!
灾难性的问题已经出现!现在想要记录打印CodingTask,并且管理CodingTask,那么需要使CodingTask继承LoggingRunnable和TransactionalRunnable!
这下总该知道问题所在了吧,Java语言中不支持多继承,也许你还在认为这种设计思想没问题只是语言的限制,例如C++就可以实现多继承,但是两个父类中的抽象方法同名!该如何分辨?因此后续发展的语言中并不支持多继承的机制。
###3. “组合”思想实现方法
以上实现方式采用的是开发人员最为熟知的“继承”思想,但是这一路实现发现其总多弊端,接下来转换实现思想:变继承关系为组合关系,采用Decorator Pattern 装饰模式来实现。
(1)CodingTask
作为一个可复用的系统需要注重低耦合度,之前的CodingTask绑在了LoggingRunnable或TransactionalRunnable上,这是非常不灵活的,保持单一原则,CodingTask只做自己的事,那就是写代码(模拟),至于需不需要打印log、管理与否与它没有任何关系!
public class CodingTask implements Runnable
@Override
public void run()
System.out.println("Writing code.");
try
Thread.sleep(5000);
catch (InterruptedException e)
throw new RuntimeException(e);
(2)LoggingRunnable
LoggingRunnable也是一个Runnable,但是它内部又有一个runnable,与这个CodingTask进行连接,即执行LoggingRunnable实际上是在执行内部中真正的runnable,并且此runnable是可以配置的。这样CodingTask其实与LoggingRunnable没有任何关系,保持了系统的低耦合度!
如上图,以new LoggingRunnable(new CodingTask().run())
这种组合的方式来调用。
类中的成员应该是private或者public,当看到protected限制范围时,需自省使用它的原由,是否合理必须!
public class LoggingRunnable implements Runnable
private final Runnable innerRunnable;
public LoggingRunnable(Runnable innerRunnable)
this.innerRunnable = innerRunnable;//构造的时候去配置内部真正执行的runnable
@Override
public void run()
long startTime = System.currentTimeMillis();
System.out.println("Task started at "
+ startTime);
innerRunnable.run();
System.out.println("Task finished. Elapsed time: "
+ (System.currentTimeMillis() - startTime));
(3)TransactionalRunnable
和LoggingRunnable 同理,TransactionalRunnable也做出相应的改变:
public class TransactionalRunnable implements Runnable
private final Runnable innerRunnable;
public TransactionalRunnable(Runnable innerRunnable)
this.innerRunnable = innerRunnable;
@Override
public void run()
boolean shouldRollback = false;
try
beginTransaction();
innerRunnable.run();
catch (Exception e)
shouldRollback = true;
throw e;
finally
if (shouldRollback)
rollback();
else
commit();
private void commit()
System.out.println("commit");
private void rollback()
System.out.println("rollback");
private void beginTransaction()
System.out.println("beginTransaction");
完成以上代码之后,再次强调一下重点:保持低耦合度和单一原则,CodingTask只是做一个任务:模拟写代码,与其他类毫无关联,并且以组合的方式同LoggingRunnable或TransactionalRunnable结合,注意此处LoggingRunnable或TransactionalRunnable内部中是以配置的方式来决定内部真正的runnable
(4)Tester测试类
- 执行CodingTask,单纯写代码
new CodingTask().run();
结果
执行正常,写了5秒代码停止。
- 执行CodingTask,同时需要记录时间
new LoggingRunnable(
new CodingTask(0)).run();
结果
执行正常,写了5秒代码停止,并且在“写代码”开始结束的时候打印日志。
- 执行CodingTask,同时需要记录时间,还要Transaction,再套一层
new TransactionalRunnable(
new LoggingRunnable(
new CodingTask())).run();
结果
执行正常,写了5秒代码停止,并且在“写代码”开始结束的时候打印日志,同时打印了commit,但是以上代码还是有点问题,那就是打印时间的日志位置应当在开始和末尾,所以我们需要对测试的代码进行一些改动,先开始记录,再开始Transaction:
new LoggingRunnable(
new TransactionalRunnable(
new CodingTask(0))).run();
结果
总结
使用装饰模式后,使得这几个类之间可以互相组合使用,还可以套更多的层,使用Transaction的逻辑去装饰CodingTask,用Logging的逻辑再去装饰Transaction。
CodingTask是一个实际的task,真正做事的runnable,而Logging或Transaction是一个装饰,内部会放置真正的runnable,它执行实际上是在执行内部的runnable,这样便可在此基础上根据特定的需求完成逻辑。
四. 如何创建对象
此部分将讲解设计模式中另一个重要思想,即如何创建对象。
1. 使用new 创建的缺点
第一反应创建对象不是一件很简单么?直接 new 就行了,但是殊不知其中的隐患,使用new 来创建的缺点:
- 编译时必须决定创建哪个类的对象
- 参数意义不明确
new LoggingRunnable(
new TransactionalRunnable(
new CodingTask(0))).run();
结合以上代码示例来理解第一个缺点,即写代码时就要决定创建哪些类对象,在上例中,CodingTask、TransactionalRunnable、LoggingRunnable是在写代码的时候就要决定的,但是并不一定是这样!现在只需要知道run一个task,至于需不需要Transaction或者Logging不是现在需要决定的。
第二个缺点很常见,若直接查看创建new语句,很难直接根据参数理解其具体意义,需要我们更进一步查看其类实现,事实上足够优秀的代码可以做到见参数知其意。
接下来介绍两个方法解决此问题。
###2. Abstact Factory Pattern
task = new LoggingTask(new CodingTask());
task = taskFactory.createCodingTask();
比较以上两种实现方式,第一种很常见,但是其绑定了LoggingTask、CodingTask。
第二种只绑定了taskFactory,它是一个抽象的接口,可以createCodingTask,没有绑定任何类,其含义也很直接,获取CodingTask,其内部是如何实现也不需要知道,因此此方式完全杜绝了“编译时必须决定创建哪个类的对象”的缺点。
###3. Builder Pattern
以上抽象工厂模式方法解决了第一个问题“编译时必须决定创建哪个类的对象”,而第二个缺点“参数意义不明确”可根据Builder模式解决。首先查看比较以下两种实现方式:
employee = new Employee(
oldEmployee.getName(), 15000);
employee = Employee.fromExisting(oldEmployee)
.withSalary(15000)
.build();
同样第一种方式是最常见的,但是这一句创建代码根本无法理解其参数意义,除非进一步查看其类的构造函数实现。
而第二种方式就是采用Builder模式实现对象创建,其实相信大家对Builder模式并不模式,它采用一种链式方法来构造对象中的成员,非常适合成员变量多的类构造,可动态选择创建参数。
想要更仔细理解Builder模式可查看Builder模式 详解及学习使用
以上只谈论了有关设计模式的一部分思想,其中着重谈论了“变继承关系为组合关系”的思想,继承关系确实是Java语言的一大特性,但是这并不意味着可以滥用继承关系,虽然继承可以复用对基类的成员变量或者函数,还可以增加或修改基类的行为,但它同时也有其限制性,这之间的利弊需要开发者自行衡量。但是还有别的方法,转换思维,学会变继承关系为组合关系,会让整个系统的耦合度更低,这也是设计模式的精髓所在。
若有错误,虚心指教~
以上是关于谈谈有关设计模式的思想精髓:变继承关系为组合关系如何创建对象(单例状态装饰者模式)的主要内容,如果未能解决你的问题,请参考以下文章