前端进阶| 深入学习面向对象设计原则

引言

面向对象编程(Object-Oriented ProgrammingOOP)是一种常用的编程范式,它通过将数据和与之相关的操作封装在一起,提供了一种更有组织和易于理解的方式来构建应用程序。在JavaScript中,我们可以使用面向对象的设计原则来创建高质量、可维护和可扩展的代码。

本文我们将介绍一些重要的面向对象设计原则,并解释它们在JavaScript中的应用。这些原则包括但不限于单一职责原则、开放封闭原则、里式替换原则和依赖倒置原则。

通过遵循这些原则,我们可以达到解耦和模块化的目标,使代码更具可读性、可维护性和可扩展性。我们将深入探讨每个原则的定义、核心思想和在实际项目中的应用。

1. 单一职责原则(Single Responsibility Principle,SRP)

概念

单一职责原则指出一个类应该只有一个引起变化的原因。换句话说,一个类应该只负责一个单一的职责。

案例分析

class Logger {constructor() {this.logs = [];}logError(message) {this.logs.push(`[Error] ${new Date().toISOString()}: ${message}`);this.saveLogsToFile();this.sendEmailNotification();}logInfo(message) {this.logs.push(`[Info] ${new Date().toISOString()}: ${message}`);this.saveLogsToFile();}saveLogsToFile() {// 将日志保存到文件中的实现}sendEmailNotification() {// 发送错误通知邮件的实现}
}

在上面的示例中,我们有一个Logger类用于日志记录。然而,根据SRP原则,一个类应该只负责一个单一的职责。在这个例子中,Logger类同时负责了记录日志、保存日志到文件和发送邮件通知。这违反了SRP原则。

为了遵守SRP原则,我们可以将这些职责拆分成不同的类:

class Logger {constructor() {this.logs = [];}logError(message) {this.logs.push(`[Error] ${new Date().toISOString()}: ${message}`);}logInfo(message) {this.logs.push(`[Info] ${new Date().toISOString()}: ${message}`);}
}class FileSaver {saveLogsToFile(logger) {// 将日志保存到文件中的实现}
}class EmailNotifier {sendEmailNotification(logger) {// 发送错误通知邮件的实现}
}

在重构后的代码中,我们将日志记录的职责交给了Logger类,将保存日志到文件的职责交给了FileSaver类,将发送邮件通知的职责交给了EmailNotifier类。每个类都只负责一个单一的职责,符合SRP原则。

总结

通过遵守SRP原则,我们能够提高代码的可维护性和可扩展性。当需求发生变化时,我们只需要修改相关的类,而不需要影响其他职责,从而降低了代码之间的依赖性。这样的设计能够使代码更加灵活、可测试和易于扩展。

2. 开放封闭原则(Open-Closed Principle,OCP)

概念

开放封闭原则指出软件中的对象(类、模块、函数等)应该对扩展开放,对修改封闭。换句话说,当需要增加新功能时,应该通过扩展现有代码来实现,而不是修改已有代码。

案例分析

// 抽象的运算类
class Operation {calculate() {throw new Error('Method not implemented.');}
}// 加法运算类
class Addition extends Operation {constructor(a, b) {super();this.a = a;this.b = b;}calculate() {return this.a + this.b;}
}// 减法运算类
class Subtraction extends Operation {constructor(a, b) {super();this.a = a;this.b = b;}calculate() {return this.a - this.b;}
}// 计算器类
class Calculator {constructor() {this.operations = [];}addOperation(operation) {this.operations.push(operation);}getTotal() {let total = 0;this.operations.forEach(operation => {total += operation.calculate();});return total;}
}const calculator = new Calculator();
calculator.addOperation(new Addition(2, 3));
calculator.addOperation(new Subtraction(5, 1));
calculator.addOperation(new Addition(4, 6));
console.log(`Total: ${calculator.getTotal()}`); // Output: Total: 19

在上面的示例中,我们有一个抽象的运算类Operation,以及两个具体的运算类AdditionSubtraction。根据OCP原则,我们应该通过扩展现有代码来增加新的运算功能,而不是修改现有代码。

Calculator类是一个计算器类,它可以执行多个运算并累加结果。在Calculator类的getTotal方法中,我们使用了多态性,通过调用运算对象的calculate方法来计算运算结果。

通过使用OCP原则,当需要添加新的运算时,我们只需要创建一个新的运算类继承自Operation,实现自己的计算逻辑,并将其添加到计算器对象中,而不需要修改原有的代码。这种方式能够保持原有代码的稳定性和可维护性,同时也使得代码更灵活、可扩展。

总结

OCP原则提倡使用抽象和多态来实现可扩展性和可维护性。通过使用继承、多态等面向对象的特性,我们可以以开放的方式添加新功能,同时封闭对原有代码的修改,从而提高代码的可复用性和可扩展性。

3. 里氏替换原则(Liskov Substitution Principle,LSP)

概念

里氏替换原则指出子类对象应该能够替代其父类对象出现在程序中的任何地方,而不引起错误或异常。

案例分析

class Rectangle {constructor(width, height) {this.width = width;this.height = height;}setWidth(width) {this.width = width;}setHeight(height) {this.height = height;}getArea() {return this.width * this.height;}
}class Square extends Rectangle {constructor(sideLength) {super(sideLength, sideLength);}setWidth(width) {this.width = width;this.height = width;}setHeight(height) {this.width = height;this.height = height;}
}function printArea(rectangle) {rectangle.setWidth(4);rectangle.setHeight(5);console.log(`Area: ${rectangle.getArea()}`);
}const rectangle = new Rectangle(2, 3);
const square = new Square(5);printArea(rectangle); // Output: Area: 20
printArea(square); // Output: Area: 25

在上面的示例中,我们有一个父类Rectangle和一个子类Square。根据LSP原则,我们应该能够用子类对象替代父类对象出现在程序中的任何地方,而不引起错误或异常。

Rectangle类中,我们有一个setWidth方法和一个setHeight方法来分别设置宽度和高度,以及一个getArea方法来计算矩形的面积。

Square类继承自Rectangle类,但是它重写了父类的setWidthsetHeight方法,使得无论设置宽度还是高度,都会同时改变宽度和高度,以保证正方形的特性。

printArea函数中,我们接受一个Rectangle对象,并设置宽度和高度为4和5,然后打印结果。根据LSP原则,我们可以在函数中使用子类的对象Square作为参数,因为子类应该能够替代父类而不引起错误。

总结

通过遵守LSP原则,我们可以提高代码的可扩展性和可维护性。如果我们遇到新的子类,我们可以放心地将其用作父类的替代,而不必担心引发错误或异常。

4. 接口隔离原则(Interface Segregation Principle,ISP)

概念

接口隔离原则指出客户端不应该强制依赖它不需要的接口,应该将接口分离成更小和更具体的接口。这样做可以减少不必要的依赖关系,提高代码的灵活性和可扩展性。

案例分析

假设我们有一个社交媒体应用程序,其中有不同类型的用户,包括普通用户和管理员用户。我们需要为这些不同的用户提供不同的功能,例如登录、发布文章和发送消息。根据接口隔离原则,我们可以将这些功能拆分成更小和更具的接口,以便客户端只需依赖它们所需的接口。

// 不遵循接口隔离原则的代码示例
class User {constructor(username, password) {this.username = username;this.password = password;}login() {// 用户登录逻辑}logout() {// 用户退出逻辑}publishArticle() {// 发布文章的逻辑}sendMessage() {// 发送消息的逻辑}
}// 遵循接口隔离原则的代码示例
class User {constructor(username, password) {this.username = username;this.password = password;}login() {// 用户登录逻辑}logout() {// 用户退出逻辑}
}class ArticlePublisher {publishArticle() {// 发布文章的逻辑}
}class Messenger {sendMessage() {// 发送消息的逻辑}
}

在上面的示例中,我们将功能拆分为三个接口:UserArticlePublisherMessenger。普通用户只需要依赖User接口,而管理员用户可以依赖UserArticlePublisherMessenger接口。

总结

使用接口隔离原则,我们可以避免普通用户依赖不需要的接口方法,从而减少不必要的依赖关系提高代码的灵活性。如果以后需要添加新的功能接口,也可以根据需要扩展相应的接口而不会影响到其他接口的实现和客户端的代码。

5. 依赖倒置原则(Dependency Inversion Principle,DIP)

概念

依赖倒置原则指出依赖于抽象而不是具体的实现。通过使用依赖注入等技术,我们可以将依赖关系从高层模块转移到低层模块,减少模块间的依赖,提高代码的可测试性、可维护性和可扩展性。

案例分析

假设我们有一个电子商务网站,有一个购物车模块和一个支付模块。购物车模块负责管理用户的购物车,而支付模块负责处理用户的支付请求。根据依赖倒置原则,我们应该将抽象的接口作为依赖,而不是具体的实现类,以实现模块之间的解耦。

// 不遵循依赖倒置原则的代码示例
class ShoppingCart {constructor() {this.items = [];}add(item) {this.items.push(item);}calculateTotal() {let total = 0;for (let item of this.items) {total += item.price;}return total;}
}class PaymentProcessor {processPayment(amount) {// 处理支付逻辑}
}class ShoppingCartApp {constructor() {this.cart = new ShoppingCart();this.paymentProcessor = new PaymentProcessor();}checkout() {const total = this.cart.calculateTotal();this.paymentProcessor.processPayment(total);}
}// 遵循依赖倒置原则的代码示例
class ShoppingCart {constructor() {this.items = [];}add(item) {this.items.push(item);}calculateTotal() {let total = 0;for (let item of this.items) {total += item.price;}return total;}
}class PaymentProcessor {processPayment(amount) {// 处理支付逻辑}
}class ShoppingCartApp {constructor(cart, paymentProcessor) {this.cart = cart;this.paymentProcessor = paymentProcessor;}checkout() {const total = this.cart.calculateTotal();this.paymentProcessor.processPayment(total);}
}// 使用依赖注入来组装类的依赖关系
const cart = new ShoppingCart();
const paymentProcessor = new PaymentProcessor();
const app = new ShoppingCartApp(cart, paymentProcessor);

在上面的示例中,我们将购物车模块和支付模块的具体实现与应用程序的逻辑解耦,并使用依赖注入的方式在应用程序的构造函数中传入依赖的抽象接口。

总结

使用依赖倒置原则,我们可以将依赖关系从高层模块转移到低层模块,从而提高代码的可测试性、可维护性和可扩展性。通过依赖注入,我们可以在运行时动态地传入不同的实现类,使得应用程序更加灵活和可配置。另外,依赖倒置原则还可以降低模块之间的耦合度,提高代码的重用性和可理解性。

6. 迪米特法则(Law of Demeter,LoD)

概念

迪米特法则也称为最少知识原则(Least Knowledge Principle,LKP)。它强调了模块(类、对象)之间应该尽量减少直接的交互,只和自己的密友交流。

迪米特法则的目标是减少对象之间的耦合,提高系统的可维护性和可复用性。当一个对象只与少数几个密友(直接的组件、关联的类等)交互时,它的设计更加简洁、清晰,并且对外部的改变更具有抵抗力。

案例分析

class Teacher {constructor(name) {this.name = name;this.students = [];}addStudent(student) {this.students.push(student);}getStudents() {return this.students;}// ...
}class Student {constructor(name) {this.name = name;}// ...
}class School {constructor() {this.teachers = [];}addTeacher(teacher) {this.teachers.push(teacher);}// ...
}

在上面的示例中,Teacher类和Student类是两个独立的类,它们分别代表教师和学生。School类则代表学校,用于管理教师和学生的信息。

根据迪米特法则,School类不应该直接访问Teacher类和Student类的具体信息,而应该只和它们的接口进行交互。换句话说,School类应该尽量减少对其他类的依赖。

为了符合迪米特法则,我们可以修改School类如下:

class School {constructor() {this.teachers = [];}addTeacher(teacher) {this.teachers.push(teacher);teacher.getStudents().forEach(student => {// 对学生进行其他操作});}// ...
}

在修改后的School类中,我们只和Teacher类的接口进行交互,通过teacher.getStudents()方法获取学生列表,然后可以进行其他操作。

总结

通过遵守迪米特法则,我们能够减少模块之间的耦合,使得代码更加模块化、可维护和可测试。这样的设计能够提高系统的灵活性和扩展性,并且减少代码的依赖性,使得代码更具有可复用性

结语

在本文中,我们介绍了JavaScript面向对象设计原则的六个基本原则:单一职责原则(SRP)开闭原则(OCP)Liskov替换原则(LSP)接口隔离原则(ISP)依赖倒置原则(DIP)迪米特法则(LoD)

这些原则为我们提供了指导,帮助我们构建高质量可维护可扩展JavaScript代码。通过遵循这些原则,我们能够使代码更加模块化、清晰和易于理解。此外,它们还有助于降低代码的耦合度提高代码的重用性和灵活性

希望本篇文章对您在JavaScript面向对象设计方面有所启发和帮助。通过遵循这些原则,您将能够提高自己的代码质量,从而成为一名更加出色的JavaScript开发者。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/414315.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)

看到这句话的时候证明:此刻你我都在努力 加油陌生人 个人主页:Gu Gu Study 专栏:用Java学习数据结构系列 喜欢的一句话: 常常会回顾努力的自己,所以要为自己的努力留下足迹 喜欢的话可以点个赞谢谢了。 作者&#xff…

Sql查询优化--索引设计与sql优化(包含慢查询定位+explain解释计划+左匹配原则+索引失效)

本文介绍了数据库查询的索引优化方法,依次介绍了慢查询语句定位方法、索引设计与sql语句优化方法,并介绍了左匹配原则和索引失效的场景,最后介绍了explain执行计划要怎么看以调整检验索引设计是否生效和效率情况,创新介绍了如何以…

Visual Studio Code 自定义字体大小

常用编程软件自定义字体大全首页 文章目录 前言具体操作1. 打开首选项设置对话框2. 在Font Family里面输入字体 前言 Visual Studio Code 自定义字体大小,统一设置为 Cascadia Code SemiBold ,大小为 14 具体操作 【文件】>【首选项】>【设置】&…

18037 20秒后的时间

### 思路 1. 读取输入的时间,格式为小时:分钟:秒。 2. 将时间转换为秒数。 3. 增加20秒。 4. 将增加后的秒数转换回小时:分钟:秒格式。 5. 输出结果,确保小时、分钟和秒均占两个数字位,不足位用0补足。 ### 伪代码 1. 读取输入的时间字符串。…

day35-测试之性能测试JMeter的测试报告、并发数计算和性能监控

目录 一、JMeter的测试报告 1.1.聚合报告 1.2.html报告 二、JMeter的并发数计算 2.1.性能测试时的TPS,大都是根据用户真实的业务数据(运营数据)来计算的 2.2.运营数据 2.3.普通计算方法 2.4.二八原则计算方法 2.5.计算稳定性测试并发量 2.6…

vscode中如何设置不显示隐藏文件

在vscode中,有时候,会显示一些隐藏文件,如何设置让其不显示呢? 解决办法 例如:我这里有一个.vscode隐藏文件夹,是vscode默认生成的一个配置目录,我想要它不在资源管理器中进行显示。 操作步骤&a…

Java 入门指南:Java 并发编程 —— Condition 灵活管理线程间的同步

Condition Condition 是 Java 并发编程中的一种高级同步工具,它可以协助线程之间进行等待和通信。提供了一种比传统的 wait() 和 notify() 更加灵活的方式来管理线程间的同步。Condition 接口通常与 Lock 接口一起使用,允许更细粒度的控制线程的等待和唤…

Python 从入门到实战4(序列的操作)

我们的目标是:通过这一套资料学习下来,通过熟练掌握python基础,然后结合经典实例、实践相结合,使我们完全掌握python,并做到独立完成项目开发的能力。 上篇文章我们通过举例学习了python 中列表的简单操作,…

Android CCodec Codec2 (六)C2InterfaceHelper

通过前面几篇文章的学习,我们知道了Codec2参数结构,以及如何定义一个Codec2参数。接下来的几篇文章我们将简单了解上层是如何请求组件支持的参数、如何配置参数,以及参数是如何反射给上层的。本篇文章我们将了解接口参数实例化。 1、C2Interf…

SprinBoot+Vue社团管理系统的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍:CSDN认证博客专家,CSDN平台Java领域优质…

【最全深度学习介绍】基本概念、类型、应用、优缺点、与机器学习区别是什么?

《博主简介》 小伙伴们好,我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 👍感谢小伙伴们点赞、关注! 《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发…

excel规划求解结合vba宏笔记

目录 概念与配置 规划求解定义 excel设置规划求解 宏的基本操作 excel批量进行规划求解案例 加载规划求解模块 宏的设置 宏录制vba 其他案例 概念与配置 规划求解定义 运用“规划求解”定义并求解问题 - Microsoft 支持 excel设置规划求解 EXCEL规划求解的简明教程…

HarmonyOS鸿蒙开发:在线短视频流畅切换最佳实践

简介 为了帮助开发者解决在应用中在线短视频快速切换时容易出现快速切换播放时延过长的问题,将提供对应场景的解决方案。 该解决方案使用: 视频播放框架AVPlayer和滑块视图容器Swiper进行短视频滑动轮播切换。绘制组件XComponent的Surface类型动态渲染…

midwayjs 框架使用 rabbitmq 消息延迟

插件rabbitmq_delayed_message_exchange是RabbitMQ官方提供的一种用于实现延迟消息的解决方案。该插件将交换机类型扩展至x-delayed-message,这种类型的交换机能够将消息暂时挂起,直到设定的延迟时间到达,才将消息投递到绑定的队列中。这一特…

js做一个带模糊搜索、自动补全的select组件auto-input-select

效果图: 思路 原本是想弄一个输入框input,挡在原生select的前面,结果发现,原生select无论怎么弄,都无法js手动控制展开下拉选,必须点击select,这就很尴尬 然后就只能弄一个输入框input&#x…

python-变量声明、数据类型、标识符

一.变量 1.什么是变量 为什么需要变量呢? 一个程序就是一个世界,不论使用哪种高级程序语言编写代码,变量都是其程序的基本组成单位。如下图所示的sum和sub都是变量。 变量的定义: 变量相当于内存中一个数据存储空间的表示&#…

【Unity小工具】Image组件宽度、高度自适应

Unity开发中,用同一个Image进行动态加载不同尺寸的图片,在显示上会有形变此工具可以进行Image的宽度、高度自适应 实现原理 获取Image原始尺寸(sizeDelta)获取图片原始尺寸(spriteSizeDelta)公式&#xff…

Git 忽略已经提交的文件

对于未提交过的文件直接用ignore文件即可,不再赘述 对于已经提交过的文件,但是实际上不需要的,可以用git rm --cached命令 比如下图这个 .vsconfig被我误提交了或者忘了在ignore里添加了 但是我实际上不想要这个文件,那么在项目根目录打开git bash ,输入 git rm --cached .vsc…

【Hot100】LeetCode—34. 在排序数组中查找元素的第一个和最后一个位置

目录 1- 思路二分 - 左侧二分 右侧二分 2- 实现⭐34. 在排序数组中查找元素的第一个和最后一个位置——题解思路 3- ACM 实现 原题链接:34. 在排序数组中查找元素的第一个和最后一个位置 1- 思路 二分 - 左侧二分 右侧二分 右区间二分 ——> 找首次出现的位置…

unreal engine5.4.3动画重定向

UE5系列文章目录 文章目录 UE5系列文章目录前言 前言 ue5.4和ue3动画重定向之间存在差异,跟ue5.2差别更大一点,总之ue5.4越来越简化动画重定向,不想之前还需要制作RTG文件 这是ue5.3.2的制作动画重定向的界面 这是ue5.4.2的制作动画重定向…