天天看点

设计模式之美笔记二OOP 优势哪些代码设计看似是面向对象,实际是面向过程的?接口vs抽象类为什么基于接口而非实现编程?有必要为每个类都定义接口吗?设计模式之美

设计模式之美笔记二

本文极客时间课程设计模式之美的个人阅读笔记,有不尽详细之处请抱歉,文未可以订阅极客时间该课程。
  • OOP 优势
  • 哪些代码设计看似是面向对象,实际是面向过程的?
    • 滥用 getter、setter 方法
    • 滥用全局变量和全局方法
    • 定义数据和方法分离的类
  • 接口vs抽象类
    • 如何决定该用抽象类还是接口?
  • 为什么基于接口而非实现编程?有必要为每个类都定义接口吗?

OOP 优势

  • OOP 更加能够应对大规模复杂程序的开发
  • OOP 风格的代码更易复用、易扩展、易维护
  • OOP 语言更加人性化、更加高级、更加智能

Unix、Linux 等复杂系统是使用C面向过程的语言,如何看?

  • 操作系统是业务无关的,它更接近于底层计算机,因此更适合用面向过程的语言编写。而接近业务的也就是接近人的软件,则更适合用面向对象的语言编写。
  • 操作系统虽然是用面向过程的C语言实现的 但是其设计逻辑是面向对象的。C语言没有类和对象的概念,但是用结构体(struct)同样实现了信息的封装,内核源码中也不乏继承和多态思想的体现。面向对象思想,不局限于具体语言。

哪些代码设计看似是面向对象,实际是面向过程的?

滥用 getter、setter 方法

而面向对象封装的定义是:通过访问权限控制,隐藏内部数据,外部仅能通过类提供的有限的接口访问、修改内部数据。所以,暴露不应该暴露的 setter 方法,明显违反了面向对象的封装特性。数据没有访问权限控制,任何代码都可以随意修改它,代码就退化成了面向过程编程风格的了。

在设计实现类的时候,除非真的需要,否则,尽量不要给属性定义 setter 方法。除此之外,尽管 getter 方法相对 setter 方法要安全些,但是如果返回的是集合容器(比如例子中的 List 容器),也要防范集合内部数据被修改的危险。

滥用全局变量和全局方法

在面向对象编程中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。

单例类对象在全局代码中只有一份,所以,它相当于一个全局变量。

静态成员变量归属于类上的数据,被所有的实例化对象所共享,也相当于一定程度上的全局变量。

常量是一种非常常见的全局变量,比如一些代码中的配置参数,一般都设置为常量,放到一个 Constants 类中。

静态方法一般用来操作静态变量或者外部数据。你可以联想一下我们常用的各种 Utils 类,里面的方法一般都会定义成静态方法,可以在不用创建对象的情况下,直接拿来使用。静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。

Constants 类:

public class Constants {
  public static final String MYSQL_ADDR_KEY = "mysql_addr";
  public static final String MYSQL_DB_NAME_KEY = "db_name";
  public static final String MYSQL_USERNAME_KEY = "mysql_username";
  public static final String MYSQL_PASSWORD_KEY = "mysql_password";
  
  public static final String REDIS_DEFAULT_ADDR = "192.168.7.2:7234";
  public static final int REDIS_DEFAULT_MAX_TOTAL = 50;
  public static final int REDIS_DEFAULT_MAX_IDLE = 50;
  public static final int REDIS_DEFAULT_MIN_IDLE = 20;
  public static final String REDIS_DEFAULT_KEY_PREFIX = "rt:";
  
  // ...省略更多的常量定义...
}
           

缺点:

  • 影响代码的可维护性
  • 增加代码的编译时间
  • 影响代码的复用性

Utils 类 :

只包含静态方法不包含任何属性的 Utils 类,是彻彻底底的面向过程的编程风格。

我们设计 Utils 类的时候,最好也能细化一下,针对不同的功能,设计不同的 Utils 类,比如 FileUtils、IOUtils、StringUtils、UrlUtils 等,不要设计一个过于大而全的 Utils 类。

定义数据和方法分离的类

接口vs抽象类

抽象类:

  • 抽象类不允许被实例化,只能被继承。也就是说,你不能 new 一个抽象类的对象出来(Logger logger = new Logger(…); 会报编译错误)。
  • 抽象类可以包含属性和方法。方法既可以包含代码实现(比如 Logger 中的 log() 方法),也可以不包含代码实现(比如 Logger 中的 doLog() 方法)。不包含代码实现的方法叫作抽象方法。
  • 子类继承抽象类,必须实现抽象类中的所有抽象方法。对应到例子代码中就是,所有继承 Logger 抽象类的子类,都必须重写 doLog() 方法。

接口:

  • 接口不能包含属性(也就是成员变量)。
  • 接口只能声明方法,方法不能包含代码实现。
  • 类实现接口的时候,必须实现接口中声明的所有方法。

c++ 抽象类

class Box
{
   public:
      // 纯虚函数
      virtual double getVolume() = 0;
   private:
      double length;      // 长度
      double breadth;     // 宽度
      double height;      // 高度
};
           

设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。

因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。

可用于实例化对象的类被称为具体类。

C++ 只有抽象类,并没有接口

c++ 实现接口

class Strategy { // 用抽象类模拟接口
  public:
    ~Strategy();
    virtual void algorithm()=0;
  protected:
    Strategy();
};
           

抽象类 Strategy 没有定义任何属性,并且所有的方法都声明为 virtual 类型(等同于 Java 中的 abstract 关键字),这样,所有的方法都不能有代码实现,并且所有继承这个抽象类的子类,都要实现这些方法。从语法特性上来看,这个抽象类就相当于一个接口。

如何决定该用抽象类还是接口?

如果要表示一种 is-a 的关系,并且是为了解决代码复用的问题,我们就用抽象类;

如果我们要表示一种 has-a 关系,并且是为了解决抽象而非代码复用的问题,那我们就可以使用接口。

为什么基于接口而非实现编程?有必要为每个类都定义接口吗?

基于接口而非实现编程:Program to an interface, not an implementation

“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”。

“基于接口而非实现编程”这条原则中的“接口”,可以理解为编程语言中的接口或者抽象类。

在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。而抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。

“基于接口而非实现编程”的实现原则:

  • 函数的命名不能暴露任何实现细节。
  • 封装具体的实现细节。
  • 为实现类定义抽象的接口。

如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了。

设计模式之美

设计模式之美笔记二OOP 优势哪些代码设计看似是面向对象,实际是面向过程的?接口vs抽象类为什么基于接口而非实现编程?有必要为每个类都定义接口吗?设计模式之美
设计模式之美笔记二OOP 优势哪些代码设计看似是面向对象,实际是面向过程的?接口vs抽象类为什么基于接口而非实现编程?有必要为每个类都定义接口吗?设计模式之美