设计原则与思想:面向对象(11讲)

1. 什么是面向对象编程(OOP)

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

2. 什么是面向对象编程语言

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

3. 面向对象编程和面向对象编程语言之间的关系

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

4. UML 统一建模语言

  • 学习成本较高,可以适当简单学习

5. 封装、继承、多态、抽象分别解决哪些编程问题

* 封装
  • 概念:封装也叫作信息隐藏或者数据访问保护通过暴露有限的访问接口,授权外部仅能通过类提供的方法(函数)来访问内部的信息和数据
  • 作用:
  1. 保护数据不被随意修改,提高代码的可维护性
  2. 仅暴露有限的必要接口,提高类的易用性
  • 封装特性,必须使用访问控制权限
  • Demo: 金融系统简化版虚拟钱包
public class Wallet {  private String id;//钱包唯一编号  private long createTime;//钱包创建时间  private BigDecimal balance;//钱包余额  //BigDecimal 对数字进行精度计算,返回的是对象,不能通过传统+ - * / 计算,而是必须调用相应的方法  private long balanceLastModifiedTime;//上次钱包余额变更时间   //构造函数,完成对象初始化。Java构造函数的名称必须与类名相同,包括大小写;构造函数没有返回值,也不能用void修饰;  public Wallet() {     this.id = IdGenerator.getInstance().generate();//全局唯一ID生成器     this.createTime = System.currentTimeMillis();     this.balance = BigDecimal.ZERO;//0 用于和0比较大小     this.balanceLastModifiedTime = System.currentTimeMillis();//获取时间,毫秒级  }  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();  }}
  • 解析
    1. 从业务的角度来说,id、createTime 在创建钱包的时候就确定好了,之后不应该再被改动,所以,我们并没有在 Wallet 类中,暴露 id、createTime 这两个属性的任何修改方法,比如 set 方法。所以,在 Wallet 类的构造函数内部将其初始化设置好,而不是通过构造函数的参数来外部赋值。
    1. 对于钱包余额 balance 这个属性,从业务的角度来说,只能增或者减,不会被重新设置。所以,我们在 Wallet 类中,只暴露了 increaseBalance() 和 decreaseBalance() 方法,并没有暴露 set 方法。对于 balanceLastModifiedTime 这个属性,它完全是跟 balance 这个属性的修改操作绑定在一起的。只有在 balance 修改的时候,这个属性才会被修改。所以,我们把 balanceLastModifiedTime 这个属性的修改操作完全封装在了increaseBalance() 和 decreaseBalance() 两个方法中,不对外暴露任何修改这个属性的方法和业务细节。这样也可以保证 balance 和 balanceLastModifiedTime 两个数据的一致性。
抽象
  • 概念: 抽象是如何隐藏方法的具体实现,让使用者只需要关心调用哪些方法,不需要知道具体如何实现
  • 抽象可以通过接口类(Java中的interface关键字) 或 抽象类(Java中的abstract关键字)来实现
  • 作用:
  1. 提高代码的可扩展性、维护性,修改时不需要改变定义,减少代码的改动范围
  2. 抽象是处理复杂系统的有效手段,能有效过滤掉不必要关注的信息
  • Demo: 图片存储功能
public interface IPictureStorage {  // void 是空,在方法声明中表示该方法没有返回值  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) { ... }}
  • 解析
    1. 在上面的这段代码中,我们利用 Java 中的 interface 接口语法来实现抽象特性
    1. 调用者在使用图片存储功能的时候,只需要了解 IPictureStorage 这个接口类暴露了哪些方法就可以了,不需要去查看 PictureStorage 类里的具体实现逻辑
    1. 举个简单例子,比如 getAliyunPictureUrl() 就不是一个具有抽象思维的命名,如果更换存储设备,那这个命名也要随之被修改。相反,如果我们定义一个比较抽象的函数,比如叫作 getPictureUrl(),那即便内部存储方式修改了,我们也不需要修改命名。
继承
  • 概念:继承用来表示类与类之间is-a关系,可分为单继承和多继承
  • 实现继承,需要语言的语法支持,Java 使用 extends 关键字来实现继承,C++ 使用冒号(class B : public A),Python 使用 paraentheses(),Ruby 使用 <
  • 单继承:Java、PHP、C#、Ruby 等
  • 多继承:C++、Python、Perl 等
  • 作用: 提高代码复用性,比如通用的方法抽象到父类
  • 缺点: 过度使用继承,继承层次过深、过复杂,就会导致代码的可读性、可维护性变差
  • 解决思路:多用组合少用继承
多态
  • 概念:父类,被多个子类继承,如果父类的某个方法,在多个子类中被重写,表现出不同功能,就是多态(同一个类的不同子类表现不同形态)
  • 实现方法:
    1. 继承+方法重写 -Demo1
    1. 利用接口类语法(C++不支持) -Demo2
    1. 利用duck-typing语法(仅有 Python、JavaScript支持) -Demo3
  • 作用: 多态可以提高代码的扩展性复用性
  • Demo1
//继承+方法重写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));    }  }  //main()方法是Java应用程序入口方法,程序运行,第一个执行的方法就是main()方法  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() 方法。
  • 通过这三种语法机制配合在一起,我们就实现了在 test() 方法中,子类 SortedDyamicArray 替换父类 DynamicArray,执行子类 SortedDyamicArray 的 add() 方法,也就是实现了多态特性。
  • Demo2
//利用接口类实现public interface Iterator {  String hasNext();//检测序列是否还有元素  String next();//获取序列下一个元素  String remove();//将迭代器新返回的元素删除}public class Array implements Iterator {  private String[] data;    public String hasNext() { ... }  public String next() { ... }  public String remove() { ... }  //...省略其他方法...}public class LinkedList implements Iterator {  private LinkedListNode head;    public String 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() 实现。
  • Demo3
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)
  • 解析
  • 从这段代码中看出,duck-typing 实现多态的方式非常灵活。Logger 和 DB 两个类没有任何关系,既不是继承关系,也不是接口和实现的关系,但是只要它们都有定义了 record() 方法,就可以被传递到 test() 方法中
  • 在实际运行的时候,执行对应的 record() 方法。也就是说,只要两个类具有相同的方法,就可以实现多态,并不要求两个类之间有任何关系,这就是所谓的 duck-typing,是一些动态语言所特有的语法机制。而像 Java 这样的静态语言,通过继承实现多态特性,必须要求两个类之间有继承关系,通过接口实现多态特性,类必须实现对应的接口。
  • 扩展:
  • Java,PHP不支持多继承原因
多重继承有副作用:菱形继承(钻石问题)
假设类 B 和类 C 继承自类 A,且都重写了类 A 中的同一个方法,而类 D 同时继承了类 B 和类 C,那么此时类 D 会继承 B、C 的方法,那对于 B、C 重写的 A 中的方法,类 D 会继承哪一个呢?这里就会产生歧义。
  • Python多继承的实现方法
针对多继承时,多个父类的同名方法,Python采用MRO,在多继承时,判断方法、属性的调用路径
在当前类中找到方法,就直接执行,不再搜索;如果没有找到,就查找下一个类是否有对应方法,如果找到就直接执行,不再搜索;直到最后一个类,没找到就报错