首页 > 代码库 > 【设计模式总结】对常用设计模式的一些思考(未完待续。。。)

【设计模式总结】对常用设计模式的一些思考(未完待续。。。)

前言

在【Java设计模式】系列中,LZ写了十几篇关于设计模式的文章,大致是关于每种设计模式的作用、写法、优缺点、应用场景。

随着LZ自身的成长,再加上在工作中会从事一定的架构以及底层代码设计的原因,在近半年的实践中,对于设计模式的理解又有了新的认识,因此有了此文,目的是和网友朋友们分享自己对于设计模式的一些思考。LZ本人水平有限,抛砖引玉,写得不对的地方希望网友朋友们指正,也可留言相互讨论。

 

简单工厂模式

首先是简单工厂模式。

对于简单工厂模式的作用描述,LZ当时是这么写的:

原因很简单:解耦。

A对象如果要调用B对象,最简单的做法就是直接new一个B出来。这么做有一个问题,假如C类和B类实现了同一个接口/继承自同一个类,系统需要把B类修改成C类,程序不得不重写A类代码。如果程序中有100个地方new了B对象,那么就要修改100处。

这段话现在看来并不准确,因为使用简单工厂模式并没有办法完全解决这个问题。举个最简单的代码例子:

 1 public class ObjectFactory {
 2 
 3     public static Object getObject(int i) {
 4         if (i == 1) {
 5             return new Random();
 6         } else if (i == 2) {
 7             return Runtime.getRuntime();
 8         }
 9         
10         return null;
11     }
12     
13 }

调用方这么写:

 1 @Test
 2 public void testFactory() {
 3     System.out.println(ObjectFactory.getObject(1));
 4 }

假如原来代码中有100处地方,获取的是Random对象(即传入1),现在这100处我想用Runtime对象了(即传入2),即使使用了简单工厂模式,依然得修改100处,所以这个问题是没有办法避免的。

另外简单工厂模式自身还有一个小问题,就是如果工厂这边新增加了一种对象,那么工厂类必须同步新增if...else...分支,不过这个问题对于Java语言不难解决,只要定义好包路径,完全可以通过反射的方式获取到新增的对象而不需要修改工厂自身的代码。

说了半天,LZ觉得简单工厂模式的主要作用有两点:

(1)隐藏对象构造细节

(2)分离对象使用方与对象构造方,使得代码职责更明确,使得整体代码结构更优雅

先看一下第一点,举几个例子,比如JDK自带的构造不同的线程池,最终获取到的都是ExecutorService接口实现类:

1 @Test
2 public void testExecutors() {
3     ExecutorService es1 = Executors.newCachedThreadPool();
4     ExecutorService es2 = Executors.newFixedThreadPool(10);
5     ExecutorService es3 = Executors.newSingleThreadExecutor();
6     System.out.println(es1);
7     System.out.println(es2);
8     System.out.println(es3);
9 }

这个方法构造线程池是比较简单的,复杂的比如Spring构造一个Bean对象:

1 @Test
2 public void testSpring() {
3     ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");
4     Object obj = applicationContext.getBean(Object.class);
5     System.out.println(obj);
6         
7     applicationContext.close();
8     
9 }

中间流程非常长(有兴趣的可以看下我写的Spring源码分析的几篇文章),构造Bean的细节不需要也没有必要暴露给Spring使用者(当然那些想要研究框架源代码以便更好地使用框架的除外),使用者关心的只是调用工厂类的某个方法可以获取到想要的对象即可。

至于前面说的第二点,可以用设计模式六大原则的单一职责原则来理解:

单一职责原则(SRP):
1,SRP(Single Responsibilities Principle)的定义:就一个类而言,应该仅有一个引起它变化的原因。简而言之,就是功能要单一
2,如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其它职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏
3,软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离

把这段话加上我的理解就是:该使用的地方只关注使用,该构造对象的地方只关注构造对象,不需要把两段逻辑联系在一起,保持一个类或者一个方法100~200行左右的代码量,能描述清楚要做的一件事情即可

 

单例模式

第二点讲讲单例模式。

拿我比较喜欢的饿汉式单例模式的写法举例吧:

 1 public class Object {
 2 
 3     private static final Object instance = new Object();
 4     
 5     private Object() {
 6         
 7     }
 8     
 9     public static Object getInstance() {
10         return instance;
11     }
12     
13     public void functionA() {
14         
15     }
16     
17     public void functionB() {
18         
19     }
20     
21     public void functionC() {
22         
23     }
24     
25 }

