您现在的位置是:网站首页 > 行为保持的重构方法文章详情
行为保持的重构方法
陈川
【
JavaScript
】
39451人已围观
8627字
行为保持的重构方法
重构代码时保持原有行为是基本要求。行为保持意味着在不改变外部可见功能的前提下调整内部结构。JavaScript设计模式中,许多重构技巧都围绕这一原则展开。
提取函数
长函数难以维护时,将部分逻辑提取为独立函数是常见做法。关键要确保提取后的函数与原代码块行为完全一致。
// 重构前
function calculateTotal(items) {
let total = 0;
for (const item of items) {
if (!item.discount) {
total += item.price * item.quantity;
} else {
total += (item.price * 0.9) * item.quantity;
}
}
return total;
}
// 重构后
function calculateItemCost(item) {
return item.discount ? (item.price * 0.9) : item.price;
}
function calculateTotal(items) {
return items.reduce((total, item) => {
return total + calculateItemCost(item) * item.quantity;
}, 0);
}
内联函数
与提取函数相反,当函数体与其名称同样清晰时,可以考虑内联:
// 重构前
function getRating(driver) {
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(driver) {
return driver.numberOfLateDeliveries > 5;
}
// 重构后
function getRating(driver) {
return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}
替换算法
用更清晰的算法替换复杂实现时,必须确保新旧算法在所有边界条件下行为一致:
// 重构前
function findPeople(people) {
const result = [];
for (let i = 0; i < people.length; i++) {
if (people[i].age >= 18) {
result.push(people[i]);
}
}
return result;
}
// 重构后
function findPeople(people) {
return people.filter(person => person.age >= 18);
}
搬移函数
当函数与另一个类的交互比当前类更多时,考虑将其搬移:
// 重构前
class Order {
constructor(customer) {
this.customer = customer;
}
getDiscountRate() {
return this.customer.getDiscountRate();
}
}
class Customer {
getDiscountRate() {
// 计算折扣逻辑
}
}
// 重构后
class Order {
constructor(customer) {
this.customer = customer;
}
}
class Customer {
getDiscountRate() {
// 计算折扣逻辑
}
applyDiscount(order) {
return order.total * this.getDiscountRate();
}
}
封装变量
将可变数据封装起来,通过访问函数控制修改:
// 重构前
let defaultOwner = { firstName: "Martin", lastName: "Fowler" };
// 重构后
let defaultOwnerData = { firstName: "Martin", lastName: "Fowler" };
export function getDefaultOwner() {
return Object.assign({}, defaultOwnerData);
}
export function setDefaultOwner(arg) {
defaultOwnerData = arg;
}
以查询取代临时变量
消除临时变量可以使函数更清晰:
// 重构前
function calculateTotal() {
const basePrice = quantity * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
}
return basePrice * 0.98;
}
// 重构后
function calculateTotal() {
if (basePrice() > 1000) {
return basePrice() * 0.95;
}
return basePrice() * 0.98;
}
function basePrice() {
return quantity * itemPrice;
}
引入参数对象
将相关参数组合为对象提高可读性:
// 重构前
function amountInvoiced(startDate, endDate) { /* ... */ }
function amountReceived(startDate, endDate) { /* ... */ }
function amountOverdue(startDate, endDate) { /* ... */ }
// 重构后
function amountInvoiced(dateRange) { /* ... */ }
function amountReceived(dateRange) { /* ... */ }
function amountOverdue(dateRange) { /* ... */ }
分解条件表达式
复杂条件逻辑可以分解为独立函数:
// 重构前
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) {
charge = quantity * plan.summerRate;
} else {
charge = quantity * plan.regularRate + plan.regularServiceCharge;
}
// 重构后
if (summer()) {
charge = summerCharge();
} else {
charge = regularCharge();
}
function summer() {
return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd);
}
function summerCharge() {
return quantity * plan.summerRate;
}
function regularCharge() {
return quantity * plan.regularRate + plan.regularServiceCharge;
}
合并重复的条件片段
将重复代码移出条件分支:
// 重构前
if (isSpecialDeal()) {
total = price * 0.95;
send();
} else {
total = price * 0.98;
send();
}
// 重构后
if (isSpecialDeal()) {
total = price * 0.95;
} else {
total = price * 0.98;
}
send();
以多态取代条件表达式
当条件逻辑基于类型时,考虑使用多态:
// 重构前
function getSpeed(animal) {
switch (animal.type) {
case "european":
return animal.baseSpeed;
case "african":
return animal.baseSpeed - animal.loadFactor * animal.numberOfCoconuts;
case "norwegian":
return (animal.isNailed) ? 0 : animal.baseSpeed - animal.loadFactor;
}
}
// 重构后
class European {
getSpeed() {
return this.baseSpeed;
}
}
class African {
getSpeed() {
return this.baseSpeed - this.loadFactor * this.numberOfCoconuts;
}
}
class Norwegian {
getSpeed() {
return (this.isNailed) ? 0 : this.baseSpeed - this.loadFactor;
}
}
引入断言
明确表达代码假设:
function applyDiscount(order) {
if (!order.customer) return 0;
// 断言客户必须有折扣率
console.assert(order.customer.discountRate !== undefined, "Customer must have discount rate");
return order.total * order.customer.discountRate;
}
保持函数单一职责
每个函数只做一件事:
// 重构前
function processOrder(order) {
validateOrder(order);
calculateTotal(order);
saveOrder(order);
sendConfirmation(order);
}
// 重构后
function processOrder(order) {
validateOrder(order);
const total = calculateTotal(order);
const savedOrder = saveOrder(order, total);
sendConfirmation(savedOrder);
}
参数化函数
合并相似函数时,用参数区分不同行为:
// 重构前
function tenPercentRaise(person) {
person.salary = person.salary.multiply(1.1);
}
function fivePercentRaise(person) {
person.salary = person.salary.multiply(1.05);
}
// 重构后
function raise(person, factor) {
person.salary = person.salary.multiply(1 + factor);
}
移除标记参数
用显式函数替代布尔参数:
// 重构前
function setDimension(name, value, isWidth) {
if (isWidth) {
this.width = value;
} else {
this.height = value;
}
}
// 重构后
function setWidth(value) {
this.width = value;
}
function setHeight(value) {
this.height = value;
}
保持对象完整性
传递整个对象而非多个属性:
// 重构前
function calculateCost(basePrice, discountRate, taxRate) {
return basePrice * (1 - discountRate) * (1 + taxRate);
}
// 重构后
function calculateCost(pricing) {
return pricing.basePrice * (1 - pricing.discountRate) * (1 + pricing.taxRate);
}
替换魔法字面量
用常量替代魔法数字/字符串:
// 重构前
function potentialEnergy(mass, height) {
return mass * 9.81 * height;
}
// 重构后
const GRAVITATIONAL_CONSTANT = 9.81;
function potentialEnergy(mass, height) {
return mass * GRAVITATIONAL_CONSTANT * height;
}
组合替代继承
优先使用对象组合而非类继承:
// 重构前
class Employee extends Person {
// 继承所有Person方法
}
// 重构后
class Employee {
constructor(person) {
this.person = person;
}
getName() {
return this.person.getName();
}
}
延迟加载
需要时才初始化属性:
class Customer {
get orders() {
return this._orders || this.fetchOrders();
}
fetchOrders() {
this._orders = /* 从数据库加载 */;
return this._orders;
}
}
引入Null对象
避免null检查:
// 重构前
if (customer === null) {
plan = BillingPlan.basic();
} else {
plan = customer.billingPlan;
}
// 重构后
class NullCustomer {
get billingPlan() {
return BillingPlan.basic();
}
}
plan = customer.billingPlan;
提炼类
当一个类承担过多责任时:
// 重构前
class Person {
constructor() {
this.officeAreaCode = "";
this.officeNumber = "";
}
getTelephoneNumber() {
return `(${this.officeAreaCode}) ${this.officeNumber}`;
}
}
// 重构后
class TelephoneNumber {
constructor(areaCode, number) {
this.areaCode = areaCode;
this.number = number;
}
toString() {
return `(${this.areaCode}) ${this.number}`;
}
}
class Person {
constructor() {
this.telephoneNumber = new TelephoneNumber("", "");
}
getTelephoneNumber() {
return this.telephoneNumber.toString();
}
}
隐藏委托关系
减少客户端对中间对象的依赖:
// 重构前
client.department.manager = newManager;
// 重构后
class Client {
setManager(manager) {
this.department.manager = manager;
}
}
引入本地扩展
为第三方类添加方法:
// 原始Date类没有nextDay方法
Date.prototype.nextDay = function() {
return new Date(this.getTime() + 24 * 60 * 60 * 1000);
};
// 或者使用包装类
class MyDate {
constructor(date) {
this.date = date;
}
nextDay() {
return new Date(this.date.getTime() + 24 * 60 * 60 * 1000);
}
}
以命令取代函数
将函数封装为对象,支持撤销等操作:
class ChargeCommand {
constructor(customer, amount) {
this.customer = customer;
this.amount = amount;
}
execute() {
this.customer.balance += this.amount;
}
undo() {
this.customer.balance -= this.amount;
}
}
保持测试不变
重构时要确保测试继续通过:
// 重构前测试
test("calculates total", () => {
const items = [{ price: 10, quantity: 2 }];
expect(calculateTotal(items)).toBe(20);
});
// 重构后同样的测试应该继续通过
上一篇: 模式组合的测试策略
下一篇: 设计模式与代码覆盖率