为了解决同一类类似的问题而编写的代码,有的代码质量高,有的低,研究发现那些高质量代码中的相似之处并命名为一种模式。
设计模式定义:在面向对象编程中,针对特定问题优秀的解决方案(在某种场合下对某个问题的一种解决方案 )。
项目中存在一些大量的相似对象,这些对象比较占内存,就可以使用享元模式对代码进行优化;比如项目中某个接口的结构已经不能符合目前的需求,但又不想直接修改带接口结构,那么就可以借助适配器模式来处理。
在面向对象开发中,通过对封装,继承,多态和组合等思想的反复使用,可以实现一些编程技巧。
设计模式本身是一种思想,而与语言无关。只是有些语言因为自己的特性,天然对一些设计模式更加友好。
一定程度上,设计模式是有可能增加源码量同时增加复杂度,但软件开发的成本并非全在开发阶段。而设计模式能让人写出可复用,高可维护性的程序代码。
有一些设计模式的类图或者结构看起来非常像,以至于常常混淆它们。而辨别模式的关键时这个模式出现的场景和它能解决的问题。
发布-订阅模式和观察者模式都是行为设计模式,它们在目的上非常相似,即定义了对象间的一种依赖关系,使得当一个对象改变状态时,所有依赖于它的对象都会得到通知和自动更新。尽管它们在概念上相似,但在实现细节上有所不同。
观察者模式
观察者模式涉及两个角色:
主题(Subject)
和观察者(Observer)
。主题维护一系列依赖于它的观察者,任何主题状态的变化将导致所有观察者都收到通知。类图结构:
scss+------------------+ +---------------------+ | <<interface>> | | | | Subject |<--------->| Observer | +------------------+ +---------------------+ | +attach(observer)| | +update(subject) | | +detach(observer)| +---------------------+ | +notify() | ^ +------------------+ | | +-------+--------+ | | | ConcreteObserver | +-----------------+ | +update(subject) | +-------------------+
- Subject: 提供注册(attach)和注销(detach)观察者的接口,以及通知(notify)所有观察者的方法。
- Observer: 为所有具体观察者定义一个接口,在得到主题的更改通知时更新自己。
- ConcreteObserver: 实现观察者接口的具体类。
发布-订阅模式
发布-订阅模式将发送者(发布者)和接收者(订阅者)分离开来。发送者并不直接发送消息给接收者,而是通过一个中间件(通常是消息队列或事件通道),发送者和接收者都不需要知道对方的存在。
类图结构:
scss+------------------+ +---------------------+ | | | | | Publisher | | Subscriber | +------------------+ +---------------------+ | +publish(message)| | +update(message) | +------------------+ +---------------------+ | ^ | | v | +-------------------+ +-----------+----------+ | | | | | MessageQueue |<----->| ConcreteSubscriber | +-------------------+ +----------------------+ | +subscribe(subscriber) | | +update(message) | | +unsubscribe(subscriber)| +----------------------+ | +notify(message) | +------------------------+
- Publisher: 发布消息到消息队列。
- Subscriber: 订阅消息队列,接收消息。
- MessageQueue (Broker) : 中间件,维护订阅者列表和消息的分发。
- ConcreteSubscriber: 实现订阅者接口的具体类。
两种模式的主要区别在于,观察者模式通常是同步的,观察者直接接收到状态变化的通知;而发布-订阅模式则是异步的,发布者和订阅者通过消息队列(或事件总线)进行通信,解耦了发送者和接收者。
动态类型的语言中面向接口编程是一件相对容易的事情,而在静态类型的语言中,面向接口编程往往要通过抽象类或者接口将函数参数能接受的数据类型的范围扩大(对象的向上转型:当给一个变量赋值时,这个变量的类型既可以是这个类本身,也可以是这个类的超类)。而对象的真正类型则是父类(超类)下面的细分子类,只有这样才能在避免语言的类型检测系统的报错,同时能体现出函数的多态性。
多态的作用是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。
假设有一个绘图应用程序,需要绘制不同类型的图形,如圆形和矩形。没有使用多态性,你可能会使用条件分支语句来判断每个图形的类型,并调用相应的绘制方法:
javascript// 未使用多态性的示例 function drawShape(shape) { if (shape.type === 'circle') { drawCircle(shape); } else if (shape.type === 'rectangle') { drawRectangle(shape); } } function drawCircle(circle) { console.log(`Drawing a circle with radius ${circle.radius}`); // 实际的绘图代码 } function drawRectangle(rectangle) { console.log(`Drawing a rectangle with width ${rectangle.width} and height ${rectangle.height}`); // 实际的绘图代码 }
使用多态性,可以消除这些条件分支语句,通过定义一个通用的
draw
方法来处理所有类型的图形。每个图形类都实现自己的draw
方法,如下所示:
scala// 使用多态性的示例 // 定义一个通用的Shape类 class Shape { draw() { throw new Error('This method should be implemented by subclasses'); } } // 定义Circle类,继承自Shape class Circle extends Shape { constructor(radius) { super(); this.radius = radius; } draw() { console.log(`Drawing a circle with radius ${this.radius}`); // 实际的绘图代码 } } // 定义Rectangle类,继承自Shape class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } draw() { console.log(`Drawing a rectangle with width ${this.width} and height ${this.height}`); // 实际的绘图代码 } } // 使用多态性来绘制不同的图形 function drawShape(shape) { shape.draw(); } const circle = new Circle(10); const rectangle = new Rectangle(20, 30); drawShape(circle); // 输出: Drawing a circle with radius 10 drawShape(rectangle); // 输出: Drawing a rectangle with width 20 and height 30
将行为分布到各个对象中,并让这些对象各自负责自己的行为和属性,这就是面向对象编程。
基础知识盲区:
new 构造函数时,如果构造函数内部显式的返回一个对象类型的值,那么最终返回值就是这个对象类型的值,而不是this指代的那个对象。构造函数不显式的返回任何类型的数据或者返回的是一个非引用类型的数据时,则默认返回的是this指代的对象值。
在使用call或者apply时,如果传入的第一个参数时null,那么函数体中的this在非严格模式下会指向默认的宿主对象。
v8源码中关于push的实现:
kotlin
function ArrayPush(){
var n = TO_UINT32(this.length) // 被push的对象的length
var m = %_ArgumentsLength() // push的参数个数
for(var i = 0;i<m;i++){
this[i+n] = %_Arguments(i) // 复制元素, 说明对象本身可以支持属性的读取
}
this.length = n+m // 修改length属性值 对象的length属性可读可写
return this.length
}
函数的length 属性是一个只读属性,表示的是形参的个数。如果通过push.call将函数传入作为this,则会报错说函数的length是一个只读属性。