SOLID 原则是面向对象设计的五个基本原则,旨在帮助开发者创建可维护、可扩展和可重用的代码。虽然这些原则起源于面向对象编程,但它们可以有效地应用于 JavaScript
。本文通过JS
中的真实示例解释了每个原则。
原则: 每个类或模块应该只有一个单一的职责,即只负责一项功能。这样可以降低类之间的耦合度,提高代码的可维护性。
例如下面的 react
代码,我们经常看到组件负责太多事情——例如管理UI
和业务逻辑。
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUserData();
}, [userId]);
async function fetchUserData() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
}
return <div>{user?.name}</div>;
}
UserProfile
组件违反了SRP
,因为它同时处理UI
渲染和数据提取。重构之后
// Custom hook for fetching user data
function useUserData(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
async function fetchUserData() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
}
fetchUserData();
}, [userId]);
return user;
}
// UI Component
function UserProfile({ userId }) {
const user = useUserData(userId); // Moved data fetching logic to a hook
return <div>{user?.name}</div>;
}
我们将数据获取逻辑与 UI
分离,让每个部分负责单个任务。
原则: 软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着你应该能够通过扩展现有代码来添加新功能,而不需要修改已有的代码。
例如下面的 JavaScript
代码,有一个运行良好的表单验证功能,但将来可能需要额外的验证逻辑。每当您需要新的验证规则时,您就必须修改此功能,从而违反 OCP
function validate(input) {
if (input.length < 5) {
return 'Input is too short';
}
if (!input.includes('@')) {
return 'Invalid email';
}
return 'Valid input';
}
重构之后,我们可以扩展验证规则,而不需要修改原有的验证函数,遵循OCP
function validate(input, rules) {
return rules.map(rule => rule(input)).find(result => result !== 'Valid') || 'Valid input';
}
const lengthRule = input => input.length >= 5 ? 'Valid' : 'Input is too short';
const emailRule = input => input.includes('@') ? 'Valid' : 'Invalid email';
validate('test@domain.com', [lengthRule, emailRule]);
原则: 子类应该能够替代其父类,并且在程序中可以无缝使用。换句话说,使用子类的对象时,程序的正确性不应受到影响。
例如react
中,当使用高阶组件(HOC
)或有条件地渲染不同组件时,LSP
有助于确保所有组件的行为都可预测
但是下面的代码中,组件不能互换,因为它们使用不同的 props
(onClick
与 href
)。
function Button({ onClick }) {
return <button onClick={onClick}>Click me</button>;
}
function LinkButton({ href }) {
return <a href={href}>Click me</a>;
}
// Inconsistent use of onClick and href makes substitution difficult
<Button onClick={() => {}} />;
<LinkButton href="/home" />;
重构之后,两个组件(Button
和 LinkButton
)在语义上都是正确的,遵守 HTML 可访问性标准,并且在遵循 LSP 时行为一致
function Actionable({ onClick, href, children }) {
if (href) {
return <a href={href}>{children}</a>;
} else {
return <button onClick={onClick}>{children}</button>;
}
}
function Button({ onClick }) {
return <Actionable onClick={onClick}>Click me</Actionable>;
}
function LinkButton({ href }) {
return <Actionable href={href}>Go Home</Actionable>;
}
原则: 不应该强迫一个类依赖于它不使用的方法。应该将大接口分解成小接口,以便实现更精确的依赖关系,降低类之间的耦合度。
例如下面的 React
组件有时会收到不必要的 props
,导致代码紧密耦合且臃肿,
function MultiPurposeComponent({ user, posts, comments }) {
return (
<div>
<UserProfile user={user} />
<UserPosts posts={posts} />
<UserComments comments={comments} />
</div>
);
}
重构之后,通过将组件拆分为更小的组件,每个组件仅依赖于它实际使用的数据。
function UserProfileComponent({ user }) {
return <UserProfile user={user} />;
}
function UserPostsComponent({ posts }) {
return <UserPosts posts={posts} />;
}
function UserCommentsComponent({ comments }) {
return <UserComments comments={comments} />;
}
原则: 高级模块不应该依赖于低级模块。两者都应该依赖于抽象(例如接口)
下面的代码中,UserComponent
与 fetchUser
函数紧密耦合。
function fetchUser(userId) {
return fetch(`/api/users/${userId}`).then(res => res.json());
}
function UserComponent({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
重构之后,通过将 fetchUserData
注入组件,我们可以轻松地交换实现以进行测试或用于不同的用例。
function UserComponent({ userId, fetchUserData }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUserData(userId).then(setUser);
}, [userId, fetchUserData]);
return <div>{user?.name}</div>;
}
// Usage
<UserComponent userId={1} fetchUserData={fetchUser} />;
SOLID
原则对于确保您的代码干净、可维护且可扩展非常有效,即使在 JavaScript
和 TypeScript
框架中也是如此。应用这些原则使开发人员能够编写灵活且可重复使用的代码,这些代码易于随着需求的发展而扩展和重构。通过遵循 SOLID
,您可以使您的代码库变得强大并为未来的增长做好准备
本文翻译的原文地址:Applying SOLID Principles in JavaScript and TypeScript Framework