然后我们调用的时候,会使用如下的方式调用functionA()、functionB()、functionC()三个方法:

1 @Test
2 public void testSingleton() {
3     Object.getInstance().functionA();
4     Object.getInstance().functionB();
5     Object.getInstance().functionC();
6 }

这么做是没有问题,使用单例模式可以保证Object类在对象池(也就是堆)中只被创建一次,节省了系统的开销。但是问题是:是否需要使用单例模式,为什么一定要把Object这个对象实例化出来?

意思是Java里面有static关键字,如果将functionA()、functionB()、functionC()都加上static关键字,那么调用方完全可以使用如下方式调用:

1 @Test
2 public void testSingleton() {
3     Object.functionA();
4     Object.functionB();
5     Object.functionC();
6 }

对象都不用实例化出来了,岂不是更加节省空间?

这个问题总结起来就到了使用static关键字调用方法和使用单例模式调用方法的区别上了,关于这两种做法有什么区别,我个人的看法是没什么区别。所谓区别,说到底,也就是两种,哪种消耗内存更少,哪种调用效率更高对吧,逐一看一下:

  • 从内存消耗上来看,真没什么区别,static方法也好,实例方法也好,都是占用一定的内存的,但这些方法都是类初始化的时候被加载,加载完毕被存储在方法区中
  • 从调用效率上来看,也没什么区别,方法最终在解析阶段被翻译为直接引用,存储在方法区中,然后调用方法的时候拿这个直接引用去调用方法(学过C、C++的可能会比较好理解这一点,这叫做函数指针,意思是每个方法在内存中都有一个地址,可以直接通过这个地址拿到方法的起始位置,然后开始调用方法)

所以,无论从内存消耗还是调用效率上,通过static调用方法和通过单例模式调用方法,都没多大区别,所以,我认为这种单例的写法,也是完全可以把所有的方法都直接写成静态的。使用单例模式,无非是更加符合面向对象(OO)的编程原则而已。

写代码这个事情,除了让代码更优雅、更简洁、更可维护、更可复用这些众所周知的之外,不就是图个简单吗,怎么写得简单怎么来,所以用哪种方式调用方法在我个人看来真的是纯粹看个人喜好,说一下我个人的原则:整个类代码比较少的,一两百行乃至更少的,使用static直接调方法,不实例化对象;整个类代码比较多的,逻辑比较复杂的,使用单例模式

毕竟,单例单例,这个对象还是存在的,那必然可以继承。整个类代码比较多的,其中有一个或者多个方法不符合我当前业务逻辑,没法继承,使用静态方法直接调用的话,得把整个类都复制一遍,然后改其中几个方法,相对麻烦;使用单例的话,其中有一个或者多个方法不符合我当前业务逻辑,直接继承一下改这几个方法就可以了。类代码比较少的类,反正复制黏贴改一下也无所谓。

 

模板模式

接着是模板模式,模板模式我本人并没有专门写过文章,因此这里网上找了一篇我认为把模板模式讲清楚的文章。

对于一个架构师、CTO,反正只要涉及到写底层代码的程序员而言,模板模式都是非常重要的。模板模式简单说就是代码设计人员定义好整个代码处理流程,将变化的地方抽象出来,交给子类去实现。根据我自己的经验,模板模式的使用,对于代码设计人员来说有两个难点:

(1)主流程必须定义得足够宽松,保证子类有足够的空间去扩展

(2)主流程必须定义得足够严谨,保证抽离出来的部分都是关键的部分

这两点看似有点矛盾,其实是不矛盾的。第一点是站在扩展性的角度而言,第二点是站在业务角度而言的。假如有这么一段模板代码:

 1 public abstract class Template {
 2 
 3     protected abstract void a();
 4     protected abstract void b();
 5     protected abstract void c();
 6     
 7     public void process(int i, int j) {
 8         if (i == 1 || i == 2 || i == 3) {
 9             a();
10         } else if (i == 4 || i == 4 || i == 5) {
11             if (j > 1) {
12                 b();
13             } else {
14                 a();
15             }
16         } else if (i == 6) {
17             if (j < 10) {
18                 c();
19             } else {
20                 b();
21             }
22         } else {
23             c();
24         }
25     }
26     
27 }

