面向对象 OOP 和 UML 类图 - 前端开发的必备编程思想
当谈到面向对象编程(Object-Oriented Programming,OOP)时,它是一种编程范式,其中程序的结构是基于对象的概念。在面向对象编程中,问题被分解为一组相互作用的对象,每个对象都有自己的状态(属性)和行为(方法)。这种方式使得代码更加模块化、可重用和易于理解。
面向对象编程中的核心概念包括:
- 类(Class):类是对象的蓝图或模板,描述了对象的属性和方法。它定义了对象的共同特征和行为。例如,可以定义一个名为"Person"的类,它具有属性(如姓名、年龄)和方法(如说话、行走)。
- 对象(Object):对象是类的实例,它具有类定义的属性和方法。通过实例化类,可以创建对象。例如,可以通过实例化"Person"类创建一个名为"John"的对象,它具有特定的姓名和年龄,并可以执行相应的方法。
- 封装(Encapsulation):封装是将数据和操作封装在一个单元(类)中,以实现信息隐藏和保护数据的安全性。通过定义类的公共接口(方法)来访问和操作对象的数据,而对于类的内部实现细节则是私有的。
- 继承(Inheritance):继承是一种机制,允许一个类继承另一个类的属性和方法。通过继承,子类可以重用父类的代码,并可以在不修改父类的情况下添加自己的特定行为。
- 多态(Polymorphism):多态是指同一操作对不同对象的不同响应方式。通过多态,可以使用统一的接口来处理不同类型的对象,从而提高代码的灵活性和可扩展性。
UML(Unified Modeling Language)类图是一种用于可视化和描述类、对象、关系和行为的图形化表示方法。它是一种常用的软件工程工具,用于设计和分析系统。在类图中,可以表示类之间的关系(如继承、关联、聚合等)和类的属性和方法。
类图中的一些常见元素包括:
- 类(Class):用矩形框表示类,包含类的名称、属性和方法。
- 属性(Attribute):表示类的状态或特征,通常以名称和类型的形式表示。
- 方法(Method):表示类的行为或操作,通常以名称和参数列表的形式表示。
- 关联关系(Association):表示类之间的关联关系,可以是单向或双向的。
- 继承关系(Inheritance):表示一个类继承另一个类的关系,通常用箭头指向父类。
- 聚合关系(Aggregation):表示一种弱的关联关系,表示整体与部分之间的关系。
- 组合关系(Composition):表示一种强的关联关系,表示整体与部分之间的关系,部分不能独立存在。
UML 类图提供了一种可视化的方式来描述系统的结构和行为,有助于开发人员和设计师更好地理解和沟通系统的设计。它在软件开发过程中起到了重要的指导和文档作用。
多态
多态性的关键在于继承和方法重写。通过继承,子类可以继承父类的方法,并且可以在子类中重新实现这些方法以适应子类的特定需求
js
class Shape {
calculateArea() {
console.log("This is the base class for shapes.");
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
calculateArea() {
console.log("Area of the rectangle:", this.width * this.height);
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
calculateArea() {
console.log("Area of the circle:", Math.PI * this.radius * this.radius);
}
}
// 多态性的体现
const shapes = [new Rectangle(5, 10), new Circle(3)];
shapes.forEach((shape) => {
shape.calculateArea();
});
在这个示例中,我们有一个基类 Shape
和两个派生类 Rectangle
和 Circle
。Shape
类中定义了一个 calculateArea()
方法,而 Rectangle
和 Circle
类都重写了这个方法以计算它们各自形状的面积。
在主程序中,我们创建了一个包含 Rectangle
和 Circle
对象的数组 shapes
。然后,我们使用 forEach
方法遍历数组中的每个元素,并调用它们的 calculateArea()
方法。
由于 shapes
数组中的每个元素都是 Shape
类型的引用,但实际指向的对象是不同的,所以在调用 calculateArea()
方法时,会根据对象的实际类型调用相应的重写方法。
输出结果将根据每个对象的类型而不同,分别计算矩形和圆的面积。
这个例子展示了 JavaScript 中多态性的特性,允许使用相同的方法名在不同类型的对象上产生不同的行为。这提供了更灵活和可扩展的代码结构。
设计模式只是套路,设计原则是指导思想
设计模式可以被视为一种套路,可以帮助开发人员解决特定类型的问题,并提供了一种经过验证的方法。
而设计原则是指导思想,它们提供了关于如何设计良好的软件架构和代码的指导。设计原则是广泛适用的准则,可以指导开发人员做出合理的设计决策,以实现可维护、可扩展和可重用的代码
以下是一些常见的设计原则:
- 单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因,即一个类应该只有一个职责。
- 开放封闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即在不修改现有代码的情况下,通过添加新代码来扩展功能。
- 里氏替换原则(Liskov Substitution Principle,LSP):子类对象应该能够替换其父类对象,而不会影响程序的正确性。
- 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于具体实现细节,具体实现细节应该依赖于抽象。
- 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不需要的接口。一个类不应该强迫它的客户端依赖于它们不使用的方法。
- 迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象有尽可能少的了解,只与最直接的朋友通信。
这些设计原则提供了一些通用的指导原则,帮助开发人员构建具有良好设计和高内聚低耦合的软件系统。设计模式则是在实践中应用这些设计原则的具体实现方式。
场景设计模式
工厂模式、单例模式(全局只允许有一个实例)、观察者模式、迭代器模式、原型模式、装饰器模式、代理模式
工厂模式
什么是工厂模式,它主要解决什么问题
工厂模式是一种创建对象的设计模式,它主要解决了对象的实例化过程与使用过程之间的耦合问题。它通过引入一个工厂类或接口,将对象的创建逻辑封装起来,客户端代码只需要通过工厂来创建对象,而无需直接使用 new
关键字实例化对象
工厂模式的主要目标是将对象的创建与使用分离,以提供更好的灵活性和可维护性。它可以解决以下问题:
- 隐藏对象的创建过程:使用工厂模式,客户端代码无需了解具体的对象创建细节,只需要知道如何通过工厂来创建对象。这样可以将对象的创建逻辑封装在工厂中,隐藏起来,使客户端代码更加简洁和易读。
- 解耦合:工厂模式通过引入工厂类或接口,将客户端代码与具体的产品类解耦。客户端只需要与工厂进行交互,而不需要直接依赖具体的产品类。这样可以降低代码的耦合度,提高代码的可维护性和扩展性。
- 提供灵活性:通过工厂模式,可以根据需要选择不同的工厂来创建不同的产品对象。这样可以在不修改客户端代码的情况下,更换或新增具体的产品类。工厂模式提供了一种可扩展的机制,使系统更具灵活性。
js
// 抽象产品接口
interface Product {
operation(): void
}
// 具体产品类A
class ConcreteProductA implements Product {
operation(): void {
console.log('具体产品A的操作')
}
}
// 具体产品类B
class ConcreteProductB implements Product {
operation(): void {
console.log('具体产品B的操作')
}
}
// 抽象工厂接口
interface Factory {
creteProduct(): Product
}
// 具体工厂类A
class ConcreteFactoryA implements Factory {
creteProduct(): Product {
return new ConcreteProductA();
}
}
// 具体工厂类B
class ConcreteFactoryB implements Factory {
creteProduct(): Product {
return new ConcreteProductB();
}
}
// 创建具体工厂对象
const factoryA: Factory = new ConcreteFactoryA();
const factoryB: Factory = new ConcreteFactoryB();
// 使用工厂A创建产品A
const productA: Product = factoryA.creteProduct();
productA.operation(); // 输出:具体产品A的操作
// 使用工厂B创建产品B
const productB: Product = factoryB.creteProduct();
productB.operation(); // 输出:具体产品B的操作
这里,我们通过工厂对象的 createProduct()
方法来创建具体的产品对象,并调用产品对象的方法进行操作。通过工厂模式,我们可以根据需要选择不同的工厂来创建不同的产品,而不需要直接关注具体的产品类
工厂模式缺点
工厂模式作为一种常用的设计模式,虽然有很多优点,但也存在一些缺点,包括:
- 增加了代码复杂性:引入工厂模式会增加额外的类和接口,增加了代码的复杂性和理解难度。工厂模式需要定义抽象工厂、具体工厂和产品接口等,这些额外的结构和层级可能会使代码变得更加复杂。
- 增加了系统的抽象性:工厂模式通过引入抽象工厂和产品接口,将对象的创建过程进行了抽象和封装。这种抽象性可能会导致系统的理解和调试变得困难,特别是对于初学者或新加入的开发人员来说。
- 不易于扩展和变化:尽管工厂模式提供了一种灵活的方式来创建对象,但当需要添加新的产品类型时,需要修改工厂类和产品接口的定义,这可能导致代码的修改和重构。这种扩展性的局限性可能会增加代码的维护成本。
- 增加了系统的依赖性:使用工厂模式会增加系统中类之间的依赖关系。客户端代码必须依赖于工厂接口和产品接口,这种依赖关系可能会增加系统的耦合度,使得代码更难以理解和修改。
- 可能引入过多的工厂类:随着系统的复杂性增加,可能需要引入多个具体工厂类来创建不同类型的产品。这可能导致工厂类的数量增加,使得代码变得冗长和复杂。
尽管工厂模式存在一些缺点,但在适当的场景下仍然是一种有价值的设计模式。它可以提供灵活性、可维护性和可扩展性,尤其在需要将对象的创建过程与使用过程分离,并提供统一的接口来访问对象时,工厂模式是一个有用的选择。
工厂模式的场景-jQuery
在 jQuery 库中,工厂模式被广泛应用于创建和操作 DOM 元素。jQuery 提供了一个全局函数 $
,它实际上是一个工厂函数,用于创建 jQuery 对象
下面是一个简单的示例,展示了如何使用 jQuery 的工厂模式来创建和操作 DOM 元素:
js
// 创建一个 div 元素
var div = $('<div></div>');
// 添加类名和文本内容
div.addClass('myDiv').text('Hello, jQuery!');
// 将元素添加到文档中的 body 元素中
$('body').append(div);
在上述示例中,$('<div></div>')
使用 $
工厂函数创建了一个 jQuery 对象,它封装了一个新创建的 <div>
元素。然后,我们可以使用 jQuery 提供的方法来操作这个 jQuery 对象,例如 addClass()
和 text()
方法。最后,通过调用 $('body').append(div)
将这个元素添加到文档中的 <body>
元素中。
通过使用工厂模式,jQuery 提供了一种简洁而灵活的方式来创建和操作 DOM 元素。它隐藏了底层的 DOM 操作细节,使开发人员能够以更简洁的方式编写代码,并提供了丰富的方法和功能来操作 DOM 元素。
除了创建和操作 DOM 元素,jQuery 还使用工厂模式来创建和操作其他对象,例如 AJAX 请求对象、事件对象等。工厂模式使得 jQuery 能够提供一致的接口和易于使用的功能,成为了 Web 开发中常用的工具库之一。
工厂模式的场景-Vue和React的createElement
在 Vue 和 React 中,工厂模式被用于创建虚拟 DOM 元素。虚拟 DOM 元素是用于描述 UI 的 JavaScript 对象,它们最终会被渲染成实际的 DOM 元素。
在 Vue 中,使用 createElement
函数创建虚拟 DOM 元素。createElement
是一个工厂函数,它接受三个参数:标签名、属性对象和子元素。通过调用 createElement
函数,可以创建一个虚拟 DOM 元素。
以下是一个使用 Vue 的 createElement
创建虚拟 DOM 元素的示例:
js
new Vue({
el: '#app',
render: function(createElement) {
return createElement('div', { class: 'myDiv' }, 'Hello, Vue!');
}
});
在上述示例中,render
函数使用 createElement
工厂函数创建了一个虚拟 DOM 元素 <div>
,它具有类名为 'myDiv'
,并且文本内容为 'Hello, Vue!'
。这个虚拟 DOM 元素最终会被渲染成实际的 DOM 元素,并插入到具有 id 为 'app'
的元素中。
类似地,在 React 中,使用 React.createElement
函数创建虚拟 DOM 元素。React.createElement
也是一个工厂函数,它接受三个参数:标签名或组件、属性对象和子元素。通过调用 React.createElement
函数,可以创建一个虚拟 DOM 元素。
以下是一个使用 React 的 createElement
创建虚拟 DOM 元素的示例:
js
ReactDOM.render(
React.createElement('div', { className: 'myDiv' }, 'Hello, React!'),
document.getElementById('app')
);
在上述示例中,React.createElement
工厂函数创建了一个虚拟 DOM 元素 <div>
,它具有类名为 'myDiv'
,并且文本内容为 'Hello, React!'
。通过调用 ReactDOM.render
将这个虚拟 DOM 元素渲染成实际的 DOM 元素,并插入到具有 id 为 'app'
的元素中。
通过使用工厂模式,Vue 和 React 提供了一种简洁而灵活的方式来创建虚拟 DOM 元素。工厂模式隐藏了底层的 DOM 操作细节,使开发人员能够以声明式的方式描述 UI,并提供了丰富的功能和组件来构建复杂的应用程序界面。
单例模式 - 全局只允许有一个实例,多则出错
什么是单例模式,它主要解决什么问题
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
单例模式解决的主要问题是控制对象的实例化过程,确保在整个应用程序中只有一个实例存在。这在某些情况下是很有用的,例如:
- 资源共享:单例模式可以用来管理共享的资源,例如数据库连接池、线程池等。通过使用单例模式,可以确保所有的请求都使用同一个资源实例,避免资源的重复创建和管理。
- 对象跨越多个模块的访问:单例模式可以提供一个全局访问点,使得不同模块中的代码可以方便地访问同一个对象实例。这在需要共享数据或协调不同模块之间操作时非常有用。
- 控制实例数量:有些情况下,我们需要限制一个类的实例数量,确保只有一个实例存在。例如,某些设备驱动程序只允许有一个实例,或者某些配置信息只需要加载一次。
单例模式通过将类的实例化过程封装在类内部,并提供一个静态方法或属性来访问该实例,确保只有一个实例被创建并全局可访问。这样可以简化代码的使用方式,避免重复创建实例,同时提供了一种集中管理和控制对象实例的方式。
然而,单例模式也有一些缺点,例如可能引入全局状态和共享状态的问题,使得代码的依赖关系变得复杂。因此,在使用单例模式时需要谨慎考虑,并确保它真正解决了问题并符合应用程序的设计需求。
单例模式缺点
当使用单例模式时,需要注意以下一些潜在的缺点:
- 难以扩展和测试:由于单例模式创建了一个全局唯一的实例,它可能会导致代码的扩展和测试变得困难。其他部分的代码依赖于单例对象,如果需要修改或替换该对象,可能需要修改大量的代码。
- 引入全局状态:单例模式将实例对象设为全局可访问,这可能导致全局状态的引入。全局状态的管理变得复杂,可能会增加代码的耦合性和维护难度。
- 增加代码的耦合性:使用单例模式可能会增加代码的耦合性。其他部分的代码可能会直接依赖于单例对象,这使得单例对象的修改变得困难,可能需要同时修改依赖于该对象的其他代码。
- 可能引入并发问题:如果在多线程或异步环境中使用单例模式,可能会引入并发问题。当多个线程或任务同时访问单例对象时,需要考虑线程安全性和同步机制,以避免数据竞争和不一致的状态。
- 难以进行单元测试:由于单例对象通常在整个应用程序中被共享和访问,它可能会导致单元测试变得困难。在单元测试中,我们通常希望能够独立地测试每个模块,但单例对象的全局可访问性可能会干扰测试的隔离性。
下面是一个例子,展示了单例模式的一些缺点:
js
var Singleton = (function() {
var instance;
function Singleton() {
this.counter = 0;
}
Singleton.prototype.incrementCounter = function() {
this.counter++;
};
return {
getInstance: function() {
if (!instance) {
instance = new Singleton();
}
return instance;
}
};
})();
var singletonInstance1 = Singleton.getInstance();
var singletonInstance2 = Singleton.getInstance();
singletonInstance1.incrementCounter();
console.log(singletonInstance2.counter); // 输出: 1
singletonInstance2.incrementCounter();
console.log(singletonInstance1.counter); // 输出: 2
在上述示例中,我们使用单例模式创建了一个计数器对象。然而,由于单例对象是全局共享的,对计数器对象的操作会影响到其他部分的代码。这可能导致并发问题和难以预测的行为。此外,由于全局状态的引入,单元测试变得困难,无法独立地测试每个模块。
单例模式示例
js
// 立即执行函数创建单例对象
var Singleton = (function(){
// 单例实例
var instance;
// 私有属性和方法
function initialize() {
var privateVariable = "私有属性"
function privateMethod() {
console.log("私有方法")
}
return {
// 共有属性和方法
publicMethod: function() {
console.log("公有方法")
},
publicVariable: "公有属性",
getPrivateVariable: function() {
return privateVariable;
}
}
}
// 获取单例的实例和方法
return {
getInstance: function() {
if(!instance) {
instance = initialize()
}
return instance;
}
}
})()
- 当使用 TypeScript 实现单例模式时,我们可以通过一个示例来说明其用法。假设我们有一个日志记录器,我们希望在整个应用程序中共享同一个日志记录器实例。
ts
class Logger {
private static instance: Logger;
private logs: string[];
private constructor() {
this.logs = [];
}
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public log(message: string): void {
this.logs.push(message);
}
public printLogs(): void {
console.log(this.logs);
}
}
// 使用单例日志记录器
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
logger1.log("Message 1");
logger2.log("Message 2");
logger1.printLogs(); // 输出: ["Message 1", "Message 2"]
logger2.printLogs(); // 输出: ["Message 1", "Message 2"]
在上述示例中,我们定义了一个名为 Logger
的类,它实现了单例模式。
- 类中的
instance
是一个私有的静态成员,用于存储单例实例。 - 构造函数
constructor
被设为私有,以防止直接实例化Logger
类。 getInstance
是一个公共的静态方法,用于获取单例实例。如果实例不存在,则创建一个新的实例并将其赋值给instance
,然后返回该实例。如果instance
已经存在,直接返回它。logs
是一个私有成员,用于存储日志消息。log
是一个公共方法,用于向日志记录器添加消息。printLogs
是一个公共方法,用于打印日志消息。
在示例中,我们通过调用 Logger.getInstance()
方法来获取单例实例。我们创建了两个变量 logger1
和 logger2
,它们实际上引用的是同一个日志记录器实例。
然后,我们分别向 logger1
和 logger2
添加了两条日志消息。由于它们引用的是同一个实例,这些日志消息都会被添加到同一个日志记录器中。
最后,我们分别调用 logger1.printLogs()
和 logger2.printLogs()
方法来打印日志消息。可以看到,两个日志记录器实例中都包含了相同的日志消息。
这个示例展示了如何使用 TypeScript 实现单例模式,并通过共享单例实例在整个应用程序中进行日志记录。这种方式可以确保我们在应用程序中共享同一个日志记录器,方便统一管理和查看日志消息。
单例模式的场景-登录框
ts
class LoginBox {
private static instance: LoginBox;
private loggedIn: boolean;
private constructor() {
this.loggedIn = false;
}
public static getInstance(): LoginBox {
if (!LoginBox.instance) {
LoginBox.instance = new LoginBox();
}
return LoginBox.instance;
}
public login(username: string, password: string): void {
// 执行登录逻辑,验证用户名和密码
// ...
// 登录成功
this.loggedIn = true;
console.log("User logged in");
}
public logout(): void {
// 执行登出逻辑
// ...
// 登出成功
this.loggedIn = false;
console.log("User logged out");
}
public isLoggedIn(): boolean {
return this.loggedIn;
}
}
// 使用登录框
const loginBox1 = LoginBox.getInstance();
const loginBox2 = LoginBox.getInstance();
loginBox1.login("username", "password");
console.log(loginBox1.isLoggedIn()); // 输出: true
console.log(loginBox2.isLoggedIn()); // 输出: true
loginBox2.logout();
console.log(loginBox1.isLoggedIn()); // 输出: false
console.log(loginBox2.isLoggedIn()); // 输出: false
在上述示例中,我们定义了一个名为 LoginBox
的类,它实现了单例模式。
- 类中的
instance
是一个私有的静态成员,用于存储单例实例。 - 构造函数
constructor
被设为私有,以防止直接实例化LoginBox
类。 getInstance
是一个公共的静态方法,用于获取单例实例。如果实例不存在,则创建一个新的实例并将其赋值给instance
,然后返回该实例。如果instance
已经存在,直接返回它。loggedIn
是一个私有成员,用于表示用户的登录状态。login
是一个公共方法,用于执行用户登录逻辑,并更新登录状态。logout
是一个公共方法,用于执行用户登出逻辑,并更新登录状态。isLoggedIn
是一个公共方法,用于检查用户的登录状态。
在示例中,我们通过调用 LoginBox.getInstance()
方法来获取单例实例。我们创建了两个变量 loginBox1
和 loginBox2
,它们实际上引用的是同一个登录框实例。
然后,我们通过调用 loginBox1.login("username", "password")
方法进行用户登录。由于 loginBox1
和 loginBox2
引用的是同一个实例,因此它们的登录状态是相互影响的。
最后,我们分别调用 loginBox1.isLoggedIn()
和 loginBox2.isLoggedIn()
方法来检查登录状态。可以看到,无论是通过 loginBox1
还是 loginBox2
访问,它们都返回相同的登录状态。
这个示例展示了如何使用单例模式实现登录框,并确保在整个应用程序中只存在一个登录框实例。这样可以保证用户的登录状态在全局范围内的一致性,并且方便地管理用户的登录和登出操作。
观察者模式
什么是观察者模式,它解决什么问题
观察者模式(Observer Pattern)是一种行为设计模式,用于在对象之间建立一种一对多的依赖关系,当一个对象的状态发生改变时,它的所有依赖对象都会收到通知并自动更新。
观察者模式解决的问题是对象之间的解耦和通信问题。当多个对象之间存在一种依赖关系,一个对象的状态改变需要通知其他对象进行相应的处理时,使用观察者模式可以有效地实现这种通信机制。
以下是观察者模式的几个关键角色:
- Subject(主题) :也称为被观察者或可观察对象,它维护一组观察者对象,并在状态发生改变时通知观察者。
- Observer(观察者) :也称为订阅者或监听者,它定义了一个接口,用于接收主题的通知并进行相应的处理。
- ConcreteSubject(具体主题) :实现主题接口,维护具体的状态,并在状态改变时通知观察者。
- ConcreteObserver(具体观察者) :实现观察者接口,定义具体的处理逻辑,接收主题的通知并进行相应的操作。
观察者模式的工作原理如下:
- 主题对象(Subject)维护了一个观察者列表,并提供方法用于注册、注销和通知观察者。
- 观察者对象(Observer)通过注册方法将自身添加到主题对象的观察者列表中,以便接收通知。
- 当主题对象的状态发生改变时,它会遍历观察者列表,并调用每个观察者的通知方法,将状态改变的信息传递给观察者。
- 观察者收到通知后,根据接收到的信息进行相应的处理,可能会更新自身的状态或执行其他操作。
观察者模式的优点包括:
- 解耦性:主题和观察者之间的关系是松散耦合的,它们可以独立地进行扩展和修改,而不会相互影响。
- 可维护性:由于对象之间的依赖关系明确,代码的维护和调试更加容易。
- 可扩展性:可以很方便地添加新的观察者对象,而不需要修改现有的代码。
观察者模式适用于以下情况:
- 当一个对象的改变需要同时影响其他对象,并且不希望对象之间紧密耦合时。
- 当一个对象的改变需要通知一组对象,而不知道具体有多少个对象需要通知时。
- 当需要确保对象之间的一致性,避免手动维护对象之间的关联关系时。
总而言之,观察者模式提供了一种松散耦合的通信机制,使得对象之间的依赖关系更加灵活和可扩展。它解决了对象之间解耦和通信的问题,提高了代码的可维护性和可扩展性。