乐趣区

设计模式之美二-面向对象1

设计原则与思想:面向对象(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,在多继承时,判断方法、属性的调用路径
在当前类中找到方法,就直接执行,不再搜索; 如果没有找到,就查找下一个类是否有对应方法,如果找到就直接执行,不再搜索; 直到最后一个类,没找到就报错

退出移动版