|
1 | | -# 策略模式——独一无二的对象 |
| 1 | +# 策略模式——略施小计就彻底消除了多重 if else |
2 | 2 |
|
3 | | -> 面试官:带笔了吧,那写两种单例模式的实现方法吧 |
| 3 | +> 最近接手了一个新项目,有段按不同类型走不同检验逻辑的代码,将近小 10 个 `if -else` 判断,真正的“屎山”代码。 |
4 | 4 | > |
5 | | -> 沙沙沙刷刷刷~~~ 写好了 |
| 5 | +> 所以在项目迭代的时候,就打算重构一下,写设计方案后,刚好再总结总结策略模式。 |
6 | 6 | > |
7 | | -> 面试官:你这个是怎么保证线程安全的,那你知道,volatile 关键字? 类加载器?锁机制???? |
8 | | -> 点赞+收藏 就学会系列,文章收录在 GitHub [JavaKeeper](https://github.com/Jstarfish/JavaKeeper) ,N线互联网开发必备技能兵器谱 |
| 7 | +> 先贴个阿里的《 Java 开发手册》中的一个规范 |
9 | 8 |
|
| 9 | + |
10 | 10 |
|
| 11 | +我们先不探讨其他方式,主要讲策略模式。 |
11 | 12 |
|
12 | | - |
13 | 13 |
|
14 | | -单例模式,从我看 《Java 10分钟入门》那天就听过的一个设计模式,还被面试过好几次的设计模式问题,今天一网打尽~~ |
15 | 14 |
|
16 | | -有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。 |
| 15 | +## 定义 |
17 | 16 |
|
18 | | -## 单例模式的定义 |
| 17 | +**策略模式**:封装可以互换的行为,并使用委托来决定要使用哪一个。 |
19 | 18 |
|
20 | | -**单例模式确保一个类只有一个实例,并提供一个全局唯一访问点** |
| 19 | +策略模式是一种**行为设计模式**, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。 |
21 | 20 |
|
| 21 | +> 用人话翻译后就是:运行时我给你这个类的方法传不同的 “key”,你这个方法会执行不同的业务逻辑。 |
| 22 | +> |
| 23 | +> 细品一下,这不就是 if else 干的事吗? |
22 | 24 |
|
23 | 25 |
|
24 | | -## 单例模式的类图 |
25 | 26 |
|
26 | | - |
| 27 | +先直观的看下传统的多重 `if else` 代码 |
27 | 28 |
|
28 | | -## 单例模式的实现 |
| 29 | +```java |
| 30 | +public String getCheckResult(String type) { |
| 31 | + if ("校验1".equals(type)) { |
| 32 | + return "执行业务逻辑1"; |
| 33 | + } else if ("校验2".equals(type)) { |
| 34 | + return "执行业务逻辑2"; |
| 35 | + } else if ("校验3".equals(type)) { |
| 36 | + return "执行业务逻辑3"; |
| 37 | + } else if ("校验4".equals(type)) { |
| 38 | + return "执行业务逻辑4"; |
| 39 | + } else if ("校验5".equals(type)) { |
| 40 | + return "执行业务逻辑5"; |
| 41 | + } else if ("校验6".equals(type)) { |
| 42 | + return "执行业务逻辑6"; |
| 43 | + } else if ("校验7".equals(type)) { |
| 44 | + return "执行业务逻辑7"; |
| 45 | + } else if ("校验8".equals(type)) { |
| 46 | + return "执行业务逻辑8"; |
| 47 | + } else if ("校验9".equals(type)) { |
| 48 | + return "执行业务逻辑9"; |
| 49 | + } |
| 50 | + return "不在处理的逻辑中返回业务错误"; |
| 51 | +} |
| 52 | +``` |
29 | 53 |
|
30 | | -### 饿汉式 |
| 54 | +这么看,你要是还觉得挺清晰的话,想象下这些 return 里是各种复杂的业务逻辑方法~~ |
31 | 55 |
|
32 | | -- static 变量在类装载的时候进行初始化 |
33 | | -- 多个实例的 static 变量会共享同一块内存区域 |
| 56 | + |
34 | 57 |
|
35 | | -用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉 |
| 58 | +网上的示例很多,比如不同路线的规划、不同支付方式的选择 都是典型的 if else 问题,也都是典型的策略模式问题,我们先看下策略模式的类图~ |
36 | 59 |
|
37 | | -```java |
38 | | -public class Singleton { |
39 | 60 |
|
40 | | - //构造私有化,防止直接new |
41 | | - private Singleton(){} |
42 | 61 |
|
43 | | - //静态初始化器(static initializer)中创建实例,保证线程安全 |
44 | | - private static Singleton instance = new Singleton(); |
| 62 | +## 类图 |
45 | 63 |
|
46 | | - public static Singleton getInstance(){ |
47 | | - return instance; |
48 | | - } |
49 | | -} |
50 | | -``` |
| 64 | + |
| 65 | + |
| 66 | +策略模式涉及到三个角色: |
51 | 67 |
|
52 | | -饿汉式是线程安全的,JVM在加载类时马上创建唯一的实例对象,且只会装载一次。 |
| 68 | +- **Strategy**:策略接口或者策略抽象类,并且策略执行的接口(Context 使用这个接口来调用具体的策略实现算法) |
| 69 | +- **ConcreateStrategy**:实现策略接口的具体策略类 |
| 70 | +- **Context**:上下文类,持有具体策略类的实例,并负责调用相关的算法 |
53 | 71 |
|
54 | | -Java 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的ClassLoader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个ClassLoader的话,就会有多个实例) |
55 | 72 |
|
56 | | -### 懒汉式 |
57 | 73 |
|
58 | | -懒汉式,就是实例在用到的时候才去创建,比较“懒” |
| 74 | +应用策略模式来解决问题的思路 |
59 | 75 |
|
60 | | -单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载Lazy Load,就是一开始不要加载资源或数据,等到要使用的时候才加载) |
| 76 | +## 实例 |
61 | 77 |
|
62 | | -#### 同步方法 |
| 78 | +先看看最简单的策略模式 demo: |
| 79 | + |
| 80 | +1、策略接口 |
63 | 81 |
|
64 | 82 | ```java |
65 | | -public class Singleton { |
66 | | - private static Singleton singleton; |
| 83 | +public interface Strategy { |
| 84 | + void operate(); |
| 85 | +} |
| 86 | +``` |
67 | 87 |
|
68 | | - private Singleton(){} |
| 88 | +2、具体的算法实现 |
69 | 89 |
|
70 | | - //解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐 |
71 | | - public static synchronized Singleton getInstance(){ |
72 | | - if(singleton == null){ |
73 | | - singleton = new Singleton(); |
74 | | - } |
75 | | - return singleton; |
| 90 | +```java |
| 91 | +public class ConcreteStrategyA implements Strategy { |
| 92 | + @Override |
| 93 | + public void operate() { |
| 94 | + //具体的算法实现 |
| 95 | + System.out.println("执行业务逻辑A"); |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +public class ConcreteStrategyB implements Strategy { |
| 100 | + @Override |
| 101 | + public void operate() { |
| 102 | + //具体的算法实现 |
| 103 | + System.out.println("执行业务逻辑B"); |
76 | 104 | } |
77 | 105 | } |
78 | 106 | ``` |
79 | 107 |
|
| 108 | +3、上下文的实现 |
80 | 109 |
|
| 110 | +```java |
| 111 | +public class Context { |
81 | 112 |
|
82 | | -#### 双重检查加锁 |
| 113 | + //持有一个具体的策略对象 |
| 114 | + private Strategy strategy; |
83 | 115 |
|
84 | | -```java |
85 | | -public class Singleton { |
86 | | - |
87 | | - //volatitle关键词确保,多线程正确处理singleton |
88 | | - private static volatile Singleton singleton; |
89 | | - |
90 | | - private Singleton(){} |
91 | | - |
92 | | - public static Singleton getInstance(){ |
93 | | - if(singleton ==null){ |
94 | | - synchronized (Singleton.class){ |
95 | | - if(singleton == null){ |
96 | | - singleton = new Singleton(); |
97 | | - } |
98 | | - } |
99 | | - } |
100 | | - return singleton; |
| 116 | + //构造方法,传入具体的策略对象 |
| 117 | + public Context(Strategy strategy){ |
| 118 | + this.strategy = strategy; |
| 119 | + } |
| 120 | + |
| 121 | + public void doSomething(){ |
| 122 | + //调用具体的策略对象进操作 |
| 123 | + strategy.operate(); |
101 | 124 | } |
102 | 125 | } |
103 | 126 | ``` |
104 | 127 |
|
105 | | -Double-Check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。 |
| 128 | +4、客户端使用 |
106 | 129 |
|
107 | | -双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高 |
| 130 | +```java |
| 131 | +public static void main(String[] args) { |
| 132 | + Context context = new Context(new ConcreteStrategyA()); |
| 133 | + context.doSomething(); |
| 134 | +} |
| 135 | +``` |
108 | 136 |
|
109 | | -**volatile**:volatile一般用于多线程的可见性,这里用来防止**指令重排**(防止new Singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。 |
110 | 137 |
|
111 | | -##### 指令重排 |
112 | 138 |
|
113 | | -指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。 |
| 139 | +## 解析策略模式 |
114 | 140 |
|
115 | | -Java中创建一个对象,往往包含三个过程。对于singleton = new Singleton(),这不是一个原子操作,在 JVM 中包含如下三个过程。 |
| 141 | +策略模式的功能就是把具体的算法实现从具体的业务处理中独立出来,把它们实现成单独的算法类,从而形成一系列算法,并让这些算法可以互相替换。 |
116 | 142 |
|
117 | | -1. 给 singleton 分配内存 |
| 143 | +> 策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。 |
118 | 144 |
|
119 | | -2. 调用 Singleton 的构造函数来初始化成员变量,形成实例 |
120 | 145 |
|
121 | | -3. 将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了) |
122 | 146 |
|
123 | | -但是,由于JVM会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。 |
| 147 | +实际上,每个策略算法具体实现的功能,就是原来在 if-else 结构中的具体实现,每个 if-else 语句都是一个平等的功能结构,可以说是兄弟关系。 |
124 | 148 |
|
| 149 | +策略模式呢,就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文与具体的策略类进行交互 |
125 | 150 |
|
| 151 | +所以说,策略模式只是在代码结构上的一个调整, |
126 | 152 |
|
127 | | -### 静态内部类 |
| 153 | + 『 **策略模式 = 实现策略接口(或抽象类)的每个策略类 + 上下文的逻辑分派** 』 |
128 | 154 |
|
129 | | -```java |
130 | | -public class Singleton { |
| 155 | + |
131 | 156 |
|
132 | | - private Singleton(){} |
| 157 | +> 策略模式的本质:分离算法,选择实现 ——《研磨设计模式》 |
133 | 158 |
|
134 | | - private static class SingletonInstance{ |
135 | | - private static final Singleton INSTANCE = new Singleton(); |
136 | | - } |
137 | | - |
138 | | - public static Singleton getInstance(){ |
139 | | - return SingletonInstance.INSTANCE; |
140 | | - } |
141 | | -} |
142 | | -``` |
| 159 | +即使用了策略模式,你该写的业务逻辑照常写,到逻辑分派的时候,只是变相的 `if-else`。 |
143 | 160 |
|
144 | | -采用类加载的机制来保证初始化实例时只有一个线程; |
| 161 | +而它的优化点是抽象了出了接口,将业务逻辑封装成一个一个的实现类,任意地替换。在复杂场景(业务逻辑较多)时比直接 `if-else` 更好维护和扩展些。 |
145 | 162 |
|
146 | | -静态内部类方式在Singleton 类被装载的时候并不会立即实例化,而是在调用getInstance的时候,才去装载内部类SingletonInstance ,从而完成Singleton的实例化 |
147 | 163 |
|
148 | | -类的静态属性只会在第一次加载类的时候初始化,所以,JVM帮我们保证了线程的安全性,在类初始化时,其他线程无法进入 |
149 | 164 |
|
150 | | -优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用 |
| 165 | +### 谁来选择具体的策略算法 |
151 | 166 |
|
152 | | -### 枚举 |
| 167 | +如果你手写了上边的 demo,就会发现,这玩意不及 `if-else` 来的顺手,尤其是在判断逻辑的时候,每个逻辑都要要构造一个上下文对象,费劲。 |
153 | 168 |
|
154 | | -```java |
155 | | -enum Singleton{ |
156 | | - INSTANCE; |
157 | | - public void method(){} |
158 | | -} |
159 | | -``` |
| 169 | +其实,策略模式中,我们可以自己定义谁来选择具体的策略算法,有两种: |
| 170 | + |
| 171 | +- 客户端:当使用上下文时,由客户端选择,像我们的 demo |
| 172 | +- 上下文:客户端不用选,由上下文选 |
| 173 | + |
| 174 | + |
| 175 | + |
| 176 | +### 优缺点 |
| 177 | + |
| 178 | +#### 优点: |
| 179 | + |
| 180 | +- 定义一系列算法:策略模式的功能就是定义一系列算法,实现让这些算法可以相互替换,所以为这一系列算法定义公共的接口,以约束一系列算法要实现的功能。如果这一系列算法具有公共功能,可以把策略接口实现为抽象类,把这些公共功能实现到父类 |
| 181 | +- 避免多重条件语句:也就是避免大量的 `if-else` |
| 182 | +- 更好的扩展性(完全符合开闭原则):策略模式中扩展新的策略实现很容易,无需对上下文修改,只增加新的策略实现类就可以 |
| 183 | + |
| 184 | +#### 缺点: |
| 185 | + |
| 186 | +- 客户必须了解每种策略的不同 |
| 187 | +- 增加了对象数:每个具体策略都封装成了类,可能备选的策略会很多 |
| 188 | +- 只适合扁平的算法结构: |
| 189 | + |
| 190 | + |
| 191 | + |
| 192 | +### 适用场景 |
| 193 | + |
| 194 | +> 策略模式的本质:分离算法,选择实现 |
| 195 | +
|
| 196 | +- 当你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时,可使用策略模式。 |
| 197 | + |
| 198 | +- 当你有许多仅在执行某些行为时略有不同的相似类时(它们之间的区别仅在于它们的行为),使用策略模式可以动态地让一个对象在许多行为中选择一种行为 |
| 199 | + |
| 200 | +- 如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来。 |
| 201 | + |
| 202 | + 策略模式让你能将各种算法的代码、 内部数据和依赖关系与其他代码隔离开来。 不同客户端可通过一个简单接口执行算法, 并能在运行时进行切换。 |
| 203 | + |
| 204 | +- **当类中使用了复杂条件运算符以在同一算法的不同变体中切换时,可使用该模式** |
| 205 | + |
| 206 | + - 策略模式将所有继承自同样接口的算法抽取到独立类中, 因此不再需要条件语句。 原始对象并不实现所有算法的变体, 而是将执行工作委派给其中的一个独立算法对象。 |
| 207 | + |
| 208 | + |
| 209 | + |
| 210 | +实际使用中,往往不会只是单一的某个设计模式的套用,一般都会混合使用,而且模式之间的结合也是没有定势的,要具体问题具体分析。 |
| 211 | + |
| 212 | +策略模式往往会结合其他模式一起使用, |
| 213 | + |
| 214 | +### 策略模式在 JDK 中的应用 |
160 | 215 |
|
161 | | -借助JDK5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于Java1.5中才加入enum特性,所以使用的人并不多。 |
| 216 | +#### 策略模式在 Spring 中的应用 |
162 | 217 |
|
163 | | -这种方式是《Effective Java》 作者Josh Bloch 提倡的方式。 |
| 218 | +https://mp.weixin.qq.com/s?__biz=MzAxODcyNjEzNQ==&mid=2247487480&idx=2&sn=461a012afd41d4e2466a9024b81b6e39&chksm=9bd0a260aca72b760e429753beb1fa12270c18b5cc6829538ccbfefcc23214fefc0aadd575c8&scene=27#wechat_redirect |
164 | 219 |
|
165 | 220 |
|
166 | 221 |
|
167 | | -## 单例模式在JDK 中的源码分析 |
| 222 | +最后: |
168 | 223 |
|
169 | | -JDK 中,`java.lang.Runtime` 就是经典的单例模式(饿汉式) |
| 224 | +并不是说,看到if-else 就想着用策略模式去优化,业务逻辑简单,可能几个枚举,或者几个卫语句就搞定的场景,就不用非得硬套设计模式了,杀鸡焉用牛刀,是吧,可以看看参考文章第一篇 |
170 | 225 |
|
171 | | - |
172 | 226 |
|
173 | 227 |
|
| 228 | +参考与感谢: |
174 | 229 |
|
175 | | -## 单例模式注意事项和细节 |
| 230 | +- [《用 Map + 函数式接口来实现策略模式》](https://www.cnblogs.com/keeya/p/13187727.html) |
| 231 | +- 《研磨设计模式》 |
176 | 232 |
|
177 | | -- 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 |
178 | | -- 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new |
179 | | -- 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等) |
|
0 commit comments