一、高质量代码的评判标准:

  1. 可维护性:在不破化原有代码设计、不引入新的bug的情况下,能够快速的修改或者添加代码

  1. 可读性:我们需要看代码是否符合编码规范、命名是否达意、注释是否详尽、函数是否长短合适、模块划分是否清晰、是否符合高内聚低耦合等等

  1. 可扩展性:代码预留了一些功能扩展点,你可以把新功能代码,直接插到扩展点上,而不需要因为要添加一个功能而大动干戈,改动大量的原始代码。“对修改关闭,对扩展开放”这条设计原则。

  1. 灵活性:

①当我们添加一个新的功能代码的时候,原有的代码已经预留好了扩展点,我们不需要修改原有的代码,只要在扩展点上添加新的代码即可

②当我们要实现一个功能的时候,发现原有代码中,已经抽象出了很多底层可以复用的模块、类等代码,我们可以拿来直接使用。

③当我们使用某组接口的时候,如果这组接口可以应对各种使用场景,满足各种不同的需求

  1. 简洁性:我们在编写代码的时候,往往也会把简单、清晰放到首位。

  1. 可复用性:

①比如,当讲到面向对象特性的时候,我们会讲到继承、多态存在的目的之一,就是为了提高代码的可复用性;

②当讲到设计原则的时候,我们会讲到单一职责原则也跟代码的可复用性相关;

③当讲到重构技巧的时候,我们会讲到解耦、高内聚、模块化等都能提高代码的可复用性。

  1. 可测试性:单元测试比较容易编写

二、面向对象:

  1. 封装:封装也叫作信息隐藏或者数据访问保护

  1. 抽象:例如调用者在使用图片存储功能的时候,只需要了解 IPictureStorage 这个接口类暴露了哪些方法就可以了,不需要去查看 PictureStorage 类里的具体实现逻辑。

  1. 继承:单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。

  1. 多态:在实际的代码运行过程中,调用子类新的功能逻辑,而不是在原有代码上做修改。这就遵从了“对修改关闭、对扩展开放”的设计原则,提高代码的扩展性。

三、面向对象设计理念:

1、相关联的数据封装在一个类里,例如在 ShoppingCart 类中定义一个 clear() 方法,将清空购物车的业务逻辑封装在里面(itemsCount、totalPrice、items 三者数据同步),透明地给调用者使用。

2、我们可以通过 Java 提供的 Collections.unmodifiableList() 方法,让 getter 方法返回一个不可被修改的 UnmodifiableList 集合容器

四、面向过程设计理念:

  1. 针对不同的功能,静态方法无需属性,设计不同的 Utils 类,比如 FileUtils、IOUtils、StringUtils、UrlUtils 等,不要设计一个过于大而全的 Utils 类。

五、接口VS抽象类的区别

1、特性区别:

抽象类特性:

①抽象类不允许被实例化,只能被继承。也就是说,你不能 new 一个抽象类的对象出来(Logger logger = new Logger(...); 会报编译错误)。

②抽象类可以包含属性和方法。方法既可以包含代码实现(比如 Logger 中的 log() 方法),也可以不包含代码实现(比如 Logger 中的 doLog() 方法)。不包含代码实现的方法叫作抽象方法。

③子类继承抽象类,必须实现抽象类中的所有抽象方法。

接口特性:

①接口不能包含属性(也就是成员变量)。

②接口只能声明方法,方法不能包含代码实现。

③类实现接口的时候,必须实现接口中声明的所有方法。

2、使用区别:

抽象类使用:

①在 Logger 中定义一个空的方法,会影响代码的可读性。

②当创建一个新的子类继承 Logger 父类的时候,我们有可能会忘记重新实现 log() 方法。之前基于抽象类的设计思路,编译器会强制要求子类重写 log() 方法,否则会报编译错误

③不能实例化,减少误用风险

接口使用:

①接口就更侧重于解耦。接口是对行为的一种抽象,相当于一组协议或者契约,你可以联想类比一下 API 接口。

运用场景:

①如果我们要表示一种 is-a 的关系,并且是为了解决代码复用的问题,我们就用抽象类;

②如果我们要表示一种 has-a 关系,并且是为了解决抽象而非代码复用的问题,那我们就可以使用接口。

