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

news2024/9/23 11:23:51

引言

面向对象编程(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.coloradmin.cn/o/2094021.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【持续更新】【Google Play版】淘宝最新国际版10.36.10.20启动更快

功能和国内比基本是差不多的,只不过没有应用内乱七八糟的弹窗,用起来比较舒服,启动也比较快。 像这种软件如何保证是 官方 的呢?毕竟涉及到财产,还是要小心些的。 很简单,修改过的 app 会提示“签名不一致…

Unity本地化id查找器,luaparser函数参数查找

前言: 适用范围:Unity 中需要查找所有预制体里面的某一个脚本的属性值,或者Lua脚本里面的某一个属性值 本文介绍如何查找预制体和Lua脚本里面调用的本地化id 下面首先介绍改插件的功能以及使用方法,然后对该插件的原理进行说明 使…

SOMEIP_ETS_075: Wrong_Message_Type

测试目的: 验证当设备(DUT)接收到一个包含错误消息类型的SOME/IP请求时,是否能够返回错误消息或忽略该请求。 描述 本测试用例旨在检查DUT在处理一个echoUINT8方法的SOME/IP消息时,如果消息中包含的消息类型不正确&…

【超详细】Linux开发环境搭建指南 | Ubuntu

文章目录 虚拟机安装对比Virtual Box 下载ubuntu 操作系统下载Virtual Box 安装安装ubuntu设置中文语言共享文件夹设置添加输入法安装步骤,参考官方教程 安装 vscode解决主机不能通过ssh连接宿主机网络连接几种网络连接区别主机和宿主机相互 ping通 网络代理 虚拟机…

为k8s准备docker 私有仓库 harbor

目录 一、 环境准备 二、部署docker 二、为Registry提加密传输 三、为仓库建立登陆认证 四、构建harbor 五、上传镜像 然后就是在200上面进行配置了 一、 环境准备 我是用两台虚拟机(红帽9)的,一台172.25.254.10用来部署加密认证仓库,另一台172.2…

从零上手CV竞赛:YOLO方案 # Datawhale AI夏令营

文章目录 平台参赛平台云平台 Task 1 从零上手CV竞赛下载baseline相关文件一键运行baseline!(大约需要25分钟)赛题解析数据集提交结果违法标准注意事项 下载生成的文件结果如图最后要记得关机 不然一直消耗算力 Task 2 建模方案解读与进阶物体…

【大数据算法】时间亚线性算法之:串相等判定算法。

串相等判定算法 1、引言2、串相等判定算法2.1 定义2.2 核心原理2.3 应用场景2.4 算法公式2.4.1 Rabin-Karp算法2.4.2 哈希函数 2.5 代码示例 3、总结 1、引言 小屌丝:鱼哥, 啥是串相等判定算法啊 小鱼:这个… en…en… 小屌丝:咋…

ai免费生成ppt软件有哪些?我推荐秒出PPT

现在市面上的ai生成PPT软件非常的多。有收费的,也有不收费的,生成的效果也各不相同。要选择一个性价比高的确实难度不小。 我在使用了不少的产品之后,说一说我的心得。 首先就是,大部分免费的ai生成ppt产品并不好用。我怕不排除…

解决Selenium已安装,在pycharm导入时报错

搭建设selenium环境时,selenium已安装,但是在pycharm中使用“from selenium import webdriver”语句时红线报错 解决方案: 1.file->settings进入设置 2.点击加号,搜索‘selenium’安装 3,等待安装完成&#xff0…

windows 上安装 Anaconda

下载 立即下载 |蟒蛇 (anaconda.com) 安装

Nginx: 高可用和与虚拟路由冗余协议VRRP原理以及KeepAlived软件架构

Nginx 服务的高可用 1 )服务可用 假定是这样一个最传统的一个CS模式的一个客户服务器模式 这里有用户和一台服务器服务器可能是mysql, 也可能是webserver, 或其他服务器 想实现服务可用的一个三要素 1.1 ) server 需要公网的ip地址以及申请一个域名1.2 ) 需要服务软…

MIG IP核详解

1.MIG IP核简介 MIG(Memory Interface Generators) IP 核是Xilinx公司针对DDR存储器开发的 IP,里面集成存储器控制模块,实现DDR读写操作的控制流程,下图是7系列的 MIG IP 核结构框图。MIG IP 核对外分出了两组接口。左侧是用户接口&#xff…

自制实战吃鸡手柄原理

在前面的讨论中,通过类似物理点击的方式来实现了声控触发射击键的点击 【Arduino】自制声控点击器(吼叫吃鸡助手)_辅助机械臂物理物理键盘点击器神器-CSDN博客 为了更有实战效果,我们可以把玩具枪改造为一个手柄,这样…

2024年Java最新面试题总结(三年经验)

目录 第一章、基础技术栈1.1)集合,string等基础问题1、arraylist ,linkedlist的区别,为啥集合有的快有的慢2、字符串倒叙输出2.1、字符串常用方法2.2、字符串号拼接的底层原理3、讲一下Java的集合框架4、定义线程安全的map&#x…

进制数基础知识 与 位运算(基础版)

目录 1. 计算机常用进制类别 2. 10进制转各种进制(取余倒置法) 3. 二进制转8进制、16进制 3.1 二进制转8进制 3.2 二进制转16进制 4. 原码、反码、补码 5. 整型提升 与 算术转换 5.1 整型提升 5.2 算术转换 6. 移位操作符 6.1 左移操作符( &l…

【spring】学习笔记2:sample、boot功能和组件设计

Spring自带了一个强大的Web框架,名为Spring MVC。Spring MVC的核心 是控制器(controller)的理念。控制器是处理请求并以某种方式进行信息 响应的类。在面向浏览器的应用中,控制器会填充可选的数据模型并将请求 传递给一个视图,以便于生成返回给浏览器的HTML。在pom.xml文件…

前端开发——熟悉WebSocket(包含示例)

最近在开发中需要调用第三方API,现在大家的API基本上都是使用WebSocket来进行的,前端也必须来学一手了 什么是WebSocket 参考:https://blog.csdn.net/L2043524156/article/details/139271715 有如下四个好处: 双向通信&#x…

C# .Net 条码批量自动打印框架 基于Bartender 10.1 V1.0

调用Bartender打印模板,批量打印条码。 需要有一定的C#开发能力。 非常适合工厂中从ERP拿取信息后,批量打印出货条码。 提供全部源代码,毫无保留。 Winform程序,使用了依赖注入(微软DI),数据库访问的ORM为Dapper。…

位图 —— 哈希思想的产物

目录 1.学习位图的前置知识 计算机中数据存储的单位 C中数据类型的大小 2.位图的讲解 位图的引出 位图的使用 位图的实现 位图完整代码 3.位图的总结 位图的优缺点 优点 缺点 1.学习位图的前置知识 计算机中数据存储的单位 想要学习位图,首先要明白什…

在Windows10系统快速启用telnet功能

打开Windows控制面板 → 程序 → 启用或关闭Windows功能 勾选Telnet客户端 然后确定 启用后windowR 打开运行,输入cmd回车 使用telnet命令回车 可以直接使用telnet命令(如果提示telnet是外部命令需要重启电脑) 输入 ?/help 可查看帮助 到…