算是读书笔记吧
极客时间--设计模式之美
面向对象
面向对象编程 -- OOP(Object Oriented Programming)
一种编程范式或编程风格
以类或对象作为组织代码的基本单元。
并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。
面向对象编程语言 -- OOPL(Object Oriented Programming Language)
支持类或对象的语法机制
更严格来讲,他需要有成熟的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)。
为什么说是严格来讲,因为不同的语言在设计的时候都有自己的考量,会出现一些区别。
比如GO不支持继承、OC不支持多继承。但不能说他们就不是OOPL。
OOPL并不是OOP的充要条件,只是让我们更优雅的实现OOP
- 一般来讲, 面向对象编程都是通过使用面向对象编程语言来进行的。
- 不用面向对象编程语言,我们照样可以进行面向对象编程。
- 即便我们使用面向对象编程语言,写出来的代码也不一定是面向对象编程风格的,也有可能是面向过程编程风格的。
面向对象分析 -- OOA(Object Oriented Analysis)、面向对象设计 -- OOD(Object Oriented Design)
面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做
二者的产出是类的设计:
包括程序被拆解为哪些类、每个类有哪些属性方法、类与类之间如何交互等等。
面向对象的特性
封装(Encapsulation)
What:隐藏信息,保护数据访问。
How:暴露有限接口和属性,需要编程语言提供访问控制的语法(私有属性、方法等等)。
Why:提高代码可维护性;降低接口复杂度,提高类的易用性。
抽象(Abstraction)
What: 隐藏具体实现,使用者只需关心功能,无需关心实现。
How: 通过接口类或者抽象类实现,特殊语法机制非必须。
Why: 提高代码的扩展性、维护性;降低复杂度,减少细节负担。
继承(Inheritance)
What: 表示 is-a 关系,分为单继承和多继承。
How: 需要编程语言提供特殊语法机制。例如 Java 的 “extends”,C++ 的 “:” 。
Why: 解决代码复用问题。
多态(Polymorphism)
What: 子类替换父类,在运行时调用子类的实现。
How: 需要编程语言提供特殊的语法机制。比如继承、接口类、duck-typing。
Why: 提高代码扩展性和复用性。
为什么有些语言不支持多继承
主要是为了避免二义性的钻石问题(菱形继承)
假设类 B 和类 C 继承自类 A,且都重写了类 A 中的同一个方法,而类 D 同时继承了类 B 和类 C,那么此时类 D 会继承 B、C 的方法,那对于 B、C 重写的 A 中的方法,类 D 会继承哪一个呢?这里就会产生歧义。
当然,这可以从编译器的角度解决,让我们强制重写。
面向过程
面向过程编程语言
Basic、Pascal、C 等不支持类和对象两个语法概念,以及丰富的面向对象编程特性。
面向过程编程
在组织代码的时候,以过程(或方法)作为组织代码的基本单元。数据和方法相分离
struct User {
char name[64];
int age;
char gender[16];
};
struct User parse_to_user(char* text) {
// 将text(“小王&28&男”)解析成结构体struct User
}
char* format_to_text(struct User user) {
// 将结构体struct User格式化成文本("小王\t28\t男")
}
void sort_users_by_age(struct User users[]) {
// 按照年龄从小到大排序users
}
void format_user_file(char* origin_file_path, char* new_file_path) {
// open files...
struct User users[1024]; // 假设最大1024个用户
int count = 0;
while(1) { // read until the file is empty
struct User user = parse_to_user(line);
users[count++] = user;
}
sort_users_by_age(users);
for (int i = 0; i < count; ++i) {
char* formatted_user_text = format_to_text(users[i]);
// write to new file...
}
// close files...
}
int main(char** args, int argv) {
format_user_file("/home/zheng/user.txt", "/home/zheng/formatted_users.txt");
}
面向对象编程相比面向过程编程的优势
主要体现在提高程序员的开发效率
- 复杂类型的程序开发
对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。
- 易扩展、易复用、易维护
面向对象编程相比面向过程编程,具有更加丰富的特性(封装、抽象、继承、多态)。
- 更加人性化、更加高级、更加智能
人类最开始跟机器打交道是通过 0、1 这样的二进制指令,然后是汇编语言,再之后才出现了高级编程语言。
编程语言越来越人性化,让人跟机器打交道越来越容易。笼统点讲,就是编程语言越来越高级。
我们在使用二进制指令、汇编语言、面向过程编程语言,是在思考,如何设计一组指令,告诉机器去执行这组指令,操作某些数据,帮我们完成某个任务。
在进行面向对象编程时候,是在思考,如何给业务建模,如何将真实的世界映射为类或者对象,这让我们更加能聚焦到业务本身,而不是思考如何跟机器打交道。
面向过程与面向对象
面向过程的语言一定不好吗
使用任何一个编程语言编写的程序,最终执行上都要落实到CPU一条一条指令的执行(无论通过虚拟机解释执行,还是直接编译为机器码),CPU看不到是使用何种语言编写的程序。
对于所有编程语言最终目的是两种:提高硬件的运行效率和提高程序员的开发效率。然而这两种很难兼得。
C语言在效率方面几乎做到了极致,它更适合挖掘硬件的价值,如:C语言用数组char a[8],经过编译以后变成了(基地址+偏移量)的方式。
对于CPU来说,没有运算比加法更快,它的执行效率的算法复杂度是O(1)的。从执行效率这个方面看,开发操作系统和贴近硬件的底层程序,C语言是极好的选择。
而且虽然操作系统是用C语言写的,但是面向对象的思想早已深入到操作系统的源代码中。
C语言带来的问题是内存越界、野指针、内存泄露等。它只关心程序飞的高不高,不关心程序猿飞的累不累。
为了解脱程序员,提高开发效率,设计了OOP等更“智能”的编程语言,但是开发容易毕竟来源于对底层的一层一层又一层的包装。完成一个特定操作有了更多的中间环节, 占用了更大的内存空间, 占用了更多的CPU运算。
从这个角度看,OOP这种高级语言的流行是因为硬件越来越便宜了。我们可以想象如果大众消费级的主控芯片仍然是单核600MHz为主流,运行Android系统点击一个界面需要2秒才能响应,那我们现在用的大部分手机程序绝对不是使用JAVA开发的,Android操作系统也不可能建立起这么大的生态。
面向对象的语言,写出的也可能是面向过程的代码
有些代码设计看似是面向对象,实际是面向过程的
- 滥用 getter、setter 方法
暴露不应该暴露的 setter 方法,明显违反了面向对象的封装特性
在设计实现类的时候,除非真的需要,否则尽量不要给属性定义 setter 方法。除此之外,尽管 getter 方法相对 setter 方法要安全些,但是如果返回的是集合容器,那也要防范集合内部数据被修改的风险。
- 滥用全局变量和全局方法
大而全的Constants 类、Utils 类、静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。
对于这两种类的设计,我们尽量能做到职责单一,定义一些细化的小类,比如 RedisConstants、FileUtils...
在定义之前,也要问一下自己,你真的需要单独定义这样一个 Utils 类吗?是否可以把 Utils 类中的某些方法定义到其他类中呢?
之后,你还是觉得确实有必要去定义这样一个 Utils 类,那就大胆地去定义它吧。
因为即便在面向对象编程中,我们也并不是完全排斥面向过程风格的代码。只要它能为我们写出好的代码贡献力量,我们就可以适度地去使用。
- 定义数据和方法分离的类
基于贫血模型的开发模式(传统的 MVC 结构)分为 Model 层、Controller 层、View 层这三层。
虽然贫血模型违背了面向对象的思想,但是仍旧流行。因为其具有实现简单和上手快的特点。
不要敌视面向过程编程
面向对象编程离不开基础的面向过程编程
类中每个方法的实现逻辑,正是面向过程风格的代码。
只要我们能避免面向过程编程风格的一些弊端,控制好它的副作用,在掌控范围内为我们所用,我们就大可不用避讳在面向对象编程中写面向过程风格的代码。
领域驱动设计-- Domain Driven Design(DDD)
在构建复杂业务时,相比于贫血模型MVC,DDD会将业务仔细划分并将其相关的功能聚合到充血的Domain领域模型中,让处理业务的Service层依赖这个Domain类进行工作。
类似MVP但是和MVP又不同,MVP的Person是把整套可复用的业务逻辑全部迁移,而Domain则更加具体,具体到某个特定业务。
- 简而言之就是把杂糅在一起的业务进行一次精确抽象,把归属于某一特定业务的工作抽象出一个Domain类进行处理
比如钱包的收支、冻结、透支等一系列操作就会被抽象到Wallet的领域模型中。
而数据的读取,格式化,接口的调用,相互的转账等定制化操作依旧留在Service中。
以达到复用和解耦的目的。
新人用直接阅读充血模型的行为方法,起码能够很快理解70%左右的业务逻辑。
领域模型相当于可复用的业务中间层
新功能需求的开发,都基于之前定义好的这些领域模型来完成。为什么说DDD真正的面向对象
DDD第一原则:将数据和操作结合。(贫血模型将数据和操作分离,破坏了封装特性,违反OOP的原则。)
DDD第二原则:界限上下文。这是将“单一指责”应用于我们的领域模型。DDD 也并非银弹
对于业务不复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的 DDD 开发模式有点大材小用,无法发挥作用。
相反,对于业务复杂的系统开发来说,基于充血模型的 DDD 开发模式,因为前期需要在设计上投入更多时间和精力,来提高代码的复用性和可维护性,所以相比基于贫血模型的开发模式,更加有优势。
里式替换 -- LSP (Liskov Substitution Principle)
子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
里式替换利用了多态特性,只是子类并不会更改父类的行为期望结果(输入、输入、异常)。
举个例子:
父类Transporter负责进行网络传输
子类SecurityTransporter在网络传输的时候,添加了appToken安全认证参数。
子类完美继承父类的设计初衷,并做了增强。
简单来讲,你可以用子类替换父类出现的任何位置,而不需要做单元测试。
-
里式替换和多态的区别
- 多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法
它是一种代码实现的思路。 - 式替换是一种设计原则,是用来指导继承关系中子类该如何设计的
子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
-
实现里式替换
父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。
实现子类时,必须保证以下几点:
- 父类函数声明要实现的功能
- 父类对输入、输出、异常的约定
- 注释中所罗列的任何特殊说明