3、设计思路区别:

①抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)。

②而接口正好相反,它是一种自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再去考虑具体的实现。

六、接口

  1. ”基于接口而非实现编程”的原则:

①命名要足够通用,函数的命名不能暴露任何实现细节。比如,前面提到的 uploadToAliyun() 就不符合要求,应该改为去掉 aliyun 这样的字眼,改为更加抽象的命名方式,比如:upload()。

②与特定实现有关的方法不要定义在接口中,封装具体的实现细节。比如,跟阿里云相关的特殊上传(或下载)流程不应该暴露给调用者。我们对上传(或下载)流程进行封装,对外提供一个包裹所有上传(或下载)细节的方法,给调用者使用。

③为实现类定义抽象的接口。具体的实现类都依赖统一的接口定义,遵从一致的上传功能协议。使用者依赖接口,而不是具体的实现类来编程。

七、多用组合少用继承:

1、继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性

2、通过组合和委托技术来消除代码重复

public class Url {//...省略属性和方法
}public class Crawler {private Url url; // 组合public Crawler() {this.url = new Url();}//...
}public class PageAnalyzer {private Url url; // 组合public PageAnalyzer() {this.url = new Url();}//..
}

3、使用场景:

①如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。

②反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。

例如多态:FeignClient 是一个外部类,我们没有权限去修改这部分代码,但是我们希望能重写这个类在运行时执行的 encode() 函数。这个时候,我们只能采用继承来实现了。

八、贫血模型MVC

1、组织代码:

①UserEntity 和 UserRepository 组成了数据访问层

②UserBo 和 UserService 组成了业务逻辑层

③UserVo 和 UserController 在这里属于接口层。

像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。同理,UserEntity、UserVo 都是基于贫血模型设计的。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。

九、充血模型DDD

1、基于充血模型的 DDD 开发模式实现的代码,也是按照 MVC 三层架构分层的。Controller 层还是负责暴露接口,Repository 层还是负责数据存取,Service 层负责核心业务逻辑。它跟基于贫血模型的传统开发模式的区别主要在 Service 层。

2、在基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain 就相当于贫血模型中的 BO。不过,Domain 与 BO 的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。

3、基于贫血模型的传统的开发模式,重 Service 轻 BO;基于充血模型的 DDD 开发模式,轻 Service 重 Domain。

4、应用基于充血模型的 DDD 的开发模式,在这种开发模式下,我们需要事先理清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。

5、DDD中Domain做数据联动处理

6、充血模型DDD中service的功能:

①Service 类负责与 Repository 交流。

②Service 类负责跨领域模型的业务聚合功能。VirtualWalletService 类中的 transfer() 转账函数会涉及两个钱包的操作,因此这部分业务逻辑无法放到 VirtualWallet 类中,所以,我们暂且把转账业务放到 VirtualWalletService 类中了。

③Service 类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中。

十、需求拆解

1、拆解出来的每个功能点要尽可能的小。每个功能点只负责做一件很小的事情(专业叫法是“单一职责) 2、针对这种复杂的需求开发,我们首先要做的是进行模块划分,将需求先简单划分成几个小的、独立的功能模块,然后再在模块内部,应用我们刚刚讲的方法,进行面向对象设计。而模块的划分和识别,跟类的划分和识别,是类似的套路。

3、识别出需求描述中的动词,作为候选的方法,再进一步过滤筛选。类比一下方法的识别,我们可以把功能点中涉及的名词,作为候选属性,然后同样进行过滤筛选。

例如:

第一个细节:并不是所有出现的名词都被定义为类的属性,比如 URL、AppID、密码、时间戳这几个名词,我们把它作为了方法的参数。

第二个细节:我们还需要挖掘一些没有出现在功能点描述中属性,比如 createTime,expireTimeInterval,它们用在 isExpired() 函数中,用来判定 token 是否过期。

第三个细节:我们还给 AuthToken 类添加了一个功能点描述中没有提到的方法 getToken()。

第一个细节告诉我们,从业务模型上来说,不应该属于这个类的属性和方法,不应该被放到这个类里。比如 URL、AppID 这些信息,从业务模型上来说,不应该属于 AuthToken,所以我们不应该放到这个类中。

