转:设计模式之美

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门前我以大牛之名许愿

思念像斐波那契数列般漫延

当软工沦落在设计的文档间

算法依旧是永垂不朽的诗篇

我给你的爱写在程序间

深藏在最长不下降子序列里面

几万组数据流过后发现

我的心依然不变

我给你的爱写在程序间

深藏在最长不下降子序列里面

用无尽的代码刻下了永远

那已保存千年的誓言

一切又重演我算了很多遍

时间复杂度还是趋于无限

我只想要这样永远链接在你的身边

技术分享

技术分享

技术分享

技术分享

编程,思维的碰撞!

技术分享

以上是关于转:设计模式之美的主要内容,如果未能解决你的问题,请参考以下文章

算法之美

算法之美

设计模式之美 精华总结 笔记

设计模式之美

设计模式之美-创建型-单例模式

设计模式之美-行为型-观察者模式