3. 坏代码的滋味
3.1 神秘命名(Mysterious Name)
运用难以了解的变量名、函数名等,会导致代码难以阅读和保护。能够考虑更改动量名、函数名,使其愈加明晰易懂。
// 坏代码示例
function fn(a, b, c) {
for (let i = 0; i < a.length; i++) {
const x = a[i];
if (x[b] === c) {
return x;
}
}
}
// 改进后的代码示例
function findObjectByKey(array, key, value) {
for (let i = 0; i < array.length; i++) {
const object = array[i];
if(object[key] === value) {
return object;
}
}
}
在上面的示例中,咱们将 fn
函数的称号改成了愈加具体的 findObjectByKey
,而且给函数参数增加了描述性的称号,使得代码愈加易懂。
神秘命名不只存在于函数和变量称号中,也或许呈现在类名、特点名等其他方面。关于这些状况,相同需求留意称号的明晰和易懂性。
3.2 重复代码(Duplicated Code)
重复代码往往会导致保护本钱的增加,一起也难以确保代码的正确性。能够考虑将重复代码封装成函数或许类,然后防止代码重复。
// 坏代码示例
function renderHeader() {
const header = document.createElement('header');
const logo = document.createElement('img');
logo.src = '/images/logo.png';
const nav = document.createElement('nav');
const homeLink = document.createElement('a');
homeLink.href = '/';
homeLink.textContent = 'Home';
const aboutLink = document.createElement('a');
aboutLink.href = '/about';
aboutLink.textContent = 'About';
nav.appendChild(homeLink);
nav.appendChild(aboutLink);
header.appendChild(logo);
header.appendChild(nav);
document.body.appendChild(header);
}
function renderFooter() {
const footer = document.createElement('footer');
const copyright = document.createElement('p');
copyright.textContent = 'Copyright 2023';
footer.appendChild(copyright);
document.body.appendChild(footer);
}
// 改进后的代码示例
function createElement(type, attributes, children) {
const element = document.createElement(type);
for (const [key, value] of Object.entries(attributes)) {
element.setAttribute(key, value);
}
if (children) {
for (const child of children) {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child);
}
}
}
return element;
}
function renderHeader() {
const header = createElement('header', null, [
createElement('img', { src: '/images/logo.png' }),
createElement('nav', null, [
createElement('a', { href: '/' }, 'Home'),
createElement('a', { href: '/about' }, 'About')
])
]);
document.body.appendChild(header);
}
function renderFooter() {
const footer = createElement('footer', null, [
createElement('p', null, 'Copyright 2023')
]);
document.body.appendChild(footer);
}
在上面的示例中,咱们将 renderHeader
和 renderFooter
函数中的重复代码封装成了名为 createElement
的函数。这样做既防止了代码重复,也使得代码愈加明晰易懂。
3.3 过长函数(Long Function)
过长的函数往往表示这个函数承当了太多的责任,难以理清函数的逻辑。能够考虑将其拆分红多个小函数,每个函数只承当一个责任。
// 坏代码示例
function calculateScore() {
// 大块逻辑
// ...
}
// 改进后的代码示例
function calculateScore() {
const baseScore = getBaseScore();
const bonusScore = getBonusScore();
return baseScore + bonusScore;
}
function getBaseScore() {
// ...
}
function getBonusScore() {
// ...
}
在上面的示例中,咱们将 calculateScore
函数拆分红了 getBaseScore
和 getBonusScore
函数。这样做既使得代码愈加易懂,也方便了单元测试。
除了将函数拆分红多个小函数之外,还能够考虑运用命名杰出的辅助函数等办法来改进过长函数的问题。
3.4 过长参数列表(Long Parameter List)
当函数的参数列表过长时,会导致函数难以了解和保护。能够考虑运用目标字面量或许从头规划函数参数,使参数愈加简练明了。
// 坏代码示例
function createUser(name, age, gender, email, address) {
// ...
}
// 改进后的代码示例
function createUser(user) {
// ...
}
const user = {
name: '张三',
age: 18,
gender: '男',
email: 'zhangsan@example.com',
address: '上海市'
};
createUser(user);
在上面的示例中,咱们将 createUser
函数的五个参数合并成了一个目标字面量 user
。这样做既减少了参数数量,也使得代码愈加明晰易懂。
除了运用目标字面量之外,还能够考虑从头规划函数参数,使其愈加简练明了。比方能够将多个参数拆分红多个函数,每个函数只处理其间的一部分数据。
3.5 大局数据(Global Data)
大局变量或许会被其他部分意外修正,导致呈现难以追踪的过错。能够考虑将大局变量转化为局部变量,或许封装成模块等。
// 坏代码示例
let count = 0;
function incrementCount() {
count++;
}
// 改进后的代码示例
function createCounter() {
let count = 0;
function incrementCount() {
count++;
}
return {
getCount: () => count,
incrementCount
};
}
const counter = createCounter();
counter.incrementCount();
console.log(counter.getCount()); // 1
在上面的示例中,咱们将大局变量 count
转化为了局部变量,而且用一个闭包函数 createCounter
封装了 incrementCount
函数和 getCount
函数。这样做既防止了大局变量带来的危险,也使得代码愈加可保护和明晰。
除了运用闭包函数之外,还能够考虑将大局变量封装成模块、类等办法,然后防止大局变量的运用。
3.6 可变数据(Mutable Data)
可变数据往往会导致代码难以了解和保护。能够考虑运用不行变数据结构,如不行变目标或函数式编程等办法来防止可变性。
// 坏代码示例
let items = ['item1', 'item2', 'item3'];
function addItem(item) {
items.push(item);
}
// 改进后的代码示例
const items = ['item1', 'item2', 'item3'];
function addItem(items, item) {
return [...items, item];
}
const newItems = addItem(items, 'item4');
console.log(items); // ['item1', 'item2', 'item3']
console.log(newItems); // ['item1', 'item2', 'item3', 'item4']
在上面的示例中,咱们运用了不行变数组 items
和纯函数 addItem
来代替了可变数组和修正函数,然后防止了可变性带来的问题。这样做还有一个优点便是,不行变数据更易于进行单元测试,因为它们的行为愈加可预测。
除了运用不行变数据结构之外,还能够考虑运用函数式编程的思维,将操作封装成函数,而且防止副作用(比方修正大局变量等)。这样做能够进步代码的可组合性和可重用性。
3.7 发散式改动(Divergent Change)
当需求变更时,多个不相关的当地都需求修正相同的代码,这种状况称为发散式改动。能够考虑运用面向目标编程的思维,将改动封装成类,而且遵从单一责任准则等规划准则。
// 坏代码示例
function showProductImage(product) {
// 显现产品图片的代码...
}
function logProductClicked(product) {
// 记载用户点击产品的日志的代码...
}
function updateCart(product) {
// 更新购物车的代码...
}
// 改进后的代码示例
class Product {
constructor(name, image) {
this.name = name;
this.image = image;
}
showImage() {
// 显现产品图片的代码...
}
logClicked() {
// 记载用户点击产品的日志的代码...
}
}
class Cart {
constructor() {
this.items = [];
}
addProduct(product) {
// 增加产品到购物车的代码...
}
}
const product = new Product('iPhone', 'iphone.png');
product.showImage();
product.logClicked();
const cart = new Cart();
cart.addProduct(product);
在上面的示例中,咱们运用了面向目标编程的思维,将显现产品图片、记载用户点击产品、更新购物车等逻辑别离封装到了 Product
和 Cart
两个类中。这样做既防止了代码的发散式改动,也使得代码愈加可保护和明晰。
除了运用面向目标编程之外,还能够考虑运用函数式编程的思维,将改动封装成纯函数,而且遵从单一责任准则等规划准则。
3.8 霰弹式修正(Shotgun Surgery)
霰弹式修正(Shotgun Surgery)指的是关于一个改动需求的请求,需求在多个不同的当地进行修正,而这些修正的位置涣散在代码的多个当地。这样的代码一般难以保护和测试,因为修正会牵一发而动全身,使得代码愈加脆弱和简单犯错。
以下是一个JavaScript代码示例:
// 界说了一个获取用户信息的函数 getUserInfo
function getUserInfo(userId) {
// 根据 userId 获取用户信息并回来
// ...
}
// 界说了一个更新用户信息的函数 updateUserInfo
function updateUserInfo(userId, name, email) {
// 根据 userId 更新用户的姓名和电子邮件
// ...
}
// 界说了一个删去用户信息的函数 deleteUserInfo
function deleteUserInfo(userId) {
// 根据 userId 删去用户信息
// ...
}
// 在运用程序中的某处调用了 getUserInfo 函数
const user = getUserInfo(123);
// 在另一个当地调用了 updateUserInfo 函数,更新用户信息
updateUserInfo(123, 'Alice', 'alice@example.com');
// 然后在另一个当地调用了 deleteUserInfo 函数,删去用户信息
deleteUserInfo(123);
在上面这个比方中,假如咱们需求更改 userId 的数据类型或许称号,那么就必须修正 getUserInfo
, updateUserInfo
和 deleteUserInfo
这三个函数。这种状况下,咱们说代码中存在“霰弹式修正”,因为修正的当地涣散在多个不同的函数中,这样会带来很大的保护本钱和过错危险。
为了解决这个问题,咱们能够考虑将这些操作封装成一个用户办理类,这样就能够将一切的修正都集中到一个当地。例如:
class UserManager {
constructor() {
this.users = new Map();
}
getUser(userId) {
return this.users.get(userId);
}
updateUser(userId, name, email) {
const user = this.getUser(userId);
if (user) {
user.name = name;
user.email = email;
}
}
deleteUser(userId) {
this.users.delete(userId);
}
}
// 在运用程序中的某处实例化 UserManager 类
const userManager = new UserManager();
// 在运用程序中调用 UserManager 的办法来获取、更新或删去用户信息
const user = userManager.getUser(123);
userManager.updateUser(123, 'Alice', 'alice@example.com');
userManager.deleteUser(123);
如此一来,假如咱们需求更改 userId 的数据类型或许称号,只需求修正 UserManager
类中的代码即可,而不用在代码的多个当地进行修正。这样能够让代码愈加明晰、简练和易于保护。
3.9 依恋情结(Feature Envy)
到某个函数为了核算某个值,从另一个目标那儿调用几乎半打 的取值函数。疗法显而易见:这个函数想跟这些数据待在一起,那就运用搬移函数(198)把它移过去。
class Order {
constructor(items, customer) {
this.items = items
this.customer = customer
}
totalPrice() {
let result = 0
this.items.forEach(item => {
/** 重构前 */
// let basePrice = item.getPrice()
// let discount = item.getDiscount(this.customer)
// result += basePrice - discount
/** 重构后 */
result += item.getFinalPrice(this.customer)
});
return result
}
}
3.10 数据泥团(Data Clumps)
数据泥团是一种代码坏滋味,指的是代码中多个当地运用相同的组合参数。这种状况下,能够将这些参数封装到一个目标中来消除数据泥团。
以下是一个JavaScript示例代码,演示了数据泥团的问题:
function createOrder(customerName, customerEmail, itemName, itemPrice) {
// ... create an order ...
}
let customerName = 'John Doe';
let customerEmail = 'john.doe@example.com';
let itemName = 'Widget';
let itemPrice = 10.0;
createOrder(customerName, customerEmail, itemName, itemPrice);
在上面的代码中,createOrder
函数接纳四个参数: customerName
、customerEmail
、itemName
和 itemPrice
。可是,这些参数在多个当地被运用,因而或许会导致数据泥团的问题。
解决办法之一是将这些参数封装成一个目标,如下所示:
class Customer {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class Item {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
class Order {
constructor(customer, items) {
this.customer = customer;
this.items = items;
}
getTotalPrice() {
let totalPrice = 0;
for (let i = 0; i < this.items.length; i++) {
totalPrice += this.items[i].price;
}
return totalPrice;
}
}
let customer = new Customer('John Doe', 'john.doe@example.com');
let items = [ new Item('Widget', 10.0) ];
let order = new Order(customer, items);
在这个重构后的代码中,Customer
和 Item
类被用来封装了 customerName
、customerEmail
、itemName
和 itemPrice
参数。Order
类现在接纳一个 Customer
目标和一个 Item
数组作为参数。
这种办法供给了以下优点:
- 能够消除数据泥团,因为一切相关信息都被封装到一个目标中。
- 能够轻松地增加或删去与
Customer
和Item
相关的特点,而无需更改createOrder
函数的签名。 - 能够运用
Customer
和Item
类来履行其他操作,例如对Customer
和Item
的验证和存储。
3.11 根本类型偏执(Primitive Obsession)
根本类型偏执是一种代码坏滋味,指的是过度运用根本类型代替目标。这种状况下,能够将根本类型封装成目标,以便增加更多的行为和状况。
以下是一个JavaScript示例代码,演示了根本类型偏执的问题:
function calculateCircleArea(radius) {
return Math.PI * radius * radius;
}
let radius = 5;
let area = calculateCircleArea(radius);
在上面的代码中,calculateCircleArea
函数接纳一个数字半径,并回来圆的面积。可是, radius
这个变量是一个根本类型,它没有任何状况或行为。假如需求向办法中传递更多参数,就需求增加其他参数,或许重载该办法。
解决办法之一是创立一个 Circle
目标,如下所示:
class Circle {
constructor(radius) {
this.radius = radius;
}
getArea() {
return Math.PI * this.radius * this.radius;
}
}
let circle = new Circle(5);
let area = circle.getArea();
在这个重构后的代码中,Circle
类被用来封装圆的半径,并增加了一个 getArea
办法。现在,能够创立一个 Circle
目标,调用 getArea
办法获取其面积。
这种办法供给了以下优点:
- 能够防止运用根本类型,然后消除根本类型偏执的问题。
- 能够轻松地增加更多的行为和状况,例如圆的直径、周长等。
- 能够将
Circle
目标作为参数传递给其他办法,然后防止增加更多的参数或重载办法。
3.12 重复的 switch (Repeated Switches)
重复的 switch 是指在代码中存在多个 switch 句子,而且它们的结构和逻辑十分类似,乃至有些 case 分支的处理办法也是如出一辙的。这种状况下,代码中的重复性会增加保护本钱,下降可读性和可保护性。
以下是一个 JavaScript 代码示例,展现了怎么经过函数、目标映射等技巧来消除重复的 switch:
function processAnimal(animal) {
const animalActions = {
'dog': function() {
console.log('This is a dog.');
},
'cat': function() {
console.log('This is a cat.');
},
'bird': function() {
console.log('This is a bird.');
}
};
if (animal in animalActions) {
animalActions[animal]();
} else {
console.log('Unknown animal type');
}
}
在这个示例中,咱们界说了一个表示动物类型的字符串变量 animal
,并运用目标 animalActions
将每种动物类型与对应的处理函数相关起来。在 processAnimal
函数中,咱们首要检查传入的动物类型是否包括在 animalActions
目标中,假如存在,则调用相应的处理函数;否则输犯过错信息。
经过运用目标映射和函数调用,咱们防止了在代码中呈现重复的 switch 句子。假如需求增加新的动物类型,只需在 animalActions
目标中增加相应的处理函数即可,不需求修正 processAnimal
函数本身。这种做法不只减少了代码中的重复性,还进步了代码的可保护性和扩展性。
3.13 循环句子(Loops)
循环句子是一种常见的编程结构,用于在代码中重复履行某段逻辑。尽管循环句子能够协助咱们完成杂乱的算法和操控流程,但过多的循环或许会导致代码难以保护、呈现性能问题等坏滋味。
以下是一个 JavaScript 代码示例,展现了怎么经过函数式编程技巧来代替循环句子:
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, val) => acc + val, 0);
console.log(sum); // 输出 15
在这个示例中,咱们界说了一个包括数字的数组 numbers
,并运用 reduce
函数将数组元素累加起来。reduce
函数承受两个参数:一个累加器函数和一个初始值。在每次迭代中,累加器函数会将上一次的成果(或初始值)和当前元素作为参数进行运算,并回来新的累加成果,终究回来整个数组的累加成果。
经过运用函数式编程的技巧,咱们防止了在代码中呈现显式的循环句子。这种做法不只减少了代码中的重复性,还进步了代码的可读性和可保护性。当需求对数组进行其他操作时,咱们只需运用其他高阶函数(例如 map
、filter
等)即可,无需编写显式的循环句子。
3.14 冗赘的元素(Lazy Element)
程序元素(如类和函数)能给代码增加结构,然后支持改动、促进复用或许哪怕仅仅供给更好的姓名也好,但有时咱们真的不需求这层额定的结构。
// before
function isNumBiggerThen5 (num) {
return num > 5
}
consol.log(isNumBiggerThen5(num) ? 'a' : 'b')
// after
consol.log(num > 5 ? 'a' : 'b)
3.15 纸上谈兵通用性(Speculative Generality)
纸上谈兵通用性(Speculative Generality)是指在代码中增加了许多不必要的、未来或许需求的功用,但这些功用往往并没有被运用到。这会使得代码变得杂乱、难以保护,而且增加了开发和测试的工作量。
以下是一个或许存在纸上谈兵通用性问题的JavaScript代码示例:
function calculateArea(shapeType, dimensions) {
if (shapeType === 'rectangle') {
const [length, width] = dimensions;
return length * width;
} else if (shapeType === 'circle') {
const [radius] = dimensions;
return Math.PI * radius * radius;
} else if (shapeType === 'triangle') {
const [base, height] = dimensions;
return 0.5 * base * height;
} else {
// handle unsupported shape type
throw new Error('Unsupported shape type');
}
}
上面的代码完成了核算不同形状的面积和体积的功用。尽管这是一个通用的完成,但它包括了许多未来或许需求的但目前并不需求的形状类型,而且每次增加新的形状类型都需求修正函数。假如只需求核算少数几种形状的面积和体积时,这种完成会显得过于杂乱。
为了解决这个问题,能够将这两个函数拆分红更小的、专心于单个形状类型的函数。例如,能够编写一个名为 calculateRectangleArea
的函数,它只核算矩形的面积。这样做能够使代码更明晰、易于保护,而且减少了未来需求变更的工作量。
3.16 暂时字段(Temporary Field)
暂时字段(Temporary Field)是指一个目标或类中存在一些只在特定状况下运用的字段,这些字段关于目标或类的状况来说是无关紧要的,可是它们会占用额定的内存空间,而且或许导致代码难以了解和保护。
以下是一个或许存在暂时字段问题的JavaScript代码示例:
class Order {
constructor(items) {
this.items = items;
}
calculateTotalPrice() {
let total = 0;
for (let item of this.items) {
total += item.price * item.quantity;
}
if (total > 100) {
this.discountApplied = true; // 设置暂时字段
return total * 0.9; // 打九折优惠
} else {
this.discountApplied = false; // 设置暂时字段
return total;
}
}
}
上面的代码完成了一个 Order
类,其间每个订单都有一些产品项(items
),每个产品项包括价格和数量。calculateTotalPrice
办法核算订单的总价,而且假如总价大于 100100,则将打九折的优惠运用到总价上,并设置暂时字段 discountApplied
来表示是否运用了优惠。
这种完成办法存在两个问题:
-
discountApplied
是一个只在特定状况下运用的暂时字段,它不属于Order
目标的状况,因而会增加代码的杂乱性。 -
calculateTotalPrice
办法完成了核算总价和运用优惠两个不同的功用,这违反了单一责任准则。
为了解决这个问题,能够将 calculateTotalPrice
办法拆分红两个办法,一个担任核算总价,另一个担任运用优惠。这样做能够使代码愈加明晰、易于了解和保护,而且减少了暂时字段的运用。
class Order {
constructor(items) {
this.items = items;
}
calculateTotalPrice() {
let total = 0;
for (let item of this.items) {
total += item.price * item.quantity;
}
return total;
}
applyDiscount(total) {
if (total > 100) {
return total * 0.9;
} else {
return total;
}
}
}
上面的重构后的代码中,calculateTotalPrice
办法只担任核算订单的总价,applyDiscount
办法担任判断是否应该运用优惠并回来优惠后的价格。这样做能够防止运用暂时字段,而且契合单一责任准则。
3.17 过长的音讯链(Message Chains)
过长的音讯链(Message Chains)是指在拜访一个目标的特点时,经过多个点号衔接多个目标而构成的一条链。这样做会使代码变得难以了解和保护,而且增加了耦合度。
例如,在一个在线购物网站中,有一个订单类(Order),存储着顾客(Customer)的信息,如下所示:
class Order {
constructor(customer) {
this.customer = customer;
}
getCustomerEmail() {
return this.customer.contactInfo.email;
}
}
class Customer {
constructor(contactInfo) {
this.contactInfo = contactInfo;
}
}
class ContactInfo {
constructor(email) {
this.email = email;
}
}
在上面的比方中,Order
类需求获取顾客的邮箱地址,可是却需求运用 this.customer.contactInfo.email
这条音讯链来获取。这条音讯链不只不直观,而且简单犯错。
重构这段代码,咱们能够运用“隐藏托付联系”(Hide Delegate)重构技巧,将音讯链放到 Customer
类中,在 Order
类中只需求调用 customer.getEmail()
办法即可,如下所示:
class Order {
constructor(customer) {
this.customer = customer;
}
getCustomerEmail() {
return this.customer.getEmail();
}
}
class Customer {
constructor(contactInfo) {
this.contactInfo = contactInfo;
}
getEmail() {
return this.contactInfo.email;
}
}
class ContactInfo {
constructor(email) {
this.email = email;
}
}
经过这样的重构,咱们将音讯链隐藏在了 Customer
类中,使得代码愈加直观和易于保护。一起也下降了耦合度,当咱们需求修正顾客信息时,只需求在 Customer
类中修正即可,不会影响到 Order
类。
3.18 中间人(Middle Man)
中间人(Middle Man)是指一个类仅仅转发请求给其他类,而且没有任何自己的逻辑。这样做会增加代码的杂乱性和保护本钱,因为咱们需求在多个类之间跳转才干了解代码。
下面是一个比方,在一个在线图书销售网站中,有一个 CustomerService
类用于处理客户信息,可是该类仅仅将请求转发给了另外一个类 CustomerRepository
,并没有自己的事务逻辑:
class CustomerService {
constructor(customerRepository) {
this.customerRepository = customerRepository;
}
getCustomerById(id) {
return this.customerRepository.getCustomerById(id);
}
}
class CustomerRepository {
constructor(customers) {
this.customers = customers;
}
getCustomerById(id) {
return this.customers.find((customer) => customer.id === id);
}
}
经过观察上面的代码,咱们能够看到 CustomerService
类仅仅直接调用 CustomerRepository
类中的办法来获取数据,没有完成任何自己的事务逻辑。这种状况就被称为中间人(Middle Man)。
针对这样的状况,咱们能够运用“移除中间人”(Remove Middle Man)重构技巧,将 CustomerService
类中的逻辑直接合并到 CustomerRepository
类中,如下所示:
class CustomerRepository {
constructor(customers) {
this.customers = customers;
}
getCustomerById(id) {
return this.customers.find((customer) => customer.id === id);
}
getAllCustomers() {
// 处理获取一切客户信息的逻辑
}
updateCustomer(customer) {
// 处理更新客户信息的逻辑
}
deleteCustomer(id) {
// 处理删去客户信息的逻辑
}
}
经过这样的重构,咱们将 CustomerService
类中的逻辑直接合并到了 CustomerRepository
类中,使得代码愈加简练和易于保护。一起也防止了多个类之间的跳转,下降了杂乱性。
3.19 内情交易(Insider Trading)
假定有两个模块A和B,它们都对一些数据感兴趣。咱们能够测验运用JavaScript来完成这个场景。
首要,咱们能够新建一个模块C,专门用于办理这些共用的数据。在模块C中,咱们能够界说一个目标来存储这些数据:
// 模块C
const sharedData = {
data1: 'value1',
data2: 'value2'
};
然后,模块A和模块B都能够引入模块C,并拜访其间的共享数据:
// 模块A
import { sharedData } from './moduleC';
console.log(sharedData.data1); // 输出 "value1"
// 模块B
import { sharedData } from './moduleC';
console.log(sharedData.data2); // 输出 "value2"
另一种办法是运用托付联系,将模块B变成模块A和模块C之间的中介。具体来说,模块A能够暴露出一个托付接口,让模块B能够经过它来拜访共享数据:
// 模块A
const sharedData = {
data1: 'value1',
data2: 'value2'
};
export const delegate = {
getData(key) {
return sharedData[key];
}
};
// 模块B
import { delegate } from './moduleA';
console.log(delegate.getData('data1')); // 输出 "value1"
console.log(delegate.getData('data2')); // 输出 "value2"
这样,模块B就能够经过托付接口来拜访模块A中的共享数据,防止了直接拜访共享数据或许带来的潜在问题。
3.20 过大的类(Large Class)
过大的类(Large Class)是指一个类承当了太多的责任,代码量十分巨大,难以保护和了解。这种状况下,咱们能够采纳以下措施来改进代码规划:
-
分化类:将一个过大的类拆分红多个小类,每个小类专心于处理特定的责任。
-
运用组合联系:将一个类中的某些功用提取出来构成独立的类,并经过组合联系将它们集成到本来的类中。
-
提炼接口:关于过大的类,咱们能够将一些通用的、可复用的办法提炼成接口,并让该类完成这些接口,然后下降类的杂乱度。
下面是一个针对过大的类的示例,假定咱们有一个名为Order
的类,它包括了订单相关的许多操作,比方增加产品、核算价格、保存订单等。这个类的代码量十分巨大,难以保护和了解。咱们能够将它分化成多个小类,每个小类专心于处理特定的责任。
// 过大的类
class Order {
constructor() {
this.items = [];
// ...
}
addItem(item) {
// ...
}
calculatePrice() {
// ...
}
saveOrder() {
// ...
}
}
// 分化后的类
class Order {
constructor() {
this.items = [];
}
addItem(item) {
// ...
}
}
class PriceCalculator {
constructor(order) {
this.order = order;
}
calculatePrice() {
// ...
}
}
class OrderSaver {
constructor(order) {
this.order = order;
}
saveOrder() {
// ...
}
}
在这个示例中,咱们将本来的Order
类分化成了三个小类:Order
、PriceCalculator
和OrderSaver
。Order
类只担任办理订单中的产品,PriceCalculator
类担任核算订单的价格,OrderSaver
类担任保存订单到数据库中。经过分化类,咱们将杂乱的逻辑拆分红了多个小的责任,进行了更好的代码组织。
另一种办法是运用组合联系,将本来的类中某些功用提取出来构成独立的类,并经过组合联系将它们集成到本来的类中:
// 过大的类
class Order {
constructor() {
this.items = [];
this.priceCalculator = new PriceCalculator(this);
this.orderSaver = new OrderSaver(this);
}
addItem(item) {
// ...
}
calculatePrice() {
return this.priceCalculator.calculatePrice();
}
saveOrder() {
this.orderSaver.saveOrder();
}
}
class PriceCalculator {
constructor(order) {
this.order = order;
}
calculatePrice() {
// ...
}
}
class OrderSaver {
constructor(order) {
this.order = order;
}
saveOrder() {
// ...
}
}
在这个示例中,咱们将PriceCalculator
和OrderSaver
提取出来构成了独立的类,并经过组合联系将它们集成到本来的Order
类中。这种办法比较适用于某些功用特别杂乱、可重用性较高的场景。
最后,咱们能够运用接口提炼,将一些通用的、可复用的办法提炼成接口,并让类完成这些接口,然后下降类的杂乱度。
3.21 异曲同工的类(Alternative Classes with Different Interfaces)
异曲同工的类是指具有类似功用和完成细节但接口不同的两个或多个类。这一般会导致代码重复和不必要的冗余。
下面是一个JavaScript示例,其间有两个类Square
和Rectangle
,它们都有核算面积和周长的办法。尽管它们完成了相同的功用,可是因为它们的接口不同,调用它们的代码需求针对每个类编写不同的代码:
class Square {
constructor(length) {
this.length = length;
}
area() {
return Math.pow(this.length, 2);
}
perimeter() {
return 4 * this.length;
}
}
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
perimeter() {
return 2 * (this.width + this.height);
}
}
// 调用Square类的代码
const square = new Square(5);
const squareArea = square.area();
const squarePerimeter = square.perimeter();
// 调用Rectangle类的代码
const rectangle = new Rectangle(5, 7);
const rectangleArea = rectangle.area();
const rectanglePerimeter = rectangle.perimeter();
为了消除这种重复,能够运用接口一致两个类的办法称号和参数列表。例如,在此示例中,咱们能够创立一个名为Shape
的接口,并将Square
和Rectangle
类别离完成该接口。此后,咱们只需求编写一次调用代码即可运用这两个类。
以下是重构后的示例:
// 界说通用接口Shape
class Shape {
area() {}
perimeter() {}
}
// Square类完成Shape接口
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
area() {
return Math.pow(this.length, 2);
}
perimeter() {
return 4 * this.length;
}
}
// Rectangle类完成Shape接口
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
perimeter() {
return 2 * (this.width + this.height);
}
}
// 调用代码不再需求针对每个类别离编写
const shapes = [new Square(5), new Rectangle(5, 7)];
shapes.forEach((shape) => {
console.log(shape.area());
console.log(shape.perimeter());
});
经过这种办法,消除了代码重复和冗余,并使得代码更易于保护和扩展。
3.22 纯数据类(Data Class)
这样的类仅仅一种不会说话的数据容器,它们几乎一 定被其他类过分细琐地操控着。
纯数据类常常意味着行为被放在了过错的当地。也便是说,只要把处理数据 的行为从客户端搬移到纯数据类里来,就能使状况大为改观。
纯数据类是指只包括数据而不包括任何事务逻辑的类。在这种状况下,假如咱们发现该类中存在很多的处理数据的行为,那么很或许是因为这些行为被放置在了过错的位置上。
以下是一个纯数据类的 JavaScript 代码示例:
class User {
constructor(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
getName() {
return this.name;
}
getAge() {
return this.age;
}
getGender() {
return this.gender;
}
// 处理数据的行为被放在了客户端
toObject() {
return {
name: this.name,
age: this.age,
gender: this.gender
};
}
}
在上述代码中,User
类是一个纯数据类,它只包括用户的姓名、年纪和性别等根本信息。可是,toObject()
办法却将处理数据的行为放置在了客户端中,这或许会导致类的责任不明确,使得代码难以保护。
为了改进这个问题,咱们应该将处理数据的行为放到 User
类内部,例如修正代码如下:
class User {
constructor(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
getName() {
return this.name;
}
getAge() {
return this.age;
}
getGender() {
return this.gender;
}
// 处理数据的行为被放在了纯数据类中
toObject() {
return {
name: this.getName(),
age: this.getAge(),
gender: this.getGender()
};
}
}
经过将 toObject()
办法移到 User
类内部,咱们能够使得数据处理行为与 User
类的根本信息聚合在一起,然后进步代码的可读性和可保护性。
代码烂味2:过于依赖外部类的特点
假如一个纯数据类依赖于其他类的特点,那么这个类就不再是一个真正的纯数据类。在这种状况下,咱们能够考虑将这些依赖性转移到其他类中,或许是将这些依赖性经过结构函数注入。
例如,以下是一个依赖于其他类的特点的数据类:
class Order {
constructor(orderId, customerId) {
this.orderId = orderId;
this.customer = new Customer(customerId);
}
getOrderDetails() {
const customerName = this.customer.getName();
const orderDate = this.getOrderDate();
return `Order details:\nOrder ID: ${this.orderId}\nCustomer name: ${customerName}\nOrder date: ${orderDate}`;
}
getOrderDate() {
// get order date from API
return '2023-06-17';
}
}
class Customer {
constructor(customerId) {
this.customerId = customerId;
}
getName() {
// get name from API
return 'John';
}
}
能够经过将依赖性注入结构函数中改写:
class Order {
constructor(orderId, customer) {
this.orderId = orderId;
this.customer = customer;
}
getOrderDetails() {
const customerName = this.customer.getName();
const orderDate = this.getOrderDate();
return `Order details:\nOrder ID: ${this.orderId}\nCustomer name: ${customerName}\nOrder date: ${orderDate}`;
}
getOrderDate() {
// get order date from API
return '2023-06-17';
}
}
class Customer {
constructor(customerId) {
this.customerId = customerId;
}
getName() {
// get name from API
return 'John';
}
}
// usage
const customer = new Customer(1);
const order = new Order(1001, customer);
console.log(order.getOrderDetails());
这样,咱们就把纯数据类和事务逻辑分离开来,使得代码更易保护和可扩展。另外,这种办法还使得代码愈加灵敏,咱们能够经过注入不同的依赖来完成不同的事务需求。
代码烂味3:过于暴露内部状况
假如一个纯数据类暴露它的内部状况给客户端,那么它会破坏封装性,而且很难确保这些状况的一致性。在这种状况下,咱们能够考虑运用拜访操控修饰符(如private、protected等)来限制对内部状况的拜访。
3.23 被回绝的遗赠(Refused Bequest)
被回绝的遗赠(Refused Bequest)是坏滋味之一,一般指的是子类从父类承继了一些特点或办法,但并不需求或许不想要这些特点或办法,因而在子类中对它们进行了重写或删去操作。这或许会导致代码冗余、不必要的杂乱性和难以保护。
以下是一个运用JavaScript语言的示例代码:
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {
console.log('The animal is eating.');
}
}
class Cat extends Animal {
constructor(name, age, color) {
super(name, age);
this.color = color;
}
// 子类不需求承继的办法
sleep() {
console.log('The cat is sleeping.');
}
// 子类重写父类的办法
eat() {
console.log('The cat is eating fish.');
}
}
const myCat = new Cat('Tom', 2, 'gray');
myCat.sleep(); // 调用子类新增的办法
myCat.eat(); // 调用子类重写的办法
在上面的代码中,Animal
类有一个eat
办法,而Cat
类承继了Animal
类,并在其间重写了eat
办法和新增了一个sleep
办法。可是,在实际运用中,或许并不需求Animal
类的eat
办法,或许Cat
类只需求部分承继Animal
类的特点和办法。因而,为了防止被回绝的遗赠这种坏滋味,咱们能够运用组合和接口阻隔准则等技能来改进代码规划。
3.24 注释(Comments)
当你感觉需求编撰注释时,请先测验重构,试着让一切注释都变得多余。
假如你不知道该做什么,这才是注释的杰出运用时机。除了用来记叙将来的计划之外,注释还能够用来符号你并无十足把握的区域。你能够在注释里写下自己“为什么做某某事”。