第二、第三个细节告诉我们,在设计类具有哪些属性和方法的时候,不能单纯地依赖当下的需求,还要分析这个类从业务模型上来讲,理应具有哪些属性和方法。这样可以一方面保证类定义的完整性,另一方面不仅为当下的需求还为未来的需求做些准备。

十一、定义类与类之间的交互关系

泛化(Generalization)可以简单理解为继承关系。

实现(Realization)一般是指接口和实现类之间的关系。

组合(Composition)也是一种包含关系。只要 B 类对象是 A 类对象的成员变量,那我们就称,A 类跟 B 类是组合关系,比如鸟与翅膀之间的关系

依赖(Dependency)是一种比关联关系更加弱的关系,包含关联关系。不管是 B 类对象是 A 类对象的成员变量,还是 A 类的方法使用 B 类对象作为参数或者返回值、局部变量,只要 B 类对象和 A 类对象有任何使用关系,我们都称它们有依赖关系。

十二、单一职责原则:

1、类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;

2、类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;

3、私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;

4、比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;

5、类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。

十三、对扩展开放、对修改关闭原则:

1、方法:多态、依赖注入、基于接口而非实现编程

2、两种意识:

① 封装意识:入参传入对象

② 扩展意识、抽象意识:处理方法使用数组遍历和多态

十四、里式替换原则:

1、行为约定包括:

函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。

2、违背里氏替换原则例子:

① 子类违背父类声明要实现的功能父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照创建日期来给订单排序的。那子类的设计就违背里式替换原则。

② 子类违背父类对输入、输出、异常的约定在父类中,某个函数约定:运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null。那子类的设计就违背里式替换原则。

在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就抛出,也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。

③ 子类违背父类注释中所罗列的任何特殊说明父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。

3、验证方法:

那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全地遵守父类的约定,子类有可能违背了里式替换原则。

十五、接口隔离原则:

1、把“接口”理解为一组 API 接口集合:

在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。

2、把“接口”理解为单个 API 接口或函数:

函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。

注意:接口隔离原则与单一职责的区别:

单一职责原则针对的是模块、类、接口的设计。

而接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同。

它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

4、把“接口”理解为 OOP 中的接口概念

①接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

②如果我们的接口粒度比较小,那涉及改动的类就比较少。

十六、控制反转:

1、控制反转(IOC):

这里的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员“反转”到了框架。

2、依赖注入:

不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

public class StupidStudent {private SmartStudent smartStudent;// 通过构造函数、函数参数方式传入给类使用public StupidStudent(SmartStudent smartStudent) {this.smartStudent = smartStudent;}public doHomewrok() {smartStudent.doHomework();System.out.println("学渣抄作业");}
}
public class StudentTest {public static void main(String[] args) {SmartStudent smartStudent = new SmartStudent();// 通过构造函数、函数参数方式传入给类使用StupidStudent stupidStudent = new StupidStudent(smartStudent);stupidStudent.doHomework();}
}

3、依赖注入框架(DI Framework):

我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

4、依赖反转原则(DIP)

依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。

高层模块不依赖低层模块,它们共同依赖同一个抽象。

抽象不要依赖具体实现细节,具体实现细节依赖抽象。

十七、KISS原则:

1、不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。

2、不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。

3、不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。

十八、YAGNI原则:

1、不要去设计当前用不到的功能;不要去编写当前用不到的代码。这条原则的核心思想就是:不要做过度设计。

2、YAGNI与KISS原则的区别:

① KISS 原则讲的是“如何做”的问题(尽量保持简单)

② 而 YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做)。

十九、DRY原则:

1、实现逻辑重复:

尽管代码的实现逻辑是相同的,但语义不同,我们判定它并不违反 DRY 原则。对于包含重复代码的问题,我们可以通过抽象成更细粒度函数的方式来解决。比如将校验只包含 a~z、0~9、dot 的逻辑封装成 boolean onlyContains(String str, String charlist); 函数。

2、功能语义重复:

在项目中,统一一种实现思路,所有用到判断 IP 地址是否合法的地方,都统一调用同一个函数。

3、代码执行重复:

去除两次重复执行的代码逻辑

二十、代码复用性:

1、如何提高代码复用性:

①减少代码耦合

②满足单一职责原则

③模块化

