转:设计模式之美

发布时间 2023-06-10 17:53:22作者: MyMemo

转自: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) { ... }
}

图形说明

  • 泛化:空心三角箭头实线
  • 实现:空心三角箭头虚线
  • 聚合:空心菱形箭头实线
  • 组合:实心菱形箭头实线
  • 关联:实心三角箭头实现
  • 依赖:实心三角箭头虚线

image