或许你听说过 DRY 原则,但我打赌,你理解的肯定有偏差;或许你从未听过,那太好了,本文会让你受益匪浅,对你的编码一定有指导作用,甚至对你的工作生活都有些许启发。
DRY,Don’t Repeat Yourself
Repeat Yourself:多个地方表达相同的含义。
Repeat Yourself 的坏处:
知识和意图
的复制,强调多个地方表达的东西其实是相同的,只是表达方式不同。Q:知识和意图 这两个词比较抽象,如果具体到编码,指代的什么呢?
理解误区:
“复制粘贴”代码只是代码重复的一种特例,很多情况下,都不是你想的那样。
# 定义账户类
class MyAccount(object):
__slots__ = ['fee', 'balance']
def __init__(self, fee, balance):
self.fee = fee
self.balance = balance
# 定义打印函数
def printAccount(account):
if account.fee < 0:
print('fee:%10.2f' % -account.fee)
else:
print('fee:%10.2f' % account.fee)
if account.balance < 0:
print('balance:%10.2f' % -account.balance)
else:
print('balance:%10.2f' % account.balance)
# 函数调用
myAccount = MyAccount(100, -300)
printAccount(myAccount)
以上代码没有复制粘贴,但仍有两处重复。
第一处重复:负数处理。修改:
def printAccount(account):
print('fee: %10.2f' % formatMoney(account.fee))
print('balance: %10.2f' % formatMoney(account.balance))
def formatMoney(amount):
return abs(amount)
第二处重复:print 的格式。修改:
def printAccount(account):
myPrint('fee', account.fee)
myPrint('balance', account.balance)
def myPrint(label, amount):
print('%s: %10.2f' %(label, formatMoney(amount)))
def formatMoney(amount):
return abs(amount)
Q:所有代码的重复都是知识的重复么?
这里的文档是广义上的,还包括注释等。
比如方法的注释把方法中的逻辑分支都描述了一遍,函数的意图就被描述了两次(注释、代码各一次)。只要经过两次需求变更,代码和注释就会变得不一致。
private static boolean isAllTicketsStatusOk(FlightOrder order, BigOrder bigOrder) {
/*
* 三种情况,都能申请邮递:
* 1. 退票单 || 该机票已经退了
* 2. 飞后寄 && 已乘机(只作用于新单,新单乘机后,自行递归原单进行处理)
* 3. 立即寄 && 已出票、已改签、已乘机(由于打印行程单任务可能拖延时间,故加入已乘机)
*/
Predicate<FlightOrderTicket> isTicketStatusOk = item -> item.getStatus() == Returned
|| bigOrder.getDistInfo().getDistMode() == DistMode.DistAfterRide && item.getStatus() == Ride
// Changed 表达的是原单
|| bigOrder.getDistInfo().getDistMode() == DistMode.DistImmediately && item.getStatus().in(Changed, TicketSuccess, Ride);
boolean isAllTicketsStatusOk = order.getOrderTickets().stream()
.allMatch(isTicketStatusOk);
LOG.debug("isAllTicketsStatusOk: {}", isAllTicketsStatusOk);
return isAllTicketsStatusOk;
}
写注释是好习惯,这里绝对不是让你为了规避 DRY 原则,把注释全部删掉。
但是,注释掩盖不了糟糕的代码。
如果是为了掩饰方法中糟糕或者晦涩难懂的代码,这时候应该重构代码。
推荐:
其实就是常说的数据冗余。
class Line {
Point x;
Point y;
double length;
}
x、y 两点即可确定连线的长度,length 字段明显重复了,应该改成方法:
double length() {
}
即使不在同一个类,也可能构成重复。
举个例子,假设一个行程 route,包含多个航段 segment,route 有的距离,segment 上也有距离。
class Route {
List<Segment> segments;
int distance;
}
class Segment {
int distance;
}
route 上的距离是其下所有 segment 距离之和,定义成字段,就重复了,改成方法。
class Route {
List<Segment> segments;
int getDistance() {
return this.segments.stream()
.map(item -> item.getDistance())
.sum();
}
}
class Segment {
int distance;
}
数据库实体类比较特殊,有时候要考虑性能因素,采取了冗余措施。
比如下面例子的 amount 字段。
@Table
class FlightOrder {
BigDecimal amount;
List<FlightOrderPassenger> passengers;
}
@Table
class FlightOrderPassenger {
BigDecimal amount;
List<FlightOrderPassengerTicket> tickets;
FlightOrder belongedOrder;
}
@Table
class FlightOrderPassengerTicket {
BigDecimal amount;
FlightOrderPassenger belongedPassenger;
}
将更新冗余字段的逻辑封装在类内部,集中处理。
@Table
class FlightOrderPassengerTicket {
BigDecimal amount;
FlightOrderPassenger belongedPassenger;
void setAmount(BigDecimal amount) {
this.amount = amount;
this.resetOrderAmout();
}
void addAmount (BigDecimal amount) {
this.amount = this.amount + amount;
this.resetOrderAmout();
}
private resetOrderAmout() {
var passenger = this.belongedPassenger;
passenger.amount = passenger.getTickets().stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
var order = this.belongedPassenger.belongedOrder;
order.amount = order.getPassengers().stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
推荐:
主要描述的和外部打交道的时候,不可避免的重复。代码必须持有外部系统已经蕴含的知识(表征)。包括 API、数据 schema,错误码等等。
对于 API,客户端代码、API 定义、服务端代码,两两之间存在重复。
推荐:
以上两种方式,都消除了 API 定义、服务端代码之间的重复,不足是无法消灭客户端重复,但也可以非常便利的手动触发完成重复消除。
实体类对数据表的定义和数据库实际的表结构存在重复。
推荐:
准确的说,是不同服务(开发人员)对同一需求都有自己的实现。它最大的问题是不在同一服务很难实现共用,尤其是前后端,存在语言壁垒。
最容易产生重复的就是校验规则,手机号、邮箱校验等,不同服务不同的开发人员都有自己的实现。
推荐:
但是,无法破除跨语言的壁垒。
知识和意图
的重复。包含 代码重复
、文档重复
、数据重复
、表征重复
、开发人员重复
。知识和意图
的重复,消灭反而讲不通。知识
的重复,因为性能等方面的考量,予以保留,但应妥善对待。规则终究是规则,思想终究是思想。实践起来困难重重,忠告: