设计模式(design pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。本文以面试题作为切入点,介绍了设计模式的常见问题。我们需要掌握各种设计模式的原理、实现、设计意图和应用场景,搞清楚能解决什么问题。本文是设计模式第一讲:设计原则
推荐书籍:
书籍 | 语言 | 难易程度 |
---|---|---|
《大话设计模式》 | java | 学起来最简单 |
《Head First 设计模式》 | java | 自学设计模式最好的教材,学起来简单,缺点是缺乏实际工程实践 |
《图解设计模式》 | java | 适合入门学习 |
《编写可读代码的艺术》 | 入门 | |
《设计模式》(刘伟,清华大学出版社) | java | 入门教材 |
《人人都懂设计模式:从生活中领悟设计模式:Python实现》 | python | |
《设计模式:可复用面向对象软件的基础》GOF | 基于C++ | 枯燥,适合理论提高 |
《重构 - 改善既有代码设计》 | 代码坏味道和相应代码的最佳实践。 | |
《人月神话》 | 这本书可能也有点过时了。但还是经典书 | |
《代码整洁之道》 | 细节之处的效率,完美和简单。 |
推荐课程:
1、编写高质量代码
2、提高复杂代码的设计和开发能力
3、让读源码、学框架事半功倍
4、为你的职场发展做铺垫
5、应对面试中设计模式相关问题 从功利的角度
场景1: 使用枚举创建全局唯一的对象 单例模式
enum Singleton {
INSTANCE; //属性
public void sayOK() {
System.out.println("ok");
}
}
为啥枚举创建单例优秀:1、无法通过new来随意创建对象,其构造函数为private. 2、枚举本质上是个final类 3、避免反射创建单例对象 4、避免通过序列化创建单例对象
能保证线程安全
场景2:使用工厂模式实现 Spring BeanFactory?
场景3:使用@Builder Builder 模式用来创建复杂对象
场景4:原型模式:通过对象的序列化实现深拷贝 Spring的 BeanUtil 反射 Json序列化和反序列化
场景5:通过AOP代理模式记录操作日志,清理缓存,实现多数据源
场景6:使用适配器模式做接口兼容升级
场景7:使用观察者模式解耦业务 Spring Event / Guava EventBus / MQ
场景8:使用模板方法模式,提供拓展点,让子类去实现,优化开放平台代码,巡检项目
场景9:使用策略模式为框架提供扩展点,降低代码耦合,附件项目
场景10:使用职责链模式优化商品规则校验引擎
① 代码重用性(相同功能代码,不用多次编写)
②可读性 (编程规范性, 便于其他程序员阅读和理解)
③可扩展性 (当需要增加新功能时,非常方便,称为可维护)
④可靠性 (当我们增加新功能后,对原来的功能没有影响)
要满足编码规范:以公司开发规约、静态代码规约为前提,是否遵守了编码规范,是否遵循了最佳实践。除了形式上的要求外,更重要的是命名规范。目标是提高代码的可读性,降低代码可维护性成本。
代码质量高:因为代码质量好坏直接决定了软件的可维护性成本的高低。代码质量应该更多的应该从可测性,可读性,可理解性,容变性等代码可维护性维度去衡量
如何编写高质量代码如图所示:
分为如下几个方面:
具体而言:
① 使用面向对象中的继承、多态能让我们写出可复用的代码;
② 设计原则中的 单一职责、DRY(Don’t RepeatYourself)、基于接口而非实现、里式替换原则 等,可以让我们写出可复用、灵活、可读性好、易扩展、易维护的代码;
③ 编码规范能让我们写出可读性好的代码;
④ 持续重构可以时刻保持代码的可维护性;
⑤设计模式可以让我们写出易扩展的代码。
设计原则是各类设计模式的基础
对于每一种设计原则,我们需要掌握它的设计初衷,能解决哪些编程问题,有哪些应用场景。只有这样,我们才能在项目中灵活恰当地应用这些原则。
1、单一职责原则
设计初衷:对类来说的,即一个类或模块应该只负责一项职责。
解决了哪些编程问题:
有哪些应用场景
2、接口隔离原则
设计初衷:客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
解决了哪些编程问题:todo
有哪些应用场景:todo
3、依赖倒转原则 (面向接口编程)
设计初衷:高层模块不应该依赖低层模块,应该依赖其抽象
建议:低层模块尽量都要有抽象类或接口。
解决了哪些编程问题:todo
有哪些应用场景:todo
4、里式替换原则
设计初衷:教你如何正确的使用继承 (引用基类的地方必须能透明地使用其子类的对象)
解决了哪些编程问题:todo
有哪些应用场景:todo
5、开闭原则(最基础、最重要)
设计初衷:模块、类、方法等 对扩展开放、对修改关闭
解决了哪些编程问题:
有哪些应用场景:见第4个标题
6、迪米特原则
设计初衷:最少知道原则,即一个类对自己依赖的类知道的越少越好
陌生的类最好不要以局部变量的形式出现在类的内部。
高内聚 低耦合
解决了哪些编程问题:todo
有哪些应用场景:见第5个标题
7、合成复用原则
理论:越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。
添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
案例: API 接口监控告警的代码
AlertRule 存储告警规则,可以自由设置。Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。NotificationEmergencyLevel 表示通知的紧急程度,包括 SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要),不同的紧急程度对应不同的发送渠道。
public class Alert {
private AlertRule rule;
private Notification notification;
/* 使用构造函数来初始化,可以防止npe */
public Alert(AlertRule rule, Notification notification) {
this.rule = rule;
this.notification = notification;
}
public void check(String api, long requestCount, long errorCount, long durationOfSeconds) {
long tps = requestCount / durationOfSeconds;
if (tps > rule.getMatchedRule(api).getMaxTps()) {
notification.notify(NotificationEmergencyLevel.URGENCY, "...");
}
if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
notification.notify(NotificationEmergencyLevel.SEVERE, "...");
}
}
}
问题:业务逻辑主要集中在 check() 函数中。当接口的 TPS 超过某个预先设置的最大值时,以及当接口请求出错数大于某个最大允许值时,就会触发告警,通知接口的相关负责人或者团队。现在,如果我们需要添加一个功能,当每秒钟接口超时请求个数,超过某个预先设置的最大阈值时,我们也要触发告警发送通知。这个时候,我们该如何改动代码呢?
主要的改动有两处:第一处是修改 check() 函数的入参,添加一个新的统计数据 timeoutCount,表示超时接口请求数;第二处是在 check() 函数中添加新的告警逻辑。
public class Alert {
// ...省略AlertRule/Notification属性和构造函数...
// 改动一:添加参数timeoutCount
public void check(String api, long requestCount, long errorCount, long timeoutCount, long durationOfSeconds) {
long tps = requestCount / durationOfSeconds;
if (tps > rule.getMatchedRule(api).getMaxTps()) {
notification.notify(NotificationEmergencyLevel.URGENCY, "...");
}
if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
notification.notify(NotificationEmergencyLevel.SEVERE, "...");
}
// 改动二:添加接口超时处理逻辑
long timeoutTps = timeoutCount / durationOfSeconds;
if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {
notification.notify(NotificationEmergencyLevel.URGENCY, "...");
}
}
}
存在的问题:
我们先重构一下之前的 Alert 代码,让它的扩展性更好一些。
重构的内容主要包含两部分:
具体实现逻辑如下所示
public class Alert {
private List<AlertHandler> alertHandlers = new ArrayList<>();
public void addAlertHandler(AlertHandler alertHandler) {
this.alertHandlers.add(alertHandler);
}
public void check(ApiStatInfo apiStatInfo) {
for (AlertHandler handler : alertHandlers) {
handler.check(apiStatInfo);
}
}
}
public class ApiStatInfo {
//省略constructor/getter/setter方法
private String api;
private long requestCount;
private long errorCount;
private long durationOfSeconds;
}
public abstract class AlertHandler {
protected AlertRule rule;
protected Notification notification;
public AlertHandler(AlertRule rule, Notification notification) {
this.rule = rule;
this.notification = notification;
}
public abstract void check(ApiStatInfo apiStatInfo);
}
public class TpsAlertHandler extends AlertHandler {
public TpsAlertHandler(AlertRule rule, Notification notification) {
super(rule, notification);
}
@Override
public void check(ApiStatInfo apiStatInfo) {
long tps = apiStatInfo.getRequestCount()/ apiStatInfo.getDurationOfSeconds();
if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()) {
notification.notify(NotificationEmergencyLevel.URGENCY, "...");
}
}
}
public class ErrorAlertHandler extends AlertHandler {
public ErrorAlertHandler(AlertRule rule, Notification notification){
super(rule, notification);
}
@Override
public void check(ApiStatInfo apiStatInfo) {
if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
notification.notify(NotificationEmergencyLevel.SEVERE, "...");
}
}
}
再来看下,重构之后的 Alert 该如何使用呢?
ApplicationContext 是一个单例类,负责 Alert 的创建、组装(alertRule 和 notification 的依赖注入)、初始化(添加 handlers)工作。
public class ApplicationContext {
private AlertRule alertRule;
private Notification notification;
private Alert alert;
public void initializeBeans() {
alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码
notification = new Notification(/*.省略参数.*/); //省略一些初始化代码
alert = new Alert();
alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));
alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));
}
public Alert getAlert() { return alert; }
// 饿汉式单例
private static final ApplicationContext instance = new ApplicationContext();
private ApplicationContext() {
initializeBeans();
}
public static ApplicationContext getInstance() {
return instance;
}
}
public class Demo {
public static void main(String[] args) {
ApiStatInfo apiStatInfo = new ApiStatInfo();
// ...省略设置apiStatInfo数据值的代码
ApplicationContext.getInstance().getAlert().check(apiStatInfo);
}
}
补充:更好的方式,借助Spring IOC的依赖注入功能
@Component
public class AlertFactory {
// 关键功能 Spring 会自动将 AlertHandler 接口的类注入到这个Map中
@Autowired
private Map<String, AlertHandler> alertHandleryMap;
public AlertRule getBy(String alertEnum) {
return alertHandleryMap.get(alertEnum);
}
}
现在,我们再来看下,基于重构之后的代码,如果再添加上面讲到的那个新功能,每秒钟接口超时请求个数超过某个最大阈值就告警,我们又该如何改动代码呢?
主要的改动有下面四处。
public class Alert { // 代码未改动... }
public class ApiStatInfo {//省略constructor/getter/setter方法
private String api;
private long requestCount;
private long errorCount;
private long durationOfSeconds;
private long timeoutCount; // 改动一:添加新字段
}
public abstract class AlertHandler { //代码未改动... }
public class TpsAlertHandler extends AlertHandler {//代码未改动...}
public class ErrorAlertHandler extends AlertHandler {//代码未改动...}
// 改动二:添加新的handler
public class TimeoutAlertHandler extends AlertHandler {//省略代码...}
public class ApplicationContext {
private AlertRule alertRule;
private Notification notification;
private Alert alert;
public void initializeBeans() {
alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码
notification = new Notification(/*.省略参数.*/); //省略一些初始化代码
alert = new Alert();
alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));
alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));
// 改动三:注册handler
alert.addAlertHandler(new TimeoutAlertHandler(alertRule, notification));
}
//...省略其他未改动代码...
}
public class Demo {
public static void main(String[] args) {
ApiStatInfo apiStatInfo = new ApiStatInfo();
// ...省略apiStatInfo的set字段代码
apiStatInfo.setTimeoutCount(289); // 改动四:设置tiemoutCount值
ApplicationContext.getInstance().getAlert().check(apiStatInfo);
}
}
重构之后的代码更加灵活和易扩展。如果我们要想添加新的告警逻辑,只需要基于扩展的方式创建新的 handler 类即可,不需要改动原来的 check() 函数的逻辑。而且,我们只需要为新的 handler 类添加单元测试,老的单元测试都不会失败,也不用修改。
指导思想:为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。这些“潜意识”可能比任何开发技巧都重要。
在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。
在众多的设计原则、思想、模式中,最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态等)
对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。
什么是高内聚?
什么是低耦合?
不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口
案例:实现了简化版的搜索引擎爬取网页的功能。代码中包含三个主要的类。其中
public class NetworkTransporter {
// 省略属性和其他方法...
public Byte[] send(HtmlRequest htmlRequest) {
//...
}
}
public class HtmlDownloader {
private NetworkTransporter transporter;// 通过构造函数或 IOC 注入
public Html downloadHtml(String url) {
Byte[] rawHtml = transporter.send(new HtmlRequest(url));
return new Html(rawHtml);
}
}
public class Document {
private Html html;
private String url;
public Document(String url) {
this.url = url;
HtmlDownloader downloader = new HtmlDownloader();
this.html = downloader.downloadHtml(url);
}
//...
}
缺陷有哪些?
1、首先,我们来看NetworkTransporter类。作为一个底层网络通信类,我们希望它的功能
尽可能通用,而不只是服务于下载 HTML,所以,我们不应该直接依赖太具体的发送对象
HtmlRequest;
public class NetworkTransporter {
// 省略属性和其他方法...
public Byte[] send(String address, Byte[] data) {
//...
}
}
2、我们再来看 HtmlDownloader 类。这个类的设计没有问题。不过,我们修改了NetworkTransporter的send()函数的定义,而这个类用到了send()函数,所以我们需要对它做相应的修改;
public class HtmlDownloader {
private NetworkTransporter transporter;// 通过构造函数或IOC注入
// HtmlDownloader 这里也要有相应的修改
public Html downloadHtml(String url) {
HtmlRequest htmlRequest = new HtmlRequest(url);
Byte[] rawHtml = transporter.send(htmlRequest.getAddress(), htmlRequest.getContent().getBytes());
return new Html(rawHtml);
}
}
3、我们来看下 Document 类。问题主要有三点。
downloader.downloadHtml()
逻辑复杂,耗时长,不应该放到构造函数中,会影响代码的可测试性。public class Document {
private Html html;
private String url;
public Document(String url, Html html) {
this.html = html;
this.url = url;
}
//...
}
// 通过一个工厂方法来创建 Document
public class DocumentFactory {
private HtmlDownloader downloader;
public DocumentFactory(HtmlDownloader downloader) {
this.downloader = downloader;
}
public Document createDocument(String url) {
Html html = downloader.downloadHtml(url);
return new Document(url, html);
}
}
背景:合成复用原则的设计初衷是:尽量使用组合的方式,而不是使用继承 来达到复用的原则
原因:继承会让两个类的耦合性增强,即组合比继承具有更高的灵活性
案例:
假设我们要设计一个关于鸟的类。我们将“鸟类”这样一个抽象的事物概念,定义为一个抽 象类 AbstractBird。所有更细分的鸟,比如麻雀、鸽子、乌鸦等,都继承这个抽象类。
然后我们提供一个 fly() 方法,让子类实现。尽管大部分鸟都会飞,但也有特例,比如鸵鸟就不会飞。鸵鸟继承具有 fly() 方法的父类,那鸵鸟就具有“飞”这样的行为,这显然不符合我们对现实世界中事物的认识。那么我在鸵鸟这个子类中重写 fly() 方法,让它抛出 UnSupportedMethodException 异常不就可以了吗?具体的代码实现如下所示
public class AbstractBird {
//... 省略其他属性和方法...
public void fly() {
//...
}
}
// 鸵鸟
public class Ostrich extends AbstractBird {
//... 省略其他属性和方法...
public void fly() {
throw new UnSupportedMethodException("I can't fly.'");
}
}
这种设计思路虽然可以解决问题,但不够优美。因为除了鸵鸟之外,不会飞的鸟还有很多, 比如企鹅。对于这些不会飞的鸟来说,我们都需要重写 fly() 方法,抛出异常。这样的设计,一方面,徒增了编码的工作量;另一方面,也违背了迪米特法则,暴露不该暴露的接口给外部,增加了类使用过程中被误用的概率。
方法:那我们再通过 AbstractBird 类派生出两个更加细分的抽象类:
如果我们还关注“鸟会不会叫”,那这个时候,我们又该如何设计类之间的继承关系呢?
如果我们还需要考虑“是否会下蛋”这样一个行为,那估计就要组合爆炸了。
继承最大的问题就在于:继承层次过深或者继承关系过于复杂会影响到代码的可读性和可维护性。
怎么解决呢?
我们使用组合(composition)、接口、委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的问题。
针对“会飞”这样一个行为特 性,我们可以定义一个 Flyable 接口,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这 些行为特性,我们可以类似地定义 Tweetable 接口、EggLayable 接口
public interface Flyable {
void fly();
}
public interface Tweetable {
void tweet();
}
public interface EggLayable {
void layEgg();
}
// 鸵鸟
public class Ostrich implements Tweetable, EggLayable {
//... 省略其他属性和方法...
@Override
public void tweet() {
//...
}
@Override
public void layEgg() {
//...
}
}
// 麻雀
public class Sparrow implements Flayable, Tweetable, EggLayable {
//... 省略其他属性和方法...
@Override
public void fly() {
//...
}
@Override
public void tweet() {
//...
}
@Override
public void layEgg() {
//...
}
}
代码重复的问题该怎么解决呢?
public interface Flyable {
void fly();
}
public class FlyAbility implements Flyable {
@Override
public void fly() {
//...
}
}
// 省略 Tweetable/TweetAbility/EggLayable/EggLayAbility
// 鸵鸟
public class Ostrich implements Tweetable, EggLayable {
private TweetAbility tweetAbility = new TweetAbility(); // 组合
private EggLayAbility eggLayAbility = new EggLayAbility(); // 组合
//... 省略其他属性和方法...
@Override
public void tweet() {
tweetAbility.tweet(); // 委托
}
@Override
public void layEgg() {
eggLayAbility.layEgg(); // 委托
}
}
尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。在实际的项目开发中,我们还是要根据具体的情况,来选择该用继承还是组合。如果类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承。反之,我们就尽量使用组合来替代继承。除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。
问题背景–什么是基于贫血模型的开发模式:
传统编程模型案例:
// Controller+VO(View Object) //
public class UserController {
private UserService userService; // 通过构造函数或者 IOC 框架注入
public UserVo getUserById(Long userId) {
UserBo userBo = userService.getUserById(userId);
UserVo userVo = [...convert userBo to userVo...];
return userVo;
}
}
public class UserVo {// 省略其他属性、get/set/construct 方法
private Long id;
private String name;
private String cellphone;
}
// Service+BO(Business Object) //
public class UserService {
private UserRepository userRepository; // 通过构造函数或者 IOC 框架注入
public UserBo getUserById(Long userId) {
UserEntity userEntity = userRepository.getUserById(userId);
UserBo userBo = [...convert userEntity to userBo...];
return userBo;
}
}
public class UserBo {// 省略其他属性、get/set/construct 方法
private Long id;
private String name;
private String cellphone;
}
// Repository+Entity //
public class UserRepository {
public UserEntity getUserById(Long userId) { //... }
}
public class UserEntity {// 省略其他属性、get/set/construct 方法
private Long id;
private String name;
private String cellphone;
}
像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。同理,UserEntity、UserVo 都是基于贫血模型设计的。
什么是基于充血模型的 DDD 开发模式?
示例代码:
@Data
public class VirtualWallet {
private Long id;
private Long createTime = System.currentTimeMillis();
//余额
private BigDecimal balance = BigDecimal.ZERO;
// 是否允许超支
private boolean isAllowedOverdraft = true;
// 超支金额
private BigDecimal overdraftAmount = BigDecimal.ZERO;
// 冻结总额
private BigDecimal frozenAmount = BigDecimal.ZERO;
public VirtualWallet(Long preAllocatedId) {
this.id = preAllocatedId;
}
public void freeze(BigDecimal amount) { ... }
public void unfreeze(BigDecimal amount) { ...}
public void increaseOverdraftAmount(BigDecimal amount) { ... }
public void decreaseOverdraftAmount(BigDecimal amount) { ... }
public void closeOverdraft() { ... }
public void openOverdraft() { ... }
public BigDecimal balance() {
return this.balance;
}
// 获取余额
public BigDecimal getAvaliableBalance() {
BigDecimal totalAvaliableBalance = this.balance.subtract(this.frozenAmount)
if (isAllowedOverdraft) {
totalAvaliableBalance += this.overdraftAmount;
}
return totalAvaliableBalance;
}
// 借记
public void debit(BigDecimal amount) {
BigDecimal totalAvaliableBalance = getAvaliableBalance();
if (totoalAvaliableBalance.compareTo(amount) < 0) {
throw new InsufficientBalanceException(...);
}
this.balance.subtract(amount);
}
// 信贷
public void credit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException(...);
}
this.balance.add(amount);
}
}
为什么基于贫血模型的传统开发模式如此受欢迎?
我对充血模型的看法就是:
亮点1:将充血模型用在类目属性代码中,业务不停在拓展,如何高效兼容现有业务
问题1:值不值得变为充血模型。
demo如下所示:
问题2:后续拓展的需求,如何在DDD上补充代码呢?
DDD辩证思考与灵活应用
Service 类主要有下面这样几个职责
1.Service 类负责与 Repository 交流。
2、Service 类负责跨领域模型的业务聚合功能;
3、Service 类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中。
答案是没有必要。Controller 层主要负责接口的暴露,Repository 层主要负责与数据库打交道,这两层包含的业务逻辑并不多,如果业务逻辑比较简单,就没必要做充血建模,即便设计成充血模型,类也非常单薄,看起来也很奇怪。
对于Repository 层Entity,一般来讲,我们把它传递到 Service 层之后,就会转化成 BO 或者 Domain 来继续后面的业务逻辑。Entity 的生命周期到此就结束了,所以也并不会被到处任意修改;
对于Controller 层VO,它主要是作为接口的数据传输承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑、只包含数据。
从设计原则和思想的角度来看
1、分层与模块化
2、基于接口通信
3、高内聚、松耦合
4、为扩展而设计
5、KISS(Keep it Simple, Stupid) 首要原则
6、最小惊奇原则
从研发管理和开发技巧的角度来看
1、 吹毛求疵般地执行编码规范
2、编写高质量的单元测试
3、不流于形式的 Code Review
4、编写技术文档
5、持续重构、重构、重构
6、对项目与团队进行拆分
通过 Code Reviwe 保持项目的代码质量
1、为什么要进行 Code Review?Code Review 的价值?☆
2、如何在团队中落地执行 Code Review?
3、Code Review的重点?
确认代码功能:代码实现的功能满足产品需求,逻辑的严谨和合理性是最基本的要求。同时需要考虑适当的扩展性,在代码的可扩展性和过度设计做出权衡,不编写无用逻辑和一些与代码功能无关的附加代码。
编码规范:以集团开发规约、静态代码规约为前提,是否遵守了编码规范,遵循了最佳实践。除了形式上的要求外,更重要的是命名规范。目标是提高代码的可读性,降低代码可维护性成本。
潜在的BUG:可能在最坏情况下出现问题的代码,包括常见的线程安全、业务逻辑准确性、系统边界范围、参数校验,以及存在安全漏洞(业务鉴权、灰产可利用漏洞)的代码。
文档和注释:过少(缺少必要信息)、过多(没有信息量)、过时的文档或注释,总之文档和注释要与时俱进,与最新代码保持同步。其实很多时候个人觉得良好的变量、函数命名是最好的注释,好的代码胜过注释。
重复代码:当一个项目在不断开发迭代、功能累加的过程中,重复代码的出现几乎是 不可避免的,通常可以通过PMD工具进行检测。类型体系之外的重复代码处理通常可以封装到对应的Util类或者Helper类中,类体系之内的重复代码通常可以通过继承、模板模式等方法来解决。
复杂度:代码结构太复杂(如圈复杂度高),难以理解、测试和维护。
监控与报警:基于产品的需求逻辑,需要有些指标来证明业务是正常工作的,如果发生异常需要有监控、报警指标通知研发人员处理,review业务需求对应的监控与报警指标也是Code Review的重点事项。
测试覆盖率:编写单元测试,特别是针对复杂代码的测试覆盖是否足够。
说明: 有缺陷的底层数据结构容易导致系统风险上升, 可扩展性下降, 重构成本也会因历史数据迁移和系统平滑过渡而
陡然增加, 所以, 存储方案和数据结构需要认真地进行设计和评审, 生产环境提交执行后, 需要进行 double check。
正例: 评审内容包括存储介质选型、 表结构设计能否满足技术方案、 存取性能和存储空间能否满足业务发展、 表或字段
之间的辩证关系、 字段名称、 字段类型、 索引等;数据结构变更(如在原有表中新增字段)也需要在评审通过后上线
说明:状态图的核心是对象状态,首先明确对象有多少种状态,然后明确两两状态之间是否存在直接转换关系,再明确触发状态转换的条件是什么。
正例:淘宝订单状态有已下单、待付款、已付款、待发货、已发货、已收货等。比如已下单与已收货这两种状态之间是不可能有直接转换关系的。
说明:时序图反映了一系列对象间的交互与协作关系,清晰立体地反映系统的调用纵深链路。
说明:类图像建筑领域的施工图,如果搭平房,可能不需要,但如果建造蚂蚁 Z 空间大楼,肯定需要详细的施工图。
说明:活动图是流程图的扩展,增加了能够体现协作关系的对象泳道,支持表示并发等。
说明:系统依赖的第三方服务被降级或屏蔽后,依然不会影响主干流程继续进行,仅影响信息展示、或消息通知等非关键功能,那么这些服务称为弱依赖。
正例:当系统弱依赖于多个外部服务时,如果下游服务耗时过长,则会严重影响当前调用者,必须采取相应降级措施,比如,当调用链路中某个下游服务调用的平均响应时间或错误率超过阈值时,系统自动进行降级或熔断操作,屏蔽弱依赖负面影响,保护当前系统主干功能可用。
反例:某个疫情相关的二维码出错: “服务器开了点小差,请稍后重试” ,不可用时长持续很久,引起社会高度关注,原因可能为调用的外部依赖服务 RT 过高而导致系统假死,而在显示端没有做降级预案,只能直接抛错给用户
说明:单一原则最易理解却是最难实现的一条规则,随着系统演进,很多时候,忘记了类设计的初衷
说明:不得已使用继承的话,必须符合里氏代换原则,此原则说父类能够出现的地方子类一定能够出现,比如,“把钱交出来” , 钱的子类美元、欧元、人民币等都可以出现。
说明:低层次模块依赖于高层次模块的抽象,方便系统间的解耦。
说明:极端情况下,交付的代码是不可修改的,同一业务域内的需求变化,通过模块或类的扩展来实现
说明:随着代码的重复次数不断增加,维护成本指数级上升。随意复制和粘贴代码,必然会导致代码的重复,在维护代码时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
正例:一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,这个时候请抽取:private boolean checkParam(DTO dto) {...}
说明:敏捷开发是快速交付迭代可用的系统,省略多余的设计方案,摒弃传统的审批流程,但核心关键点上的必要设计和文档沉淀是需要的。
反例:某团队为了业务快速发展,敏捷成了产品经理催进度的借口,系统中均是勉强能运行但像面条一样的代码,可维护性和可扩展性极差,一年之后,不得不进行大规模重构,得不偿失
说明:避免为了设计而设计,系统设计文档有助于后期的系统维护和重构,所以设计结果需要进行分类归档保存。
说明:世间众多设计模式其实就是一种设计模式:即隔离变化点的模式。
正例:极致扩展性的标志,就是需求的新增,不会在原有代码交付物上进行任何形式的修改
说明:识别和表达完全是两回事,很多人错误地认为识别到系统难点在哪里,表达只是自然而然的事情,但是大家在设计评审中经常出现语焉不详,甚至是词不达意的情况。准确地表达系统难点需要具备如下能力:表达规则和表达工具的熟练性。抽象思维和总结能力的局限性。基础知识体系的完备性。深入浅出的生动表达力。
说明:代码的深度调用,模块层面上的依赖关系网,业务场景逻辑,非功能性需求等问题要相应的文档来完整地呈现。
正例:登录场景中,输入框的按钮都需要考虑 tab 键聚焦,符合自然逻辑的操作顺序如下,“输入用户名,输入密码,输入验证码,点击登录”,其中验证码实现语音验证方式。如有自定义标签实现的控件设置控件类型可使用 role 属性。