设计模式 Acemyzoe

奥卡姆剃刀 : 如无必要,勿增实体

目的:告别写被人吐槽的烂代码,提高复杂代码的设计开发能力,让读源码学框架事半功倍。

如何评价代码质量高低?

  • 可维护性 maintainability

主观、侧面: bug是否容易修复,修改、添加功能是否能够轻松完成。

  • 可读性 readability

代码是否符合编码规范、命名是否达意、注释是否详尽、函数是否长短合适、模块划分是否清晰、是否符合高内聚低耦合等等。

code review 测验代码可读性

  • 可拓展性 extensibility

代码预留了一些功能扩展点,可以把新功能代码直接插到扩展点上。

“对修改关闭,对扩展开放”

  • 灵活性 flexibility

代码易扩展、易复用或者易用

  • 简洁性 simplicity

KISS

  • 可复用性 reusability

DRY (Don’t Repeat Yourself)

  • 可测试性 testability

思考:函数是较小的可复用单位,面向对象把可复用单位提升到类层次,设计模式把可复用对提升到框架层次。

面向对象

面向对象中的继承、多态能让我们写出可复用的代码

面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。

面向对象软件开发:OOA>OOD>OOP (分析>设计>编程)

UML(Unified Model Language) 统一建模语言

封装、抽象、继承、多态

  • 封装:隐藏信息、保护数据
  • 抽象 : 隐藏方法的具体实现
  • 继承:Java 使用extends 关键字,C++ 使用冒号(class B : public A),Python 使用(),Ruby 使用 <。
  • 多态:子类可以替换父类。用接口类来实现多态特性;duck-typing(只要两个类具有相同的方法,就可以实现多态)

面向对象编程 VS 面向过程编程

  • 面向过程风格的代码被组织成了一组方法集合及其数据结构,方法和数据结构的定义是分开的。
  • 面向对象风格的代码被组织成一组类,方法和数据结构被绑定一起,定义在类中。

接口 VS 抽象类

  • 接口:协议/约定,实现面向对象的抽象多态和基于接口而非实现的设计原则,为了代码复用。
    • 接口不能包含属性(也就是成员变量)
    • 接口只能声明方法,方法不能包含代码实现。
    • 类实现接口的时候,必须实现接口中声明的所有方法。
  • 抽象类:实现面向对象的继承和模板设计模式,侧重于解耦。
    • 抽象类不允许实例化,只能被继承
    • 抽象类可以包含属性和方法
    • 子类继承抽象类,必须实现抽象类中的所有抽象方法
  • C++ 只支持抽象类,不支持接口;Python abc 抽象基类(可用普通类/duck-typing来模拟)。
  • 表示is-a的关系,为解决代码复用的问题,用抽象类。(先有子类的代码重复,然后再抽象成上层的父类。)
  • 表示has-a / behaves like的关系,为解决抽象的问题,用接口。(先设计接口,再去考虑具体的实现。)

基于接口而非实现编程 / 基于抽象而非实现编程

  • 函数的命名不能暴露任何实现细节
  • 封装具体的实现细节
  • 为实现类定义抽象的接口

  • 多用组合少用继承
    • 继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。
    • 这三个作用可以通过组合、接口、委托三个技术手段来达成。

面向过程的贫血模型和面向对象的充血模型

  • 基于贫血模型的 MVC 三层架构开发模式

    • Model/View/Controller 展示层、逻辑层、数据层
      • 后端项目分为 Repository 数据访问、Service业务逻辑 、Controller暴露接口
      • 贫血模型的Service层将数据与操作分离

    大部分都是SQL 驱动(SQL-Driven)的开发模式。一个后端接口的开发,看接口需要的数据对应到数据库中,需要哪张表或者哪几张表,然后编写 SQL 语句来获取数据。之后就是定义类,然后模板式地往对应的 Repository、Service、Controller 类中添加代码。

  • 基于充血模型的 DDD (领域驱动设计)开发模式

    • 更适合业务复杂的系统开发,比如金融系统
    • DDD :用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互
    • 微服务加速了领域驱动设计的盛行。关键在于对业务的熟悉程度。
    • 充血模型的Service层分service和domain,包含数据和业务逻辑

    应用基于充血模型的 DDD 的开发模式,需要事先理清楚所有的业务,定义领域模型所包含的属性和 方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。

设计原则

设计原则中的单一职责、DRY、基于接口而非实现、里式替换原则等,可以让我们写出可复用、灵活、可性好、易扩展、易维护的代码;

SOLID 原则 -SRP

