在 JavaScript 和 TypeScript 中應用 SOLID 原則
SOLID 原则是编写干净、可扩展和可维护的软件的基础。虽然这些原则起源于面向对象编程(OOP),但它们也适用于 JavaScript(JS)和 TypeScript(TS)中的前端框架,例如 React 和 Angular。本文将通过 JS 和 TS 中的实际示例来解释每个原则。
zh: (此处省略)
1. 单一职责原则(SRP)原则: 一个类或模块应该只有一个变更的理由。这意味着它应该只为一个功能负责。
- JavaScript (React) 例子:
在 React 中,我们常常会遇到承担太多职责的组件——比如这样的情况,这些组件既要管理用户界面,又要处理业务逻辑。
反模式:
这是一种不好的实践方式。
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 组件不仅负责UI渲染,还负责数据加载,因此违反了SRP。
重新整理:
// 这是一个自定义钩子,用于获取用户数据
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 组件
function UserProfile({ userId }) {
const user = useUserData(userId); // 将数据获取逻辑移到钩子
return <div>{user?.name}</div>;
}
全屏显示 退出全屏
通过使用一个自定义钩子(如useUserData),我们将数据获取逻辑和UI分离,使每一部分只负责一项工作。
- TypeScript(Angular)示例:
在 Angular 这个框架中,服务和组件有时候可能会因为承担多种职责而变得杂乱。
反模式行为:
反模式,也称为不良模式或常见坏习惯,是指在软件开发或其他领域中反复出现的低效或有害的做法。
@Injectable()
export class UserService {
constructor(private http: HttpClient) {}
getUser(userId: string) {
return this.http.get(`/api/users/${userId}`);
}
updateUserProfile(userId: string, data: any) {
// 更新资料并处理通知事宜
return this.http.put(`/api/users/${userId}`, data).subscribe(() => {
console.log('已更新用户');
alert('资料更新成功了');
});
}
}
进入全屏 退出全屏
这个 UserService 负责多种任务:取回、修改和处理通知信息。
重写:
@Injectable()
export class UserService {
constructor(private http: HttpClient) {}
getUser(userId: string) {
return this.http.get(`/api/users/${userId}`);
}
updateUserProfile(userId: string, data: 数据) {
return this.http.put(`/api/users/${userId}`, data);
}
}
// 分离的通知服务
@Injectable()
export class NotificationService {
提醒(message: string) {
alert(message);
}
}
全屏模式退出全屏
通过将通知处理拆分到一个单独的服务中(通知服务),我们确保每个类只负责一件事。
zh: zh: ……
2. 开闭原则(OCP,开放封闭原则)原则: 软件模块应该易于扩展但难于修改。这意味着你可以扩展模块的功能而不改动其源代码。
- JavaScript (React) 例子:
你可能有一个运行良好的表单验证函数,但未来可能需要增加额外的验证逻辑。
常见错误模式,
function validate(input) {
if (input.length < 5) {
return '输入太短了';
}
if (!input.includes('@')) {
return '这不是有效的电子邮件地址';
}
return '输入有效';
}
开启全屏 关闭全屏
每次需要新增一个验证规则时,都得修改这个函数的内容,这样就违反了开闭原则(OCP)。
重构
function validate(input, rules) {
return rules.map(rule => rule(input)).find(result => result !== 'Valid') || '有效输入';
}
const lengthRule = input => input.length >= 5 ? 'Valid' : '输入太短了';
const emailRule = input => input.includes('@') ? 'Valid' : '无效的电子邮件';
validate('[email protected]', [lengthRule, emailRule]);
全屏模式 退出全屏
现在,我们可以扩展验证规则的范围而无需修改原始的 validate 函数,从而遵循 OCP(开放封闭原则)。
- TypeScript(Angular)示例:
在 Angular 中,服务和组件应当被设计成允许添加新功能而不改动核心逻辑。
不良模式:
export class NotificationService {
send(type: 'email' | 'sms', message: string) {
if (type === 'email') {
// 发送电子邮件
} else if (type === 'sms') {
// 发送短信
}
}
}
切换到全屏 / 退出全屏
此服务不符合OCP原则,因为每次添加新的通知类型时(例如,推送通知,),你就需要去修改发送方法。
优化:
interface 通知接口 {
发送(message: string): void;
}
@Injectable()
export class 电子邮件通知 implements 通知接口 {
发送(message: string) {
// 发送邮件的逻辑
}
}
@Injectable()
export class 短信通知 implements 通知接口 {
发送(message: string) {
// 发送短信的逻辑
}
}
@Injectable()
export class 通知服务类 {
constructor(private 通知s: 通知接口[]) {}
发送通知(message: string) {
this.通知s.forEach(n => n.发送(message));
}
}
点击全屏看 点击退出全屏
现在,添加新的通知类型只需创建新的类即可,而无需修改NotificationService(NotificationService)。
zh: (内容省略)
3. Liskov 替换原则 (LSP)原则: 子类必须可以替代基类。派生类或其它组件可以用来替换基类而不会影响程序的正确性。
- JavaScript (React) 例子:
当我们使用高阶组件(HOCs)或根据条件渲染不同的组件时,LSP可以帮助确保所有组件的行为都更加可预测。
反模式:
function Button({ onClick }) {
return <button onClick={onClick}>点击我</button>;
}
function LinkButton({ href }) {
return <a href={href}>点击我</a>;
}
<Button onClick={() => {}} />;
<LinkButton href="/home" />;
点击全屏模式,再次点击退出全屏。
在这里,Button 和 LinkButton 不一致。一个使用了 onClick,另一个使用了 href,这使得替换变得困难。
改写:
function Clickable({ children, onClick }) {
return <div onClick={onClick}>{children}</div>;
}
function Button({ onClick }) {
return <Clickable onClick={onClick}>
<button>点我</button>
</Clickable>;
}
function LinkButton({ href }) {
return <Clickable onClick={() => window.location.href = href}>
<a href={href}>点我</a>
</Clickable>;
}
切换到全屏模式 切换回正常模式
目前,Button 和 LinkButton 遵循 LSP,它们的行为相似。
- TypeScript(Angular中的示例):
反模式:
class Rectangle {
constructor(protected width: number, protected height: number) {}
area() {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(size: number) {
super(size, size);
}
setWidth(width: number) {
this.width = width;
this.height = width; // 这样做违反了Liskov替换原则
}
}
进入全屏,退出全屏
在 Square 中修改 setWidth 方法会违背 LSP 原则,因为 Square 的行为与 Rectangle 不同,这不符合 LSP 原则。
重构代码:
</TRANSLATION>
class Shape {
面积(): number {
抛出新错误('方法未实现');
}
}
class 矩形 extends Shape {
constructor(private 宽: number, private 高: number) {
超类();
}
面积() {
返回 this.宽 * this.高;
}
}
class 正方形 extends Shape {
constructor(private 边长: number) {
超类();
}
面积() {
返回 this.边长 * this.边长;
}
}
全屏模式(按X退出)
这样一来,Square和Rectangle可以互换,这样不会违反LSP。
zh: (省略)
4. 接口隔离原则 (ISP):准则: 客户不应被迫依赖他们未使用的接口。
- JavaScript (React) 例子:
React组件可能会收到多余的props,导致代码紧密耦合且臃肿。
反模式:
function 多功能组件({ user, posts, comments }) {
return (
<div>
<用户资料 user={user} />
<用户帖子 posts={posts} />
<用户评论 comments={comments} />
</div>
);
}
进入全屏 退出全屏
在这里,组件依赖于多个属性,哪怕它有时候并不需要用到这些属性。
重构代码:
function 用户资料组件({ 用户 }) {
return <UserProfile 用户={用户} />;
}
function 用户帖子组件({ 帖子 }) {
return <UserPosts 帖子={帖子} />;
}
function 用户评论组件({ 评论 }) {
return <UserComments 评论={评论} />;
}
进入全屏 退出全屏
通过这种方式,将组件拆分成更小的部分,每个拆分后的部分仅依赖实际需要的数据。
在Angular中使用TypeScript的示例
常见误区:
interface 工人 {
工作(): void;
用餐(): void;
}
class 人类工人 implements 工人 {
工作() {
console.log('工作');
}
用餐() {
console.log('用餐');
}
}
class 机器人工人 implements 工人 {
工作() {
console.log('工作');
}
用餐() {
throw new Error('机器人不进食'); // 违背了ISP原则
}
}
全屏, 退出全屏
在这里,RobotWorker不得不实现一个无关紧要的eat方法。
重构:
interface Worker {
work(): void;
}
interface Eater {
eat(): void;
}
class HumanWorker implements Worker, Eater {
work() {
console.log('在工作');
}
eat() {
console.log('在吃饭');
}
}
class RobotWorker implements Worker {
work() {
console.log('在工作');
}
}
进入全屏模式,退出全屏模式
通过将Worker和Eater接口分离,我们确保客户端仅依赖于所需的功能。
zh: zh: (此处省略)
第五 依赖倒置原则(DIP):原则: 高层模块不应该依赖于底层模块,两者都应依赖于抽象概念,如接口。
- JavaScript (React) 例子:
反模式(Anti-pattern):
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>;
}
切换到全屏 退出全屏
在这里,UserComponent 和 fetchUser 函数紧密结合。
改写:
function UserComponent({ userId, fetchUserData }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUserData(userId).then(setUser);
}, [userId, fetchUserData]);
return <div>{user?.name}</div>;
}
// 法
<UserComponent userId={1} fetchUserData={fetchUser} />;
进入全屏 退出全屏
通过在组件中引入 fetchUserData,我们可以在测试或不同场景下轻松替换其实现。
- TypeScript(Angular)示例:
常见陷阱: 在编程或设计中,反模式指那些看似合理但实则会导致问题的常见做法:
@Injectable()
export class UserService {
constructor(private http: HttpClient) {}
getUser(userId: string) {
return this.http.get(`/api/users/${userId}`);
}
}
@Injectable()
export class UserComponent {
constructor(private userService: UserService) {}
loadUser(userId: string) {
this.userService.getUser(userId).subscribe(user => console.log(user));
}
}
点击这里切换到全屏 点击这里退出全屏
UserComponent 与 UserService 紧密耦合,这使得更换 UserService 变得很麻烦。
代码重构:
interface 用户信息服务 {
取用户(userId: string): Observable<用户>;
}
@Injectable()
export class Api用户信息服务 implements 用户信息服务 {
constructor(private http: HttpClient) {}
取用户(userId: string) {
return this.http.get<用户>(`/api/users/${userId}`);
}
}
@Injectable()
export class 用户组件类 {
constructor(private service: 用户信息服务) {}
取用户(userId: string) {
this.service.get(userId).subscribe(用户信息 => console.log(用户信息));
}
}
点击全屏显示 点击退出全屏
通过依赖UserService接口,UserComponent 现在从具体的 ApiUserService 实现中解耦。
接下来做什么?
不论你是在前端使用 React 或 Angular 等框架,还是在后端使用 Node.js 技术,SOLID 原则都作为指南,确保你的软件架构保持稳固。
将这些原则融入您的项目中:
- 定期实践: 重构现有代码库以应用SOLID原则,并确保代码符合这些原则。
- 与团队合作: 通过代码审查和关于干净架构的讨论来促进最佳实践。
-
保持好奇: SOLID原则只是开始。 探索其他架构模式,如MVC、MVVM或CQRS,这些模式基于这些基本原则,可以帮助你进一步优化设计。
-
- *
SOLID 原则是确保你的代码干净、易于维护和可扩展,非常有效的方法,即使是在 JavaScript 和 TypeScript 框架(如 React 和 Angular)中也同样适用。应用这些原则可以使开发人员编写灵活且可重用的代码,这样的代码易于扩展和重构,以适应不断变化的需求。通过遵循 SOLID,让你的代码库既稳健又为未来的增长做好准备。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章