js 应用的几大原则 及 应用实例

js 应用的几大原则

  1. 单一职责原则(SRP)
    • 解释:一个函数或者一个对象应该只有一个职责。这意味着函数应该只做一件事情并且把它做好。例如,一个函数只负责计算两个数的和,而不应该同时进行和的计算、结果的打印以及数据的存储等多个任务。
    • 示例
// 符合单一职责原则的函数
function addNumbers(a, b) {
    return a + b;
}
// 不符合单一职责原则的函数
function addAndPrint(a, b) {
    let sum = a + b;
    console.log(sum);
    return sum;
}
  1. 开闭原则(OCP)
    • 解释:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。在JavaScript中,这意味着当需要添加新功能时,应该通过扩展代码(如添加新函数、新类)而不是修改现有稳定的代码来实现。
    • 示例:假设我们有一个计算图形面积的函数,最初只支持计算矩形面积。
// 原始的计算矩形面积的函数
function calculateRectangleArea(width, height) {
    return width * height;
}
// 扩展支持计算圆形面积,不修改原函数,通过添加新函数来实现开闭原则
function calculateCircleArea(radius) {
    return Math.PI * radius * radius;
}
  1. 里氏替换原则(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); // 这里用Dog类的实例替换Animal类的实例,程序行为正确
  1. 接口隔离原则(ISP)
    • 解释:在JavaScript中,虽然没有像传统编程语言中严格的接口概念,但可以理解为函数或者对象暴露的方法应该是最小化且与调用者相关的。不要强迫客户端(调用代码)依赖它们不需要的方法。
    • 示例:假设我们有一个包含多种方法的对象,不同的使用者可能只需要其中一部分方法。
