0x1、学习导读
学习算法
→ 是为了写出 高效 的代码;
学习设计模式
→ 是为了写出 高质量 (可扩展、可读、可维护)的代码;
很多开发仔写了很多年代码,Coding水平却没啥长进,原因是日常工作都是CV、修修补补的重复劳动。编写的代码大都止步于能用就好、能跑就行,能力自然停留在"会干活"的层面,只能算一个代码搬运的 熟练工。
① 学习设计模式的理由
- ①
应付面试
;
- ②
少写烂代码
(写的代码维护费劲,增删功能,常常牵一发而动全身);
- ③
提高复杂代码的设计和开发能力
(开发一个与业务无关的通用功能模块,力不从心,不止从何入手);
- ④
读源码、学框架事半功倍
(琢磨不透作者的设计思路,一些明显的设计思路要花费很多时间才能参悟);
- ⑤
职场发展做铺垫
(成为技术大牛的基本功,成为Leader指导培训新人,code review,招聘等);
② 如何判断代码质量的好坏
对一段代码的质量评价,常常具有很强的主观性,每个人的评判标准不一,这跟工程师自身经验有极大关系。
闷头写代码,在没 有人指导和阅读借鉴优秀源码 的情况下,很容易有种 自己的代码已经写得足够好 的错觉。
代码质量常用的几个评价标准:
- ①
可维护性
(Maintainability) → 较直观角度:Bug容易修复、修改添加功能轻松,则主观认为是易维护的; - ②
可读性
(Readability) → 好的验证手段:code review,别人可以轻松读懂你写的代码,说明代码可读性好; - ③
扩展性
(Extensibility) → 代码预留扩展点,添加功能直接插,无需大动干戈改动大量原始代码; - ④
灵活性
(Flexibility) → 一段代码易扩展、易复用或易用,可以称这段代码写得比较灵活; - ⑤
简洁性
(Simplicity) → 代码尽量写得简洁,逻辑清晰,符合KISS原则; - ⑥
可复用性
(Reusability) → 尽量减少重复代码的编写,复用已有代码; - ⑦
可测试性
(Testability) → 代码比较难写单元测试,基本上能说明代码设计得有问题;
如何才能写出搞质量代码?
- ①
面向对象设计思想
→ 因其具有丰富的特性(封装、抽象、继承、多态),可实现很多复杂的设计思路,基础; - ②
设计原则
→ 代码设计的经验总结,对某些场景下应用何种设计模式,有指导意义; - ③
设计模式
→ 针对软件开发中常见的设计问题,总结出来的一套解决方案或设计思路; - ④
编码规范
→ 主要解决代码可读性问题,更偏重代码细节,持续小重构依赖的理论基础; - ⑤
重构技巧
→ 利用前面这四种理论,作为保持代码质量不下降的有效手段;
0x2、面向对象(OOP)
① 概念相关
面向过程编程 (OPP,Procedure Oriented Programming)
以 过程 为基础的编程范式/风格,主要关注 怎么做,即完成任务的具体细节,主要特点是数据与方法相互分离,流程化拼接一组顺序执行的方法,来操作数据完成某项功能。
面向对象编程 (OOP,Object Oriented Programming)
以 类或对象 为基础的编程范式/风格,主要关注 "谁来做",即完成任务的对象,将封装、抽象、继承、多态四个特性,作为代码设计与实现的基石。
面向过程编程语言
不支持类与对象语法概念,不支持丰富的面向对象特性,仅支持面向过程编程。
面向对象编程语言
支持类与对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。另外,用面向对象语言编写的代码不一定就是面向对象编程风格的,也可能是面向过程的编程风格。
面向对象分析 (OOA,Object Oriented Analysis) 与 面向对象设计 (OOD,Object Oriented Design)
围绕着对象或类做需求分析与设计,前者搞清楚 做什么,后者搞清楚 怎么做,两个阶段的最终产出是 类的设计,包括程序被拆解成哪些类、每个类有哪些属性方法、类与类间如何交互等。而OOP就是将这两者的产出翻译成代码的过程。
OOP对比OPP编程有什么优势:
- 大规模复杂程序开发,程序处理流程并非单一主线,而是错综复杂的网状结果,OOP更易应对;
- OOP相比OPP,具有更多丰富特性,利用这些特性写出来的代码,更加易扩展、易复用、易维护;
- OOP语言比起OPP语言,更加人性化、更加高级、更加智能 。
② 封装 (Encapsulation)
信息隐藏或数据访问保护
,表现为:类暴露有限的访问接口(函数),授权外部仅能通过这些方式访问/修改内部信息或数据。
如Java中:使用 private
关键字设置访问限制,提供 getter
和 setter
供外部对数据仅限有限的操作和访问。
封装的意义:
对类中属性的访问不加限制,可在任何代码中随意访问篡改,看似很灵活,却带来了 不可控问题。属性的修改逻辑可能散落在代码的各个角落,势必影响代码的可读性、可维护性。
一个封装的简单例子:
public class UserCredential { private String id; // 用户ID private String key; // 用户Key private long lastVerifyTime; // 上次校验时间 private long verifyCount; // 校验次数 public UserCredential(String id, String key) { this.id = id; this.key = key; } public Long getLastVerifyTime() { return lastVerifyTime; } public void setLastVerifyTime(long lastVerifyTime) { this.lastVerifyTime = lastVerifyTime; } public void increaseVerifyCount() { verifyCount++; } public long getVerifyCount() { return verifyCount; } }
代码解析:(对上面四个属性的访问进行了限制)
- id、key → 创建用户凭证实例时就确定好,不该改动,所以不暴露访问或修改的方法;
- lastVerifyTime → 每次验证凭证都更新这个值,有时也需要这个值,所以暴露getter和setter方法;
- verifyCount → 每次校验都更新这个值,只会增且是+1,有时也需要这个值,所以暴露increase和getter方法;
Tips:设计实现类时,除非真的需要,否则,尽量不要给属性定义setter方法,除此之外,getter方法虽然相对setter安全写,但如果返回的是集合容器(如List),要注意防范集合内部数据被修改的危险。
封装带来的好处:
减轻代码调用者对该类的学习负担(背后的业务细节),不必了解每个属性,可以放心地调用暴露的方法。
② 抽象 (Abstraction)
如何隐藏方法的具体实现
,表现为:调用者只需关心方法提供的功能,而不需要知道功能是如何实现的。
在面向对象编程中,常利用编程语言提供的 接口类
(如Java中的Interface)或 抽象类
(如Java中的abstract) 这两种语法机制来实现抽象。
一个抽象的简单例子:
public interface IImageLoader { public void loadImage(String url); } public class MemoryImageLoader implements IImageLoader { @Override public void loadImage(String url) { ... } } public class DiskImageLoader implements IImageLoader { @Override public void loadImage(String url) { ... } }
代码解析:
调用者在加载图片时,只需了解IImageLoader接口类暴露了什么方法,而不需要查看MemoryImageLoader和DiskImageLoader中的具体实现细节。
另外,抽象有时会被排除在面向对象的四大特性之外,原因是:
抽象这个特性,其实可以不借助接口类或抽象类这类特殊语法机制实现,类的方法通过编程语言中的 "
函数
" 语法实现。通过函数包裹具体实现逻辑,调用者无需研究具体的实现逻辑,通过函数名、注释或文档了解到函数功能,即可直接使用,这本身就是一种抽象
。
抽象的意义:
在面对复杂系统时,人脑能承受的信息复杂度是有限的,抽象这种只关注功能点不关注实现的设计思路,可以帮我们过滤掉很多非必要的信息。另外,很多设计原则也体现了抽象这种设计思想。
④ 继承 (Inheritance)
用来表示类之间的is-a关系
,比如:猫是一种哺乳动物。根据遗传关系划分可以划分为:单继承和多继承。
单继承只能继承一个父类,多继承可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。
为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持,比如Java用extends,C++使用冒号(:)等。
有些编程语言只支持单继承,不支持多继承,比如Java,而有些编程语言都支持,比如C++。
继承的意义:
代码复用,两个类具有相同属性或方法,将这部分代码抽取到父类中,让两个类继承父类,子类重用父类代码,避免代码重复。另外,应 避免过度使用继承,继承的层次过深过复杂,就会导致代码可读性、可维护性变差。