设计模式之美-课程笔记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的修改绑定的,所以他的更改操作也写在了前文的两个方法中。

  3. 对于这个特性,需要编程语言提供访问权限控制这个语法机制。比如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) { ... }
    }
    

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

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

意义、解决的问题

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

    • 语言要支持父类对象可以引用子类对象。DynamicArray dynamicArray = new SortedDynamicArray();
    • 第二个是语言要支持继承,SortedDynamicArray要继承了DynamicArray才可以支持SortedDynamicArray传给DynamicArray
    • 第三个语法机制是语言要支持子类可以重写(override)父类的方法。SortedDynamicArray的add方法。
  4. 对于多态的实现,除了利用“继承+方法重写”实现,还有另外两种,一种是利用接口类语法,另一个是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方法。

    2. 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。