前端开发人员应该了解的软件工程原理
作为前端开发人员,我们常常专注于创建美观的用户界面。然而,我们必须记住,美观也体现在内在,对像素级完美的追求也应该体现在代码的组织和结构上。在本文中,我们将探讨一些前端开发人员应该了解并在项目中应用的基本软件工程原则。
1. 避免重复(不要重复自己)
DRY 原则强调代码重用性和可维护性的重要性。通过将通用功能提取到可重用的组件、函数或模块中,避免代码重复。遵循 DRY 原则可以减少代码重复,提高可维护性,并使代码库更加模块化。React 鼓励采用组件驱动架构,将职责分离,以便于未来开发和扩展。
以一个简单的电商应用中的产品页面为例。我们希望看到一个待售产品列表。我们可以将页面拆分成更小的、可重用的组件。
成分:
- 产品卡片:显示单个产品及其名称、价格和描述。
- 产品列表:显示产品列表。
// ProductCard.js
import React from 'react';
const ProductCard = ({ product }) => {
return (
<div>
<h2>{product.name}</h2>
<p>Price: ${product.price}</p>
<p>Description: {product.description}</p>
</div>
);
};
export default ProductCard;
// ProductList.js
import React, { useState } from 'react';
import ProductCard from './ProductCard';
const ProductList = () => {
const [products, setProducts] = useState([
{ id: 1, name: 'Product 1', price: 9.99, description: 'Description 1' },
{ id: 2, name: 'Product 2', price: 19.99, description: 'Description 2' },
// ...
]);
return (
<div>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
};
export default ProductList;
在这个例子中,我们可以看到,通过将与产品相关的逻辑分离到组件中,我们可以在组件的功能ProductCard中重用它,从而避免列表页面中每个产品项都编写重复代码。mapProductList
2. SOLID 原则
SOLID 是一个首字母缩写词,代表面向对象设计的五个关键原则:
- 单一责任原则(SRP):每个模块或课程应该只有一个变更理由。
- 开放/封闭原则(OCP):软件实体应该对扩展开放,但对修改关闭。
- 里斯科夫替换原则(LSP):子类型应该能够替换其基本类型,而不会改变程序的正确性。
- 接口隔离原则(ISP):不应强迫客户端依赖它们不使用的接口。
- 依赖倒置原则(DIP):高层模块不应该依赖于底层模块。两者都应该依赖于抽象概念。
让我们来看看如何在 React TypeScript 组件中应用里氏替换原则 (LSP):
// Vehicle.ts
interface Vehicle {
drive(): void;
name: string;
}
// Car.ts
class Car implements Vehicle {
constructor(private name: string) {
this.name = name;
}
drive(): void {
console.log(`Driving a ${this.name}`);
}
}
// Motorcycle.ts
class Motorcycle implements Vehicle {
constructor(private name: string) {
this.name = name;
}
drive(): void {
console.log(`Riding a ${this.name}`);
}
}
// App.tsx
import React from 'react';
import { Vehicle } from './Vehicle';
import Car from './Car';
import Motorcycle from './Motorcycle';
function VehicleComponent(props: { vehicle: Vehicle }) {
props.vehicle.drive();
return <div>Driving a {props.vehicle.name}</div>;
}
const App = () => {
const car = new Car();
const motorcycle = new Motorcycle();
return (
<div>
<VehicleComponent vehicle={car} />
<VehicleComponent vehicle={motorcycle} />
</div>
);
};
export default App;
在这个例子中,我们定义了一个Vehicle接口,它定义了name属性和drive方法。然后我们有两个具体的实现:` CarA` 和 ` MotorcycleB`,它们都实现了该Vehicle接口。
在 App 组件中,我们创建了 `Vehicle`Car和`Vehicle` 的实例Motorcycle,并将它们传递给 `VehicleComponent`。`VehicleComponent`VehicleComponent会调用传入的 `Vehicle` 对象的 `drive` 方法。
LSP 确保我们可以用子类型替换Car接口,Motorcycle而Vehicle不会改变程序的正确性。它能VehicleComponent与 `A`Car和 ` MotorcycleB` 实例无缝协作,证明了子类型可以替换其基类型。
3. KISS(保持简单,笨蛋)
KISS 原则提倡设计和实现上的简洁性。编写易于理解、简单明了且只做好一件事的代码。避免不必要的复杂性和过度设计,因为这从长远来看会导致混乱和维护难题。
我们来看一个Counter组件的两种实现方式。
// Complex Counter
import React, { useState, useEffect } from 'react';
import { debounce } from 'lodash';
const ComplexCounter = () => {
const [count, setCount] = useState(0);
const [clicked, setClicked] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (clicked) {
setCount(prev => prev + 1)
setClicked(false)
}
}, [clicked, setClicked]);
const handleClick = (clicked: boolean) => {
setClicked(!clicked);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => handleClick(clicked)}>Increment</button>
</div>
);
};
export default ComplexCounter;
// Simple Counter
import React, { useState } from 'react';
const SimpleCounter = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
};
export default SimpleCounter;
我们发现这种ComplexCounter实现方式更难理解和维护,也更容易出错。
它引入了一个不必要的状态变量clicked和一个useEffect钩子函数。
这是一个反面教材,展示了如何错误地实现 React 组件。
4. YAGNI(你不需要它)
YAGNI 原则提醒我们,不要基于对未来需求的推测而过早地添加功能。相反,应该专注于正确实现当前所需的功能。这一点在构建以用户为中心的产品时尤为重要。最好不要基于对用户需求的臆测而引入新功能,而应该采用合适的用户研究框架和原型设计方法。
遵循 YAGNI 原则,可以避免不必要的复杂性,缩短开发时间,并保持代码库的精简。
5. 整洁的代码
简洁的代码应具备可读性、易理解性和易维护性。遵循编码规范和最佳实践,使用有意义的变量名,并编写能够自我解释的代码。保持函数和类的简洁和专注,坚持统一的格式,并力求代码库清晰明了。
让我们来看一个简单的实用函数,它用于出于数据安全目的而隐藏用户的部分私人信息。
const hashUsersPrivateInformation = (privateInformation: string): string => {
// Calculate the length of the private info to determine how many characters to mask
const maxLength = privateInformation.length > 4 ? privateInformation.length - 4 : privateInformation.length;
// Create a regular expression pattern to match the desired number of characters
const regexPattern = `.{1,${maxLength}}`;
const regex = new RegExp(regexPattern);
return privateInformation.replace(regex, (match) => '*'.repeat(match.length));
};
我们看到:
- 该函数的名称本身就说明了它的功能。
- 它包含一些对其他开发者有帮助的评论。
- 它的主要目的显而易见。
我们应该尽量使代码结构保持一致。
结论
将这些软件工程原则融入到前端开发工作流程中,可以帮助你编写更高质量的代码,改善与团队成员的协作,并构建健壮且可扩展的应用程序。软件工程不仅仅是编写代码;它还关乎为复杂问题创建可靠、易维护且优雅的解决方案。
文章来源:https://dev.to/gboladetrue/software-engineering-principles-every-frontend-developer-should-know-1ej7