单一职责原则 Single Responsibility Principle

  • A class or module should have a single responsibility.
  • 持续重构:粗粒度的类 > 细粒度的类
    • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,考虑对类拆分。
    • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,考虑拆分。
    • 私有方法过多,能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用。
    • 比较难给类起一个合适名字或用业务名词概括,说明类的职责定义不够清晰。
    • 类中大量的方法都是集中操作类中的某几个属性,将这几个属性和对应的方法拆分出来。

SOLID 原则 -OCP

开闭原则 Open Closed Principle

  • software entities (modules, classes, functions, etc.) should be open for extension ,but closed for modification.
  • 在写代码的时候后,思考一下这段代码未来可能有哪些需求变更、如何设计代码结构,预留扩展点。在识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。
  • 利用多态、依赖注入、抽象意识、基于接口而非实现编程,来实现“对扩展开放、对修改关闭”。

SOLID 原则 -LSP

里式替换原则 Liskov Substitution Principle

  • If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program.
  • 子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
  • Design By Contract 子类在设计的时候,要遵守父类的行为约定/协议,在保证兼容的前提条件下做扩展和调整,相当于细粒度的开闭原则。

SOLID 原则 -ISP

接口隔离原则 Interface Segregation Principle

  • Clients should not be forced to depend upon interfaces that they do not use.
  • 接口调用者不应该强迫依赖它不需要的接口。
  • 接口调用者只使用部分接口或者接口部分功能,则接口设计NO SRP
  • 多个特定客户端接口要好于一个宽泛用途的接口.

SOLID 原则 -DIP

依赖反转原则 Dependency Inversion Principle

  • High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions. 用来指导框架层面的设计:高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细 节,具体实现细节依赖抽象。

  • 控制反转 IOC Inversion Of Control

“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员“反转”到了框架。

  • 依赖注入 DI Dependency Injection

不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

More

  • KISS 原则 Keep It Simple and Stupid.

    • 不要使用同事可能不懂的技术来实现代码
    • 不要重复造轮子,要善于使用已经有的工具类库
    • 不要过度优化
  • YAGNI 原则 You Ain’t Gonna Need It

  • DRY 原则 Don’t Repeat Yourself

    • 实现逻辑重复、功能语义重复、代码执行重复
    • 代码复用意识:。在设计每个模块、类、函数的时候,要像设计一个外部 API 一样去思考它的复用性。
  • LOD 原则 Law of Demeter

    高内聚、松耦合 : 对于类,“高内聚”用来指导类本身的设计,相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。“松耦合”用来指导类与类之间依赖关系的设计。

    • 又称:最小知识原则 The Least Knowledge Principle

    Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.

    • 不该有直接依赖关系的类之间,不要有依赖
    • 有依赖关系的类之间,尽量只依赖必要的接口
    • 迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。

业务系统开发

端到端(end to end)开发一个完整的系统,工作包括:前期的需求沟通分析、中期的代码设计实现、后期的系统上线维护等。

  • 需求分析

爱因斯坦说过,“创造的一大秘诀是要懂得如何隐藏你的来源”

产品设计文档(PRD)、线框图、、用户用例(user case)

  • 系统设计

聚焦架构层面,针对模块。

  1. 合理地将功能划分到不同模块。为了避免业务知识的耦合,让下层系统更加通用,下层系统(也就是被调用的系统)不应该包含太多上层系统(也就是调用系统)的业务信息。
  2. 设计模块与模块之间的交互关系。常见交互方式一种是同步接口调用,另一种是利用消息中间件异步调用。
  3. 设计模块的接口、数据库、业务模型
  4. 业务模型/业务逻辑:Controller 层负责接口暴露,Repository 层负责数据读写,Service 层负责核心业务逻辑。
    1. 分层的作用:代码复用、隔离变化、提高代码可测试性。
    2. 应对复杂系统:水平方向基于业务拆分-模块化;垂直方向基于流程拆分-分层。

设计模式

设计模式可以让我们写出易扩展的代码

  • 创建型
    • 常用:单例模式、工厂模式、建造者模式
    • 不常用:原型模式
  • 结构型
    • 常用:代理模式、桥接模式、装饰者模式、适配器模式
    • 不常用:门面模式、组合模式、享元模式
  • 行为型
    • 常用:观察者模式、模板模式、策略模式、职责链模式、迭代器模式、状态模式
    • 不常用:访问者模式、备忘录模式、命令模式、解释器模式、中介模式

创建型模式

创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。

  • 单例模式用来创建全局唯一的对象。
  • 工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。
  • 建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
  • 原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的。

单例模式

Singleton Design Pattern 一个类只允许创建一个对象(或者实例)

实例:处理资源访问冲突、表示全局唯一类

实现方式:饿汉式、懒汉式、双重检测、静态内部类、枚举。