let utilityObject = {
    // 计算两个数的和
    add: function (a, b) {
        return a + b;
    },
    // 计算两个数的差
    subtract: function (a, b) {
        return a - b;
    },
    // 将字符串转换为大写
    toUpperCase: function (str) {
        return str.toUpperCase();
    }
};
// 使用者1只需要加法和减法运算
function user1(util) {
    let sum = util.add(3, 5);
    let diff = util.subtract(7, 2);
    console.log(sum, diff);
}
// 使用者2只需要字符串转换方法
function user2(util) {
    let newStr = util.toUpperCase("hello");
    console.log(newStr);
}
user1(utilityObject);
user2(utilityObject);
  1. 依赖倒置原则(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();
  1. 最少知识原则(迪米特法则)
    • 解释:一个对象应该对其他对象有最少的了解。在JavaScript中,这意味着尽量减少对象之间的耦合,一个模块或者对象不应该深入了解其他模块或对象内部的细节,只需要和它直接相关的部分进行交互。
    • 示例
// 假设我们有三个对象:A、B、C
class A {
    constructor() {
        this.b = new B();
    }
    doSomething() {
        // A对象只通过B对象的公共方法来获取结果,不关心B对象内部如何处理
        let result = this.b.getResult();
        console.log(result);
    }
}
class B {
    constructor() {
        this.c = new C();
    }
    getResult() {
        // B对象内部使用C对象的公共方法来获取数据,不暴露C对象的细节给A对象
        return this.c.getData();
    }
}
class C {
    getData() {
        return "Some data";
    }
}
let a = new A();
a.doSomething();
  1. 组合/聚合复用原则(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 中原则应用体现

  1. 单一职责原则(SRP)
    • 原则解释:一个组件或者函数应该只有一个职责。在Vue 3中,reactive函数就体现了这一原则。
    • 源码实例reactive函数主要职责是将一个普通对象转换为响应式对象。其核心代码(简化版)如下:
function reactive(target) {
    if (isReadonly(target)) {
        return target;
    }
    return createReactiveObject(
        target,
        false,
        mutableHandlers,
        mutableCollectionHandlers
    );
}
  • 这个函数专注于完成对象的响应式转换这一个任务。它通过判断对象是否为只读,然后调用createReactiveObject函数来创建响应式对象,不会涉及到例如组件渲染或者事件处理等其他职责。
  1. 开闭原则(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的应用可以方便地扩展功能,符合开闭原则。
  1. 里氏替换原则(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的基础上进行扩展,在渲染流程等行为上保持了一致性,类似于里氏替换原则中子类可以替换父类并且程序行为正确的思想。
  1. 接口隔离原则(ISP)
    • 原则解释:函数或者对象暴露的方法应该是最小化且与调用者相关的。在Vue 3的refreactive接口设计中可以体现。
    • 源码实例ref函数主要用于创建一个包含响应式数据的引用对象,reactive函数用于创建一个响应式对象。
import { ref, reactive } from 'vue';
const count = ref(0);
const state = reactive({
    name: 'John',
    age: 30
});
  • 它们各自提供了简洁且有针对性的功能接口。ref针对基本数据类型的响应式处理,reactive针对对象类型的响应式处理。用户可以根据自己的需求选择合适的接口,而不会暴露过多不必要的功能。这符合接口隔离原则,使得开发者在使用这些功能时只关注自己需要的部分。
  1. 依赖倒置原则(DIP)
    • 原则解释:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。在Vue 3的响应式系统和组件系统的分层设计中有体现。
    • 源码实例:在Vue 3的响应式系统中,组件(高层模块)依赖于响应式数据(抽象概念,通过refreactive等函数创建)。而响应式数据的实现细节(如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属性的更新),而不依赖于响应式系统如何在底层实现数据的监听和更新。这符合依赖倒置原则,使得组件和响应式系统的耦合度降低,便于维护和扩展。
  1. 最少知识原则(迪米特法则)
    • 原则解释:一个对象应该对其他对象有最少的了解。在Vue 3的事件处理机制中有体现。
    • 源码实例:在组件内部进行事件处理时,组件只需要知道如何处理事件,而不需要了解事件是如何在DOM层面或者其他组件内部传播的细节。
<template>
  <button @click="handleClick">Click Me</button>
</template>
<script setup>
function handleClick() {
    console.log('Button clicked');
}
</script>
  • 这个组件中的handleClick函数只关心自己被触发后要执行的逻辑,不需要知道@click事件是如何在浏览器的DOM事件系统中被捕获、冒泡等细节,也不需要了解其他组件是否也在监听这个事件,符合最少知识原则。
  1. 组合/聚合复用原则(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 原则应用体现

  1. 单一职责原则(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组件专注于按钮相关的功能,包括维护自身的状态(点击次数)和处理点击事件。它不会同时负责获取数据、进行数据验证等其他无关职责。
  1. 开闭原则(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组件的情况下,为其添加新的功能(权限验证),实现了对扩展开放,对修改关闭。
  1. 里氏替换原则(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的基础上进行了合理的扩展,符合里氏替换原则。
  1. 接口隔离原则(ISP)
    • 原则解释:函数或者对象暴露的方法应该是最小化且与调用者相关的。
    • 体现示例
      • React的PropsState设计体现了接口隔离原则。一个组件通过Props接收外部传入的数据和方法,并且这些Props通常是与组件功能直接相关的。例如,一个List组件接收一个items数组作为Props
const List = ({ items }) => {
    return (
        <ul>
            {items.map((item, index) => (
                <li key={index}>{item}</li>
            ))}
        </ul>
    );
};
  • 组件只暴露了它需要的接口(这里是接收items作为Props),而不是接收大量无关的数据和方法,使得组件的接口简洁且与调用者(使用List组件的地方)相关,符合接口隔离原则。
  1. 依赖倒置原则(DIP)
    • 原则解释:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
    • 体现示例
      • 在React中,组件(高层模块)和数据获取逻辑(低层模块)的关系可以体现这一原则。例如,使用React - Redux时,组件并不直接依赖于具体的数据获取方式(如Axios请求等细节),而是依赖于Redux提供的抽象(connect函数或者useSelectoruseDispatch钩子)。
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;
  • 这里组件通过useSelectoruseDispatch依赖于Redux的抽象接口来获取和更新数据,而不依赖于具体的数据是如何从服务器获取的细节,符合依赖倒置原则。
  1. 最少知识原则(迪米特法则)
    • 原则解释:一个对象应该对其他对象有最少的了解。
    • 体现示例
      • 在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,不需要了解父组件内部是如何获取数据、如何设置状态等其他细节,符合最少知识原则。
  1. 组合/聚合复用原则(CARP)
    • 原则解释:优先使用组合或聚合关系来实现复用,而不是继承。
    • 体现示例
      • React的Hooks是组合/聚合复用原则的典型体现。例如,useStateuseEffect钩子可以在不同的组件中组合使用,以构建各种功能。
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>
    );
};
  • 这里ComponentAComponentB通过组合useStateuseEffect钩子来构建自己的功能,而不是通过继承一个通用的基类来实现,体现了组合/聚合复用原则。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值