从看懂到看开——初探设计模式
本文档主要介绍了四种设计模式:单例模式、责任链模式、装饰模式和代理模式。
为什么选择这四种模式?因为设计模式是一种很玄的东西。作为初探,越看越迷糊,只能选择一些不那么迷糊的模式进行介绍,就当做抛砖引玉吧。
撰写本文档主要参考了《设计模式之禅》这本书,如有错漏还请告知。
1.单例模式
顾名思义,只有一个实例,而且自行实例化并向整个系统提供这个实例。
Ensure a class has only one instance, and provide a global point of access to it.
典型示例
首先是单例类。
public class SingleObject {
private static SingleObject instance = new SingleObject();
private SingleObject(){}
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
然后是场景类。
public class SingletonPatternDemo {
public static void main(String[] args) {
SingleObject object = SingleObject.getInstance();
object.showMessage();
}
}
优缺点分析
下面两段摘书,单例模式比较简单,有所了解可以跳过。
优点:
1.单例模式只有一个实例,减少了内存的开销,尤其是当一个对象需要频繁地创建、销毁且创建或销毁的性能又无法优化时,单例模式的优势就非常明显。
2.由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决。
3.单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
4.单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
缺点:
1.没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
2.单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock
的方式虚拟一个对象。
3.单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。
小结:
单例模式就像双刃剑,优点是只有一个,缺点也是只有一个。
使用情景
1.要求生成唯一序列号的环境;
2.在整个项目中需要一个共享访问点或共享数据,例如spring configuration
或一个Web
页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;
3.创建一个对象需要消耗的资源过多,如要访问IO
和数据库等资源;
4.需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式。
结合项目使用
MES的流程表单中有一个属性类型是自动生成的编码,那生成编码就可以做成单例的工具类。
2.责任链模式
使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.Chain the receiving objects and pass the request along the chain until an object handles it.
典型示例
目前的项目中都引入了log4j
记录一些运行时信息,运行时信息被分成了不同的等级,比如error
、info
、debug
等,此处便以此为蓝本,简化出一个日志记录器来描述责任链模式。
首先定义处理对象的基类和三种日志类型。
public abstract class AbstractLogger {
public static int INFO = 1;
public static int DEBUG = 2;
public static int ERROR = 3;
protected int level;
protected AbstractLogger nextLogger;
public void setNextLogger(AbstractLogger nextLogger){
this.nextLogger = nextLogger;
}
public void logMessage(int level, String message){
if(this.level <= level){
write(message);
}
if(nextLogger !=null){
nextLogger.logMessage(level, message);
}
}
abstract protected void write(String message);
}
接下来是三种类型的实现类。
public class ConsoleLogger extends AbstractLogger {
public ConsoleLogger(int level){
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("Standard Console::Logger: " + message);
}
}
public class ErrorLogger extends AbstractLogger {
public ErrorLogger(int level){
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("Error Console::Logger: " + message);
}
}
public class FileLogger extends AbstractLogger {
public FileLogger(int level){
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("File::Logger: " + message);
}
}
最后是场景类。
public class ChainPatternDemo {
private static AbstractLogger getChainOfLoggers(){
AbstractLogger errorLogger = new ErrorLogger(AbstractLogger.ERROR);
AbstractLogger fileLogger = new FileLogger(AbstractLogger.DEBUG);
AbstractLogger consoleLogger = new ConsoleLogger(AbstractLogger.INFO);
errorLogger.setNextLogger(fileLogger);
fileLogger.setNextLogger(consoleLogger);
return errorLogger;
}
public static void main(String[] args) {
AbstractLogger loggerChain = getChainOfLoggers();
loggerChain.logMessage(AbstractLogger.INFO,
"This is an information.");
loggerChain.logMessage(AbstractLogger.DEBUG,
"This is an debug level information.");
loggerChain.logMessage(AbstractLogger.ERROR,
"This is an error information.");
}
}
优缺点分析
优点:
1.将请求和处理解耦,请求者不必关心是谁处理的,处理者也不必对请求者知根知底。
2.灵活,可以动态调配处理的顺序,也很容易对处理类进行扩展。
缺点:
在链很长的时候性能堪忧,也不方便调试,因此要注意控制链的长度。
小结:
责任链模式看起来有点像js
中的冒泡事件,也有点像一个膨胀的switch
,case
的数量不受限制。
使用情景
从定义中的请求、处理、返回结果这些词,很难不让人联想到Servlet
中的FilterChain
。比起这种从设计最初就比较容易想到的场景,作为补救措施出现的可能性也比较高,比如需求变更导致的同质业务增加。
结合项目使用
维修平台项目中,注册环节,制造商管理员和普通用户需要填写的内容不同,处理也稍有不同。可以通过责任链模式进行处理,注册统一接口,设置下一个处理对象,依次判断处理。
设置下一个处理对象的模式也不一定要通过显式的方式,也许可以通过链表结构来实现。
3.装饰模式
动态地给一个对象添加一些额外的职责,装饰模式比继承更灵活。
Attach additional responsibilities to an object dynamically keeping the same interface.Decorators provide a flexible alternative to subclassing for extending functionality.
典型示例
装饰模式的结构稍微复杂一点,先上类图。
-
component
是一个接口,也就是最原始的对象。 -
concrete component
是component
的具体实现。 -
decorator
是装饰器。 -
concrete decorator
是装饰器的具体实现。
下面以画图为例,描述装饰模式,首先是对象。
public interface Shape {
void draw();
}
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Shape: Rectangle");
}
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Shape: Circle");
}
}
然后是装饰器。
public abstract class ShapeDecorator implements Shape {
protected Shape decoratedShape;
public ShapeDecorator(Shape decoratedShape){
this.decoratedShape = decoratedShape;
}
public void draw(){
decoratedShape.draw();
}
}
public class RedShapeDecorator extends ShapeDecorator {
public RedShapeDecorator(Shape decoratedShape) {
super(decoratedShape);
}
@Override
public void draw() {
decoratedShape.draw();
setRedBorder(decoratedShape);
}
private void setRedBorder(Shape decoratedShape){
System.out.println("Border Color: Red");
}
}
最后是场景类。
public class DecoratorPatternDemo {
public static void main(String[] args) {
Shape circle = new Circle();
Shape redCircle = new RedShapeDecorator(new Circle());
Shape redRectangle = new RedShapeDecorator(new Rectangle());
System.out.println("Circle with normal border");
circle.draw();
System.out.println("\nCircle of red border");
redCircle.draw();
System.out.println("\nRectangle of red border");
redRectangle.draw();
}
}
优缺点分析
优点:
对象和装饰器解耦,装饰器可以动态扩展,比较灵活。
缺点:
多层装饰是复杂的。因此要尽量减少装饰类的数量,以便降低系统的复杂度。
小结:
1.装饰模式是继承的一种替代方案,子类无限扩充是灾难,装饰太多也是。
2.继承是静态的,装饰是动态的。在某些情况下,装饰更加易于维护。
使用情景
就像定义提到的,继承可以使用的地方,都可以考虑是否装饰模式更合适。
比如需要动态添加或撤销功能的时候,如果是在继承关系中要拆掉中间层的封装,不改动对象基本上不可能,而装饰器就比较灵活,在场景类中去掉就可以。
类似模式对比
参见第四小节代理模式的模式对比。
结合项目使用
也许表单属性定义的继承实现可以用装饰模式替代。
4.代理模式(委托模式)
为其他对象提供一种代理以控制对这个对象的访问。
Provide a surrogate or placeholder for another object to control access to it.
典型示例
代理模式的结构稍微复杂一点,先上类图。
-
Subject
无特殊要求,就是对象需要实现的功能。 -
RealSubject
是实现了Subject
接口。 -
Proxy
是代理类,在代理类中可进行预处理和善后工作。
下面以图片加载为例,描述代理模式,首先是加载图片的接口和实现。
public interface Image {
void display();
}
public class RealImage implements Image {
private String fileName;
public RealImage(String fileName){
this.fileName = fileName;
loadFromDisk(fileName);
}
@Override
public void display() {
System.out.println("Displaying " + fileName);
}
private void loadFromDisk(String fileName){
System.out.println("Loading " + fileName);
}
}
然后是代理类,代理类也要实现功能。
public class ProxyImage implements Image{
private RealImage realImage;
private String fileName;
public ProxyImage(String fileName){
this.fileName = fileName;
}
@Override
public void display() {
if(realImage == null){
realImage = new RealImage(fileName);
}
realImage.display();
}
}
最后是场景类。
public class ProxyPatternDemo {
public static void main(String[] args) {
Image image = new ProxyImage("test_10mb.jpg");
image.display();
System.out.println("");
image.display();
}
}
第一次需要从磁盘加载,第二次就不需要了。(这个实例没有书中代练游戏的场景那么生动,有兴趣的可以看书。)
优缺点分析
优点:
1.职责清晰
真实的实现类就负责主要功能的实现,代理类则处理一下辅助功能。比如打游戏是主要功能,而结算游戏升级费用就是辅助功能。
2.高扩展性
真实的实现类在实际场景下难免发生变更,但是只要接口没有变更,代理类就无需修改。
3.智能化
在上一小结的例子中看不出来智能化在哪里,这一特点具体体现在“动态代理”的实现中。本文档对代理模式的扩展不做赘述,感兴趣的可以看书。
缺点:
有些类型的代理模式可能会造成请求的处理速度变慢;有些代理模式的实现非常复杂。
小结:
1.代理模式就是对目标方法进行拦截(控制),然后在目标方法的前后加塞其他功能(增强)。
2.代理模式有诸多扩展,看书的体会就是既视感很强,spring到底用了多少模式。
使用情景
1、远程代理。 2、虚拟代理。 3、Copy-on-Write
代理。 4、保护代理。 5、Cache
代理。 6、防火墙代理。 7、同步化代理。 8、智能引用代理。
目前项目中已经用到的比如Spring AOP
,利用切面做消息通知。
类似模式对比
与第三小节的装饰模式进行对比,两者的概念其实有些类似,但是从类图中可以看到,代理模式更加强势,它决定了实际对象执行或者不执行,装饰器就只是增强,代理更多的是控制。
结合项目使用
利用现有的AOP做日志或消息通知。
5.小结
coding不易,架构更不易,且行且珍惜。