工厂模式

Factory Design Pattern

当创建逻辑比较复杂时考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。

实例:规则配置解析,代码中存在 if-else 分支判断,动态地根据不同的类型创建不同的对象

  • 简单工厂/静态工厂方法

    一个类只负责对象的创建,类一般以“Factory”结尾,创建对象的方法一般以create 开头

  • 工厂方法

    将复杂的创建逻辑拆分到多个工厂类

  • 抽象工厂

建造者模式

builder模式/生成器模式

让建造者类来负责对象的创建工作,用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。

顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨。

原型模式

对象的创建成本比较大时,利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象。

对象的创建成本比较大:对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取。

实现方式:深拷贝、浅拷贝

结构型模式

结构型设计模式主要解决“类或对象的组合或组装”问题,即将不同功能代码解耦。

  • 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
  • 桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
  • 装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
  • 适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

代理模式

Proxy Design Pattern :在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。一般情况下,我们让代理类和原始类实现同样的接口,或者让代理类继承原始类的方法。

动态代理(Dynamic Proxy),就是不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类。

应用场景:业务系统的非功能性需求开发比如:监控、统计、鉴权、限流、事务、幂等、日志。

​ 在 RPC(远程代理)、缓存中的应用。

桥接模式

Bridge Design Pattern :Decouple an abstraction from its implementation so that the two can vary independently. 将抽象和实现解耦,让它们可以独立变化。

组合优于继承。

装饰器模式

解决继承关系过于复杂的问题,通过组合来替代继承。主要作用是给原始类添加增强功能。

适配器模式

Adapter Design Pattern 让原本由于接口不兼容而不能一起工作的类可以一起工作

两种实现方式:类适配器 (使用继承实现) 和对象适配器 (使用组合实现)

使用场景(事后补救):封装有缺陷的接口设计、统一多个类的接口设计、替换依赖的外部系统、兼容老版本接口、适配不同格式的数据

行为型模式

行为型设计模式主要解决的就是“类或对象之间的交互”问题,即将不同的行为代码解耦。

观察者模式

Observer Design Pattern 或称发布订阅模式(Publish-Subscribe Design Pattern)。

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

Subject-Observer、Publisher-Subscriber、Producer-Consumer、Event Emitter-Event Listener、Dispatcher-Listener。

模板模式

Template Method Design Pattern

Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure. 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

模板模式的两大作用:复用和扩展。复用指所有的子类可以复用父类中提供的模板方法的代码。扩展指框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。回调具有同样功能。

应用场景上看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。

代码实现上看,回调基于组合,把一个对象传递给另一个对象。模板模式基于继承,子类重写父类的抽 象方法。

策略模式

Strategy Design Pattern

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. 定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。

  • 策略类的定义,包含一个策略接口和一组实现这个接口的策略类
  • 策略的创建,一般会通过类型(type)来判断创建哪个策略来使用
  • 策略的使用,运行时动态确定使用哪种策略

应用场景:利用它来避免冗长的 if-else 或 switch 分支判断。

​ 本质上点讲,是借助“查表法”,根据 type 查表替代根据 type 分支判断。

职责链模式

Chain Of Responsibility Design Pattern

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it. 将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。

应用场景:常用来开发框架的过滤器和拦截器,让框架的使用者在不需要修改框架源码的情况下,添加新的过滤拦截功能。

状态模式

状态机/有限状态机 FSM ,Finite State Machine

状态机有3 个组成部分:状态(State)、事件(Event)、动作(Action)

实现方式:分支逻辑法、查表法、状态模式

迭代器模式

Iterator Design Pattern

用来遍历集合对象/容器,如数组、链表、树、图。

一个完整的迭代器模式,一般会涉及容器和容器迭代器两部分内容。

为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。

容器中需要定义 iterator() 方法,用来创建迭代器。迭代器接口中需要定义hasNext()、currentItem()、next() 三个最基本的方法。容器对象通过依赖注入传递到迭代器类中。

编程规范

编码规范能让我们写出可读性好的代码

代码重构

持续重构可以时刻保持代码的可维护性

  • 重构的目的(why)、对象(what)、时机(when)、方法(how)
  • 保证重构不出错的技术手段:单元测试 (代码的可测试性)
    • 单元测试的测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。
  • 两种不同规模的重构:大重构(大规模高层次)和小重构(小规模低层次、编程规范)。
    • 大型重构:系统、模块、代码结构、类与类之间的关系等的重构
      • 重构的手段有:分层、模块化、解耦、抽象可复用组件
    • 小型重构:对类、函数、变量等代码级别的重构
      • 编码规范:规范命名、规范注释、消除超大类或函数、提取重复代码