美文网首页
面向对象(二)--特性

面向对象(二)--特性

作者: 凯玲之恋 | 来源:发表于2020-04-07 00:23 被阅读0次

    尽管大部分面向对象编程语言都提供了相应的语法机制来支持,但不同的编程语言实现这四大特性的语法机制可能会有所不同

    1、封装(Encapsulation)

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

    封装主要讲的是如何隐藏信息、保护数据

    1.1 封装特性的定义

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

    Wallet 类主要有四个属性(也可以叫作成员变量).其中,id 表示钱包的唯一编号,createTime 表示钱包创建的时间,balance 表示钱包中的余额,balanceLastModifiedTime 表示上次钱包余额变更的时间。

    我们参照封装特性,对钱包的这四个属性的访问方式进行了限制。调用者只允许通过下面这六个方法来访问或者修改钱包里的数据

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

    从业务的角度来说,id、createTime 在创建钱包的时候就确定好了,之后不应该再被改动,所以,我们并没有在 Wallet 类中,暴露 id、createTime 这两个属性的任何修改方法,比如 set 方法。

    而且,这两个属性的初始化设置,对于 Wallet 类的调用者来说,也应该是透明的,所以,我们在 Wallet 类的构造函数内部将其初始化设置好,而不是通过构造函数的参数来外部赋值。

    对于钱包余额 balance 这个属性,从业务的角度来说,只能增或者减,不会被重新设置。所以,我们在 Wallet 类中,只暴露了 increaseBalance() 和 decreaseBalance() 方法,并没有暴露 set 方法。

    对于 balanceLastModifiedTime 这个属性,它完全是跟 balance 这个属性的修改操作绑定在一起的。只有在 balance 修改的时候,这个属性才会被修改。所以,我们把 balanceLastModifiedTime 这个属性的修改操作完全封装在了 increaseBalance() 和 decreaseBalance() 两个方法中,不对外暴露任何修改这个属性的方法和业务细节。

    对于封装这个特性,我们需要编程语言本身提供一定的语法机制来支持。这个语法机制就是访问权限控制。

    例子中的 private、public 等关键字就是 Java 语言中的访问权限控制语法。

    如果 Java 语言没有提供访问权限控制语法,所有的属性默认都是 public 的,那任意外部代码都可以通过类似 wallet.id=123; 这样的方式直接访问、修改属性,也就没办法达到隐藏信息和保护数据的目的了,也就无法支持封装特性了。

    1.2 封装的意义

    如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活

    过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性

    类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。

    如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多。

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

    2 抽象(Abstraction)

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

    在面向对象编程中,我们常借助编程语言提供的接口类(比如 Java 中的 interface 关键字语法)或者抽象类(比如 Java 中的 abstract 关键字语法)这两种语法机制,来实现抽象这一特性。

    2.1 抽象特性

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

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

    实际上,抽象这个特性是非常容易实现的,并不需要非得依靠接口类或者抽象类这些特殊语法机制来支持。换句话说,并不是说一定要为实现类(PictureStorage)抽象出接口类(IPictureStorage),才叫作抽象。即便不编写 IPictureStorage 接口类,单纯的 PictureStorage 类本身就满足抽象特性。

    类的方法是通过编程语言中的“函数”这一语法机制来实现的。通过函数包裹具体的实现逻辑,这本身就是一种抽象

    调用者在使用函数的时候,并不需要去研究函数内部的实现逻辑,只需要通过函数的命名、注释或者文档,了解其提供了什么功能,就可以直接使用了。

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

    2.2 抽象的意义

    抽象及其前面讲到的封装都是人类处理复杂性的有效手段。

    抽象作为一种只关注功能点不关注实现的设计思路,正好帮我们的大脑过滤掉许多非必要的信息

    抽象作为一个非常宽泛的设计思想,在代码设计中,起到非常重要的指导作用

    比如基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(降低代码的耦合性)等

    在定义(或者叫命名)类的方法的时候,也要有抽象思维,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义。

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

    3 继承(Inheritance)

    3.1 继承特性

    继承是用来表示类之间的 is-a 关系,比如猫是一种哺乳动物。

    从继承关系上来讲,继承可以分为两种模式,单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。

    为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持,比如 Java 使用 extends 关键字来实现继承,C++ 使用冒号(class B : public A),Python 使用 paraentheses(),Ruby 使用 <。不过,有些编程语言只支持单继承,不支持多重继承,比如 Java、PHP、C#、Ruby 等,而有些编程语言既支持单重继承,也支持多重继承,比如 C++、Python、Perl 等。

    3.2 继承存在的意义

    继承最大的一个好处就是代码复用。
    假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。

    我们代码中有一个猫类,有一个哺乳动物类。猫属于哺乳动物,从人类认知的角度上来说,是一种 is-a 关系。我们通过继承来关联两个类,反应真实世界中的这种关系,非常符合人类的认知,而且,从设计的角度来说,也有一种结构美感。

    继承的概念很好理解,也很容易使用。不过,过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。

    4 多态(Polymorphism)

    4.1 多态特性

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

    4.1.1继承加方法重写

    
    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() 方法。

    通过这三种语法机制配合在一起,我们就实现了在 test() 方法中,子类 SortedDyamicArray 替换父类 DynamicArray,执行子类 SortedDyamicArray 的 add() 方法,也就是实现了多态特性。

    4.1.2 duck-typing

    duck-typing 只有一些动态语言才支持,比如 Python、JavaScript 等。
    Python 代码

    
    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)
    

    Logger 和 DB 两个类没有任何关系,既不是继承关系,也不是接口和实现的关系,但是只要它们都有定义了 record() 方法,就可以被传递到 test() 方法中,在实际运行的时候,执行对应的 record() 方法。

    只要两个类具有相同的方法,就可以实现多态,并不要求两个类之间有任何关系,这就是所谓的 duck-typing,是一些动态语言所特有的语法机制。

    4.2 多态特性存在的意义

    多态特性能提高代码的可扩展性和复用性。

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

    除此之外,多态也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。

    参考

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

    相关文章

      网友评论

          本文标题:面向对象(二)--特性

          本文链接:https://www.haomeiwen.com/subject/yjjwphtx.html