算是读书笔记吧
极客时间--设计模式之美
单一职责原则 -- SRP(Single Responsibility Principle)
A class or module should have a single reponsibility
一个类或者模块只负责完成一个职责(或者功能)
-
类和模块
其实类和模块都可以看做抽象集合
本质上都是一个领域的抽象,类作为方法的聚合抽象、模块作为类的聚合抽象。
-
完成单一职责
不要设计大而全的类,要设计粒度小、功能单一的类。
简单来说,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成粒度更小的类。
-
不要过度拆分
业务是否相干,职责是否单一。很多时候具有主观性,也就是程序员对模块使命的理解。
对于没办法完全说服自己进行拆分的两个功能:
我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构。
-
一些可以借鉴的量化指标
- 类中的代码行数、函数或属性过多
会影响代码的可读性和可维护性
,我们就需要考虑对类进行拆分; - 类依赖的其他类过多,或者依赖类的其他类过多
不符合高内聚、低耦合
的设计思想,我们就需要考虑对类进行拆分; - 私有方法过多
我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性
; - 比较难给类起一个合适名字
很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰
; - 类中大量的方法都是集中操作类中的某几个属性
比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。也能提高代码复用性
。
-
一个类多少行代码合适?
这个也很难明确的量化,就像问大厨“放盐少许”中的“少许”是多少一样。
不过当一个类的代码,读起来让你头大了,实现某个功能时不知道该用哪个函数了,想用哪个函数翻半天都找不到了,只用到一个小功能要引入整个类(类中包含很多无关此功能实现的函数)的时候,这就说明类的行数、函数、属性过多了。
接口隔离 -- ISP(Interface Segregation Principle)
Clients should not be forced to depend upon interfaces that they do not use
客户端(使用者)不应该强迫依赖它不需要的接口
-
一组 API 接口集合
如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口
public interface UserService {
boolean register(String cellphone, String password);
boolean login(String cellphone, String password);
UserInfo getUserInfoById(long id);
UserInfo getUserInfoByCellphone(String cellphone);
}
// 删除用户接口不应该暴露给客户端
public interface RestrictedUserService {
boolean deleteUserByCellphone(String cellphone);
boolean deleteUserById(long id);
}
public class UserServiceImpl implements UserService, RestrictedUserService {
// ...省略实现代码...
}
-
把“接口”理解为单个 API 接口或函数
函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现
以一个统计用类来举例:
public class Statistics {
private Long max;
private Long min;
private Long average;
private Long sum;
private Long percentile99;
private Long percentile999;
//...省略constructor/getter/setter等方法...
}
public Statistics count(Collection<Long> dataSet) {
Statistics statistics = new Statistics();
//...省略计算逻辑...
return statistics;
}
count方法内部会计算各种各样的结果,对单个结果的修改,都需要修改count方法。
如果大部分结果的使用频率不高,那么每次调用count,也会进行许多无用计算。
我们可以把接口更加细化,支持单个条件的获取
public Long max(Collection<Long> dataSet) { //... }
public Long min(Collection<Long> dataSet) { //... }
public Long average(Colletion<Long> dataSet) { //... }
// ...省略其他统计函数...
-
把“接口”理解为 OOP 中的接口概念
主要体现在面向接口/协议编程中
不要设计大而全的接口,通过按类型、功能划分的方式细化接口粒度。
在复用性以及扩展性上都有好处。
大而全的接口,也会强迫接入者实现无用方法,不利于后期修改维护。
依赖反转原则
高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。
所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。
简而言之,依赖反转希望调用者的执行,不依赖被具体的调用者。
这个具体的被调用者,指的是具体的类。
如何能够不依赖具体的类?答案是面向接口编程。二者共同依赖同一个抽象(接口)。
以发电厂为电器供应电力为例
发电厂并不依赖具体的电器,而是通过共同的抽象(电源插口),与具体的电器相连。
在新增电器时,发电厂并不需要对其进行单独的设置,只要把这个电器也接入电源插口即可即可。
这样设计好处有两点:
- 低层次模块更加通用,适用性更广
- 高层次模块没有依赖低层次模块的具体实现,方便低层次模块的替换
保持简单 -- KISS原则
KISS原则主要想说“如何做”的问题:尽量保持简单
他的描述有好几个
Keep It Simple and Stupid.
Keep It Short and Simple.
Keep It Simple and Straightforward.
并不是代码行数越少肯定好
比如正则表达式和一些奇技淫巧,他们行数很少,但是维护起来可能要付出大量的精力不要使用同事可能不懂的技术来实现代码
如果同时维护你的代码,要花很多时间去学习一门技术,那会大大的降低开发效率不要重复造轮子,要善于使用已经有的工具类库
在写一个新轮子之前,看一看项目文档,或者问问同事。
使用已有的工具类库,或者对其进行扩展。
不要让项目中同样的功能,出现两个工具类。
自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。不要过度优化
不要过度使用一些奇技淫巧来优化代码,牺牲代码的可读性。
比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等
过度设计 -- YAGNI原则
YAGNI 原则说的是“要不要做”的问题:当前不需要的就不要做
比如模块的可扩展性、各种设计模式的使用。
如果在可预见的范围内,并不需要就不要那样设计。
当项目的发展超出了预期,再去重构
重复代码 -- DRY原则
Don’t Repeat Yourself
不要写重复的代码
当代码的某些地方必须更改时,你是否发现自己在多个位置以多种不同格式进行了更改?
你是否需要更改代码和文档,或更改包含其的数据库架构和结构,或者…?
如果是这样,则您的代码不是DRY。
-
1.实现逻辑重复
比如登录时对于用户名
和密码
的格式校验。
二者分别叫isValidUserName() 函数和 isValidPassword()。初期二者可能校验逻辑相同,所以被copy了两份。
public class UserAuthenticator {
public void authenticate(String username, String password) {
if (!isValidUsername(username)) {
// ...throw InvalidUsernameException...
}
if (!isValidPassword(password)) {
// ...throw InvalidPasswordException...
}
//...省略其他代码...
}
private boolean isValidUsername(String username) {
// check not null, not empty
if (StringUtils.isBlank(username)) {
return false;
}
// check length: 4~64
int length = username.length();
if (length < 4 || length > 64) {
return false;
}
// contains only lowcase characters
if (!StringUtils.isAllLowerCase(username)) {
return false;
}
// contains only a~z,0~9,dot
for (int i = 0; i < length; ++i) {
char c = username.charAt(i);
if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
return false;
}
}
return true;
}
private boolean isValidPassword(String password) {
// check not null, not empty
if (StringUtils.isBlank(password)) {
return false;
}
// check length: 4~64
int length = password.length();
if (length < 4 || length > 64) {
return false;
}
// contains only lowcase characters
if (!StringUtils.isAllLowerCase(password)) {
return false;
}
// contains only a~z,0~9,dot
for (int i = 0; i < length; ++i) {
char c = password.charAt(i);
if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
return false;
}
}
return true;
}
}
在合并时,我们要注意不能将其简单的合并成isValidUserNameOrPassword(),这样会导致将来难以维护。
我们可以将其根据具体功能抽象成一个或几个单独的校验函数,分别组装进isValidUserName() 函数和 isValidPassword()中。
比如将校验只包含 a-z、0~9、dot 的逻辑封装成 boolean onlyContains(String str, String charlist); 函数。
-
功能语义重复
比如两个判断IP地址的函数isValidIp() 和 checkIfIpValid()
尽管两个函数的命名不同,实现逻辑不同,但功能是相同的。
public boolean isValidIp(String ipAddress) {
if (StringUtils.isBlank(ipAddress)) return false;
String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
return ipAddress.matches(regex);
}
public boolean checkIfIpValid(String ipAddress) {
if (StringUtils.isBlank(ipAddress)) return false;
String[] ipUnits = StringUtils.split(ipAddress, '.');
if (ipUnits.length != 4) {
return false;
}
for (int i = 0; i < 4; ++i) {
int ipUnitIntValue;
try {
ipUnitIntValue = Integer.parseInt(ipUnits[i]);
} catch (NumberFormatException e) {
return false;
}
if (ipUnitIntValue < 0 || ipUnitIntValue > 255) {
return false;
}
if (i == 0 && ipUnitIntValue == 0) {
return false;
}
}
return true;
}
尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。
我们应该在项目中,统一一种实现思路,同样语义的代码,都统一调用同一个函数。
-
3.代码执行重复
很简单,如果一个代码的调用链中。有些无用逻辑的调用或者重复调用。
就需要重构一下,将重复的逻辑抽离出来。
-
4.过多的过程性注释
写了好多的注释解释代码的执行逻辑,后续修改的这个方法的时候可能,忘记修改注释,造成对代码理解的困难。
实际应用应该使用KISS原则,将方法写的见名知意,尽量容易阅读。注释不必过多。
-
如何提升代码复用性
- 减少代码耦合
- 满足单一职责原则
- 模块化
- 业务与非业务逻辑分离
- 通用代码下沉
- 继承、多态、抽象、封装
- 应用模板等设计模式
复用意识也非常重要。
在设计每个模块、类、函数的时候,要像设计一个外部 API 一样去思考它的复用性。
-
Rule of Three
第一次编写代码的时候,我们不考虑复用性;
第二次遇到复用场景的时候,再进行重构使其复用。
需要注意的是,“Rule of Three”中的“Three”并不是真的就指确切的“三”,这里就是指“二”。
高内聚、松耦合 -- 迪米特法则(LOD)
The Least Knowledge Principle:
Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.最小知识原则
每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。
不该有直接依赖关系的类之间,不要有依赖
假如你现在要去商店买东西,你肯定不会直接把钱包给收银员,让收银员自己从里面拿钱,而是你从钱包里把钱拿出来交给收银员。
具体一些,非必要情况下,不要为了一两个属性传入整个类。
把类和类偶合起来。有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)
类似序列化与反序列化的类,二者必须在一个类中实现,当方法较少时没什么问题。
一旦实现了许多序列化与反序列化的方式,大部分代码只需要用到序列化的功能。
对于这部分使用者,没必要了解反序列化的“知识”。
那么,我们可以通过接口隔离原则,用序列化和反序列化两个接口来对两个接口进行隔离。高内聚
相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中松耦合
类与类之间的依赖关系简单清晰
即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动