js 应用的几大原则
单一职责原则(SRP)
解释 :一个函数或者一个对象应该只有一个职责。这意味着函数应该只做一件事情并且把它做好。例如,一个函数只负责计算两个数的和,而不应该同时进行和的计算、结果的打印以及数据的存储等多个任务。示例 :
function addNumbers ( a, b ) {
return a + b;
}
function addAndPrint ( a, b ) {
let sum = a + b;
console. log ( sum) ;
return sum;
}
开闭原则(OCP)
解释 :软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。在JavaScript中,这意味着当需要添加新功能时,应该通过扩展代码(如添加新函数、新类)而不是修改现有稳定的代码来实现。示例 :假设我们有一个计算图形面积的函数,最初只支持计算矩形面积。
function calculateRectangleArea ( width, height ) {
return width * height;
}
function calculateCircleArea ( radius ) {
return Math. PI * radius * radius;
}
里氏替换原则(LSP)
解释 :在JavaScript中,这意味着如果一个函数(或方法)接受一个父类型的参数,那么它应该能够正确地处理该父类型的任何子类型。简单来说,子类应该可以替换父类并且程序的行为保持正确。示例 :
class Animal {
makeSound ( ) {
console. log ( "Some generic animal sound" ) ;
}
}
class Dog extends Animal {
makeSound ( ) {
console. log ( "Woof!" ) ;
}
}
function animalSound ( animal ) {
animal. makeSound ( ) ;
}
let dog = new Dog ( ) ;
animalSound ( dog) ;
接口隔离原则(ISP)
解释 :在JavaScript中,虽然没有像传统编程语言中严格的接口概念,但可以理解为函数或者对象暴露的方法应该是最小化且与调用者相关的。不要强迫客户端(调用代码)依赖它们不需要的方法。示例 :假设我们有一个包含多种方法的对象,不同的使用者可能只需要其中一部分方法。
let utilityObject = {
add : function ( a, b ) {
return a + b;
} ,
subtract : function ( a, b ) {
return a - b;
} ,
toUpperCase : function ( str ) {
return str. toUpperCase ( ) ;
}
} ;
function user1 ( util ) {
let sum = util. add ( 3 , 5 ) ;
let diff = util. subtract ( 7 , 2 ) ;
console. log ( sum, diff) ;
}
function user2 ( util ) {
let newStr = util. toUpperCase ( "hello" ) ;
console. log ( newStr) ;
}
user1 ( utilityObject) ;
user2 ( utilityObject) ;
依赖倒置原则(DIP)
解释 :高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。在JavaScript中,这可以通过依赖注入等方式来实现。示例 :
class Logger {
log ( message ) {
throw new Error ( "Abstract method must be implemented" ) ;
}
}
class ConsoleLogger extends Logger {
log ( message ) {
console. log ( message) ;
}
}
class BusinessLogic {
constructor ( logger ) {
this . logger = logger;
}
doSomething ( ) {
let result = "Some operation result" ;
this . logger. log ( result) ;
}
}
let consoleLogger = new ConsoleLogger ( ) ;
let business = new BusinessLogic ( consoleLogger) ;
business. doSomething ( ) ;
最少知识原则(迪米特法则)
解释 :一个对象应该对其他对象有最少的了解。在JavaScript中,这意味着尽量减少对象之间的耦合,一个模块或者对象不应该深入了解其他模块或对象内部的细节,只需要和它直接相关的部分进行交互。示例 :
class A {
constructor ( ) {
this . b = new B ( ) ;
}
doSomething ( ) {
let result = this . b. getResult ( ) ;
console. log ( result) ;
}
}
class B {
constructor ( ) {
this . c = new C ( ) ;
}
getResult ( ) {
return this . c. getData ( ) ;
}
}
class C {
getData ( ) {
return "Some data" ;
}
}
let a = new A ( ) ;
a. doSomething ( ) ;
组合/聚合复用原则(CARP)
解释 :在JavaScript中,优先使用组合或聚合关系来实现复用,而不是继承。组合是指将对象作为另一个对象的属性,聚合是一种特殊的组合,指整体和部分之间的关系。通过组合或聚合,可以使代码更加灵活和可维护。示例 :
class Engine {
start ( ) {
console. log ( "Engine started" ) ;
}
}
class Car {
constructor ( ) {
this . engine = new Engine ( ) ;
}
startCar ( ) {
this . engine. start ( ) ;
console. log ( "Car started" ) ;
}
}
let myCar = new Car ( ) ;
myCar. startCar ( ) ;
vue3 中原则应用体现
单一职责原则(SRP)
原则解释 :一个组件或者函数应该只有一个职责。在Vue 3中,reactive
函数就体现了这一原则。源码实例 :reactive
函数主要职责是将一个普通对象转换为响应式对象。其核心代码(简化版)如下:
function reactive ( target ) {
if ( isReadonly ( target) ) {
return target;
}
return createReactiveObject (
target,
false ,
mutableHandlers,
mutableCollectionHandlers
) ;
}
这个函数专注于完成对象的响应式转换这一个任务。它通过判断对象是否为只读,然后调用createReactiveObject
函数来创建响应式对象,不会涉及到例如组件渲染或者事件处理等其他职责。
开闭原则(OCP)
原则解释 :软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。在Vue 3的插件系统中有所体现。源码实例 :Vue 3允许通过app.use()
方法来扩展应用。例如,定义一个简单的插件:
const myPlugin = {
install ( app ) {
app. directive ( 'my - directive' , {
mounted ( el ) {
console. log ( 'My directive mounted' ) ;
}
} ) ;
}
} ;
const app = createApp ( App) ;
app. use ( myPlugin) ;
这里app
对象(createApp
返回的应用实例)可以通过use
方法添加新的插件,而不需要修改app
对象内部关于应用核心功能(如组件渲染、响应式系统等)的代码。通过这种方式,Vue 3的应用可以方便地扩展功能,符合开闭原则。
里氏替换原则(LSP)
原则解释 :在面向对象编程中,子类应该可以替换父类并且程序的行为保持正确。在Vue 3的组件继承(虽然Vue 3组件更强调组合,但也有继承的影子)中可以看到类似思想。源码实例 :假设我们有一个基础组件BaseComponent
,和一个继承它的组件SubComponent
。
< template>
< div> Base Component</ div>
</ template>
< script>
export default {
name : 'BaseComponent'
}
</ script>
< template>
< BaseComponent> </ BaseComponent>
< div> Sub Component additional content</ div>
</ template>
< script>
import BaseComponent from './BaseComponent.vue' ;
export default {
name : 'SubComponent' ,
components : {
BaseComponent
}
}
</ script>
在这个简单的例子中,SubComponent
使用了BaseComponent
,并且添加了额外的内容。在整个Vue应用的渲染过程中,SubComponent
在包含BaseComponent
的基础上进行扩展,在渲染流程等行为上保持了一致性,类似于里氏替换原则中子类可以替换父类并且程序行为正确的思想。
接口隔离原则(ISP)
原则解释 :函数或者对象暴露的方法应该是最小化且与调用者相关的。在Vue 3的ref
和reactive
接口设计中可以体现。源码实例 :ref
函数主要用于创建一个包含响应式数据的引用对象,reactive
函数用于创建一个响应式对象。
import { ref, reactive } from 'vue' ;
const count = ref ( 0 ) ;
const state = reactive ( {
name : 'John' ,
age : 30
} ) ;
它们各自提供了简洁且有针对性的功能接口。ref
针对基本数据类型的响应式处理,reactive
针对对象类型的响应式处理。用户可以根据自己的需求选择合适的接口,而不会暴露过多不必要的功能。这符合接口隔离原则,使得开发者在使用这些功能时只关注自己需要的部分。
依赖倒置原则(DIP)
原则解释 :高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。在Vue 3的响应式系统和组件系统的分层设计中有体现。源码实例 :在Vue 3的响应式系统中,组件(高层模块)依赖于响应式数据(抽象概念,通过ref
和reactive
等函数创建)。而响应式数据的实现细节(如effect
的收集和触发、proxy
的具体操作等)被封装起来。
< script setup>
import { ref } from 'vue' ;
const count = ref ( 0 ) ;
function increment ( ) {
count. value++ ;
}
< / script>
< template>
< button @click= "increment" > { { count } } < / button>
< / template>
这里组件只依赖于响应式数据的抽象操作(如ref
函数创建的引用和其value
属性的更新),而不依赖于响应式系统如何在底层实现数据的监听和更新。这符合依赖倒置原则,使得组件和响应式系统的耦合度降低,便于维护和扩展。
最少知识原则(迪米特法则)
原则解释 :一个对象应该对其他对象有最少的了解。在Vue 3的事件处理机制中有体现。源码实例 :在组件内部进行事件处理时,组件只需要知道如何处理事件,而不需要了解事件是如何在DOM层面或者其他组件内部传播的细节。
< template>
< button @click = " handleClick" > Click Me</ button>
</ template>
< script setup >
function handleClick ( ) {
console. log ( 'Button clicked' ) ;
}
</ script>
这个组件中的handleClick
函数只关心自己被触发后要执行的逻辑,不需要知道@click
事件是如何在浏览器的DOM事件系统中被捕获、冒泡等细节,也不需要了解其他组件是否也在监听这个事件,符合最少知识原则。
组合/聚合复用原则(CARP)
原则解释 :优先使用组合或聚合关系来实现复用,而不是继承。在Vue 3的Composition API
中体现明显。源码实例 :
import { ref, onMounted } from 'vue' ;
export default {
setup ( ) {
const count = ref ( 0 ) ;
onMounted ( ( ) => {
console. log ( 'Component mounted' ) ;
} ) ;
return {
count
} ;
}
}
这里通过组合ref
函数和onMounted
生命周期钩子来构建组件的逻辑,而不是通过继承一个复杂的基类来获取这些功能。不同的组件可以根据自己的需求灵活地组合各种函数和钩子,实现功能的复用,符合组合/聚合复用原则。
react 原则应用体现
单一职责原则(SRP)
原则解释 :一个组件或者函数应该只有一个主要职责。体现示例 :
在React中,组件的设计很好地体现了这一原则。例如,一个简单的Button
组件,其主要职责就是渲染一个按钮并处理按钮的点击事件。
import React, { useState } from 'react';
const Button = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<button onClick={handleClick}>
Click me! ({count})
</button>
);
};
export default Button;
这个Button
组件专注于按钮相关的功能,包括维护自身的状态(点击次数)和处理点击事件。它不会同时负责获取数据、进行数据验证等其他无关职责。
开闭原则(OCP)
原则解释 :软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。体现示例 :
React的高阶组件(HOC)是开闭原则的一个很好体现。例如,有一个基本的组件用于显示用户信息。
const UserInfo = ({ user }) => {
return (
<div>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
</div>
);
};
现在我们想要为这个组件添加权限验证功能,而不是修改UserInfo
组件本身,我们可以创建一个高阶组件。
const withAuthorization = (WrappedComponent) => {
return (props) => {
const isAuthorized = checkAuthorization();// 假设这是一个检查权限的函数
if (isAuthorized) {
return <WrappedComponent {...props} />;
} else {
return <div>Unauthorized</div>;
}
};
};
const AuthorizedUserInfo = withAuthorization(UserInfo);
通过高阶组件,我们可以在不修改UserInfo
组件的情况下,为其添加新的功能(权限验证),实现了对扩展开放,对修改关闭。
里氏替换原则(LSP)
原则解释 :在面向对象编程中,子类应该可以替换父类并且程序的行为保持正确。在React中,组件的继承(虽然不常用,但存在这种情况)和组件的替换可以体现这一原则。体现示例 :
假设有一个基类组件BaseComponent
,它有一个基本的渲染方法。
class BaseComponent extends React.Component {
render() {
return <div>Base Component</div>;
}
}
然后有一个子类组件SubComponent
继承自BaseComponent
,并对渲染方法进行扩展。
class SubComponent extends BaseComponent {
render() {
return (
<>
{super.render()}
<p>Additional content</p>
</>
);
}
}
当在应用中使用SubComponent
替换BaseComponent
时,程序的行为(如渲染流程)能够正确地进行,因为SubComponent
在继承BaseComponent
的基础上进行了合理的扩展,符合里氏替换原则。
接口隔离原则(ISP)
原则解释 :函数或者对象暴露的方法应该是最小化且与调用者相关的。体现示例 :
React的Props
和State
设计体现了接口隔离原则。一个组件通过Props
接收外部传入的数据和方法,并且这些Props
通常是与组件功能直接相关的。例如,一个List
组件接收一个items
数组作为Props
。
const List = ({ items }) => {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
};
组件只暴露了它需要的接口(这里是接收items
作为Props
),而不是接收大量无关的数据和方法,使得组件的接口简洁且与调用者(使用List
组件的地方)相关,符合接口隔离原则。
依赖倒置原则(DIP)
原则解释 :高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。体现示例 :
在React中,组件(高层模块)和数据获取逻辑(低层模块)的关系可以体现这一原则。例如,使用React - Redux
时,组件并不直接依赖于具体的数据获取方式(如Axios
请求等细节),而是依赖于Redux
提供的抽象(connect
函数或者useSelector
和useDispatch
钩子)。
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchDataAction } from './actions';
const MyComponent = () => {
const data = useSelector((state) => state.data);
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(fetchDataAction());
}, [dispatch]);
return (
<div>
{data.map((item) => (
<p>{item}</p>
))}
</div>
);
};
export default MyComponent;
这里组件通过useSelector
和useDispatch
依赖于Redux
的抽象接口来获取和更新数据,而不依赖于具体的数据是如何从服务器获取的细节,符合依赖倒置原则。
最少知识原则(迪米特法则)
原则解释 :一个对象应该对其他对象有最少的了解。体现示例 :
在React组件的通信中,例如,一个子组件只需要知道从父组件通过Props
接收的数据和方法,不需要了解父组件内部是如何获取这些数据或者其他无关的操作。
const ParentComponent = () => {
const [data, setData] = useState([]);
const handleDataFetch = () => {
// 假设这是一个获取数据的复杂操作
const newData = fetchData();
setData(newData);
};
return (
<>
<ChildComponent data={data} onFetch={handleDataFetch} />
</>
);
};
const ChildComponent = ({ data, onFetch }) => {
return (
<>
<button onClick={onFetch}>Fetch Data</button>
<ul>
{data.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</>
);
};
子组件ChildComponent
只需要知道从父组件接收的数据data
和方法onFetch
,不需要了解父组件内部是如何获取数据、如何设置状态等其他细节,符合最少知识原则。
组合/聚合复用原则(CARP)
原则解释 :优先使用组合或聚合关系来实现复用,而不是继承。体现示例 :
React的Hooks
是组合/聚合复用原则的典型体现。例如,useState
和useEffect
钩子可以在不同的组件中组合使用,以构建各种功能。
import React, { useState, useEffect } from 'react';
const ComponentA = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('ComponentA mounted');
}, []);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>Count in ComponentA: {count}</p>
<button onClick={handleClick}>Increment in ComponentA</button>
</div>
);
};
const ComponentB = () => {
const [text, setText] = useState('');
useEffect(() => {
console.log('ComponentB mounted');
}, []);
const handleChange = (e) => {
setText(e.target.value);
};
return (
<div>
<input type="text" value={text} onChange={handleChange} />
<p>Text in ComponentB: {text}</p>
</div>
);
};
这里ComponentA
和ComponentB
通过组合useState
和useEffect
钩子来构建自己的功能,而不是通过继承一个通用的基类来实现,体现了组合/聚合复用原则。