理解面向对象

这篇博文主要想说面向对象的几大特性:封装,抽象,继承,多态,以及我们究竟如何去使用。

1. 面向对象概述

1.1 面向对象编程

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

整个编程的过程:

  • Object Oriented Analysis
  • Object Oriented Design
  • Object Oriented Programming

1.2 面向对象编程的四大特征

1.2.1 封装 Encapsulation

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

虚拟钱包,对于自身变量,用private来标注,然后通过对应的getter,setter方法允许外界来访问一部分变量,允许进行一定的修改。

没有封装则意味着不可控,即任何代码都可以被任何人访问,修改的代码可以遍布在包的任何角落,会影响代码的可读性,以及可维护性。只暴露出有限多的接口,供外界来使用。

总结: 封装是为了隐藏信息,保护数据

1.2.2 继承 Inheritance

用来表述is a的关系,java支持单继承。

继承最大的好处就是代码复用,比如两个子类的共同代码抽取到父类当中,然后父类来共同使用。

但是过度使用的话会容易导致层级数量太多,反而降低代码的可读性。

1.2.3 多态 Polymorphism

  • 继承加方法重写实现
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
  }
}
  • 利用接口类实现多态特性
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);
  }
}

多态可以很大程度上提高代码的可扩展性和复用性

1.2.4 抽象 Abstraction

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

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

使用接口或者abstract class,然后调用者就只需要知道需要传入什么参数,传出什么参数,就可以试用了。

2. 面向对象 vs 面向过程

需要对这两个概念有更深的理解,很多时候,我们是在用面向对象的语言写面向过程的代码,对于到底什么是面向对象,如何写真的面向对象的代码,我们还是有很多无法确定的地方。

面向过程的编程是一种编程范式,以过程(方法,函数,操作)作为组织代码的基本单元,以数据(可以理解为成员变量,属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。

2.1 面向对象编程的优势

2.1.1 更能够应对大规模复杂程序的开发

因为对于面向过程的编程风格来说,整个程序的处理流程会偏向于线性,流程化,但是实际应用场景中,关系错综复杂,会很难将程序拆解为一组顺序执行的方法。而面向对象的方式就可以比较好的解决这个问题了。

2.1.2 更易复用,扩展和维护

面向对象通过类这种组织方式能够将数据和方法绑定在一起,通过访问权限控制,只允许外部调用者通过类暴露的有限方法访问数据,而不会像面向过程编程那样,数据可以被任意方法的随意修改

我们通过使用多态的特性,可以在需要修改一个功能实现的时候,通过实现一个新的子类的方式,在子类当中重写原来的功能逻辑,用子类替代父类。 —- 对修改关闭,对扩展开放。

2.2 Warning/ Bad Smell - 看似面向对象的面向过程的代码

首先值得注意的是,这里提及的都是我们需要注意的地方,但并不是说我们完全不能这样子写。譬如util class,很多时候我们是需要的,因为确实可以不带数据的,只在input,output传递所有信息就够了。

2.2.1 getter setter方法的问题

当我们习惯性的给所有的属性都加上getter, setter方法的时候,其实是破坏了Java的封装的特性的,我们使用private 标注属性,再适当的设置setter,getter方法是因为我们不想将对于代码/数据的控制权交给他人,而疯狂的getter,setter方法会让Java的封装优势荡然无存,只是从原来的直接访问属性变成通过getter,setter方法来访问。没有起到任何保障安全的作用。

注意如果是集合容器的话,要防范集合内部的数据被修改的危险。另外,setter方法的使用需要谨慎些,只有在必需的时候再用。

2.2.2 全局变量和全局方法

常见的全局变量有:

  • 单例类对象
  • 静态成员变量
  • 常量

常见的全局方法有:

  • 静态方法

Constants类往往会越加越大,而且会很难维护。而且如果我们开发的其他项目需要复用这些constants,哪怕我们只使用一个,那么最终也会不得不将整个文件加载进去,没有必要,而且会变得非常的慢。我们可以将Constants类拆分为功能更加单一的多个类,或者直接将这些常量定义到对应的class当中。这也是个很好的选择。

3.1 什么是接口? 什么是抽象类?

3.1.1 抽象类定义

下面是一个模板设计的实例,Logger被用来记录日志,FileLogger和MessageQueueLogger继承Logger,分别实现两种不同的日志记录方式

// 抽象类
public abstract class Logger {
  private String name;
  private boolean enabled;
  private Level minPermittedLevel;

  public Logger(String name, boolean enabled, Level minPermittedLevel) {
    this.name = name;
    this.enabled = enabled;
    this.minPermittedLevel = minPermittedLevel;
  }

  public void log(Level level, String message) {
    boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
    if (!loggable) return;
    doLog(level, message);
  }

  protected abstract void doLog(Level level, String message);
}
// 抽象类的子类:输出日志到文件
public class FileLogger extends Logger {
  private Writer fileWriter;

  public FileLogger(String name, boolean enabled,
    Level minPermittedLevel, String filepath) {
    super(name, enabled, minPermittedLevel);
    this.fileWriter = new FileWriter(filepath); 
  }

  @Override
  public void doLog(Level level, String mesage) {
    // 格式化level和message,输出到日志文件
    fileWriter.write(...);
  }
}
// 抽象类的子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {
  private MessageQueueClient msgQueueClient;

  public MessageQueueLogger(String name, boolean enabled,
    Level minPermittedLevel, MessageQueueClient msgQueueClient) {
    super(name, enabled, minPermittedLevel);
    this.msgQueueClient = msgQueueClient;
  }

  @Override
  protected void doLog(Level level, String mesage) {
    // 格式化level和message,输出到消息中间件
    msgQueueClient.send(...);
  }
}

