转:设计模式之美
Posted My Memo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了转:设计模式之美相关的知识,希望对你有一定的参考价值。
转自:https://juejin.cn/post/7123029355365662734
1. 概述
1.1 学习导读
本文是极客时间专栏《设计模式之美》的学习笔记,详情请看原文。
学习算法:是为了写出 高效 的代码;
学习设计模式:是为了写出 高质量 (可扩展、可读、可维护)的代码;
1.2 为什么学习设计模式
- 应对面试,算法、设计模式之类问题是常问题,有备无患。
- 告别烂代码,代码能力是一个程序员最基础的能力,是一个程序员基础素养的最直接的衡量标准。代码写得好,能让你在团队中脱颖而出。写出一份漂亮的代码,你自己也会很有成就感。
- 提高复杂代码的设计和开发能力,只是完成功能、代码能用,可能并不复杂,但是要想写出易扩展、易用、易维护的代码,并不容易。刻意练习这方面的能力,让写出高质量代码成为一种习惯。
- 让读源码、学框架事半功倍,优秀的开源项目、框架、中间件,代码量、类的个数都会比较多,类结构、类之间的关系极其复杂,代码中会使用到很多设计模式、设计原则或者设计思想,学好相关知识,能让你更轻松地读懂开源项目,还能参透技术精髓,做到事半功倍。
- 职场发展做铺垫,如果你想成长为技术大牛,那就要重视基本功。如果你需要承担一些指导培养初级员的工作,你需要一套写好代码的方法论。如果你是一个技术leader,你需要为项目质量负责,代码质量低会导致线上 bug 频发,排查困难,整个团队都陷在成天修改无意义的低级 bug、在烂代码中添补丁的事情中,而一个设计良好、易维护的系统,可以解放我们的时间,让我们做些更加有意义、更能提高自己和团队能力的事情。如果你需要招聘技术人员,你要考察候选人的设计能力、代码能力,那设计模式相关的问题便是一个很好的考察点。
2. 代码质量评判标准
2.1 如何评价代码质量的高低?
对一段代码的质量评价,标准多,常常具有很强的主观性,需要综合各个维度。
2.2 最常用的评价标准有哪几个?
- 可维护性:在不破坏原有代码设计、不引入新的bug的情况下,能够快速地修改或者添加代码。
- 可读性:需要看代码是否符合编码规范、命名是否达意、注释是否详尽、函数是否长短合适、模块划分是否清晰、是否符合高内聚低耦合等等。
- 可扩展性:代码预留扩展点,你可以把新功能代码,直接插到扩展点上,无需改动大量的原始代码。
- 灵活性:一段代码易扩展、易复用或者易用,我们都可以称这段代码写得比较灵活。
- 简洁性:代码简单、逻辑清晰,也就意味着易读、易维护。思从深而行从简,真正的高手能云淡风轻地用最简单的方法解决最复杂的问题。
- 可复用性:尽量减少重复代码的编写,复用已有的代码。
- 可测试性:代码的可测试性差,比较难写单元测试,那基本上就能说明代码设计得有问题。
2.3 如何才能写出高质量的代码?
- 面向对象:因为其具有丰富的特性(封装、抽象、继承、多态),可以实现很多复杂的设计思路,是很多设计原则、设计模式等编码实现的基础。
- 设计原则:设计原则是指导我们代码设计的一些经验总结。对于某些场景下,是否应该应用某种设计模式,具有指导意义。比如,“开闭原则”是很多设计模式(策略、模板等)的指导原则。
- 设计模式:设计模式是针对软件开发中经常遇到的一些设计问题,总结出来的一套解决方案或者设计思路。大部分设计模式要解决的都是代码的可扩展性问题。从抽象程度上来讲,设计原则比设计模式更抽象。设计模式更加具体、更加可执行。
- 编程规范:编程规范主要解决的是代码的可读性问题,更加偏重代码细节,是持续的小重构依赖的理论基。
- 代码重构:利用前面四种理论,保持代码质量不下降的有效手段。
3. 面向对象
3.1 面向对象概述
3.1.1 三种主流的编程范式
- 面向过程
- 面向对象
- 函数式编程
3.1.2 面向对象
- 面向对象编程 (OOP,Object Oriented Programming):是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。
- 面向对象编程语言 (OOPL,Object Oriented Language):是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。
3.2 面向对象四大特性
3.2.1 封装
概念:信息隐藏或数据访问保护,类通过暴露有限的访问接口,授权外部仅能通过类提供的方式访问内部信息或数据。
特点:需要编程语言提供权限访问控制语法来支持,例如 Java 中的 private、protected、public 关键字
public class Wallet
private String id;
private long createTime;
private BigDecimal balance;
private long balanceLastModifiedTime;
// ...省略其他属性...
public Wallet()
this.id = IdGenerator.getInstance().generate();
this.createTime = System.currentTimeMillis();
this.balance = BigDecimal.ZERO;
this.balanceLastModifiedTime = System.currentTimeMillis();
// 注意:下面对get方法做了代码折叠,是为了减少代码所占文章的篇幅
public String getId() return this.id;
public long getCreateTime() return this.createTime;
public BigDecimal getBalance() return this.balance;
public long getBalanceLastModifiedTime() return this.balanceLastModifiedTime;
public void increaseBalance(BigDecimal increasedAmount)
if (increasedAmount.compareTo(BigDecimal.ZERO) < 0)
throw new InvalidAmountException("...");
this.balance.add(increasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
public void decreaseBalance(BigDecimal decreasedAmount)
if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0)
throw new InvalidAmountException("...");
if (decreasedAmount.compareTo(this.balance) > 0)
throw new InsufficientAmountException("...");
this.balance.subtract(decreasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
意义:
- 保护数据不被随意修改,提高代码的可维护性
- 仅暴露有限的必要接口,提高类的易用性
3.2.2 抽象
概念:隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
特点:常利用编程语言提供的 接口类(如Java中的Interface)或抽象类(如Java中的abstract) 这两种语法机制来实现抽象。
public interface IPictureStorage
void savePicture(Picture picture);
Image getPicture(String pictureId);
void deletePicture(String pictureId);
void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);
public class PictureStorage implements IPictureStorage
// ...省略其他属性...
@Override
public void savePicture(Picture picture) ...
@Override
public Image getPicture(String pictureId) ...
@Override
public void deletePicture(String pictureId) ...
@Override
public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) ...
意义:
- 修改实现不需要改变定义
- 处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息
3.2.3 继承
概念:表示类之间的is-a关系,比如:猫是一种哺乳动物。
特点:编程语言需要提供特殊的语法机制来支持。比如 Java 使用 extends 关键字来实现继承,C++ 使用冒号(class B : public A),Python 使用 parentheses (),Ruby 使用 <。
2种模式:
- 单继承表示一个子类只继承一个父类
- 多继承表示一个子类可以继承多个父类
意义:解决代码复用的问题,两个类具有相同属性或方法,将这部分代码抽取到父类中,让两个类继承父类,子类重用父类代码,避免代码重复。
缺陷:过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。
3.2.4 多态
概念:子类可以替代父类,在实际的运行过程中,调用子类的方法实现。
特点:需要编程语言提供特殊的语法机制来实现,比如继承、接口类、duck-typing。
利用继承实现多态特性。
- 第一个语法机制是编程语言要支持父类对象可以引用子类对象。
- 第二个语法机制是编程语言要支持继承。
- 第三个语法机制是编程语言要支持子类可以重写(override)父类中的方法。
public class DynamicArray
private static final int DEFAULT_CAPACITY = 10;
protected int size = 0;
protected int capacity = DEFAULT_CAPACITY;
protected Integer[] elements = new Integer[DEFAULT_CAPACITY];
public int size() return this.size;
public Integer get(int index) return elements[index];
//...省略n多方法...
public void add(Integer e)
ensureCapacity();
elements[size++] = e;
protected void ensureCapacity()
//...如果数组满了就扩容...代码省略...
public class SortedDynamicArray extends DynamicArray
@Override
public void add(Integer e)
ensureCapacity();
int i;
for (i = size-1; i>=0; --i) //保证数组中的数据有序
if (elements[i] > e)
elements[i+1] = elements[i];
else
break;
elements[i+1] = e;
++size;
public class Example
public static void test(DynamicArray dynamicArray)
dynamicArray.add(5);
dynamicArray.add(1);
dynamicArray.add(3);
for (int i = 0; i < dynamicArray.size(); ++i)
System.out.println(dynamicArray.get(i));
public static void main(String args[])
DynamicArray dynamicArray = new SortedDynamicArray();
test(dynamicArray); // 打印结果:1、3、5
利用接口类来实现多态特性。
public interface Iterator
boolean hasNext();
String next();
String remove();
public class Array implements Iterator
private String[] data;
public boolean hasNext() ...
public String next() ...
public String remove() ...
//...省略其他方法...
public class LinkedList implements Iterator
private LinkedListNode head;
public boolean hasNext() ...
public String next() ...
public String remove() ...
//...省略其他方法...
public class Demo
private static void print(Iterator iterator)
while (iterator.hasNext())
System.out.println(iterator.next());
public static void main(String[] args)
Iterator arrayIterator = new Array();
print(arrayIterator);
Iterator linkedListIterator = new LinkedList();
print(linkedListIterator);
使用duck-typing 实现多态特性。
只要两个类具有相同的方法,就可以实现多态,并不要求两个类之间有任何关系,这就是所谓的 duck-typing。
class Logger:
def record(self):
print(“I write a log into file.”)
class DB:
def record(self):
print(“I insert data into db. ”)
def test(recorder):
recorder.record()
def demo():
logger = Logger()
db = DB()
test(logger)
test(db)
意义:提高代码的扩展性和复用性,很多设计原则、设计模式、编程技巧的代码实现基础。比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。
3.3 面向过程 VS 面向对象
3.3.1 面向过程
- 面向过程编程(POP):一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。
- 面向过程编程语言(POPL):最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。
- 面向过程和面向对象最基本的区别就是,代码的组织方式不同。
3.3.2 面向对象编程 VS 面向过程编程
- 对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。面向对象编程比起面向过程编程,更能应对这种复杂类型的程序开发。
- 面向对象编程相比面向过程编程,具有更加丰富的特性(封装、抽象、继承、多态)。利用这些特性编写出来的代码,更加易扩展、易复用、易维护。
- 从编程语言跟机器打交道的方式的演进规律中,我们可以总结出:面向对象编程语言比起面向过程编程语言,更加人性化、更加高级、更加智能。
3.3.3 违反面向对象编程风格的典型设计
- 滥用getter、setter方法
- Constants类、Utils类的设计问题
- 基于贫血模型的开发模式
3.3.4 在OOP中,为什么容易写出面向过程代码
- 面向过程编程风格恰恰符合人的这种流程化思维方式。而面向对象编程风格正好相反。它是一种自底向上的思考方式。
- 面向对象编程要比面向过程编程难一些。在面向对象编程中,类的设计还是挺需要技巧,挺需要一定设计经验的。
3.3.5 面向过程编程使用场景
开发的是微小程序,面向过程的编程风格就更适合一些。
面向过程编程是面向对象编程的基础,面向对象编程离不开基础的面向过程编程。
只要我们能避免面向过程编程风格的一些弊端,控制好它的副作用,在掌控范围内为我们所用,我们就大可不用避讳在面向对象编程中写面向过程风格的代码。
3.4 面向对象分析、设计与编程
3.4.1 面向对象分析、设计与编程
面向对象分析(OOA,Object Oriented Analysis) 、面向对象设计(OOD,Object Oriented Design)、面向对象编程(OOP,Object Oriented Program),是面向对象开发的三个主要环节。
- 面向对象分析:搞清楚做什么,产出详细的需求分析
- 面向对象设计:搞清楚怎么做,将需求描述转化为具体的类
- 面向对象编程:将分析和设计的结果翻译成代码的过程
3.4.2 面向对象分析(OOA,Object Oriented Analysis)
需求分析的过程实际上是一个不断迭代优化的过程。我们不要试图一下就给出一个完美的解决方案,而是先给出一个粗糙的、基础的方案,有一个迭代的基础,通过“提出问题 - 解决问题”的方式,循序渐进地进行优化,最后得到一个足够清晰、可落地的需求描述。这样一个思考过程能让我们摆脱无从下手的窘境。
3.4.3 面向对象设计(OOD,Object Oriented Design)
面向对象设计和实现要做的事情就是把合适的代码放到合适的类中。至于到底选择哪种划分方法,判定的标准是让代码尽量地满足“松耦合、高内聚”、单一职责、对扩展开放对修改关闭等我们之前讲到的各种设计原则和思想,尽量地做到代码可复用、易读、易扩展、易维护。
面向对象分析的产出是详细的需求描述。面向对象设计的产出是类。在面向对象设计这一环节中,我们将需求描述转化为具体的类的设计。这个环节的工作可以拆分为下面四个部分。
-
划分职责进而识别出有哪些类
根据需求描述,我们把其中涉及的功能点,一个一个罗列出来,然后再去看哪些功能点职责相近,操作同样的属性,可否归为同一个类。 -
定义类及其属性和方法
我们识别出需求描述中的动词,作为候选的方法,再进一步过滤筛选出真正的方法,把功能点中涉及的名词,作为候选属性,然后同样再进行过滤筛选。 -
定义类与类之间的交互关系
UML 统一建模语言中定义了六种类之间的关系。它们分别是:泛化、实现、关联、聚合、组合、依赖。我们从更加贴近编程的角度,对类与类之间的关系做了调整,保留四个关系:泛化、实现、组合、依赖。 -
将类组装起来并提供执行入口
我们要将所有的类组装在一起,提供一个执行入口。这个入口可能是一个 main() 函数,也可能是一组给外部用的 API 接口。通过这个入口,我们能触发整个代码跑起来。
3.4.4 统一建模语言(UML,Unified Model Language)
UML 统一建模语言,面向对象设计分析的工具,常用来表达设计思路。
UML 统一建模语言定义了六种类之间的关系。它们分别是:泛化、实现、关联、聚合、组合、依赖。
- 泛化**(Generalization)可以简单理解为继承关系。
public class A ...
public class B extends A ...
- 实现(Realization)一般是指接口和实现类之间的关系。
public interface A ...
public class B implements A ...
- 聚合(Aggregation)是一种包含关系,A 类对象包含 B 类对象,B 类对象可以单独存在,比如课程与学生之间的关系。
public class A
private B b;
public A(B b)
this.b = b;
- 组合(Composition)也是一种包含关系,A 类对象包含 B 类对象,B 类对象不可单独存在,比如鸟与翅膀之间的关系。
public class A
private B b;
public A()
this.b = new B();
- 关联(Association)是一种非常弱的关系,B 类对象是 A 类的成员变量,那 B 类和 A 类就是关联关系。
public class A
private B b;
public A(B b)
this.b = b;
或者
public class A
private B b;
public A()
this.b = new B();
- 依赖(Dependency)是一种比关联关系更加弱的关系,只要 B 类对象和 A 类对象有任何使用关系,我们都称它们有依赖关系。
public class A
private B b;
public A(B b)
this.b = b;
或者
public class A
private B b;
public A()
this.b = new B();
或者
public class A
public void func(B b) ...
图形说明
- 泛化:空心三角箭头实线
- 实现:空心三角箭头虚线
- 聚合:空心菱形箭头实线
- 组合:实心菱形箭头实线
- 关联:实心三角箭头实现
- 依赖:实心三角箭头虚线
3.5 接口VS抽象类
3.5.1 抽象类特性
- 抽象类不允许被实例化,只能被继承。也就是说,你不能 new 一个抽象类的对象出来。
- 抽象类可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不实现的方法叫作抽象方法。
- 子类继承抽象类,必须实现抽象类中的所有抽象方法。
// 抽象类
public abstract class Logger
private String name;
private boolean enabled;
private Level minPermittedLevel;
public Logger(String name, boolean enabled, Level minPermittedLevel)
this.name = name;
this.enabled = enabled;
this.minPermittedLevel = minPermittedLevel;
public void log(Level level, String message)
boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
if (!loggable) return;
doLog(level, message);
protected abstract void doLog(Level level, String message);
// 抽象类的子类:输出日志到文件
public class FileLogger extends Logger
private Writer fileWriter;
public FileLogger(String name, boolean enabled,
Level minPermittedLevel, String filepath)
super(name, enabled, minPermittedLevel);
this.fileWriter = new FileWriter(filepath);
@Override
public void doLog(Level level, String mesage)
// 格式化level和message,输出到日志文件
fileWriter.write(...);
// 抽象类的子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger
private MessageQueueClient msgQueueClient;
public MessageQueueLogger(String name, boolean enabled,
Level minPermittedLevel, MessageQueueClient msgQueueClient)
super(name, enabled, minPermittedLevel);
this.msgQueueClient = msgQueueClient;
@Override
protected void doLog(Level level, String mesage)
// 格式化level和message,输出到消息中间件
msgQueueClient.send(...);
3.5.2 接口特性
- 接口不能包含属性(也就是成员变量)。
- 接口只能声明方法,方法不能包含代码实现。
- 类实现接口的时候,必须实现接口中声明的所有方法。
// 接口
public interface Filter
void doFilter(RpcRequest req) throws RpcException;
// 接口实现类:鉴权过滤器
public class AuthencationFilter implements Filter
@Override
public void doFilter(RpcRequest req) throws RpcException
//...鉴权逻辑..
// 接口实现类:限流过滤器
public class RateLimitFilter implements Filter
@Override
public void doFilter(RpcRequest req) throws RpcException
//...限流逻辑...
// 过滤器使用Demo
public class Application
// filters.add(new AuthencationFilter());
// filters.add(new RateLimitFilter());
private List<Filter> filters = new ArrayList<>();
public void handleRpcRequest(RpcRequest req)
try
for (Filter filter : filters)
filter.doFilter(req);
catch(RpcException e)
// ...处理过滤结果...
// ...省略其他处理逻辑...
3.5.3 抽象类和接口意义
- 抽象类是对成员变量和方法的抽象,是一种 is-a 关系,是为了解决代码复用问题。子类必须实现抽象方法,在某些需求下实现代码会更加优雅。
- 接口仅仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。
3.5.4 抽象类和接口应用场景
- 如果要表示一种 is-a 的关系,并且是为了解决代码复用问题,我们就用抽象类。
- 如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题,那我们就用接口。
3.6 基于接口而非实现编程
3.6.1 为什么基于接口而非实现编程
- 将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。
- 即“基于抽象而非实现编程",抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。
3.6.2 如何应用基于接口而非实现编程
- 函数的命名不能暴露任何实现细节。
- 封装具体的实现细节。
- 为实现类定义抽象的接口。 依赖统一的接口定义,使用者依赖接口,而不是具体实现类来编程。
3.6.3 是否需要为每个类定义接口
某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口。
3.7 多用组合少用继承
3.7.1 为什么不推荐使用继承
继承层次过深、过复杂,也会影响到代码的可维护性。 在这种情况下,我们应该尽量少用,甚至不用继承。
3.7.2 组合相比继承有哪些优势
- 继承的特性可以通过组合、接口、委托来达成。
- 组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题。
public interface Flyable
void fly();
public interface Tweetable
void tweet();
public interface EggLayable
void layEgg();
public class Ostrich implements Tweetable, EggLayable //鸵鸟
//... 省略其他属性和方法...
@Override
public void tweet() //...
@Override
public void layEgg() //...
public class Sparrow impelents Flyable, Tweetable, EggLayable //麻雀
//... 省略其他属性和方法...
@Override
public void fly() //...
@Override
public void tweet() //...
@Override
public void layEgg() //...
public interface Flyable
void fly();
public class FlyAbility implements Flyable
@Override
public void fly() //...
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility
public class Ostrich implements Tweetable, EggLayable //鸵鸟
private TweetAbility tweetAbility = new TweetAbility(); //组合
private EggLayAbility eggLayAbility = new EggLayAbility(); //组合
//... 省略其他属性和方法...
@Override
public void tweet()
tweetAbility.tweet(); // 委托
@Override
public void layEgg()
eggLayAbility.layEgg(); // 委托
3.7.3 如何判断该用组合还是继承
鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。
- 如果类之间的继承结构稳定,层次比较浅,关系不复杂,用继承。反之,用组合。
- 有一些设计模式、特殊的应用场景,会固定使用继承或者组合。
3.8 贫血模型VS充血模型
3.8.1 基于贫血模型的传统开发模式
MVC 三层架构中的 M 表示 Model,V 表示 View,C 表示 Controller。
很多 Web 或者 App 项目都是前后端分离的,后端负责暴露接口给前端调用。这种情况下,我们一般就将后端项目分为 Repository 层、Service 层、Controller 层。其中,Repository 层负责数据访问,Service 层负责业务逻辑,Controller 层负责暴露接口。
我们平时开发 Web 后端项目的时候,基本上都是这么组织代码的。其中,UserEntity 和 UserRepository 组成了数据访问层,UserBo 和 UserService 组成了业务逻辑层,UserVo 和 UserController 在这里属于接口层。
我们可以发现,UserBo 是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。业务逻辑集中在 UserService 中。我们通过 UserService 来操作 UserBo。换句话说,Service 层的数据和业务逻辑,被分割为 BO 和 Service 两个类中。像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。同理,UserEntity、UserVo 都是基于贫血模型设计的。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。
基于贫血模型的传统开发模式受欢迎原因主要有三点
- 大部分情况下,我们开发的系统业务可能都比较简单。
- 充血模型的设计要比贫血模型更加有难度。
- 思维已固化,转型有成本。
////////// Controller+VO(View Object) //////////
public class UserController
private UserService userService; //通过构造函数或者IOC框架注入
public UserVo getUserById(Long userId)
UserBo userBo = userService.getUserById(userId);
UserVo userVo = [...convert userBo to userVo...];
return userVo;
public class UserVo //省略其他属性、get/set/construct方法
private Long id;
private String name;
private String cellphone;
////////// Service+BO(Business Object) //////////
public class UserService
private UserRepository userRepository; //通过构造函数或者IOC框架注入
public UserBo getUserById(Long userId)
UserEntity userEntity = userRepository.getUserById(userId);
UserBo userBo = [...convert userEntity to userBo...];
return userBo;
public class UserBo //省略其他属性、get/set/construct方法
private Long id;
private String name;
private String cellphone;
////////// Repository+Entity //////////
public class UserRepository
public UserEntity getUserById(Long userId) //...
public class UserEntity //省略其他属性、get/set/construct方法
private Long id;
private String name;
private String cellphone;
3.8.2 基于充血模型的 DDD 开发模式
-
什么是DDD领域驱动设计
领域驱动设计,即 DDD,主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。领域驱动设计这个概念并不新颖,早在 2004 年就被提出了,到现在已经有十几年的历史了。不过,它被大众熟知,还是基于另一个概念的兴起,那就是微服务。除了监控、调用链追踪、API 网关等服务治理系统的开发之外,微服务还有另外一个更加重要的工作,那就是针对公司的业务,合理地做微服务拆分。而领域驱动设计恰好就是用来指导划分服务的。所以,微服务加速了领域驱动设计的盛行。
做好领域驱动设计的关键是,看你对自己所做业务的熟悉程度,而并不是对领域驱动设计这个概念本身的掌握程度。即便你对领域驱动搞得再清楚,但是对业务不熟悉,也并不一定能做出合理的领域设计。所以,不要把领域驱动设计当银弹,不要花太多的时间去过度地研究它。
-
什么是充血模型
充血模型(Rich Domain Model),数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。 -
基于充血模型的DDD开发模式
在基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain 就相当于贫血模型中的 BO。不过,Domain 与 BO 的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。而 Service 类变得非常单薄。
3.8.3 贫血模型 VS 充血模型
基于充血模型的DDD开发模式,跟基于贫血模型的传统开发模式的主要区别就在 Service 层,包含Service和Domain,Controller 层和 Repository 层的代码基本上相同,这两层包含的业务逻辑并不多,没必要做充血建模。
区别于Domain,Service 有几个职责:
- Service 类负责与 Repository 交流。保持领域模型的独立性,与其他层解耦。
- Service 类负责跨领域模型的业务聚合功能。
- Service 类负责一些非功能性及与三方系统交互的工作。 比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等。
虚拟钱包 VirtualWallet 类设计成一个充血的Domain领域模型,并且将原来在 Service 类中的部分业务逻辑移动到 VirtualWallet 类中,让 Service 类的实现依赖 VirtualWallet 类。
public class VirtualWallet // Domain领域模型(充血模型)
private Long id;
private Long createTime = System.currentTimeMillis();;
private BigDecimal balance = BigDecimal.ZERO;
public VirtualWallet(Long preAllocatedId)
this.id = preAllocatedId;
public BigDecimal balance()
return this.balance;
public void debit(BigDecimal amount)
if (this.balance.compareTo(amount) < 0)
throw new InsufficientBalanceException(...);
this.balance = this.balance.subtract(amount);
public void credit(BigDecimal amount)
if (amount.compareTo(BigDecimal.ZERO) < 0)
throw new InvalidAmountException(...);
this.balance = this.balance.add(amount);
public class VirtualWalletService
// 通过构造函数或者IOC框架注入
private VirtualWalletRepository walletRepo;
private VirtualWalletTransactionRepository transactionRepo;
public VirtualWallet getVirtualWallet(Long walletId)
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
VirtualWallet wallet = convert(walletEntity);
return wallet;
public BigDecimal getBalance(Long walletId)
return walletRepo.getBalance(walletId);
@Transactional
public void debit(Long walletId, BigDecimal amount)
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
VirtualWallet wallet = convert(walletEntity);
wallet.debit(amount);
VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
transactionEntity.setAmount(amount);
transactionEntity.setCreateTime(System.currentTimeMillis());
transactionEntity.setType(TransactionType.DEBIT);
transactionEntity.setFromWalletId(walletId);
transactionRepo.saveTransaction(transactionEntity);
walletRepo.updateBalance(walletId, wallet.balance());
@Transactional
public void credit(Long walletId, BigDecimal amount)
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
VirtualWallet wallet = convert(walletEntity);
wallet.credit(amount);
VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
transactionEntity.setAmount(amount);
transactionEntity.setCreateTime(System.currentTimeMillis());
transactionEntity.setType(TransactionType.CREDIT);
transactionEntity.setFromWalletId(walletId);
transactionRepo.saveTransaction(transactionEntity);
walletRepo.updateBalance(walletId, wallet.balance());
@Transactional
public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount)
//...跟基于贫血模型的传统开发模式的代码一样...
3.9 接口鉴权面向对象分析实战
3.9.1 第一轮基础分析
调用方每次进行接口请求的时候,都携带自己的 AppID 和密码。微服务在接收到接口调用请求之后,会解析出 AppID 和密码,跟存储在微服务端的 AppID 和密码进行比对。
3.9.2 第二轮分析优化
调用方将请求接口的 URL 跟 AppID、密码拼接在一起,然后进行加密,生成一个 token。调用方在进行接口请求的的时候,将这个 token 及 AppID,随 URL 一块传递给微服务端。微服务端接收到这些数据之后,根据 AppID 从数据库中取出对应的密码,并通过同样的 token 生成算法,生成另外一个 token。用这个新生成的 token 跟调用方传递过来的 token 对比。
3.9.3 第三轮分析优化
我们将 URL、AppID、密码、时间戳四者进行加密来生成 token。调用方在进行接口请求的时候,将 token、AppID、时间戳,随 URL 一并传递给微服务端。
微服务端在收到这些数据之后,会验证当前时间戳跟传递过来的时间戳,是否在一定的时间窗口内(比如一分钟)。
编程之美(转)
美国的贝尔实验室设计了最初的C语言
刻在UNIX操作系统距今已有三四十年
你在屏幕前凝视数据的缱绻
我却在旁轻轻敲打键盘把你的梦想展现
循环 递归 贪心 动规 是谁的从前
喜欢在匈牙利算法中你我牵手的画面
经过MSRA门前我以大牛之名许愿
思念像斐波那契数列般漫延
当软工沦落在设计的文档间
算法依旧是永垂不朽的诗篇
我给你的爱写在程序间
深藏在最长不下降子序列里面
几万组数据流过后发现
我的心依然不变
我给你的爱写在程序间
深藏在最长不下降子序列里面
用无尽的代码刻下了永远
那已保存千年的誓言
一切又重演我算了很多遍
时间复杂度还是趋于无限
我只想要这样永远链接在你的身边
编程,思维的碰撞!
以上是关于转:设计模式之美的主要内容,如果未能解决你的问题,请参考以下文章