NipGeihou's blog NipGeihou's blog
  • Java

    • 开发规范
    • 进阶笔记
    • 微服务
    • 快速开始
    • 设计模式
  • 其他

    • Golang
    • Python
    • Drat
  • Redis
  • MongoDB
  • 数据结构与算法
  • 计算机网络
  • 应用

    • Grafana
    • Prometheus
  • 容器与编排

    • KubeSphere
    • Kubernetes
    • Docker Compose
    • Docker
  • 组网

    • TailScale
    • WireGuard
  • 密码生成器
  • 英文单词生成器
🍳烹饪
🧑‍💻关于
  • 分类
  • 标签
  • 归档

NipGeihou

我见青山多妩媚,料青山见我应如是
  • Java

    • 开发规范
    • 进阶笔记
    • 微服务
    • 快速开始
    • 设计模式
  • 其他

    • Golang
    • Python
    • Drat
  • Redis
  • MongoDB
  • 数据结构与算法
  • 计算机网络
  • 应用

    • Grafana
    • Prometheus
  • 容器与编排

    • KubeSphere
    • Kubernetes
    • Docker Compose
    • Docker
  • 组网

    • TailScale
    • WireGuard
  • 密码生成器
  • 英文单词生成器
🍳烹饪
🧑‍💻关于
  • 分类
  • 标签
  • 归档
  • 设计模式之美

    • 读薄《设计模式之美》-导读
    • 读薄《设计模式之美》-理论
      • 面向对象编程
      • 面向对象编程语言
      • 面向对象编程和面向对象编程语言之间有何关系?
      • 面向对象分析和面向对象设计
      • 什么是UML?我们是否需要UML?
      • 四大特征
        • 封装(Encapsulation)
        • 抽象(Abstraction)
        • 继承(Inheritance)
        • 多态(Polymorphism)
  • 怪诞行为学

  • 读书笔记
  • 设计模式之美
NipGeihou
2022-10-16
目录

读薄《设计模式之美》-理论

# 面向对象编程

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

# 面向对象编程语言

面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。

如何判定一个编程语言是否是面向对象编程语言?

按照严格的定义(语法支持四大特征),很多语言都不能算得上面向对象编程语言

按照不严格的定义(只以类或对象作为组织代码的基本单元)来讲,现在流行的大部分编程语言都是面向对象编程语言。

实际上,对于什么是面向对象编程、什么是面向对象编程语言,并没有一个官方的、统一的定义。向对象编程诞生开始,这两个概念就在不停地演化,所以,也无法给出一个明确的定义,也没有必要给出一个明确定义。

# 面向对象编程和面向对象编程语言之间有何关系?

面向对象编程一般使用面向对象编程语言来进行,但是,不用面向对象编程语言,我们照样可以进行面向对象编程。反过来讲,即便我们使用面向对象编程语言,写出来的代码也不一定是面向对象编程风格的,也有可能是面向过程编程风格的。

# 面向对象分析和面向对象设计

  • 面向对象分析就是要搞清楚做什么

  • 面向对象设计就是要搞清楚怎么做

两个阶段最终的产出是类的设计,包括程序被拆解为哪些类,每个类有哪些属性方法、类与类之间如何交互等等。

# 什么是 UML?我们是否需要 UML?

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

UML 在互联网公司的项目开发中,用处可能并不大。为了文档化软件设计或者方便讨论软件设计,大部分情况下,我们随手画个不那么规范的草图,能够达意,方便沟通就够了,而完全按照 UML 规范来将草图标准化,所付出的代价是不值得的。

# 四大特征

# 封装(Encapsulation)

封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。

笔记

说白了就是,对象中的属性不允许随意被访问( private ),只能通过特定的方法( getId() )访问,如果对象没有提供这样的方法,说明对应的属性是不允许访问的。

封装的意义是什么?它能解决什么编程问题?

如果我们对类中属性的访问不做限制,意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。

除此之外,类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多。

这就好比,如果一个冰箱有很多按钮,你就要研究很长时间,还不一定能操作正确。相反,如果只有几个必要的按钮,比如开、停、调节温度,你一眼就能知道该如何来操作,而且操作出错的概率也会降低很多。

# 抽象(Abstraction)

抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。

在面向对象编程中,我们常借助编程语言提供的两种语法机制来实现抽象这一特性:

  1. 接口类(比如 Java 中的 interface 关键字语法)
  2. 抽象类(比如 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) { ... }
}

调用者在使用图片存储功能的时候,只需要了解 IPictureStorage 这个接口类暴露了哪些方法就可以了,不需要去查看 PictureStorage 类里的具体实现逻辑。

并不是说一定要为实现类(PictureStorage)抽象出接口类(IPictureStorage),才叫作抽象。即便不编写 IPictureStorage 接口类,单纯的 PictureStorage 类本身就满足抽象特性。

为什么抽象有时候会被排除在面向对象的四大特性之外?

抽象这个概念是一个非常通用的设计思想,并不单单用在面向对象编程中,也可以用来指导架构设计等。而且这个特性也并不需要编程语言提供特殊的语法机制来支持,只需要提供 “函数” 这一非常基础的语法机制,就可以实现抽象特性、所以,它没有很强的 “特异性”,有时候并不被看作面向对象编程的特性之一。

