SOLID 原则在前端的应用
November 15, 2024 by
简介 SOLID
原则在前端的应用
SOLID
用于面向对象编程 (OOP), 旨在解决软件开发中的复杂性和维护问题
- 单一职责
Single Responsibility Principle - SRP
- 开闭原则
Open/Closed Principle - OCP
- 里氏替换
Liskov Substitution Principle - LSP
- 接口隔离
Interface Segregation Principle - ISP
- 依赖反转
Dependency Inversion Principle - DIP
单一职责原则
一个类或模块应只有一个发生变化的原因, 仅负责一项特定功能
错误示例: (可增加)
- 一个组件承担了太多责任, 既负责 UI 渲染, 又负责业务逻辑和数据请求
修改方式: (可增加)
- 复杂 UI 组件拆分成多个小组件
- 使用自定义
hook
拆分业务逻辑
优点: (可增加)
- 职责明确
- 提高复用性
- 测试更加方便
- 代码扩展更加灵活
开闭原则
软件实体应能在不修改原有代码的情况下扩展其行为, 即 对扩展开放, 对修改封闭
举例: (可增加)
- 表单验证
-
错误 (验证逻辑集中在函数内, 不可扩展, 如需修改必须改动
validateForm
函数)function validateForm<T>(values: T) { const errors: Partial<Record<keyof T, string>> = {} if (!values.name) { errors.name = 'Name is required' } if (!values.email) { errors.email = 'Email is required' } else if (!isEmail(values.email)) { errors.email = 'Email is invalid' } return errors }
-
正确 (将验证逻辑抽象为验证器, 同时传入数据和验证器进行表单校验)
abstract class Validator { abstract validate(value: unknown): string | null } class NameValidator extends Validator { validate(value: string | undefined) { return value ? null : 'Name is required' } } class EmailValidator extends Validator { validate(value: string | undefined) { if (!value) { return 'Email is required' } return isEmail(value) ? null : 'Email is invalid' } } function validateForm<T>(values: T, validators: Record<keyof T, Validator>) { const errors: Partial<Record<keyof T, string>> = {} for (const key in validators) { const error = validators[key].validate(values[key]) if (error) { errors[key] = error } } return errors } validateForm({ name: '', email: 'test' }, { name: new NameValidator(), email: new EmailValidator() })
-
里氏替换原则
子类必须能够替换基类, 派生类或组件应该能够替换基类, 而不会影响程序的正确性
举例: (可增加)
Button
行为不一致-
错误 (
LinkButton
无法替换Button
, 因为其没有onClick
属性)function Button({ onClick }) { return <button onClick={onClick}>Click me</button> } function LinkButton({ href }) { return <a href={href}>Click me</a> } <Button onClick={() => {}} /> <LinkButton href="#" />
-
正确 (使用
Clickable
组件对两者进行封装)function Clickable({ children, onClick }) { return <div onClick={onClick}>{children}</div> } function Button({ onClick }) { return ( <Clickable onClick={onClick}> <button>Click me</button> </Clickable> ) } function LinkButton({ href }) { return ( <Clickable onClick={() => (window.location.href = href)}> <a href={href}>{children}</a> </Clickable> ) } <Clickable onClick={() => {}}>Click me</Clickable> <Link href="#">Click me</Link>
-
接口隔离原则
客户端不应该被迫依赖他们不使用的接口这个应该是我用的最多的, 因为不想写没用的代码, 也不想引入没用的代码
(原文例子感觉一般, 可增加)
依赖倒置原则
高级模块不应该依赖于低级模块, 两者都应该依赖于抽象 (例如接口)
举例: (可增加)
fetchData
-
错误 (直接在组件内部调用
fetchData
, 无法复用)function fetchData() { return fetch('https://api.example.com/data') } function App() { const [data, setData] = useState() useEffect(() => { fetchData().then((res) => setData(res)) }, []) return <div>{data}</div> }
-
正确 (将
fetchData
抽象为fetcher
, 传入fetcher
进行数据请求)interface Fetcher { fetch(url: string): Promise<any> } class FetcherImpl implements Fetcher { fetch(url: string) { return fetch(url) } } function App({ fetcher }: { fetcher: Fetcher }) { const [data, setData] = useState() useEffect(() => { fetcher.fetch('https://api.example.com/data').then((res) => setData(res)) }, []) return <div>{data}</div> } <App fetcher={new FetcherImpl()} />
-