# 一.设计原则
# 1.六大设计原则
设计原则 | 描述 |
---|---|
开闭原则 | 在不修改源代码或二进制代码的情况下,通过扩展模块来满足新需求。 |
里氏替换原则 | 子类可以扩展父类功能,但不能改变父类原有功能。 |
依赖倒置原则 | 降低客户与实现模块之间的耦合,实现开闭原则的重要途径。 |
接口隔离原则 | 约束接口,降低类对接口的依赖性。 |
迪米特法则 | 一个对象应该对其他对象有最少的了解。 |
单一职责原则 | 控制类的粒度大小,将对象解耦,提高内聚性。 |
# 2.单一职责好处?
- 类的复杂性降低,实现什么职责都有清晰明确的定义
- 可读性提高,复杂性降低,那当然可读性提高了;
- 可维护性提高,可读性提高,那当然更容易维护了!
- 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
# 3.里氏替换?
只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生主任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应.
- 子类必须完全实现父类的方法
- 子类可以有自己的个性
- 覆盖或实现父类的方法时输入参数可以被放大 (重载和重写)
- 覆写或实现父类的万方法时输出结果可以被缩小
# 4.里氏替换优劣?
优点
- 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
- 提高代码的重用性;
- 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
- 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
- 提高产品或项目的开放性。
缺点
继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
增强了耦合性。当父类的常量、变量和方法被修改时,必需要老 虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果--大片的的代码需要重构。
# 5.依赖倒置
高层模块不应该依赖低层模块,两者都应该依赖其抽象;
抽象不应该依赖细节;
细节应该依赖抽象。
IDriver 司机开车(不区分车类型),ICar 车跑(区分车类型),2 个接口
# 6.依赖倒置的优点
- 依赖倒置原则可以减少类间的耦合性,
- 提高系统的稳定性,
- 降低并行开发引起的风险,
- 提高代码的可读性和可维护性。
# 7.依赖倒置的传递
构造函数传递依赖对象
Setter 方法传递依赖对象
接口声明依赖对象
# 8.接口隔离
建立单一接口,不要建立臃肿庞大的接口。再通俗一点讲: 接口尽量细化,同时接口中的方法尽量少。与单一职责原则不是相同的吗?错,接口隔离原则与单一职责的审视角度是不相同的,单一职责要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要求接口的方法尽量少。例如一个接口的职责可能包含 10 个方法,这 10 个方法都放在一个接口中,并且提供给多个模块访问,各个模块按照规定的权限来访问,在系统外通过文档约束“不使用的方法不要访问”,按照单一职责原则是允许的,按照接口隔离原则是不允许的,因为它要求“尽量使用多个专门的接口”。专门的接口指什么?就是指提供给每个模块的都应该是单一接口,提供给几个模块就应该有几个接口,而不是建立一个庞大的臃肿的接口,容纳所有的客户端访问。
- 接口要尽量小
- 接口要高内聚
- 定制服务
- 接口设计有限度的
# 9.迪米特法则
一个对象应该对其他对象有最少得了解,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用;如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
该原则其根本思想,是强调了类之间的松耦合;类之间的耦合越弱,越利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。在类的结构设计上,每一个类都应当尽量降低成员的访问权限。
# 10.开闭原则
软件实体应该可以扩展,但不可修改。该原则是面向对象设计的核心所在,遵循这个原则可以带来面向对象技术所声称的可维护、可扩展、可复用、灵活性好。
设计人员必须对于他设计的模块应该对哪种变化封闭做出选择,必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化。最初编写程序时假设变化不会发生,当变化发生时,就创建抽象来隔离以后发生的同类变化,拒绝不成熟的抽象。
# 二.具体模式
# 1.单例模式
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
主要优点
- 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
主要缺点
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 现在很多面向对象语言(如 Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
适用场景
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
# 2.单例模式的实现
饿汉式:
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {
}
public static EagerSingleton getInstance() {
return instance;
}
}
2
3
4
5
6
7
8
9
10
懒汉式:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
//线程安全版本
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
双重检查锁模式:
public class Hand_01_Single {
private volatile static Hand_01_Single hand01Single;
public static Hand_01_Single getSingle() {
if (hand01Single == null) {
synchronized (Hand_01_Single.class) {
if (hand01Single == null) {
hand01Single = new Hand_01_Single();
}
}
}
return hand01Single;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
枚举:
通过枚举实现单例模式的优势包括:
- 线程安全:枚举实例的创建是线程安全的,无需担心多线程下的并发访问问题。
- 防止反序列化创建新实例:枚举类型默认阻止了反序列化创建新的枚举实例,保证了单例的唯一性。
- 简洁明了:实现代码非常简洁,而且枚举在语言层面上就保证了单例的特性。
public enum SingletonEnum {
INSTANCE;
// 可以在枚举中添加其他成员变量和方法
// ...
// 示例方法
public void doSomething() {
// ...
}
}
2
3
4
5
6
7
8
9
10
11
// 获取单例实例
SingletonEnum instance=SingletonEnum.INSTANCE;
// 调用方法
instance.doSomething();
2
3
4
5
# 3.工厂模式
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式、在工厂模式中,我们在创建对对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
抽象程度:
简单工厂模式 < 工厂方法模式 < 抽象工厂模式。
角色分类:
1、简单工厂模式:抽象产品、具体产品、工厂类。
2、工厂方法模式:抽象产品、具体产品、抽象工厂、具体工厂。
3、抽象工厂模式:抽象产品族、抽象产品、具体产品、抽象工厂、具体工厂。
简单工厂模式
- 工厂类封装了创建具体产品对象的函数
- 扩展性非常差,新增产品的时候,需要去修改工厂类。
- 组成
- 工厂类(ShoesFactory):工厂模式的核心类,会定义一个用于创建指定的具体实例对象的接口。
- 抽象产品类(Shoes):是具体产品类的继承的父类或实现的接口。
- 具体产品类(NiKeShoes\AdidasShoes\LiNingShoes):工厂类所创建的对象就是此具有产品实例。
public interface Sender {
void send();
}
public class MailSender implements Sender {
public void send() {
System.out.println("send mail");
}
}
public class MessageSender implements Sender {
public void send() {
System.out.println("send message !");
}
}
public class SenderFactory {
public static Sender getSender(Class<? extends Sender> clazz) {
if (clazz == null) {
return null;
}
try {
return clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public class SendTest {
public static void main(String[] args) {
SenderFactory.getSender(MailSender.class).send();
SenderFactory.getSender(MessageSender.class).send();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
工厂方法模式
定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
public interface Sender {
void send();
}
public class MailSender implements Sender {
public void send() {
System.out.println("send mail");
}
}
public class MessageSender implements Sender {
public void send() {
System.out.println("send message !");
}
}
public interface SenderFactory {
Sender getSender();
}
public class MessageSenderFactory implements SenderFactory {
public Sender getSender() {
return new MessageSender();
}
}
public class MailSenderFactory implements SenderFactory {
public Sender getSender() {
return new MailSender();
}
}
public class SendTest {
public static void main(String[] args) {
SenderFactory mailSenderFactory = new MailSenderFactory();
mailSenderFactory.getSender().send();
SenderFactory messageSenderFactory = new MessageSenderFactory();
messageSenderFactory.getSender().send();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
抽象工厂模式
提供一个创建一系列相关或者相互依赖的对象的接口,无须指定他们具体的类。
/**
* 支付接口
*/
public interface IPay {
void pay();
}
public class AliPay implements IPay {
@Override
public void pay() {
System.out.println("支付宝支付流程开始:");
}
}
public class WePay implements IPay {
@Override
public void pay() {
System.out.println("微信支付流程开始:");
}
}
public class UniPay implements IPay {
@Override
public void pay() {
System.out.println("银联支付流程开始:");
}
}
public class OverseasPay implements IPay {
@Override
public void pay() {
System.out.println("跨境支付流程开始:");
}
}
/**
* 退款接口
*/
public interface IRefund {
void pay();
}
public class AliRefund implements IRefund {
@Override
public void pay() {
System.out.println("支付宝退款流程开始:");
}
}
public class WeRefund implements IRefund {
@Override
public void pay() {
System.out.println("微信退款流程开始:");
}
}
public class UniRefund implements IRefund {
@Override
public void pay() {
System.out.println("银联退款流程开始:");
}
}
public class OverseasRefund implements IRefund {
@Override
public void pay() {
System.out.println("跨境支付退款流程开始:");
}
}
public abstract class IPaymentFactory {
abstract IPay getPayment();
abstract IRefund getRefund();
}
public class AliPaymentFactory extends IPaymentFactory {
@Override
public IPay getPayment() {
return new AliPay();
}
@Override
IRefund getRefund() {
return new AliRefund();
}
}
public class WePaymentFactory extends IPaymentFactory {
@Override
public IPay getPayment() {
return new WePay();
}
@Override
IRefund getRefund() {
return new WeRefund();
}
}
public class UniPaymentFactory extends IPaymentFactory {
@Override
public IPay getPayment() {
return new UniPay();
}
@Override
IRefund getRefund() {
return new UniRefund();
}
}
public class OverseasPaymentFactory extends IPaymentFactory {
@Override
public IPay getPayment() {
return new OverseasPay();
}
@Override
IRefund getRefund() {
return new OverseasRefund();
}
}
public class AbstractFactoryTest {
public static void main(String[] args) {
IPaymentFactory paymentFactory = new AliPaymentFactory();
paymentFactory.getPayment().pay();
paymentFactory.getRefund().refund();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# 4.模版方法模式
在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
代码复用
功能扩展
AQS-自定义锁
Mybatis-BaseExecutor
# 5.代理模式
意图:
为其他对象提供一种代理以控制对这个对象的访问。
适用性:
在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用 Proxy 模式。下面是一 些可以使用 Proxy 模式常见情况:
远程代理(Remote Proxy )为一个对象在不同的地址空间提供局部代表。NEXTSTEP[Add94] 使用 NXProxy 类实现了这一目的。Coplien[Cop92] 称这种代理为“大使” (Ambassador )。 虚代理(Virtual Proxy )根据需要创建开销很大的对象。在动机一节描述的 ImageProxy 就是这样一种代理的例子。 保护代理(Protection Proxy )控制对原始对象的访问。保护代理用于对象应该有不同 的访问权限的时候。例如,在 Choices 操作系统[ CIRM93]中 KemelProxies 为操作系统对象提供 了访问保护。 智能指引(Smart Reference )取代了简单的指针,它在访问对象时执行一些附加操作。它的典型用途包括: 对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它(也称为 SmartPointers[Ede92 ] )。
当第一次引用一个持久对象时,将它装入内存。
在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。
代理模式 与 装饰器模式:
对于两个模式,首先要说的是,装饰模式就是代理模式的一个特殊应用,两者的共同点是都具有相同的接口,不同点则是代理模式着重对代理过程的控制,而装饰模式则是对类的功能进行加强或减弱,它着重类的功能变化
# 6.命令模式
命令是对命令的封装,每一个命令都是一个操作,请求方发出请求,接收方接收请求,并执行操作。命令模式解耦了请求方和接收方,命令模式属于行为型模式
- 接收者角色(Receiver):负责具体执行一个请求
- 命令角色(ICommand):定义需要执行的所有命令行为
- 具体的命令角色(ConcreteCommand):内部维护一个 Receiver
- 请求者角色(Invoker):接收客户端的命令,并执行命令
优点
- 通过引入命令的抽象接口,实现了命令请求与实现的解耦
- 扩展性良好,可以很容易的增加新命令
缺点
- 命令类可能过多
意图:
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
适用性:
抽象出待执行的动作以参数化某对象,你可用过程语言中的回调(call back)函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。Command 模式是回调机制的一个面向对象的替代品。
在不同的时刻指定、排列和执行请求。一个 Command 对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。
支持取消操作。Command 的 Excute 操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。Command 接口必须添加一个 Unexecute 操作,该操作取消上一次 Execute 调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用 Unexecute 和 Execute 来实现重数不限的“取消”和“重做”。
支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在 Command 接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用 Execute 操作重新执行它们。
用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务( transaction)的信息系统中很常见。一个事务封装了对数据的一组变动。Command 模式提供了对事务进行建模的方法。Command 有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。
命令模式 与 策略模式:
命令模式和策略模式的类图确实很相似,只是命令模式多了一个接收者(Receiver)角色。 策略模式的意图是封装算法,它认为“算法”已经是一个完整的、不可拆分的原子业务;而命令模式则是对动作的解耦,把一个动作的执行分为执行对象(接收者角色)、执行行为(命令角色),让两者相互独立而不相互影响。
# 7.责任链模式
意图:
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
适用性:
有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定。
你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
可处理一个请求的对象集合应被动态指定。
# 8.装饰模式
意图:
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator 模式相比生成子类更为灵活。
适用性:
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
处理那些可以撤消的职责。
当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
# 9.策略模式
意图:
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
适用性:
许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。
需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间/时间权衡的算法。当这些变体实现为一个算法的类层次时[H087] ,可以使用策略模式。
算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。
一个类定义了多种行为, 并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的 Strategy 类中以代替这些条件语句。
//状态机支持的全部三种transition方式。
StateMachineBuilder<States, Events, Context> builder=StateMachineBuilderFactory.create();
//external transition
builder.externalTransition()
.from(States.STATE1)
.to(States.STATE2)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());
//internal transition
builder.internalTransition()
.within(States.STATE2)
.on(Events.INTERNAL_EVENT)
.when(checkCondition())
.perform(doAction());
//external transitions
builder.externalTransitions()
.fromAmong(States.STATE1,States.STATE2,States.STATE3)
.to(States.STATE4)
.on(Events.EVENT4)
.when(checkCondition())
.perform(doAction());
builder.build(machineId);
StateMachine<States, Events, Context> stateMachine=StateMachineFactory.get(machineId);
stateMachine.showStateMachine();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 10.适配器模式
意图:
将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适用性:
你想使用一个已经存在的类,而它的接口不符合你的需求。
你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
(仅适用于对象 Adapter )你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。
解释说明:
- Target 目标角色 该角色定义把其他类转换为何种接口,也就是我们的期望接口,例子中的 IUserInfo 接口就是目标角色。
- Adaptee 源角色 你想把谁转换成目标角色,这个“谁”就是源角色,它是已经存在的、运行良好的类或对象,经过适配器角色的包装,它会成为一个崭新、靓丽的角色。
- Adapter 适配器角色
只有当项目中没有时间做新的接口的时候,才临时使用适配器。适配器这个东西就是为补救应急而生的。
比如说,我们有一个接口,传入两个参数,name,age。现在这个接口发生了改变,需要传 3 个参数,name,age,sex。这个时候我们做一个适配器。Adapter 里边多加一个参数,多组装一个新的 bean。
# 11.原型模式
意图:
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
适用性:
当要实例化的类是在运行时刻指定时,例如,通过动态装载;或者
为了避免创建一个与产品类层次平行的工厂类层次时;或者
当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
# 12.观察者模式
意图:
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。
适用性:
当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。
当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。
# 13.门面模式
意图:
为子系统中的一组接口提供一个一致的界面,Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
适用性:
当你要为一个复杂子系统提供一个简单接口时。子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类。这使得子系统更具可重用性,也更容易对子系统进行定制,但这也给那些不需要定制子系统的用户带来一些使用上的困难。Facade 可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过 facade 层。
客户程序与抽象类的实现部分之间存在着很大的依赖性。引入 facade 将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
当你需要构建一个层次结构的子系统时,使用 facade 模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,你可以让它们仅通过 facade 进行通讯,从而简化了它们之间的依赖关系。
中介者模式 与 门面模式:
门面模式为复杂的子系统提供一个统一的访问界面,它定义的是一个高层接口,该接口使得子系统更加容易使用,避免外部模块深入到子系统内部而产生与子系统内部细节耦合的问题。中介者模式使用一个中介对象来封装一系列同事对象的交互行为,它使各对象之间不再显式地引用,从而使其耦合松散,建立一个可扩展的应用架构。
# 14.享元模式
意图:
运用共享技术有效地支持大量细粒度的对象。
适用性:
一个应用程序使用了大量的对象。
完全由于使用大量的对象,造成很大的存储开销。
对象的大多数状态都可变为外部状态。
如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
应用程序不依赖于对象标识。由于 Flyweight 对象可以被共享,对于概念上明显有别的对象,标识测试将返回真值。
# 15.备忘录模式
备忘录模式就是一个对象的备份模式,提供了一种程序数据的备份方法
- 需要保存和恢复数据的相关状态场景。
- 提供一个可回滚(rollback)的操作
- 需要监控的副本场景中。
- 数据库连接的事务管理就是用的备忘录模式
# 16.解释器模式
建议看看就得了,开发中无使用场景。(性能问题严重;类膨胀问题)
- 计算器开发;
- 人工智能,语法解析
在设计模式中,解释器模式(Interpreter Pattern)是一种行为型模式,它用于定义一种语言的文法规则,并且通过解释器来解释处理该语言中的表达式。这种模式通常用于处理复杂的语法或表达式,并将其转换为可执行的操作或执行结果。
解释器模式包含以下主要组成部分:
- 抽象表达式(Abstract Expression):定义一个抽象接口,包含一个 interpret()方法,用于解释和处理表达式。
- 终结符表达式(Terminal Expression):实现抽象表达式接口,表示语言中的终结符,即不能再进一步解释的元素。
- 非终结符表达式(Non-terminal Expression):实现抽象表达式接口,表示语言中的非终结符,可以通过递归解释其他表达式。
- 上下文(Context):包含待解释的语言表达式。
- 解释器(Interpreter):对语言进行解释和处理的类,根据表达式的文法规则进行解释。
# 三.模式分类
# 1.分类汇总
根据模式是用来做什么的,可分为
- 创建型(Creational):创建型模式主要用于创建对象
- 结构型(Structural):处理类或对象的组合
- 行为型(Behavioral):描述对类或对象怎样交互和怎样分配职责
# 2.设计模式的优点
① 可以提高程序员的思维能力、编程能力和设计能力。
② 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
③ 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。现在这样说肯定有些懵逼,需要在实际开发中才能体会得到真正的好处。
# 3.设计模式概述
创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程
- 单例模式:某个类只能有一个实例,提供一个全局的访问点。
- 简单工厂:一个工厂类根据传入的参量决定创建出那一种产品类的实例。工厂方法:定义一个创建对象的接口,让子类决定实例化那个类。
- 抽象工厂:创建相关或依赖对象的家族,而无需明确指定具体类。
- 建造者模式:封装一个复杂对象的构建过程,并可以按步骤构造。
- 原型模式:通过复制现有的实例来创建新的实例。
结构型模式:把类或对象结合在一起形成一个更大的结构
- 适配器模式:将一个类的方法接口转换成客户希望的另外一个接口。
- 组合模式:将对象组合成树形结构以表示“”部分-整体“”的层次结构。
- 装饰器模式:动态的给对象添加新的功能。
- 代理模式:为其他对象提供一个代理以便控制这个对象的访问。
- 亨元(蝇量)模式:通过共享技术来有效的支持大量细粒度的对象。
- 外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。
- 桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。
行为型模式: 类和对象如何交互,及划分责任和算法
- 迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
- 模板方法模式:定义一个算法结构,而将一些步骤延迟到子类实现。
- 策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。
- 命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
- 状态模式:允许一个对象在其对象内部状态改变时改变它的行为。
- 责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
- 备忘录模式:在不破坏封装的前提下,保持对象的内部状态。
- 观察者模式:对象间的一对多的依赖关系。
- 访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能。
- 中介者模式:用一个中介对象来封装一系列的对象交互。
- 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。
# 4.各种关系的强弱顺序
泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖
# 5.实际应用
模块 | 设计模式 | 描述 |
---|---|---|
支付模块 | 策略模式+工厂模式+单例模式+门面模式 | 使用策略来处理不同支付方式,工厂创建支付对象策略类,单例确保全局唯一,门面简化支付接口。 |
投放模块 | 责任链模式 | 使用责任链处理不同投放需求,实现请求的传递和处理。 |
订单状态模块 | 状态模式+观察者模式 | 使用状态模式管理订单状态,观察者模式跟踪状态变化。 |
平台积分模块 | 装饰者模式+享元模式 | 使用装饰者扩展积分功能,享元共享相同的积分对象。 |
开局发票模块 | 建造者模式+原型模式 | 使用建造者创建发票对象,原型模式复制相似发票。 |
审计日志模块 | 模版方法模式+迭代器模式+桥接模式 | 使用模版方法定义审计流程,迭代器遍历日志,桥接模式连接不同日志存储。 |
树形结构模块 | 组合模式+访问者模式 | 使用组合模式创建树形结构,访问者模式操作节点元素。 |
# 四.支付模块
# 1.策略模式
策略模式(Strategy Pattern)是一种比较简单的模式,也叫做政策模式。
- Context 封装角色它也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化。
- Strategy 抽象策略角色。
- ConcreteStrategy 具体策略角色
# 2.策略模式的优点
算法可以自由切换:
这是策略模式本身定义的,只要实现抽象策略,它就成为策略家族的一个成员,通过封装角色对其进行封装,保证对外提供“可自由切换”的策略。
避免使用多重条件判断 (避免的调用层的):
如果没有策略模式,我们想想看会是什么样子?一个策略家族有 5 个策略算法,一会要使用 A 策略,一会要使用 B
策略,怎么设计呢?使用多重的条件语句?多重条件语句不易维护,而且出错的概率大大增强。使用策略模式后,可以由其他模块决定采用何种策略,策略家族对外提供的访问接口就是封装类,简化了操作,同时避免了条件语句判断。
扩展性良好:
这甚至都不用说是它的优点,因为它太明显了。在现有的系统中增加一个策略太容易了,只要实现接口就可以了,其他都不用修改,类似于一个可反复拆卸的插件,这大大地符合了 OCP 原则。
# 3.策略模式的缺点
策略类数量增多:
每一个策略都是一个类,复用的可能性很小,类数量增多。
所有的策略类都需要对外暴露:
上层模块必须知道有哪些策略,然后才能决定使用哪一个策略,这与迪米特法则是相违背的,我只是想使用了一个策略,我凭什么就要了解这个策略呢?那要你的封装类还有什么意义?
# 3.门面模式
门面模式(Facade Pattern)也叫做外观模式,是一种比较常用的封装模式。门面模式注重“统一的对象”,也就是提供一个访问子系统的接口,除了这个接口不允许有任何访问子系统的行为发生,其通用类图
类图就这么简单,但是它代表的意义可是异常复杂,Subsystem Classes 是子系统所有类的简称,它可能代表一个类,也可能代表几十个对象的集合。甭管多少对象,我们把这些对象全部圈入子系统的范畴。
再简单地说,门面对象是外界访问子系统内部的唯一通道,不管子系统内部是多么杂乱无章,只要有门面对象在,就可以做到“金玉其外”。
# 4.门面模式优点
减少系统的相互依赖想想看,如果我们不使用门面模式,外界访问直接深入到子系统内部,相互之间是一种强耦合关系,你死我就死,你活我才能活,这样的强依赖是系统设计所不能接受的,门面模式的出现就很好地解决了该问题,所有的依赖都是对门面对象的依赖,与子系统无关。
提高了灵活性依赖减少了,灵活性自然提高了。不管子系统内部如何变化,只要不影响到门面对象,任你自由活动。
提高安全性想让你访问子系统的哪些业务就开通哪些逻辑,不在门面上开通的方法,你休想访问到。
# 5.门面模式的缺点
门面模式最大的缺点就是不符合开闭原则,对修改关闭,对扩展开放,看看我们那个门面对象吧,它可是重中之重,一旦在系统投产后发现有一个小错误,你怎么解决?完全遵从开闭原则,根本没办法解决。继承?覆写?都顶不上用,唯一能做的一件事就是修改门面角色的代码,这个风险相当大,这就需要大家在设计的时候慎之又慎,多思考几遍才会有好收获。
# 6.工厂模式
在工厂方法模式中,抽象产品类 Product 负责定义产品的共性,实现对事物最抽象的定义;Creator 为抽象创建类,也就是抽象工厂,具体如何创建产品类是由具体的实现工厂 ConcreteCreator 完成的。工厂方法模式的变种较多。
# 7.抽象工厂模式
抽象工厂模式(Abstract Factory Pattern)是一种创建型设计模式,它提供一个接口用于创建一系列相关或依赖对象的家族,而无需指定具体的类。该模式允许客户端使用抽象接口来创建一组相关的产品,而不必关心这些产品的具体实现细节。
让我们通过一个简单的例子来说明抽象工厂模式。假设我们要创建一个游戏,它有两个不同的阵营:精灵(Elves)和兽人(Orcs)。每个阵营都有自己的兵种,包括士兵(Soldier)和弓箭手(Archer)。
首先,我们需要定义抽象的兵种接口:
// 抽象士兵接口
interface Soldier {
void fight();
}
// 抽象弓箭手接口
interface Archer {
void shoot();
}
2
3
4
5
6
7
8
9
然后,我们为每个阵营实现具体的兵种类:
// 精灵士兵
class ElfSoldier implements Soldier {
@Override
public void fight() {
System.out.println("Elf soldier is fighting!");
}
}
// 精灵弓箭手
class ElfArcher implements Archer {
@Override
public void shoot() {
System.out.println("Elf archer is shooting!");
}
}
// 兽人士兵
class OrcSoldier implements Soldier {
@Override
public void fight() {
System.out.println("Orc soldier is fighting!");
}
}
// 兽人弓箭手
class OrcArcher implements Archer {
@Override
public void shoot() {
System.out.println("Orc archer is shooting!");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
接下来,我们创建抽象工厂接口用于创建不同阵营的兵种:
javaCopy code
// 抽象工厂接口
interface ArmyFactory {
Soldier createSoldier();
Archer createArcher();
}
2
3
4
5
6
7
8
然后,我们为每个阵营创建具体的工厂类,实现抽象工厂接口:
// 精灵工厂
class ElfArmyFactory implements ArmyFactory {
@Override
public Soldier createSoldier() {
return new ElfSoldier();
}
@Override
public Archer createArcher() {
return new ElfArcher();
}
}
// 兽人工厂
class OrcArmyFactory implements ArmyFactory {
@Override
public Soldier createSoldier() {
return new OrcSoldier();
}
@Override
public Archer createArcher() {
return new OrcArcher();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
现在,我们可以使用抽象工厂来创建不同阵营的兵种,而无需关心具体的实现细节:
public class Main {
public static void main(String[] args) {
// 创建精灵工厂
ArmyFactory elfFactory = new ElfArmyFactory();
// 创建精灵士兵和弓箭手
Soldier elfSoldier = elfFactory.createSoldier();
Archer elfArcher = elfFactory.createArcher();
// 使用精灵士兵和弓箭手
elfSoldier.fight();
elfArcher.shoot();
// 创建兽人工厂
ArmyFactory orcFactory = new OrcArmyFactory();
// 创建兽人士兵和弓箭手
Soldier orcSoldier = orcFactory.createSoldier();
Archer orcArcher = orcFactory.createArcher();
// 使用兽人士兵和弓箭手
orcSoldier.fight();
orcArcher.shoot();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
输出结果会显示不同阵营的兵种在执行各自的动作。
抽象工厂模式的优点在于,当需要扩展新的产品族(例如增加法师、骑士等)时,只需新增相应的产品类和对应的工厂类即可,而无需修改现有的代码。这样,抽象工厂模式使得系统更加灵活、易于扩展。
# 8.单例模式
单例模式的优点:
- 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
- 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在 Java EE 中采用单例模式时需要注意 JVM 垃圾回收机制)。
- 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
单例模式的缺点(其实不算缺点,是个特点):
单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。
# 8.策略模式 + 工厂模式 +单例模式+ 门面模式
模式与功能介绍:
- 策略模式:处理支付策略
- 工厂模式:依据策略枚举返回策略类
- 单例模式:策略类的单例,只需要 new 一次即可
- 门面模式:封装内部,最少暴露
项目需求概述:
用户付款模块设计。要求支持“支付宝付款 0”、“微信付款 1”、“银行卡付款 2” 三种方式。并且提供良好的扩展性,封装性。尽可能为上层调用模块提供最简洁的调用方式。
访问方式:
HTTP POST
请求PostBody入参:
包含用户 account,支付类型,支付金额,所购产品。
思路:
- 对外暴露的只有门面
- 门面里面干的事情
- 获取策略对象
- 生成策略的上下文
- 执行扣款
代码结构如图所示:
# 9.使用总结
- 编写策略接口,并根据不同的策略实现不同的策略类;
- 编写策略枚举;
- 编写策略工厂,根据策略枚举获取策略类;
- 添加单例模式,实现策略类的单例;
- 添加门面模式,封装具体实现逻辑,对外只暴露门面方法;
# 五.投放模块
# 1.责任链模式
模式定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
责任链模式的核心在“链”上,“链”是由多个处理者 ConcreteHandler 组成的。
责任链模式非常显著的优点是将请求和处理分开。请求者可以不用知道是谁处理的,处理者可以不用知道请求的全貌,两者解耦,提高系统的灵活性。
责任链有两个非常显著的缺点:一是性能问题,每个请求都是从链头遍历到链尾,特别是在链比较长的时候,性能是一个非常大的问题。二是调试不很方便,特别是链条比较长,环节比较多的时候。
# 2.责任链官方编码
//定义一个抽象的handle
public abstract class Handler {
private Handler nextHandler; //指向下一个处理者
private int level; //处理者能够处理的级别。代表一些扩展的可能性
public Handler(int level) {
this.level = level;
}
public void setNextHandler(Handler handler) {
this.nextHandler = handler;
}
// 抽象方法,子类实现
public abstract void echo(Request request);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义一个具体的handleA
public class HandleRuleA extends Handler {
public HandleRuleA(int level) {
super(level);
}
@Override
public void echo(Request request) {
System.out.println("我是处理者1,我正在处理A规则");
}
}
//定义一个具体的handleB
public class HandleRuleB extends Handler {
} //...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//客户端实现
class Client {
public static void main(String[] args) {
HandleRuleA handleRuleA = new HandleRuleA(1);
HandleRuleB handleRuleB = new HandleRuleB(2);
handleRuleA.setNextHandler(handleRuleB); //这是重点,将handleA和handleB串起来
handleRuleA.echo(new Request());
}
}
2
3
4
5
6
7
8
9
# 3.投放业务需求
使用责任链模式实现投放业务的需求:
项目需求:付款完成后的投放业务,投放业务就是要在这些资源位中展示符合当前用户的资源。在支付成功页筛选活动 banner 和推荐商品。
要求:
- 允许运营人员配置需要展示的资源,以及对资源进行过滤的规则。
- 资源的过滤规则相对灵活多变,这里体现为三点:
- 过滤规则大部分可重用,但也会有扩展和变更。
- 不同资源位的过滤规则和过滤顺序是不同的。
- 同一个资源位由于业务所处的不同阶段,过滤规则可能不同。
- 允许规则实时增减和顺序调整。(使用责任链的核心原因)
过滤规则:
- 用户个人资质是否满足投放业务;
- 用户所在城市是否在业务投放城市;
- 用户近期所购买的产品是否符合业务投放人群;
- 新用户首次购买投放指定业务。
# 4.责任链实际编码
# 5.责任链模式总结
- 编写抽象 handler;
- 编写各个逻辑 handler,继承抽象 handler,并重写方法,满足自己的校验条件;
- 在配置中心,配置相关的 handler;
- 处理器封装,集中处理各种策略,只对面提供封装后的处理器;
# 六.订单状态模块
# 1.状态模式
State——抽象状态角色(Enum 完成) 接口或抽象类,负责对象状态定义,并且封装环境角色以实现状态切换。
ConcreteState——具体状态角色 (状态变化 Enum) 每一个具体状态必须完成两个职责:本状态的行为管理以及趋向状态处理,通俗地说,就是本状态下要做的事情,以及本状态如何过渡到其他状态。
Context——环境角色 (用户触发 -- 》 controller + service) 定义客户端需要的接口,并且负责具体状态的切换。
# 2.观察者模式
观察者模式(Observer Pattern)也叫做发布订阅模式(Publish/subscribe)
Subject 被观察者 定义被观察者必须实现的职责,它必须能够动态地增加、取消观察者。它一般是抽象类或者是实现类,仅仅完成作为被观察者必须实现的职责:管理观察者并通知观察者。
ConcreteSubject 具体的被观察者 定义被观察者自己的业务逻辑,同时定义对哪些事件进行通知。
Observer 观察者 观察者接收到消息后,即进行 update(更新方法)操作,对接收到的信息进行处理。
ConcreteObserver 具体的观察者
s 是状态,e 是操作类型
public class StateMachineConfigurerAdapter<S, E> extends AbstractStateMachineConfigurerAdapter<S, E> {
public StateMachineConfigurerAdapter() {
}
}
2
3
4
# 3.状态模式 + 观察者模式(监听器模式)
模式作用:
- 状态模式:状态动态变更;
- 观察者模式:观察状态变更,并出发相应
项目需求概述:
用户从开始下订单,到支付完成,再到物流部进行发货,最终用户确认收货,整个流程涉及到很多订单状态,需要通过代码对订单状态进行管理。除此之外,用户或者物流部门每一次触发的不同操作都有可能改变订单状态。
如:用户创建订单操作 导致 订单状态为 待支付状态; 用户支付操作 导致 订单状态为待发货状态;物流部门发货操作 导致 订单状态变为待收货状态;用户确认收货操作 导致 订单状态变为 订单完成状态。
开发任务:
设计整体结算发货及收货的流程。用户创建订单 --> 支付订单 --> 发货-->收货-->订单完成。
要求:
- 创建订单成功后,订单状态初始化为 待支付。
- 订单状态包括: 待支付;待发货;待收货;订单完成。(状态模式)
- 触发订单状态变化的操作:支付订单;发货;确认收货 (观察者模式)
# 4.使用总结
- 添加枚举订单状态类,枚举操作类;
- 添加状态配置类,配置初始状态,配置状态扭转,配置持久化;
- 普通的 controller 类,普通的方法
- 执行支付或其他动作,发送状态消息到状态机,并持久化状态,依赖于 spring 的状态机;
- 状态机监听器监听到消息后,自动修改订单状态;
- 使用到了状态模式和观察者模式;
# 5.状态模式和策略模式的区别
状态模式(State Pattern)和策略模式(Strategy Pattern)是两种常见的设计模式,它们都属于行为型设计模式,但用途和实现方式有一些不同。
用途:
状态模式:状态模式用于在对象的状态发生改变时,将不同的行为封装到不同的状态类中,使得对象在不同状态下有不同的行为表现。这有助于将复杂的状态机行为拆分成可维护的部分,使对象的状态变化更加灵活和可扩展。例如,一个自动售货机可以具有不同的状态(例如,有货、无货、正在维护等),每个状态下的行为都不同。
策略模式:策略模式用于在运行时选择算法的不同实现,将算法封装在独立的策略类中,然后在运行时选择适当的策略来执行。这有助于将算法的选择与使用代码分离,使得系统更加灵活和可维护。例如,一个排序算法可以有多种实现(快速排序、冒泡排序、插入排序等),策略模式允许在运行时选择使用哪种算法。
结构差异:
状态模式的关键在于对象的状态切换和状态类之间的关联。通常,状态模式会有一个上下文类,该类包含一个状态对象,状态对象实现了共同的接口,以便在不同状态下执行相关操作。状态切换通常由上下文类控制。
策略模式的关键在于算法的选择和独立性。策略模式将算法封装在不同的策略类中,这些策略类通常实现相同的接口,以便可以互换使用。上下文类包含一个策略对象,可以在运行时选择不同的策略。
关注点:
状态模式关注对象在不同状态下的行为切换和状态管理,强调对象状态之间的转换和如何封装不同状态的行为。
策略模式关注在相同的上下文下选择不同的算法或策略,强调算法的独立性和可互换性。
示例:
状态模式示例:自动售货机的状态可以是有货、无货、正在维护等,每个状态下执行不同的操作。
策略模式示例:一个图像处理应用可以具有不同的图像滤镜策略(黑白滤镜、模糊滤镜、锐化滤镜等),用户可以在运行时选择应用哪种滤镜。
状态模式用于管理对象内部的状态转换和状态相关行为,而策略模式用于在运行时选择不同的算法或策略。这两种模式都有助于提高代码的灵活性、可维护性和可扩展性,但它们的应用场景和重点不同。
# 七.平台币积分模块
# 1.装饰者模式
Component 抽象构件 Component 是一个接口或者是抽象类,就是定义我们最核心的对象,也就是最原始的对象
ConcreteComponent 具体构件 ConcreteComponent 是最核心、最原始、最基本的接口或抽象类的实现,你要装饰的就是它。
Decorator 装饰角色 一般是一个抽象类,做什么用呢?实现接口或者抽象方法,它里面可不一定有抽象的方法呀,在它的属性里必然有一个 private 变量指向 Component 抽象构件。
具体装饰角色 ConcreteDecoratorA 和 ConcreteDecoratorB 是两个具体的装饰类,你要把你最核心的、最原始的、最基本的东西装饰成其他东西。
# 2.享元模式
享元模式(Flyweight Pattern)是池技术的重要实现方式,使用共享对象可有效地支持大量的细粒度的对象。 要求细粒度对象,那么不可避免地使得对象数量多且性质相近,那我们就将这些对象的信息分为两个部分:内部状态(intrinsic)与外部状态(extrinsic)。
- 内部状态 内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变
- 外部状态 外部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态
一般情况下,我们会将可变部分和不可变部分放到一起。因为我们最终享元的是一整个对象,既然是一整个对象,我们能够控制共享部分和不可共享部分,那么我们何必新增一个额外的类呢?
# 3.享元和单例区别
享元模式(Flyweight Pattern)和单例模式(Singleton Pattern)是两种常见的设计模式,它们的主要目的和使用场景有所不同。
享元模式(Flyweight Pattern):
享元模式的主要目的是尽可能地共享对象,以减少内存使用和提高性能。它适用于存在大量相似对象实例的情况,尤其是当这些实例具有一些共同的状态,而另一部分状态可以在使用时传递进来。享元模式通过共享对象的方式来减少内存开销,因为不同对象实例中相同的状态只需要保存一份即可。
例如,考虑一个文本编辑器,需要创建大量的字符对象(字母、数字等)。由于字符的种类是有限的,可以将每个字符作为一个享元对象,而在文本编辑器中每次需要一个字符时,都从享元池中获取。这样,只有少量的字符对象实例会被创建,从而节省了内存。
单例模式(Singleton Pattern):
单例模式的主要目的是确保一个类只有一个唯一的实例,并提供一个全局访问点。它适用于那些只需要一个共享实例的场景,例如全局配置、日志记录器等。
单例模式通常通过私有化类的构造函数,防止外部直接创建对象,然后提供一个静态方法或者属性来获取单例实例。在该静态方法中,如果实例不存在,则创建一个新的实例并返回,否则直接返回已有的实例。
区别总结:
- 目的不同:享元模式的目的是尽可能地共享对象以减少内存使用,而单例模式的目的是保证一个类只有一个实例。
- 对象复用:享元模式重点在于对象的复用,通过共享来减少重复对象的创建;而单例模式重点在于提供一个全局唯一访问点,确保只有一个实例。
- 适用场景:享元模式适用于存在大量相似对象实例且具有共享状态的情况;单例模式适用于需要全局唯一实例的情况。
虽然两者都涉及对象的共享,但是关注点和使用场景不同,所以在应用设计模式时需要根据具体情况来选择合适的模式。
# 4.平台币更新
部分商品支付完成更新平台币、红包发放等后续业务。 装饰者模式 + 享元模式(优化对象的频繁创建问题)
项目需求:
部分推销商品付款完成后,需要平台对当前用户的平台币进行更新,如淘宝的淘金币,京东商城的京豆等。
要求:
- 平台币的更新和红包发放业务为附属功能,不能影响主支付业务逻辑。
- 平台币的更新和红包发放业务只对部分推广商品有效,且依赖于商品的属性变更。商品属性变更,如需取消平台币更新和发放红包业务,不可修改代码。要做到在线实时热变更。
- 调用层无需关心该商品是否需要更新平台币或者发放红包。做到与上层调用者的完全解耦。
- 该逻辑为支付的附属业务,随着支付完成立即触发。但不可影响支付主逻辑。
# 5.使用总结
- 编写装饰器抽象类,执行老活;
- 编写装饰器新活实现类,并重写执行方法,老活新活一起执行;
- 调用的地方直接使用装饰器调用,老活新活都调用了,且互不干扰;
- 使用享元模式,避免初始化太多对象,防止内存溢出;
- 使用到了装饰器模式和享元模式;
# 八.开具发票模块
# 1.建造者模式
建造者模式(Builder Pattern)也叫做生成器模式。将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
Product 产品类 (电子发票 个人+企业)
Builder 抽象建造者 (创建产品的抽象类) 规范产品的组建,一般是由子类实现
ConcreteBuilder 具体建造者 (真实创建发票的实现类 个人+企业) 实现抽象类定义的所有方法,并且返回一个组建好的对象。
Director 导演类 (调用端,1.直接用 service 层作为导演类; 2 如果你想要对这个建造模块进行一个非常好的封装,那么就单独创建一个导演类。然后 service 调用这个导演类即可) 负责安排已有模块的顺序,然后告诉 Builder 开始建造。 对于导演类,要看我们项目的需求复杂程度,如果 service 层面只有创建的逻辑,那么就无需导演类了,如果还有其他很多相关的逻辑,就做一个导演类。
# 2.原型模式
原型模式(Prototype Pattern)的简单程度仅次于单例模式。原型模式的核心是一个 clone 方法,通过该方法进行对象的拷贝,Java 提供了一个 Cloneable 接口来标示这个对象是可拷贝的
# 3.开具电子发票
申请电子发票(企业+个人) 建造者 + 原型(JDK自己有了)
项目需求:
用户支付完成后,部分企业用户或个人账户需要开电子增值税发票,实现该功能。
要求:
由于电子增值税发票所需内容较多,而且随国家政策可能会有所修改,发票内容的添加删除及后台对象的组装尽量做到灵活。
发票开具不是高并发访问接口且无法缓存,尽量保证发票创建的性能。
代码实现:
# 4.使用总结
- 创建个人和公司相关的实体类,公司多了银行账户字段;
- 添加发票建造者抽象类;
- 添加个人发票建造者类,复写方法,填充个人发票需要的字段;
- 添加公司发票建造者类,复写方法,填充公司发票需要的字段,并校验银行账户;\
- 添加常量类,初始化个人发票和公司发票实体类,主要是保证创建发票的性能;
- 使用克隆的方式,用到了原型模式来创建实体类,方便快捷;
- 使用的时候在 service 层创建相应的实际 builder 构建者;
- 使用了建造者模式和原型模式;
# 九.审计日志和 es 滚动查询
# 1.模版方法模式
模板方法模式确实非常简单,仅仅使用了 Java 的继承机制,但它是一个应用非常广泛的模式。其中,AbstractClass 叫做抽象模板,它的方法分为两类:
基本方法:
基本方法也叫做基本操作,是由子类实现的方法,并且在模板方法被调用。模板方法:
可以有一个或几个,一般是一个具体方法,也就是一个框架,实现对基本方法的调度,完成固定的逻辑。
注意 为了防止恶意的操作,一般模板方法都加上 final 关键字,不允许被覆写。
# 2.需求
项目需求:
公司 财务、审计、法务部门需要记录用户关键核心日志,防止日后未知的纠纷。
要求:
该日志记录不是普通的日志记录,不同的日志记录内容可能不一样
可以灵活增加日志种类和处理;
核心日志包括: 登录; 订单创建; 订单支付。其中订单创建需要有相关产品信息;订单支付需要有相关产品信息以及支付方式和支付金额。
日志组装完成后,将日志信息发送到 queue 中,会由数据处理部门进行处理。
# 3.使用总结
- 创建日志的实体类;
- 创建日志的抽象类,编写抽象方法,和不被复写的方法;
- 编写日志,订单,支付日志处理器,复写自己的详细的 detail 信息;
- 使用到了模版方法模式;
# 4.迭代器模式
- Iterator 抽象迭代器 抽象迭代器负责定义访问和遍历元素的接口
- ConcreteIterator 具体迭代器 具体迭代器角色要实现迭代器接口,完成容器元素的遍历。
- Aggregate 抽象容器 容器角色负责提供创建具体迭代器角色的接口,必然提供一个类似 createIterator()这样的方法,在 Java 中一般是 iterator()方法。
- Concrete Aggregate 具体容器 具体容器实现容器接口定义的方法
# 5.需求
项目需求:
数据从 mysql 迁移至 Es,Es 数据查询的默认 fetchSize 最大为 10000. 如果查询超过 10000 条数据,需要通过 scroll 形式进行查询。
要求:
出于安全问题考虑,查询需要直连 ES-ip:9200, 不可使用第三方 Jar 包。
由于目前项目的查询方式是基于 mysql 的,为了减少改动,暂时使用 SQL 语句查询,ES-IP:9200/_sql
我们需要将结果以 stream 的形式进行返回。(避免我们的内存占用过大以及瞬时的网络带宽问题。)
# 6.使用总结
- 创建查询条件类和结果类;
- 创建迭代查询处理器,里面封装了迭代的逻辑;
- 使用到了迭代器模式;
- 迭代器模式场景较为特殊;
# 7.第三方账号登录原理
自建账号体系的注册和登录,对用户来讲,过程很繁琐,结果 很多用户并不想注册你开发的网站或 APP, 所以用户量增长缓慢。此时可考虑用第三方账号登录,比如微信登录和 QQ 登录。
当用户点击第三方登录时,会跳转到第三方登录 SDK 内部;用户输入第三方登录用户名或密码,有些第三方登录平台,可以直接调用已经登录的账号,例如:QQ;完成第三方平台登录的;登录完成后,第三方平台,或者 SDK 会回调我们的应用,在回调的信息里面,可以拿到用户在第三方平台的 OpenId,以及昵称,头像等信息。
# 8.桥接模式
桥接模式,是一个比较简单的模式。
- Abstraction——抽象化角色 它的主要职责是定义出该角色的行为,同时保存一个对实现化角色的引用,该角色一般是抽象类。
- Implementor——实现化角色 它是接口或者抽象类,定义角色必需的行为和属性。
- RefinedAbstraction——修正抽象化角色 它引用实现化角色对抽象化角色进行修正。
- ConcreteImplementor——具体实现化角色 它实现接口或抽象类定义的方法和属性。
桥梁模式的优点:
- 抽象和实现分离
- 优秀的扩充能力
- 实现细节对客户透明
# 9.多种类第三方账号登录
使用桥接模式
项目需求:
为了简化用户的登录注册流程,允许用户直接使用经过授权的第三方账号登录网站。
要求:
- 遵循开闭原则:可以新增抽象部分和实现部分,且它们之间不会互相影响。
- 遵循单一职责原则:抽象部分专注于处理高层逻辑,实现部分处理平台细节。
# 10.使用总结
- 创建登录接口;
- 支付宝登录,微博登录实现登录接口;
- 创建登录处理器抽象类,构造器需要登录接口,提供执行登录的抽象方法;
- 封装到第三方类的处理中,对外暴露抽象类,屏蔽接口实现细节;
- 使用到了桥接模式;
- 第三方登录也可以使用适配器模式;
# 十.树形结构删除与新增
# 1.组合模式
- Component 抽象构件角色 定义参加组合对象的共有方法和属性
- Leaf 叶子构件 叶子对象,其下再也没有其他的分支,也就是遍历的最小单位。
- Composite 树枝构件 树枝对象,它的作用是组合树枝节点和叶子节点形成一个树形结构。
# 2.访问者模式
表示一个作用于某对象结构中的各个(层级啊)元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
- Visitor——抽象访问者 (ItemVisitor) 抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是 visit 方法的参数定义哪些对象是可以被访问的。
- ConcreteVisitor——具体访问者 它影响访问者访问到一个类后该怎么干,要做什么事情。
- Element——抽象元素 (AbstractProductItem) 接口或者抽象类,声明接受哪一类访问者访问,程序上是通过 accept 方法中的参数来定义的。
- ConcreteElement——具体元素 (ProductItem) 实现 accept 方法,通常是 visitor.visit(this),基本上都形成了一种模式了。
- ObjectStruture——结构对象(ProductItem)
# 3.层级结构的删除与增加
项目需求:
目前商城中有很多商品目录,且层级很多。为了对层级目录进行管理,需要满足对层级目录的增加和删除。
要求:
- 层及目录是保存在 DB 中的,项目一旦初始化,需要将层及目录设置为超热点缓存。
- 支持在线对层级目录的增删。
- 前端获取一次层及目录后,每隔 24 小时对层级目录进行后台重新获取。
- 层及目录更新需要先更新 redis 缓存,再更新 DB。后台层及目录缓存应该为永不过期缓存。
# 5.使用总结
- 构建抽象节点,只包含新增和删除方法;
- 构建基础节点,添加属性,并复写抽象方法;
- 添加访问者接口;
- 添加新增访问者实现,添加新增的逻辑;
- 新增删除访问者实现,添加删除的逻辑;
- 组合模式主要是用于构建树形结构;
- 访问者模式主要是用于对组合模式的具体的行为操作,比如新增或者删除;
- 使用到了组合模式和访问者模式;