抽象类的特性:

  • 抽象类不允许被实例化,只能被继承
  • 抽象类可以包含属性与方法,方法可以包含代码实现,也可以不包含,设计成抽象方法
  • 子类继承抽象类,必须实现抽象类当中的所有抽象方法

3.1.2 接口定义

// 接口
public interface Filter {
  void doFilter(RpcRequest req) throws RpcException;
}
// 接口实现类:鉴权过滤器
public class AuthencationFilter implements Filter {
  @Override
  public void doFilter(RpcRequest req) throws RpcException {
    //...鉴权逻辑..
  }
}
// 接口实现类:限流过滤器
public class RateLimitFilter implements Filter {
  @Override
  public void doFilter(RpcRequest req) throws RpcException {
    //...限流逻辑...
  }
}
// 过滤器使用demo
public class Application {
  // filters.add(new AuthencationFilter());
  // filters.add(new RateLimitFilter());
  private List<Filter> filters = new ArrayList<>();

  public void handleRpcRequest(RpcRequest req) {
    try {
      for (Filter filter : fitlers) {
        filter.doFilter(req);
      }
    } catch(RpcException e) {
      // ...处理过滤结果...
    }
    // ...省略其他处理逻辑...
  }
}

使用interface关键字实现一个Filter接口,AuthencationFilter和RatelimiterFilter分别实现对于RPC请求的鉴权和限流的过滤功能。

  • 接口不能包含属性
  • 接口只能声明方法,方法不能包含代码实现
  • 类实现接口的时候,必须实现接口当中声明的所有方法

抽象类和继承类似,其实表征的是一种is-a的关系;而接口表征的是一种has-a的关系/ 协议,表示具有某些功能

3.2 区别/ 都能解决什么样的编程问题?

3.2.1 抽象类 存在的意义

抽象类不能实例化,只能被继承。主要是用来解决代码复用的问题的。多个子类可以继承抽象类当中定义的属性和方法,避免在子类当中,重复编写相同的代码。

为什么必须是抽象类来做代码复用呢?

  • 因为可以利用多态来做了
  • 减少父类代码被错误的直接使用的风险
  • 也可以增加代码的可读性

3.2.2 接口的存在意义

接口更侧重于解耦,接口是对行为的一种抽象,相当于一组协议或者契约。这样调用者只需要关注抽象的接口,不需要了解具体的实现

Reference

  1. https://github.com/gdhucoder/Algorithms4/blob/master/designpattern/pic/umlcheatsheet.jpg

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 stone2paul@gmail.com

文章标题:理解面向对象

文章字数:2.7k

本文作者:Leilei Chen

发布时间:2020-02-25, 13:20:52

最后更新:2020-03-04, 09:43:08

原始链接:https://www.llchen60.com/%E7%90%86%E8%A7%A3%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