④业务与非业务逻辑分离

⑤通用代码下沉

⑥继承、多态、抽象、封装

⑦应用模板等设计模式

2、复用意识也非常重要:

在设计每个模块、类、函数的时候,要像设计一个外部 API 一样去思考它的复用性。

二十一、迪米特法则:

1、高内聚:

①所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。

②相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。

③单一职责原则是实现代码高内聚非常有效的设计原则。

2、松耦合:

①在代码中,类与类之间的依赖关系简单清晰。

②即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。

③依赖注入、接口隔离、基于接口而非实现编程,以及今天讲的迪米特法则,都是为了实现代码的松耦合

3、迪米特法则:

①不该有直接依赖关系的类之间,不要有依赖;

②有依赖关系的类之间,尽量只依赖必要的接口(通过两个接口实现)

4、既满足迪米特,同时满足高内聚


public interface Serializable {String serialize(Object object);
}public interface Deserializable {Object deserialize(String text);
}public class Serialization implements Serializable, Deserializable {@Overridepublic String serialize(Object object) {String serializedResult = ...;...return serializedResult;}@Overridepublic Object deserialize(String str) {Object deserializedResult = ...;...return deserializedResult;}
}public class DemoClass_1 {private Serializable serializer;public Demo(Serializable serializer) {this.serializer = serializer;}//...
}public class DemoClass_2 {private Deserializable deserializer;public Demo(Deserializable deserializer) {this.deserializer = deserializer;}//...
}

二十二、系统设计

1. 合理地将功能划分到不同模块

2. 设计模块与模块之间的交互关系

上下层系统之间的调用倾向于通过同步接口,同层之间的调用倾向于异步消息调用。

比如,营销系统和积分系统是上下层关系,它们之间就比较推荐使用同步接口调用。

订单系统就跟营销系统完全解耦,使用异步消息调用

3. 设计模块的接口、数据库、业务模型

二十三、 facade(外观)设计模式:

在职责单一的细粒度接口之上,再封装一层粗粒度的接口给外部使用。

二十四、为什么要分 MVC 三层开发:

1. 分层能起到代码复用的作用:

同一个 Repository 可能会被多个 Service 来调用,同一个 Service 可能会被多个 Controller 调用。

2. 分层能起到隔离变化的作用:

①分层体现了一种抽象和封装的设计思想。比如,Repository 层封装了对数据库访问的操作,提供了抽象的数据访问接口。

②基于接口而非实现编程的设计思想,Service 层使用 Repository 层提供的接口,并不关心其底层依赖的是哪种具体的数据库。当我们需要替换数据库的时候,比如从 MySQL 到 Oracle,从 Oracle 到 Redis,只需要改动 Repository 层的代码,Service 层的代码完全不需要修改。

③分层之后,Controller 层中代码的频繁改动并不会影响到稳定的 Repository 层。

3. 分层能起到隔离关注点的作用:

①Repository 层只关注数据的读写。

②Service 层只关注业务逻辑,不关注数据的来源。

③Controller 层只关注与外界打交道,数据校验、封装、格式转换,并不关心业务逻辑。

④三层之间的关注点不同,分层之后,职责分明,更加符合单一职责原则,代码的内聚性更好。

4. 分层能提高代码的可测试性:

单元测试不依赖不可控的外部组件,比如数据库。分层之后,Repsitory 层的代码通过依赖注入的方式供 Service 层使用,当要测试包含核心业务逻辑的 Service 层代码的时候,我们可以用 mock 的数据源替代真实的数据库,注入到 Service 层代码中。

5. 分层能应对系统的复杂性:

①水平方向基于业务来做拆分,就是模块化;

②垂直方向基于流程来做拆分,就是这里说的分层。

二十五、BO、VO、Entity 存在的意义是什么:

1、推荐每层都定义各自的数据对象这种设计思路:

①VO、BO、Entity 并非完全一样。

②VO、BO、Entity 三个类虽然代码重复,但功能语义不重复,从职责上讲是不一样的。

③分层清晰:为了尽量减少每层之间的耦合,把职责边界划分明确,每层都会维护自己的数据对象,层与层之间通过接口交互。数据从下一层传递到上一层的时候,将下一层的数据对象转化成上一层的数据对象,再继续处理

