设计模式之美
[toc]
01 | 为什么说每个程序员都要尽早地学习并掌握设计模式相关知识?
- 为什么要学习设计模式相关的知识?
- 应对面试中的设计模式相关问题;
- 告别写被人吐槽的烂代码;
- 提高复杂代码的设计和开发能力;
- 让读源码、学框架事半功倍;
- 为你的职场发展做铺垫。
02 | 从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?
-
- 如何评价代码质量的高低?
- 主观性
- 可读性、可维护性、灵活、优雅、简洁等
-
- 最常用的评价标准有哪几个?
- 可维护性、可读性、可扩展性、灵活性、简洁性、可复用性、可测试性。
-
- 如何才能写出高质量的代码?
- 面向对象设计思想
- 设计原则
- 设计模式
- 编码规范
- 重构技巧
03 | 面向对象、设计原则、设计模式、编程规范、重构,这五者有何关系?
04 | 理论一:当谈论面向对象的时候,我们到底在谈论什么?
-
- 什么是面向对象编程?
- 面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。
-
- 什么是面向对象编程语言?
- 支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言
-
- 如何判定一个编程语言是否是面向对象编程语言?
- 严格:支持类、对象、四大特性
- 宽泛:类、对象
-
- 面向对象编程和面向对象编程语言之间有何关系?
- 面向对象编程不一定要用面向对象语言,使用面向对象语言写出的代码也不一定是面向对象语言的
-
- 什么是面向对象分析和面向对象设计?
- 面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做。
- 产出是类图
05 | 理论二:封装、抽象、继承、多态分别可以解决哪些编程问题?
-
- 关于封装特性
- private、protected、public
- 意义:
- 保护数据不被随意修改,提高代码的可维护性
- 仅暴露有限的必要接口,提高类的易用性。
-
- 关于抽象特性
- 讲如何隐藏方法的具体实现
- 意义:
- 提高代码的可扩展性、维护性,修改实现不需要改变定义,减少代码的改动范围
- 处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息。
-
- 关于继承特性
- 表示类之间的 is-a 关系
- 单继承和多继承
- 意义:解决代码复用的问题。
-
- 关于多态特性
- 多态是指子类可以替换父类
- 意义:提高代码的扩展性和复用性
06 | 理论三:面向对象相比面向过程有哪些优势?面向过程真的过时了吗?
-
- 什么是面向过程编程?什么是面向过程编程语言?
- 面向对象编程以类为组织代码的基本单元,面向过程编程则是以过程(或方法)作为组织代码的基本单元
- 数据和方法相分离
- 不支持丰富的面向对象编程特性
-
- 面向对象编程相比面向过程编程有哪些优势?
- 更能应对这种复杂类型的程序开发
- 更加丰富的特性(封装、抽象、继承、多态)
- 易扩展、易复用、易维护。
- 更加人性化、更加高级、更加智能。
07 | 理论四:哪些代码设计看似是面向对象,实际是面向过程的?
- 三种违反面向对象编程风格的典型代码设计
- 滥用 getter、setter 方法
- Constants 类、Utils 类的设计问题
- 基于贫血模型的开发模式
- 贫血模型:是指领域对象里只有get和set方法,或者包含少量的CRUD方法,所有的业务逻辑都不包含在内而是放在Business Logic层。
08 | 理论五:接口vs抽象类的区别?如何用普通的类模拟抽象类和接口?
-
- 抽象类和接口的语法特性
- 抽象类不允许被实例化,只能被继承。
- 它可以包含属性和方法。
- 子类继承抽象类,必须实现抽象类中的所有抽象方法。
- 接口只能声明方法。
- 类实现接口的时候,必须实现接口中声明的所有方法。
-
- 抽象类和接口存在的意义
- 抽象类:代码复用问题
- 接口:解耦问题,隔离接口和具体的实现,提高代码的扩展性。
-
- 抽象类和接口的应用场景区别
- 抽象类:is-a,代码复用问题
- 接口:has-a,抽象
09 | 理论六:为什么基于接口而非实现编程?有必要为每个类都定义接口吗?
- “接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”。
- 原则:可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口
- 别名:基于抽象而非实现编程
- 越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。
- 作用提高代码扩展性、灵活性、可维护性最有效的手段之一
- 具体做法:
- 函数的命名不能暴露任何实现细节。
- 封装具体的实现细节
- 为实现类定义抽象的接口。
10 | 理论七:为何说要多用组合少用继承?如何决定该用组合还是继承?
-
- 为什么不推荐使用继承?
- 继承层次过深、过复杂,也会影响到代码的可维护性
-
- 组合相比继承有哪些优势?
- 继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。而这三个作用都可以通过组合、接口、委托三个技术手段来达成
- 组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题。
-
- 如何判断该用组合还是继承?
- 多用组合少用继承
- 类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承
11 | 实战一(上):业务开发常用的基于贫血模型的MVC架构违背OOP吗?
- 基于贫血模型的传统开发模式,是典型的面向过程的编程风格。
- 基于充血模型的 DDD 开发模式,是典型的面向对象的编程风格。
15 | 理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?
-
- 如何理解单一职责原则(SRP)?
- 一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性
-
- 如何判断类的职责是否足够单一?
- 类中的代码行数、函数或者属性过多;
- 类依赖的其他类过多,或者依赖类的其他类过多;
- 私有方法过多;
- 比较难给类起一个合适的名字;
- 类中大量的方法都是集中操作类中的某几个属性。
-
- 类的职责是否设计得越单一越好?
- 会降低内聚性,也会影响代码的可维护性。
16 | 理论二:如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?
-
- 如何理解“对扩展开放、对修改关闭”?
- 添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。
- 开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
-
- 如何做到“对扩展开放、修改关闭”?
- 扩展意识、抽象意识、封装意识。
- 高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。
17 | 理论三:里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?
- 子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”
- 约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
- 里式替换原则跟多态的区别:
- 里式替换:子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。
- 多态:一种代码实现的思路
18 | 理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解?
-
- 如何理解“接口隔离原则”?
- 如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
- 那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
-
- 接口隔离原则与单一职责原则的区别
- 单一职责原则针对的是模块、类、接口的设计
- 接口隔离原则:
- 更侧重于接口的设计
- 站在使用者的角度考虑职责是否单一
19 | 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?
-
- 控制反转
- 流程的控制权从程序员“反转”给了框架。
-
- 依赖注入
- 我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。
-
- 依赖注入框架
- 由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。
-
- 依赖反转原则
- 高层模块不依赖低层模块
20 | 理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?
- KISS: 尽量保持简单。
- YAGNI:你不会需要它。
- 如何写出满足 KISS 原则的代码
- 不要使用同事可能不懂的技术来实现代码;
- 不要重复造轮子,要善于使用已经有的工具类库;
- 不要过度优化。
21 | 理论七:重复的代码就一定违背DRY吗?如何提高代码的复用性?
-
- DRY 原则
- 三种典型的代码重复:实现逻辑重复、功能语义重复和代码执行重复
-
- 代码复用性
- 减少代码耦合
- 满足单一职责原则
- 模块化
- 业务与非业务逻辑分离
- 通用代码下沉
- 继承、多态、抽象、封装
- 应用模板等设计模式
-
- 我们可以不写可复用的代码,但一定不能写重复的代码。
22 | 理论八:如何用迪米特法则(LOD)实现“高内聚、松耦合”?
-
- 如何理解“高内聚、松耦合”?
- 高内聚: 就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中
- 松耦合: 在代码中,类与类之间的依赖关系简单清晰。
-
- 如何理解“迪米特法则”?
- 不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。
27 | 理论一:什么情况下要重构?到底重构什么?又该如何重构?
-
- 重构的目的:为什么重构(why)?
- 保持项目质量、锻炼代码能力、提升成就感。
-
- 重构的对象:重构什么(what)?
- 大规模高层次的重构:
- 代码分层、模块化、解耦、梳理类之间的交互关系、抽象复用组件等等。
- 小规模低层次的重构:
- 规范命名、注释、修正函数参数过多、消除超大类、提取重复代码等等
-
- 重构的时机:什么时候重构(when)?
- 持续重构
-
- 重构的方法:如何重构(how)?
- 大规模:有组织、有计划、分阶段
- 小规模:立即、持续
28 | 理论二:为了保证重构不出错,有哪些非常能落地的技术手段?
-
- 什么是单元测试?
- 单元测试是代码层面的类或函数的测试
-
- 为什么要写单元测试?
- 发现 bug
- 集成测试的补充,覆盖边界情况
- TDD
-
- 如何编写单元测试?
- 测试用例
- 覆盖各种输入、异常、边界情况
- 单元测试的认知:
- 编写单元测试尽管繁琐,但并不是太耗时;
- 我们可以稍微放低对单元测试代码质量的要求;
- 覆盖率作为衡量单元测试质量的唯一标准是不合理的;
- 单元测试不要依赖被测代码的具体实现逻辑;
- 单元测试框架无法测试,多半是因为代码的可测试性不好。
-
- 单元测试为何难落地执行?
- 为编写单元测试预留时间
- 正确认识单元测试
29 | 理论三:什么是代码的可测试性?如何写出可测试性好的代码?
-
- 什么是代码的可测试性?
- 针对代码编写单元测试的难易程度
-
- 编写可测试性代码的最有效手段:依赖注入
-
- 常见的 Anti-Patterns
- 常见的测试不友好的代码有下面这 5 种:
- 代码中包含未决行为逻辑
- 滥用可变全局变量
- 滥用静态方法
- 使用复杂的继承关系
- 高度耦合的代码
30 | 理论四:如何通过封装、抽象、模块化、中间层等解耦代码?
-
- “解耦”为何如此重要?
- 保证代码松耦合、高内聚,是控制代码复杂度的有效手段。
- 代码高内聚、松耦合,也就是意味着,代码结构清晰、分层模块化合理、依赖关系简单、模块或类之间的耦合小,那代码整体的质量就不会差。
-
- 代码是否需要“解耦”?
- 衡量标准:
- 看修改代码是否牵一发而动全身。
- 依赖关系图的复杂性来判断是否需要解耦重构。
-
- 如何给代码“解耦”?
- 封装与抽象、中间层、模块化
- 单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特法则等
- 观察者模式
31 | 理论五:让你最快速地改善代码质量的20条编程规范(上)
-
- 关于命名
- 命名的关键是能准确达意
- 借助小下文简化命名:类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命名。
- 命名要可读、可搜索。不要使用生僻的、不好读的英文单词来命名
- 接口命名:前缀“I”;后缀“Impl”
- 抽象类的命名:前缀“Abstract”;不带
-
- 关于注释
- 注释的目的就是让代码更容易看懂
- 做什么、为什么、怎么做、如何用
- 类和函数一定要写注释
32 | 理论五:让你最快速地改善代码质量的20条编程规范(中)
-
- 函数、类多大才合适?
- 函数的代码行数不要超过 50 行的大小
-
- 一行代码多长最合适?
- 最好不要超过 IDE 显示的宽度
-
- 善用空行分割单元块
-
- 四格缩进还是两格缩进?
- 两格缩进
- 一定不要用 tab 键缩进。
-
- 大括号是否要另起一行?
- 同一行
-
- 类中成员的排列顺序
- 字母序从小到大排列
- 先写成员变量后写函数
- 先静态后普通
-
- 最好能跟业内推荐的风格、开源项目的代码风格相一致
33 | 理论五:让你最快速地改善代码质量的20条编程规范(下)
-
- 关于编码技巧
- 将复杂的逻辑提炼拆分成函数和类。
- 通过拆分成多个函数或将参数封装为对象的方式,来处理参数过多的情况。
- 函数中不要使用参数来做代码执行逻辑的控制。
- 函数设计要职责单一。
- 移除过深的嵌套层次,方法包括:去掉多余的 if 或 else 语句,使用 continue、break、return 关键字提前退出嵌套,调整执行顺序来减少嵌套,将部分嵌套逻辑抽象成函数。
- 用字面常量取代魔法数。
- 用解释性变量来解释复杂表达式,以此提高代码可读性。
-
- 统一编码规范
- 统一的编码规范
38 | 总结回顾面向对象、设计原则、编程规范、重构技巧等知识点
74 | 总结回顾23种经典设计模式的原理、背后的思想、应用场景等
-
- 单例模式
- 创建全局唯一的对象
- 实现方式:饿汉式、懒汉式、双重检测、静态内部类、枚举。
- 进程唯一单例、线程唯一单例、集群唯一单例、多例
- 缺点:
- 单例对 OOP 特性的支持不友好
- 单例会隐藏类之间的依赖关系
- 单例对代码的扩展性不友好
- 单例对代码的可测试性不友好
- 单例不支持有参数的构造函数
-
- 工厂模式
- 简单工厂、工厂方法、抽象工厂
75 | 在实际的项目开发中,如何避免过度设计?又如何避免设计不足?
78 | 开源实战二(上):从Unix开源开发学习应对大型复杂项目开发
- 如何应对复杂软件开发?
- 封装与抽象
- 分层与模块化
- 基于接口通信
- 高内聚、松耦合
- 为扩展而设计
- KISS 首要原则
- 最小惊奇原则
79 | 开源实战二(中):从Unix开源开发学习应对大型复杂项目开发
- 如何长期保证代码质量,让代码长期可维护?
- 吹毛求疵般地执行编码规范
- 编写高质量的单元测试
- 不流于形式的 Code Review
- 开发未动、文档先行
- 持续重构、重构、重构
- 对项目与团队进行拆分
80 | 开源实战二(下):从Unix开源开发学习应对大型复杂项目开发
- 为什么要 Code Review?
- Code Review 践行“三人行必有我师”
- Code Review 能摒弃“个人英雄主义”
- Code Review 能有效提高代码可读性
- Code Review 是技术传帮带的有效途径
- Code Review 保证代码不止一个人熟悉
- Code Review 能打造良好的技术氛围
- Code Review 是一种技术沟通方式
- Code Review 能提高团队的自律性
- 怎么做 Code Review?
99 | 总结回顾:在实际软件开发中常用的设计思想、原则和模式
- 对象设计分为四个环节:
- 划分职责并识别出有哪些类
- 定义类及其属性和方法
- 定义类之间的交互关系
- 组装类并提供执行入口
- 两个设计思想:
- 基于接口而非实现的设计思想
- 多用组合少用继承的设计思想
100 | 如何将设计思想、原则、模式等理论知识应用到项目中?
- 建议你把专栏中讲到的经典的设计思想、原则、模式,打印出来贴在电脑旁