哪些维度评判代码质量的好坏?
常用的评价标准
- 可维护性(maintainability):维护代码的成本
- 可读性(readability)
- 可扩展性(extensibility):码应对未来需求变化的能力
- 灵活性(flexibility)
- 简洁性(simplicity)
- 可复用性(reusability)
- 可测试性(testability)
面向对象、设计原则、设计模式、编程规范、重构
面向对象
- 面向对象的四大特性:封装、抽象、继承、多态
- 封装:信息隐藏或者数据访问保护
- 抽象:调用者在使用函数的时候,并不需要去研究函数内部的实现逻辑,只需要通过函数的命名、注释或者文档,了解其提供了什么功能,就可以直接使用了。
- 继承:代码复用(过度继承会导致代码可读性、可维护性变差)
- 多态:子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现
- 面向对象编程与面向过程编程的区别和联系
- 面向过程:方法和数据结构的定义分开
- 面向对象:方法和数据结构被绑定一起,定义在类中(能够应对大规模复杂程序的开发,更易复用、易扩展、易维护)
- 接口和抽象类的区别以及各自的应用场景
抽象类(模板设计模式)
// 抽象类
public abstract class Logger {private String name;private boolean enabled;private Level minPermittedLevel;public Logger(String name, boolean enabled, Level minPermittedLevel) {this.name = name;this.enabled = enabled;this.minPermittedLevel = minPermittedLevel;}public void log(Level level, String message) {boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValueif (!loggable) return;doLog(level, message);}protected abstract void doLog(Level level, String message);
}
// 抽象类的子类:输出日志到文件
public class FileLogger extends Logger {private Writer fileWriter;public FileLogger(String name, boolean enabled,Level minPermittedLevel, String filepath) {super(name, enabled, minPermittedLevel);this.fileWriter = new FileWriter(filepath);}@Overridepublic void doLog(Level level, String mesage) {
// 格式化level和message,输出到日志文件fileWriter.write(...);}
}// 抽象类的子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {private MessageQueueClient msgQueueClient;public MessageQueueLogger(String name, boolean enabled,Level minPermittedLevel, MessageQueueClient msgQueueClient) {super(name, enabled, minPermittedLevel);this.msgQueueClient = msgQueueClient;}@Overrideprotected void doLog(Level level, String mesage) {
// 格式化level和message,输出到消息中间件msgQueueClient.send(...);}
}
- 抽象类不允许被实例化,只能被继承。
- 抽象类可以包含属性和方法。
- 子类继承抽象类,必须实现抽象类中的所有抽象方法。
接口
public interface Filter {void doFilter(RpcRequest req) throws RpcException;
}// 接口实现类:鉴权过滤器
public class AuthencationFilter implements Filter {@Overridepublic void doFilter(RpcRequest req) throws RpcException {
//...鉴权逻辑..}
}// 接口实现类:限流过滤器
public class RateLimitFilter implements Filter {@Overridepublic void doFilter(RpcRequest req) throws RpcException {
//...限流逻辑...}
}// 过滤器使用Demo
public class Application {// filters.add(new AuthencationFilter());
// filters.add(new RateLimitFilter());private List<Filter> filters = new ArrayList<>();public void handleRpcRequest(RpcRequest req) {try {for (Filter filter : filters) {filter.doFilter(req);}} catch (RpcException e) {
// ...处理过滤结果...}
// ...省略其他处理逻辑...}
}
- 接口不能包含属性(也就是成员变量)。
- 接口只能声明方法,方法不能包含代码实现。
- 类实现接口的时候,必须实现接口中声明的所有方法。
- 基于接口而非实现编程的设计思想
- 越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。
函数的命名不能暴露任何实现细节。
封装具体的实现细节。
为实现类定义抽象的接口。
- 多用组合少用继承的设计思想
- 如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关
系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,
继承关系复杂,我们就尽量使用组合来替代继承 - 装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。
- 面向过程的贫血模型(重 Service 轻 BO)和面向对象的充血模型(轻 Service 重 Domain)
基于贫血模型的传统开发模式
// 接口
public class VirtualWalletController {// 通过构造函数或者IOC框架注入private VirtualWalletService virtualWalletService;public BigDecimal getBalance(Long walletId) { ...} //查询余额public void debit(Long walletId, BigDecimal amount) { ...} //出账public void credit(Long walletId, BigDecimal amount) { ...} //入账public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) { .
//省略查询transaction的接口}
}public class VirtualWalletBo {//省略getter/setter/constructor方法private Long id;private Long createTime;private BigDecimal balance;
}public Enum TransactionType {DEBIT,CREDIT,TRANSFER;}public class VirtualWalletService {// 通过构造函数或者IOC框架注入private VirtualWalletRepository walletRepo;private VirtualWalletTransactionRepository transactionRepo;public VirtualWalletBo getVirtualWallet(Long walletId) {VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);VirtualWalletBo walletBo = convert(walletEntity);return walletBo;}public BigDecimal getBalance(Long walletId) {return walletRepo.getBalance(walletId);}@Transactionalpublic void debit(Long walletId, BigDecimal amount) {VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);BigDecimal balance = walletEntity.getBalance();if (balance.compareTo(amount) < 0) {throw new NoSufficientBalanceException(...);}VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactitransactionEntity.setAmount(amount);transactionEntity.setCreateTime(System.currentTimeMillis());transactionEntity.setType(TransactionType.DEBIT);transactionEntity.setFromWalletId(walletId);transactionRepo.saveTransaction(transactionEntity);walletRepo.updateBalance(walletId, balance.subtract(amount));}@Transactionalpublic void credit(Long walletId, BigDecimal amount) {VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactitransactionEntity.setAmount(amount);transactionEntity.setCreateTime(System.currentTimeMillis());transactionEntity.setType(TransactionType.CREDIT);transactionEntity.setFromWalletId(walletId);transactionRepo.saveTransaction(transactionEntity);VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);BigDecimal balance = walletEntity.getBalance();walletRepo.updateBalance(walletId, balance.add(amount));}@Transactionalpublic void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactitransactionEntity.setAmount(amount);transactionEntity.setCreateTime(System.currentTimeMillis());transactionEntity.setType(TransactionType.TRANSFER);transactionEntity.setFromWalletId(fromWalletId);transactionEntity.setToWalletId(toWalletId);transactionRepo.saveTransaction(transactionEntity);debit(fromWalletId, amount);credit(toWalletId, amount);}
}
基于充血模型的 DDD 开发模式(钱包系统)
public class VirtualWallet { // Domain领域模型(充血模型)private Long id;private Long createTime = System.currentTimeMillis();private BigDecimal balance = BigDecimal.ZERO;public VirtualWallet(Long preAllocatedId) {this.id = preAllocatedId;}public BigDecimal balance() {return this.balance;}public void debit(BigDecimal amount) {if (this.balance.compareTo(amount) < 0) {throw new InsufficientBalanceException(...);}this.balance = this.balance.subtract(amount);}public void credit(BigDecimal amount) {if (amount.compareTo(BigDecimal.ZERO) < 0) {throw new InvalidAmountException(...);}this.balance = this.balance.add(amount);}
}public class VirtualWalletService {// 通过构造函数或者IOC框架注入private VirtualWalletRepository walletRepo;private VirtualWalletTransactionRepository transactionRepo;public VirtualWallet getVirtualWallet(Long walletId) {VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);VirtualWallet wallet = convert(walletEntity);return wallet;}public BigDecimal getBalance(Long walletId) {return walletRepo.getBalance(walletId);}@Transactionalpublic void debit(Long walletId, BigDecimal amount) {VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);VirtualWallet wallet = convert(walletEntity);wallet.debit(amount);VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactitransactionEntity.setAmount(amount);transactionEntity.setCreateTime(System.currentTimeMillis());transactionEntity.setType(TransactionType.DEBIT);transactionEntity.setFromWalletId(walletId);transactionRepo.saveTransaction(transactionEntity);walletRepo.updateBalance(walletId, wallet.balance());}@Transactionalpublic void credit(Long walletId, BigDecimal amount) {VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);VirtualWallet wallet = convert(walletEntity);wallet.credit(amount);VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactitransactionEntity.setAmount(amount);transactionEntity.setCreateTime(System.currentTimeMillis());transactionEntity.setType(TransactionType.CREDIT);transactionEntity.setFromWalletId(walletId);transactionRepo.saveTransaction(transactionEntity);walletRepo.updateBalance(walletId, wallet.balance());}@Transactionalpublic void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {
//...跟基于贫血模型的传统开发模式的代码一样...}
}
基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,主要区别在 Service
层。在基于充血模型的开发模式下,我们将部分原来在 Service 类中的业务逻辑移动到了一个
充血的 Domain 领域模型中,让 Service 类的实现依赖这个 Domain 类。
在基于充血模型的 DDD 开发模式下,Service 类并不会完全移除,而是负责一些不适合放在
Domain 类中的功能。比如,负责与 Repository 层打交道、跨领域模型的业务聚合功能、幂
等事务等非功能性的工作。
如何对接口鉴权这样一个功能开发做面向对象分析?
- 调用方进行接口请求的时候,将 URL、AppID、密码、时间戳拼接在一起,通过加密算法
生成 token,并且将 token、AppID、时间戳拼接在 URL 中,一并发送到微服务端。 - 微服务端在接收到调用方的接口请求之后,从请求中拆解出 token、AppID、时间戳。
- 微服务端首先检查传递过来的时间戳跟当前时间,是否在 token 失效时间窗口内。如果已
经超过失效时间,那就算接口调用鉴权失败,拒绝接口调用请求。 - 如果 token 验证没有过期失效,微服务端再从自己的存储中,取出 AppID 对应的密码,通
过同样的 token 生成算法,生成另外一个 token,与调用方传递过来的 token 进行匹配。
如果一致,则鉴权成功,允许接口调用;否则就拒绝接口调用。
进一步变化为:
- 把 URL、AppID、密码、时间戳拼接为一个字符串;
- 对字符串通过加密算法加密生成 token;
- 将 token、AppID、时间戳拼接到 URL 中,形成新的 URL;
- 解析 URL,得到 token、AppID、时间戳等信息;
- 从存储中取出 AppID 和对应的密码;
- 根据时间戳判断 token 是否过期失效;
- 验证两个 token 是否匹配;
1、2、6、7 都是跟 token 有关,负责 token 的生成、验证;(AuthToken)
3、4 都是在处理 URL,负责 URL 的拼接、解析;(Url)
5 是操作 AppID 和密码,负责从存储中读取 AppID 和密码。(CredentialStorage)
public interface ApiAuthenticator {void auth(String url);void auth(ApiRequest apiRequest);
}
public class DefaultApiAuthenticatorImpl implements ApiAuthenticator {private CredentialStorage credentialStorage;public DefaultApiAuthenticatorImpl() {this.credentialStorage = new MysqlCredentialStorage();}public DefaultApiAuthenticatorImpl(CredentialStorage credentialStorage) {this.credentialStorage = credentialStorage;}@Overridepublic void auth(String url) {ApiRequest apiRequest = ApiRequest.buildFromUrl(url);auth(apiRequest);}@Overridepublic void auth(ApiRequest apiRequest) {String appId = apiRequest.getAppId();String token = apiRequest.getToken();long timestamp = apiRequest.getTimestamp();String originalUrl = apiRequest.getOriginalUrl();AuthToken clientAuthToken = new AuthToken(token, timestamp);if (clientAuthToken.isExpired()) {throw new RuntimeException("Token is expired.");}String password = credentialStorage.getPasswordByAppId(appId);AuthToken serverAuthToken = AuthToken.generate(originalUrl, appId, password,if (!serverAuthToken.match(clientAuthToken)) {throw new RuntimeException("Token verfication failed.");}}
}
设计原则
- SOLID 原则 -SRP 单一职责原则
- SOLID 原则 -OCP 开闭原则(对扩展开放、对修改关闭)
// API 接口监控告警 这种情况进行扩展就需要对方法进行修改
public class Alert {private AlertRule rule;private Notification notification;public Alert(AlertRule rule, Notification notification) {this.rule = rule;this.notification = notification;}public void check(String api, long requestCount, long errorCount, long duration) {long tps = requestCount / durationOfSeconds;if (tps > rule.getMatchedRule(api).getMaxTps()) {notification.notify(NotificationEmergencyLevel.URGENCY, "...");}if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {notification.notify(NotificationEmergencyLevel.SEVERE, "...");}}
}
// 当每秒钟接口超时请求个数,超过某个预先设置的最大阈值时,我们也要触发告警发送通知。
代码改动
public class Alert {// ...省略AlertRule/Notification属性和构造函数...
// 改动一:添加参数timeoutCountpublic void check(String api, long requestCount, long errorCount, long timeoutCount) {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, "...");}}
}
重构–》
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);}@Overridepublic 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);}@Overridepublic void check(ApiStatInfo apiStatInfo) {if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).gnotification.notify(NotificationEmergencyLevel.SEVERE, "...");}
}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);}
}
- SOLID 原则 -LSP 里式替换原则
- 子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性(继承父类初衷,并进行增强)
- SOLID 原则 -ISP 接口隔离原则
- 要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数
- 单一职责原则针对的是模块、类、接口的设计。接口隔离原则更针对接口的设计。
- SOLID 原则 -DIP 依赖倒置原则
- 控制反转(IOC):框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。
- 依赖注入(DI):不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。
- 依赖反转: 高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。
- DRY 原则、KISS 原则、YAGNI 原则、LOD 法则
- KISS 原则(Keep It Simple and Stupid:尽量保持简单)
- 不要使用同事可能不懂的技术来实现代码。
- 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出bug 的概率会更高,维护的成本也比较高。
- 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。
- YAGNI(You Ain’t Gonna Need It:你不会需要它):
- 不要去设计当前用不到的功能;不要去编写当前用不到的代码。
- DRY原则(Don’t Repeat Yourself:(不写重复的代码))
- 三种代码重复的情况:实现逻辑重复、功能语义重复、代码执行重复。
提高复用的办法 - 减少代码耦合
- 满足单一职责原则
- 模块化
- 业务与非业务逻辑分离
- 通用代码下沉
- 继承、多态、抽象、封装
- 应用模板等设计模式
- 三种代码重复的情况:实现逻辑重复、功能语义重复、代码执行重复。
- LOD(迪米特法则:Law of Demeter):不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口
设计模式
针对软件开发中经常遇到的一些设计问题,总结出来的一套解决方案或者设计思
路。
积分系统设计案例
- 合理地将功能划分到不同模块
- 第一种划分方式是:积分赚取渠道及兑换规则、消费渠道及兑换规则的管理和维护(增删改查),不划分到积分系统中,而是放到更上层的营销系统中。这样积分系统就会变得非常简单,只需要负责增加积分、减少积分、查询积分、查询积分明细等这几个工作
- 第二种划分方式是:积分赚取渠道及兑换规则、消费渠道及兑换规则的管理和维护,分散在各个相关业务系统中,比如订单系统、评论系统、签到系统、换购商城、优惠券系统等。还是刚刚那个下订单赚取积分的例子,在这种情况下,用户下订单成功之后,订单系统根据商品对应的积分兑换比例,计算所能兑换的积分数量,然后直接调用积分系统给用户增加积分。
- 第三种划分方式是:所有的功能都划分到积分系统中,包括积分赚取渠道及兑换规则、消费渠道及兑换规则的管理和维护。还是同样的例子,用户下订单成功之后,订单系统直接告知积分系统订单交易成功,积分系统根据订单信息查询积分兑换规则,给用户增加积分。
综合考虑,我们更倾向于第一种和第二种模块划分方式。但是,不管选择这两种中的哪一种,积分系统所负责的工作是一样的,只包含积分的增、减、查询,以及积分明细的记录和查询。
-
设计模块与模块之间的交互关系
上下层系统之间的调用倾向于通过同步接口,同层之间的调用倾向于异步消息调用。比如,营销系统和积分系统是上下层关系,它们之间就比较推荐使用同步接口调用(上层是调用系统,下层是被调用系统) -
设计模块的接口、数据库、业务模型
积分流水明细表
表设计这里还可以加一个积分余额表(没有积分余额表容易造成多扣)
积分系统的接口
编程规范
编程规范主要解决的是代码的可读性问题。编码规范相对于设计原则、设计模式,更加具
体、更加偏重代码细节、更加能落地。持续的小重构依赖的理论基础主要就是编程规范。
重构
重构作为保持代码质量不下降的有效手段,利用的就是面向对象、设计原则、设计模式、编
码规范这些理论。
程序出错该返回啥?NULL、异常、错误码、空对象?
- 返回错误码
- C 语言使用错误码的情况比较多
- 返回 NULL 值
- 如果查询对象为空时,返回null也是正常行为(按照项目统一约定来),比如查询下标返回值会是Int类型,这时候可能用-1来返回
- 返回空对象
- 空对象设计模式(Null Object Design Pattern)
- 当函数返回的数据是字符串类型或者集合类型的时候,我们可以用空字符串或空集合替代NULL 值,来表示不存在的情况。
- 抛出异常对象
- 对于代码 bug(比如数组越界)以及不可恢复异常(比如数据库连接失败),即便我们捕获了,也做不了太多事情,所以,我们倾向于使用非受检异常。对于可恢复异常、业务异常,比如提现金额大于余额的异常,我们更倾向于使用受检异常,明确告知调用者需要捕获处理。
当我们面对函数抛出异常的时候,应该选择上面的哪种处理方式呢?
- 如果 func1() 抛出的异常是可以恢复,且 func2() 的调用方并不关心此异常,我们完全可以在 func2() 内将 func1() 抛出的异常吞掉;
- 如果 func1() 抛出的异常对 func2() 的调用方来说,也是可以理解的、关心的 ,并且在业务概念上有一定的相关性,我们可以选择直接将 func1 抛出的异常 re-throw;
- 如果 func1() 抛出的异常太底层,对 func2() 的调用方来说,缺乏背景去理解、且业务概念上无关,我们可以将它重新包装成调用方可以理解的新异常,然后 re-throw。