设计模式之美笔记二
本文极客时间课程设计模式之美的个人阅读笔记,有不尽详细之处请抱歉,文未可以订阅极客时间该课程。
- 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
“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”。
“基于接口而非实现编程”这条原则中的“接口”,可以理解为编程语言中的接口或者抽象类。
在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。而抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。
“基于接口而非实现编程”的实现原则:
- 函数的命名不能暴露任何实现细节。
- 封装具体的实现细节。
- 为实现类定义抽象的接口。
如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了。
设计模式之美