抽象的意义是什么?它能解决什么编程问题?

  1. 在面对复杂系统的时候,人脑能承受的信息复杂程度是有限的,所以我们必须忽略掉一些非关键性的实现细节。而抽象作为一种只关注功能点不关注实现的设计思路,正好帮我们的大脑过滤掉许多非必要的信息。

  1. 很多设计原则都体现了抽象这种设计思想:
  • 基于接口而非实现编程
  • 开闭原则(对扩展开放、对修改关闭)
  • 代码解耦(降低代码的耦合性)
  • 等等
  1. 换一个角度来考虑,我们在定义(或者叫命名)类的方法的时候,也要有抽象思维,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义。

    举个简单例子,比如 getAliyunPictureUrl() 就不是一个具有抽象思维的命名,因为某一天如果我们不再把图片存储在阿里云上,而是存储在私有云上,那这个命名也要随之被修改。相反,如果我们定义一个比较抽象的函数,比如叫作 getPictureUrl() ,那即便内部存储方式修改了,我们也不需要修改命名。

笔记

在本书中,作者的观点,只要代码中用到 “函数” 这一语法机制,就可以认为具备抽象这一设计思想,因为对于调用函数者来说,他不需要关心函数内的实现;而接口类、抽象类这两个语法机制是丰富了抽象设计思想

# 继承(Inheritance)

继承是用来表示类之间的 is-a 关系,分为两种模式:单继承和多继承。

单继承表示一个子类只继承一个父类;比如猫是一种哺乳动物

多继承表示一个子类可以继承多个父类;比如猫既是哺乳动物,又是爬行动物;

继承主要是用来解决代码复用的问题。

继承存在的意义是什么?它能解决什么编程问题?

继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。不过,这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系。


如果我们再上升一个思维层面,去思考继承这一特性,可以这么理解:我们代码中有一个猫类,有一个哺乳动物类。猫属于哺乳动物,从人类认知的角度上来说,是一种 is-a 关系。我们通过继承来关联两个类,反应真实世界中的这种关系,非常符合人类的认知,而且,从设计的角度来说,也有一种结构美感。


继承的概念很好理解,也很容易使用。不过,过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。为了了解一个类的功能,我们不仅需要查看这个类的代码,还需要按照继承关系一层一层地往上查看 “父类、父类的父类……” 的代码。还有,子类和父类高度耦合,修改父类的代码,会直接影响到子类。


所以,继承这个特性也是一个非常有争议的特性。很多人觉得继承是一种反模式。我们应该尽量少用,甚至不用。关于这个问题,在后面讲到 “多用组合少用继承” 这种设计思想的时候,我会非常详细地再讲解,这里暂时就不展开讲解了。

# 多态(Polymorphism)

多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。

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
  }
}

多态这种特性也需要编程语言提供特殊的语法机制来实现。在上面的例子中,我们用到了三个语法机制来实现多态。

  • 第一个语法机制是编程语言要支持父类对象可以引用子类对象,也就是可以将 SortedDynamicArray 传递给 DynamicArray。
  • 第二个语法机制是编程语言要支持继承,也就是 SortedDynamicArray 继承了 DynamicArray,才能将 SortedDyamicArray 传递给 DynamicArray。
  • 第三个语法机制是编程语言要支持子类可以重写(override)父类中的方法,也就是 SortedDyamicArray 重写了 DynamicArray 中的 add () 方法。

还有其他两种比较常见的的实现方式,

  • 利用接口类语法
  • 利用 duck-typing 语法(由于 Java 不支持此语法,这里就不展开了)

利用接口类来实现多态特性:

点击查看
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);
  }
}

在这段代码中,Iterator 是一个接口类,定义了一个可以遍历集合数据的迭代器。Array 和 LinkedList 都实现了接口类 Iterator。我们通过传递不同类型的实现类(Array、LinkedList)到 print (Iterator iterator) 函数中,支持动态的调用不同的 next ()、hasNext () 实现。

具体点讲就是,当我们往 print (Iterator iterator) 函数传递 Array 类型的对象的时候,print (Iterator iterator) 函数就会调用 Array 的 next ()、hasNext () 的实现逻辑;当我们往 print (Iterator iterator) 函数传递 LinkedList 类型的对象的时候,print (Iterator iterator) 函数就会调用 LinkedList 的 next ()、hasNext () 的实现逻辑。

多态特性存在的意义是什么?它能解决什么编程问题?

多态特性能提高代码的可扩展性和复用性。为什么这么说呢?我们回过头去看讲解多态特性的时候,举的第二个代码实例(Iterator 的例子)。


在那个例子中,我们利用多态的特性,仅用一个 print () 函数就可以实现遍历打印不同类型(Array、LinkedList)集合的数据。当再增加一种要遍历打印的类型的时候,比如 HashMap,我们只需让 HashMap 实现 Iterator 接口,重新实现自己的 hasNext ()、next () 等方法就可以了,完全不需要改动 print () 函数的代码。所以说,多态提高了代码的可扩展性。


如果我们不使用多态特性,我们就无法将不同的集合类型(Array、LinkedList)传递给相同的函数(print (Iterator iterator) 函数)。我们需要针对每种要遍历打印的集合,分别实现不同的 print () 函数,比如针对 Array,我们要实现 print (Array array) 函数,针对 LinkedList,我们要实现 print (LinkedList linkedList) 函数。而利用多态特性,我们只需要实现一个 print () 函数的打印逻辑,就能应对各种集合数据的打印操作,这显然提高了代码的复用性。


除此之外,多态也是很多设计模式、设计原则、编程技巧的代码实现基础:

  • 策略模式
  • 基于接口而非实现编程
  • 依赖倒置原则
  • 里式替换原则
  • 利用多态去掉冗长的 if-else 语句
  • 等等
上次更新: 2022/10/16, 17:18:12
读薄《设计模式之美》-导读
相对论的真相:为什么我们喜欢比较和攀比?

← 读薄《设计模式之美》-导读 相对论的真相:为什么我们喜欢比较和攀比?→

最近更新
01
Docker Swarm
04-18
02
安全隧道 - gost
04-17
03
Solana最佳实践
04-16
更多文章>
Theme by Vdoing | Copyright © 2018-2025 NipGeihou | 友情链接
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式