10k

设计模式之美-课程笔记1-面向对象概念们

理论一: 当谈论面向对象的时候, 我们在谈论什么(概念们)

什么是面向对象和面向对象语言

  1. 面向对象编程是一种编程范式或者编程风格。它以类或对象作为基本单元,并将封装、抽象、基础、多态四个特性作为代码设计的视线和基石。
  2. 面向对象编程语言是支持类或对象的语法机制,方便实现四大特性。

如何判断某语言是面向对象编程语言

  1. 这个语言支持类和对象的语法概念,并以此为组织代码的基本单元,就可以被粗略的认为是OOPL。

什么是面向对象分析(OOA)和面向对象设计(OOD)

  1. 面向对象分析、设计、编程实现,正好就是面向对象软件开发要经历的三个阶段。
  2. “面向对象”XX是说我们所做的事情是围绕对象来做的。程序被拆解为哪些类,每个类有什么方法,有哪些属性,类之间如何交互。解决的是做什么(OOA),以及怎么做(OOD)的问题。
  3. 他们比其他的分析和设计更加具体,更能顺利过渡到面向对象编程环节。

UML是什么?

  1. UML:unified model language, 统一建模语言。

实际上,UML 是一种非常复杂的东西。它不仅仅包含我们常提到类图,还有用例图、顺序图、活动图、状态图、组件图等。在我看来,即便仅仅使用类图,学习成本也是很高的。就单说类之间的关系,UML 就定义了很多种,比如泛化、实现、关联、聚合、组合、依赖等。

  1. 作者更建议日常开发用简单的图代替UML沟通。也许专业的工程师之间表达和沟通用他做一些正式的文档更清晰。

理论二:封装、继承、多态、抽象分别可以解决哪些编程问题?

封装

定义

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

  2. 结合一个例子说明(虚拟钱包)

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();
  }
}

钱包有四个属性(成员变量),封装思想指导我们创建了6个方法来访问他们。

String getId()
long getCreateTime()
BigDecimal getBalance()
long getBalanceLastModifiedTime()
void increaseBalance(BigDecimal increasedAmount)
void decreaseBalance(BigDecimal decreasedAmount)

从业务角度来说,id和创建时间在创建钱包对象的时候就确定了,只能访问不能改,所以我们这暴露的只有他们的get方法。

对于余额balance,他只能增减,不能被重置,所以这里也没有暴露set方法而是有void increaseBalance(BigDecimal increasedAmount)void decreaseBalance(BigDecimal decreasedAmount)。balanceLastModifiedTime是跟balance的修改绑定的,所以他的更改操作也写在了前文的两个方法中。

  1. 对于这个特性,需要编程语言提供访问权限控制这个语法机制。比如Java中的public, private这些。如果没有这个机制,所有的外部代码都可以更改任何属性,无法达到保护数据隐藏信息的目的。

意义、解决的问题

  1. 可维护性,可读性。不做限制,看起来更加灵活,但是过度灵活也意味不可控。所有的属性可以在任何地方被修改,导致代码难以维护,可读性也很低(修改属性的代码散落各地);
  2. 易用性。暴露太多不必要的方法对于用户是负担,他们不知道怎么用或者正确的用,或者不知道用哪个才是合适的。

抽象

定义

  1. 隐藏方法的实现,让调用者只需关心方法提供了什么功能。

  2. 在面向对象编程中,借用抽象和接口类这两种语法机制。

  3. 借助例子(图片存储)

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

用户只需知道对于图的操作可以有存储,获取,删除和修改即可,至于如何与内存还是硬盘连接与交互,图片如何被处理压缩一改不需要理会。

  1. 它这个概念很通用,也很宽泛,有时候并不会被看做面向对象编程的特性。

意义、解决的问题

  1. 让调用者只关注定义(功能点)而不需要关注实现。

  2. 在设计代码的时候,通过抽象,可以在后续更改实现的时候不需要修改定义和功能点。

继承

定义

  1. 表示类之间的is-a关系。(即这个东西是一类什么东西)

  2. 有单继承和多继承。多继承即继承多个父类,一个学生即是人类又是QQ黄钻会员。

  3. 实现这个特性也需要语法的支持。例如Java中的extend关键字。不过有的语言支持多继承,有的不支持。

    菱形继承问题。 即两个派生类继承同一个基类,同时两个派生类又作为基本继承给同一个派生类。这种继承形如菱形,故又称为菱形继承。

    菱形继承产生的问题:菱形继承主要有数据冗余和二义性的问题。由于最底层的派生类继承了两个基类,同时这两个基类有继承的是一个基类,故而会造成最顶部基类的两次调用,会造成数据冗余及二义性问题。

    img

意义、解决的问题

  1. 代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。不过,这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系。
  2. 但是过渡使用继承会导致继承层次过深过复杂,导致代码的可读性和可维护性变差。所以很多人推荐组合。

多态

定义

  1. 子类可以替换父类,在实际的允许过程中调用子类的方法实现。

  2. 结合例子

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
  }
}
  1. 多态需要特殊的语法机制来实现。上述例子中用到了3个。

    • 语言要支持父类对象可以引用子类对象。DynamicArray dynamicArray = new SortedDynamicArray();
    • 第二个是语言要支持继承,SortedDynamicArray要继承了DynamicArray才可以支持SortedDynamicArray传给DynamicArray
    • 第三个语法机制是语言要支持子类可以重写(override)父类的方法。SortedDynamicArray的add方法。
  2. 对于多态的实现,除了利用“继承+方法重写”实现,还有另外两种,一种是利用接口类语法,另一个是duck-typing语法,不过并不是所有语言都支持这些。duck-typing只有一些动态语言支持。

    1. 利用接口
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);
  }
}

Array和LinkedList都实现了Iterator,所以在主方法在,通过传递不同的实现类到print方法中,会调用对应的hasNext和next方法。

  1. 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)

从这段代码中,我们发现,duck-typing 实现多态的方式非常灵活。Logger 和 DB 两个类没有任何关系,既不是继承关系,也不是接口和实现的关系,但是只要它们都有定义了 record() 方法,就可以被传递到 test() 方法中,在实际运行的时候,执行对应的 record() 方法。

也就是说,只要两个类具有相同的方法,就可以实现多态,并不要求两个类之间有任何关系,这就是所谓的 duck-typing,是一些动态语言所特有的语法机制。而像 Java 这样的静态语言,通过继承实现多态特性,必须要求两个类之间有继承关系,通过接口实现多态特性,类必须实现对应的接口。

意义、解决的问题

  1. 提高了可扩展性和复用性。第二个接口实现多态的例子,我们只需要一个print函数就能打印不同类型的集合。当我们再需要增加一个Hashmap的时候,只需让他实现Iterator接口重写next和hasNext方法即可, 不需要改print。
Thoughts? Leave a comment