我不知道这段代码例子举得妥当不妥当,但我想说说我想表达的意思:这段模板代码定义得足够严谨,但是缺乏扩展性。因为我认为在抽象方法前后加太多的业务逻辑,比如太多的条件、太多的循环,会很容易将一些需要抽象让子类自己去实现的逻辑放在公共逻辑里面,这样会导致两个问题:

(1)抽象部分细分太厉害,导致扩展性降低,子类只能按照定义死的逻辑去写,比如a()方法中有一些值需要在c()方法中使用就只能通过ThreadLocal或者某些公共类去实现,反而增加了代码的难度

(2)子类发现该抽象的部分被放到公共逻辑里面去了,无法完成代码要求

最后提一点,我认为模板模式对梳理代码思路是非常有用的。因为模板模式的核心是抽象,因此在遇到比较复杂的业务流程的时候,不妨尝试一下使用模板模式,对核心部分进行抽象,以梳理逻辑,也是一种不错的思路,至少我用这种方法写出过一版比较复杂的代码。

 

策略模式

策略模式,一种可以认为和模板模式有一点点像的设计模式,至于策略模式和模板模式之间的区别,后面视篇幅再聊。

策略模式其实比较简单,但是在使用中我有一点点的新认识,举个例子吧:

 1 public void functionA() {
 2     // 一段逻辑,100行
 3 
 4     System.out.println();
 5     System.out.println();
 6     System.out.println();
 7     System.out.println();
 8     System.out.println();
 9     System.out.println();   
10 }

一个很正常的方法funtionA(),里面有段很长(就假设是这里的100行的代码),以后改代码的时候发现这100行代码写得有点问题,这时候怎么办,有两种做法:

(1)直接删除这100行代码。但是直接删除的话,有可能后来写代码的人想查看以前写的代码,怎么办?肯定有人提出用版本管理工具SVN、Git啊,不都可以查看代码历史记录吗?但是,一来这样比较麻烦每次都要查看代码历史记录,二来如果当时的网络不好无法查看代码历史记录呢?

(2)直接注释这100行代码,在下面写新的逻辑。这样的话,可以是可以查看以前的代码了,但是长长的百行注释放在那边,非常影响代码的可读性,非常不推荐

这个时候,就推荐使用策略模式了,这100行逻辑完全可以抽象为一段策略,所有策略的实现放在一个package下,这样既把原有的代码保留了下来,可以在同一个package下方便地查看,又可以根据需求更换策略,非常方便。

应网友朋友要求,补充一下代码,这样的,functionA()可以这么改,首先定义一段抽象策略:

1 package org.xrq.test.design.strategy;
2 
3 public interface Strategy {
4 
5     public void opt();
6     
7 }

然后定义一个策略A:

 1 package org.xrq.test.design.strategy.impl;
 2 
 3 import org.xrq.test.design.strategy.Strategy;
 4 
 5 public class StrategyA implements Strategy {
 6 
 7     @Override
 8     public void opt() {
 9         
10     }
11     
12 }

用的时候这么使用:

 1 public class UseStrategy {
 2 
 3     private Strategy strategy;
 4     
 5     public UseStrategy(Strategy strategy) {
 6         this.strategy= strategy; 
 7     }
 8     
 9     public void function() {
10         strategy.opt();
11         
12         System.out.println();
13         System.out.println();
14         System.out.println();
15         System.out.println();
16         System.out.println();
17         System.out.println();
18     }
19     
20 }

使用UseStrategy类的时候,只要在构造函数中传入new StrategyA()即可。此时,如果要换策略,可以在同一个package下定义一个策略B:

 1 package org.xrq.test.design.strategy.impl;
 2 
 3 import org.xrq.test.design.strategy.Strategy;
 4 
 5 public class StrategyB implements Strategy {
 6 
 7     @Override
 8     public void opt() {
 9         
10     }
11     
12 }

使用使用UseStrategy类的时候,需要更换策略,可在构造函数中传入new StrategyB()。这样一种写法,就达到了我说的目的:

1、代码的实现更加优雅,调用方只需要传入不同的Strategy接口的实现即可

2、原有的代码被保留了下来,因为所有的策略都放在同一个package下,可以方便地查看原来的代码是怎么写的

 

【设计模式总结】对常用设计模式的一些思考(未完待续。。。)