2、既然 VO、BO、Entity 不能合并,那如何解决代码重复的问题呢:

这里我们还可以将公共的字段抽取到公共的类中,VO、BO、Entity 通过组合关系来复用这个类的代码。

3、代码重复问题解决了,那不同分层之间的数据对象该如何互相转化呢:

①当下一层的数据通过接口调用传递到上一层之后,我们需要将它转化成上一层对应的数据对象类型。

②比如,Service 层从 Repository 层获取的 Entity 之后,将其转化成 BO,再继续业务逻辑的处理。

③Java 中提供了多种数据对象转化工具,比如 BeanUtils、Dozer 等

4、VO、BO、Entity 都是基于贫血模型的,我们还需要定义每个字段的 set 方法,会导致数据被随意修改。那到底该怎么办好呢?

Entity 和 VO 的生命周期是有限的,都仅限在本层范围内。设计的问题本身就没有最优解,只有权衡,BO特殊处理

二十六:非功能性需求分析

1、框架的易用性:

框架是否易集成、易插拔、跟业务代码是否松耦合、提供的接口是否够灵活

2、性能:

一方面,我们希望它是低延迟的,也就是说,统计代码不影响或很少影响接口本身的响应时间;

另一方面,我们希望框架本身对内存的消耗不能太大。

3、扩展性:

这里所说的扩展是从框架使用者的角度来说的,特指使用者可以在不修改框架源码,甚至不拿到框架源码的情况下,为框架扩展新的功能。这就有点类似给框架开发插件

例如:


Feign feign = Feign.builder().logger(new CustomizedLogger()).encoder(new FormEncoder(new JacksonEncoder())).decoder(new JacksonDecoder()).errorDecoder(new ResponseErrorDecoder()).requestInterceptor(new RequestHeadersInterceptor()).build();public class RequestHeadersInterceptor implements RequestInterceptor {  @Overridepublic void apply(RequestTemplate template) {template.header("appId", "...");template.header("version", "...");template.header("timestamp", "...");template.header("token", "...");template.header("idempotent-token", "...");template.header("sequence-id", "...");
}public class CustomizedLogger extends feign.Logger {//...
}public class ResponseErrorDecoder implements ErrorDecoder {@Overridepublic Exception decode(String methodKey, Response response) {//...}
}

4、容错性:

我们要对框架可能存在的各种异常情况都考虑全面,对外暴露的接口抛出的所有运行时、非运行时异常都进行捕获处理。

5、通用性:

为了提高框架的复用性,能够灵活应用到各种场景中。

二十七、框架设计:

  1. 借鉴 TDD(测试驱动开发)和 Prototype(最小原型)的思想,先聚焦于一个简单的应用场景,基于此设计实现一个简单的原型,是迭代设计的基础。

二十八、面向对象设计与实现:

1. 划分职责进而识别出有哪些类

2. 定义类及属性和方法,定义类与类之间的关系:

动词是方法、名词是属性

3. 将类组装起来并提供执行入口

二十九、重构

1、重构的意义:

① 首先,重构是时刻保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。

② 其次,优秀的代码或架构不是一开始就能完全设计好的,就像优秀的公司和产品也都是迭代出来的。我们无法 100% 遇见未来的需求,也没有足够的精力、时间、资源为遥远的未来买单,所以,随着系统的演进,重构代码也是不可避免的。

③ 最后,重构是避免过度设计的有效手段。在我们维护代码的过程中,真正遇到问题的时候,再对代码进行重构,能有效避免前期投入太多时间做过度的设计,做到有的放矢。

2、重构规模:

①大规模高层次重构包括对代码分层、模块化、解耦、梳理类之间的交互关系、抽象复用组件等等。这部分工作利用的更多的是比较抽象、比较顶层的设计思想、原则、模式。

注意:大规模高层次的重构难度比较大,需要组织、有计划地进行,分阶段地小步快跑,时刻让代码处于一个可运行的状态。

②小规模低层次的重构包括规范命名、注释、修正函数参数过多、消除超大类、提取重复代码等等编程细节问题,主要是针对类、函数级别的重构。小规模低层次的重构更多的是利用编码规范这一理论知识。

注意:而小规模低层次的重构,因为影响范围小,改动耗时短,所以,只要你愿意并且有时间,随时随地都可以去做。

三十、单元测试

1、认知:

①编写单元测试尽管繁琐,但并不是太耗时;

②我们可以稍微放低单元测试的质量要求;

③覆盖率作为衡量单元测试好坏的唯一标准是不合理的;写单元测试一般不需要了解代码的实现逻辑;

④单元测试框架无法测试多半是代码的可测试性不好。

2、原因:

单元测试除了能有效地为重构保驾护航之外,也是保证代码质量最有效的两个手段之一(另一个是 Code Review)

3、方式:

写单元测试就是针对代码设计各种测试用例,以覆盖各种输入、异常、边界情况,并将其翻译成代码。

三十一、提高代码可测试性:

1、使用依赖注入:将控制反转,控制权给到上层

2、测试性不好的代码:

① 未决行为:

所谓的未决行为逻辑就是,代码的输出是随机或者说不确定的,比如,跟时间、随机数有关的代码。

② 全局变量:

滥用全局变量也让编写单元测试变得困难

③ 静态方法:

只有在这个静态方法执行耗时太长、依赖外部资源、逻辑复杂、行为未决等情况下,我们才需要在单元测试中 mock 这个静态方法

④ 复杂继承:

如果父类需要 mock 某个依赖对象才能进行单元测试,那所有的子类、子类的子类……在编写单元测试的时候,都要 mock 这个依赖对象。

⑤ 高耦合代码

如果一个类职责很重,需要依赖十几个外部对象才能完成工作,代码高度耦合,那我们在编写单元测试的时候,可能需要 mock 这十几个依赖的对象。

三十二、解耦:

1、解耦方法:

①封装和抽象:

封装和抽象可以有效地隐藏实现的复杂性,隔离实现的易变性,给依赖的模块提供稳定且易用的抽象接口。

②中间层:

第一阶段:引入一个中间层,包裹老的接口,提供新的接口定义。

第二阶段:新开发的代码依赖中间层提供的新接口。

第三阶段:将依赖老接口的代码改为调用新接口。

第四阶段:确保所有的代码都调用新接口之后,删除掉老的接口。

③模块化:

将每个模块都当作一个独立的 lib 一样来开发,只提供封装了内部实现细节的接口给其他模块使用

④其他设计思想和原则:

单一职责原则

基于接口而非实现编程

依赖注入

多用组合少用继承

迪米特法则

三十三:编码规范:

1、命名:

① 命名的一个原则就是以能准确达意为目标

② 利用上下文简化命名:


User user = new User();
user.getName(); // 借助user对象这个上下文

③ 命名要可读、可搜索:统一规约是很重要的,能减少很多不必要的麻烦,例如统一用get前缀

④ 如何命名接口和抽象类:加前缀“I”,表示一个 Interface。比如 IUserService,对应的实现类命名为 UserService。

2、注释:

① 注释比代码承载的信息更多

② 注释起到总结性作用、文档的作用

③ 一些总结性注释能让代码结构更清晰

④ 注释的内容主要包含这样三个方面:做什么、为什么、怎么做。

注意:对于一些复杂的类和接口,我们可能还需要写明“如何用”。

3、代码风格:

① 函数不超过50行

② 善用空行分割单元块:

在类内部,成员变量与函数之间、静态成员变量与普通成员变量之间、函数之间,甚至成员变量之间,都可以通过添加空行的方式,让不同模块的代码之间的界限更加明确。

③ Java 语言倾向于两格缩进,一定不要用 tab 键缩进。

④ Java 程序员喜欢把大括号跟上一条语句放到一起

⑤ 类中成员的排列顺序:

在类中,成员变量排在函数的前面。

成员变量之间或函数之间,都是按照“先静态(静态函数或静态成员变量)、后普通(非静态函数或非静态成员变量)”的方式来排列的。

除此之外,成员变量之间或函数之间,还会按照作用域范围从大到小的顺序来排列,先写 public 成员变量或函数,然后是 protected 的,最后是 private 的。

4、把代码分割成更小的单元块:函数是否职责单一

5、避免函数参数过多:将函数的参数封装成对象。

6、函数设计要职责单一

7、移除过深的嵌套层次,方法包括:去掉多余的 if 或 else 语句,使用 continue、break、return 关键字提前退出嵌套,调整执行顺序来减少嵌套,将部分嵌套逻辑抽象成函数。

8、学会使用解释性变量:常量取代魔法数字

9、一定要制定统一的编码规范,并且通过 Code Review 督促执行,

三十四:代码质量审核:

技术:

1、目录设置是否合理、模块划分是否清晰、代码结构是否满足“高内聚、松耦合”?

2、是否遵循经典的设计原则和设计思想(SOLID、DRY、KISS、YAGNI、LOD 等)?

3、设计模式是否应用得当?

4、是否有过度设计?

5、代码是否容易扩展?如果要添加新功能,是否容易实现?

6、代码是否可以复用?

7、是否可以复用已有的项目代码或类库?是否有重复造轮子?

8、代码是否容易测试?

9、单元测试是否全面覆盖了各种正常和异常的情况?

10、代码是否易读?

11、是否符合编码规范(比如命名和注释是否恰当、代码风格是否一致等)?

业务:

1、代码是否实现了预期的业务需求?

2、逻辑是否正确?

3、是否处理了各种异常情况?

4、日志打印是否得当?

5、是否方便 debug 排查问题?

6、接口是否易用?

7、是否支持幂等、事务等?

8、代码是否存在并发问题?

9、是否线程安全?

10、性能是否有优化空间,比如,SQL、算法是否可以优化?

11、是否有安全漏洞?

12、比如输入输出校验是否全面?

三十五、重构步骤:

1、第一轮重构:提高代码的可读性

2、第二轮重构:提高代码的可测试性

依赖注入之所以能提高代码可测试性,主要是因为,通过这样的方式我们能轻松地用 mock 对象替换依赖的真实对象。

3、第三轮重构:编写完善的单元测试

4、第四轮重构:所有重构完成之后添加注释

三十六、函数出错应该返回啥

1、错误码:编程语言中有异常这种语法机制,那就尽量不要使用错误码。

2、NULL 值:如果某个函数有可能返回 NULL 值,我们在使用它的时候,忘记了做 NULL 值判断,就有可能会抛出空指针异常(Null Pointer Exception,缩写为 NPE)。

3、空对象

4、异常对象:

编译时异常(Compile Exception):受检异常(Checked Exception)

运行时异常(Runtime Exception):非受检异常(Unchecked Exception)

三十七、如何处理函数抛出的异常?

1、直接吞掉:

如果 func1() 抛出的异常是可以恢复,且 func2() 的调用方并不关心此异常,我们完全可以在 func2() 内将 func1() 抛出的异常吞掉;

2、原封不动地 re-throw:

如果 func1() 抛出的异常对 func2() 的调用方来说,也是可以理解的、关心的 ,并且在业务概念上有一定的相关性,我们可以选择直接将 func1 抛出的异常 re-throw;

3、包装成新的异常 re-throw:

如果 func1() 抛出的异常太底层,对 func2() 的调用方来说,缺乏背景去理解、且业务概念上无关,我们可以将它重新包装成调用方可以理解的新异常,然后 re-throw。

设计原则与思想【面向对象、设计原则、编程规范、重构技巧】相关推荐

  1. 设计原则与思想:设计原则12讲

    文章目录 设计原则与思想:设计原则(12讲) 理论一:对于单一职责原则,如何判定某个类的职责是否够"单一"? 如何理解单一职责原则(SRP)? 如何判断类的职责是否足够单一? 类的 ...

  2. 004.设计原则与思想:设计原则

    设计原则 一.理论一:对于单一职责原则,如何判定某个类的职责是否够"单一"? 1. 如何理解单一职责原则(SRP)? 二. 如何做到"对扩展开放.修改关闭"?扩 ...

  3. 编码原则总结:面向对象设计的SOLID原则

    S.O.L.I.D是 面向对象设计和编程(OOD&OOP)中几个重要的编码原则(Programming Priciple)的首字母缩写 缩写 全称 中文 SRP The Single Resp ...

  4. 【设计模式之美 设计原则与思想:设计原则】22 | 理论八:如何用迪米特法则(LOD)实现“高内聚、松耦合”?

    今天,我们讲最后一个设计原则:迪米特法则.尽管它不像 SOLID.KISS.DRY 原则那样,人尽皆知,但它却非常实用.利用这个原则,能够帮我们实现代码的"高内聚.松耦合".今天, ...

  5. 【设计模式之美 设计原则与思想:设计原则】23 | 实战一(上):针对业务系统的开发,如何做需求分析和设计?

    对于一个工程师来说,如果要追求长远发展,你就不能一直只把自己放在执行者的角色,不能只是一个代码实现者,你还要有独立负责一个系统的能力,能端到端(end to end)开发一个完整的系统.这其中的工作就 ...

  6. 模块独立性(一种软件设计原则)和面向对象设计原则

    模块独立性 模块化设计是指将软件分解为多个独立模块,不同的模块具有不同的功能和职责.每个模块可以独立的进行开发.测试,最后组装成完整的软件. 模块独立性是指软件系统中每个模块只涉及软件要求的子功能,而 ...

  7. (软件工程复习核心重点)第十章面向对象设计-第一节:面向对象设计的基本概念与准则

    文章目录 一:面向对象设计概念 (1)定义 (2)设计与分析的关系 (3)分类 二:面向对象的设计准则 (1)模块化 (2)抽象 (3)信息隐藏 (4)低耦合 A:交互耦合 B:继承耦合 (5)高内聚 ...

  8. 设计模式之设计原则与思想:设计原则(二)

    文章目录 KISS 原则 YAGNI 原则 DRY 原则 代码复用性(Code Reusability) 何为"高内聚.松耦合"? 迪米特法则(LOD) 为什么要分 MVC 三层开 ...

  9. 【软件架构】软件架构设计常用概念、原则与思想

    导读 本文一文总结软件架构设计常用概念.原则与思想,包括面向对象六大原则,DID原则,ACID.CAP.BASE理论,中间层思想,缓存思想等. 软件架构设计常用概念.原则与思想 面向对象设计六大原则 ...

  10. 技术图文:01 面向对象设计原则

    01 面向对象设计原则 知识结构: 一碟开胃的小菜 小菜今年计算机专业大四了,学了不少软件开发方面的东西,也学着编了些小程序,踌躇满志,一心要找一个好单位.当投递了无数简历后,终于收到了一个单位的面试 ...

最新文章

  1. idgenerator 会重复吗_终极版:分布式唯一ID的几种生成方案
  2. MFC常用类、成员函数、数组类、Cstring类、CTime类、CPoint类
  3. sqli-labs第一关(MySql基础)
  4. 数学--数论-数论函数-欧拉函数
  5. gradle引入依赖:_Gradle入门:依赖管理
  6. mysql 集群 qps_MySQL Cluster:如何通过扩展为MySQL带来2亿QPS
  7. 我这几年呆的这几个公司
  8. python:栈的理解与应用,让你快速入门Python
  9. The constructor someMethod() is not accessible due to restriction on required library
  10. Youki的Matlab命名规则
  11. 力扣-5 最长回文子串
  12. html轮播图显示失败_html简单的二级菜单制作
  13. 怎么对接口做幂等性操作?
  14. 一级计算机第65套题,全国计算机一级考试题库(附答案).pdf
  15. codeblocks下载安装及问题解决
  16. linux socket非阻塞之 send 和sendto函数
  17. 图解数字签名-数字证书-公钥加密-私钥签名原理
  18. mysql添加多字段唯一索引吗_mysql多字段唯一索引
  19. A Primer on Memory Consistency and Cache Coherence—第五章 Relaxed Memory Model
  20. Pytorch(一) —— 相关库和函数

热门文章

  1. Mysql价格降低20%应该怎么写_mysql优化建议20条
  2. 【python经典练习题100-试题6】打印字母C H
  3. md文件自动生成目录[docsify]
  4. 笔记一、网络编程入门
  5. Java实现抓娃娃_简易抓娃娃机H5的代码实现
  6. UVA307的一组测试数据
  7. 微信公众号绑定开发者错误 该微信用户未开启“公众号安全助手”的消息接收功能
  8. 店宝宝:“会员电商”,电商发展的新风口
  9. mysql 100w exists_exists方法如何在Mysql数据库中使用
  10. ElasticSearch term和match查询机制解析和隐藏的查询问题