今天我们模拟小米智能家居应用场景,融合单例模式、组合模式、观察者模式做一个综合应用案例。具体场景如下:
- 小米门铃为主人开门,触发开门事件;
- 小米智能控制台监测到开门事件,自动启动小米空调和小米电视;
思路分析
- 小米控制台,在家庭单位内只有一个实例,我们使用单例模式;
- 小米控制台通过startWork这一统一接口控制全部智能家居,对于有相同接口的不同实例进行统一调度,我们可以应用组合模式;
- 小米控制台监听小米门铃的开门事件,观察者模式;
定义【组合】和【组件】两个父类接口
后续由小米控制台对【组合】做具体实现,小米电视和小米空调对【组件】做具体实现
/* 组合模式 【组合】父类定义 */
class Compose {
constructor (name){
this.name = name
// components 组件调度列表
this.components = []
}
// 添加组件到调度列表
addComponent(component){
this.components.push(component)
}
// 轮询所有组件 令其开始工作
startWork(){
this.components.forEach(component=>{
component.startWork()
})
}
}
/* 组合模式 【组件】父类定义 */
class Component {
// 接收组件名称
constructor(name){
this.name = name
}
// 所以组件统一调度接口 这里留白等待子类做具体实现
startWork(){}
}
定义小米控制台类、小米电视类、小米空调类
小米控制台类,通过继承【组合】父类,实现了添加设备和指挥所有设备开始工作
/*
小米控制台
继承【组合】父类 实现添加组件、开始工作两个方法
*/
class XiaomiControl extends Compose {
constructor (){
// 调用父类方法为设备命名
super("小米总控台")
}
}
定义小米空调和小米电视类
继承【组件】父类,实现统一调度接口,为后续控制台对它们做统一调度做好准备
class XiaomiKongtiao extends Component {
constructor(){
super("小米空调")
}
// 对统一调度接口做具体实现
startWork(){
console.log(`${this.name}开始调节室温`);
}
}
class XiaomiTV extends Component {
constructor(){
super("小米电视")
}
// 对统一调度接口做具体实现
startWork(){
console.log(`${this.name}自动切换到您喜欢的节目`);
}
}
设置小米控制台为全局唯一单例
好理解,一个家庭只需要一个控制台实例,通过getXmcSingleton()方法获取该实例
// instance 实例 singleton 单例
let xmcInstance = null
function getXmcSingleton() {
xmcInstance = xmcInstance === null? new XiaomiControl() : xmcInstance
return xmcInstance
}
实现统一调度
// 获取小米控制台实例
// const xmControl = new XiaomiControl()
const xmControl = getXmcSingleton()
// 创建小米电视和小米空调实例
const xmKongtiao = new XiaomiKongtiao()
const xmTv = new XiaomiTV()
// 添加电视和空调到调度列表
xmControl.addComponent(xmKongtiao)
xmControl.addComponent(xmTv)
// 所有智能家居开始工作
xmControl.startWork()
小米控制台控制所有智能设备开始工作的时机,为小米门铃开门的一刹那,所以接下来我们通过应用观察者模式实现这一需求;
定义被观察者(数据)和观察者两个父类
被观察者要实现注册观察者、注销观察者、改变数据状态、触发事件(所以观察者响应)四个接口
/* 提供被观察数据的父类接口 */
class Observable {
constructor(){
// 预备存储事件类型和对应的观察者列表
this.typeListeners = {}
// 预备存储事件类型和对应的值
this.typeValue = {}
}
// 注册观察者
addListener(type,...listeners){
if(Object.prototype.hasOwnProperty.call(this.typeListeners,type)){
this.typeListeners[type].push(...listeners)
}else{
this.typeListeners[type] = [...listeners]
}
}
// 注销观察者
removeListener(type,listener){
if(Object.prototype.hasOwnProperty.call(this.typeListeners,type)){
for(let i=this.typeListeners[type].length-1;i>=0;i--){
const item = this.typeListeners[type][i]
if(item === listener){
this.typeListeners[type].splice(i,1)
}
}
}
}
// 引起数据变化
setValue(type,newValue){
this.typeValue[type] = newValue
// 数据变化触发事件
this.trigger(type,newValue)
}
// 触发观察者响应
trigger(type,newValue){
if(Object.prototype.hasOwnProperty.call(this.typeListeners,type)){
this.typeListeners[type].forEach(item=>{
item.onEventHappen(type,newValue)
})
}
}
}
观察者接口主要需要提供事件响应接口的定义
/* 观察者父类 */
class Observer {
// 事件响应统一接口,留白待子类做具体实现
onEventHappen(type,newValue){}
}
小米门铃实现【被观察者/数据】父类,通过继承获得注册设备、注销设备、改变数据和触发事件等具体功能;
在开关门时,会引起doorOpen事件的数据变化,从而触发观察者的响应动作
/* 小米门铃 实现被观察数据接口 开门关门就是事件 */
class XiaomiDoorbell extends Observable {
// 开门时数据变化 父类帮你会【触发事件】(被观察者所提供的接口)
open(){
this.setValue("doorOpen",true)
}
close(){
this.setValue("doorOpen",false)
}
}
小米控制台对观察者父类做具体实现,这里由于前面小米控制台已经继承过一遍【组合】这个父类,而在JavaScript中并不存在标准的多继承,所以这里我们只需要令其实现观察者的onEventHappen(type,value)接口,就能事实上令其成为一个具有观察者功能的类;
扩展后的小米控制台类代码如下:
/*
小米控制台
继承【组合】父类 实现添加组件、开始工作两个方法
*/
class XiaomiControl extends Compose {
constructor (){
// 调用父类方法为设备命名
super("小米总控台")
}
// 对【观察者】“父类”的事件响应接口做具体实现
// 成为一个“事实上”的观察者
onEventHappen(type,newValue){
switch (type) {
case "doorOpen":
if(newValue === true){
this.startWork()
}
break;
default:
break;
}
}
}
OK,一切准备就绪,接下来创建门铃,添加小米控制台为观察者,并触发开门事件
// 创建小米门铃实例
const xmdb = new XiaomiDoorbell()
// 添加小米控制台到doorOpen事件的观察者列表
xmdb.addListener("doorOpen",xmControl)
// 通过按钮、定时器等触发小米门铃开门,进而触发小米控制台响应并令所有设备开始工作
// 这里简化一下
xmdb.open()
完整代码如下:
<script>
/* 提供被观察数据的父类接口 */
class Observable {
constructor(){
// 预备存储事件类型和对应的观察者列表
this.typeListeners = {}
// 预备存储事件类型和对应的值
this.typeValue = {}
}
// 注册观察者
addListener(type,...listeners){
if(Object.prototype.hasOwnProperty.call(this.typeListeners,type)){
this.typeListeners[type].push(...listeners)
}else{
this.typeListeners[type] = [...listeners]
}
}
// 注销观察者
removeListener(type,listener){
if(Object.prototype.hasOwnProperty.call(this.typeListeners,type)){
for(let i=this.typeListeners[type].length-1;i>=0;i--){
const item = this.typeListeners[type][i]
if(item === listener){
this.typeListeners[type].splice(i,1)
}
}
}
}
// 引起数据变化
setValue(type,newValue){
this.typeValue[type] = newValue
// 数据变化触发事件
this.trigger(type,newValue)
}
// 触发观察者响应
trigger(type,newValue){
if(Object.prototype.hasOwnProperty.call(this.typeListeners,type)){
this.typeListeners[type].forEach(item=>{
item.onEventHappen(type,newValue)
})
}
}
}
/* 观察者父类 */
class Observer {
// 事件响应统一接口,留白待子类做具体实现
onEventHappen(type,newValue){}
}
/* 组合模式 【组合】父类定义 */
class Compose {
constructor (name){
this.name = name
// components 组件调度列表
this.components = []
}
// 添加组件到调度列表
addComponent(component){
this.components.push(component)
}
// 轮询所有组件 令其开始工作
startWork(){
this.components.forEach(component=>{
component.startWork()
})
}
}
/* 组合模式 【组件】父类定义 */
class Component {
// 接收组件名称
constructor(name){
this.name = name
}
// 所以组件统一调度接口 这里留白等待子类做具体实现
startWork(){}
}
// instance 实例 singleton 单例
let xmcInstance = null
function getXmcSingleton() {
xmcInstance = xmcInstance === null? new XiaomiControl() : xmcInstance
return xmcInstance
}
/* 小米门铃 实现被观察数据接口 开门关门就是事件 */
class XiaomiDoorbell extends Observable {
// 开门时数据变化 父类帮你会【触发事件】(被观察者所提供的接口)
open(){
this.setValue("doorOpen",true)
}
close(){
this.setValue("doorOpen",false)
}
}
/*
小米控制台
继承【组合】父类 实现添加组件、开始工作两个方法
*/
class XiaomiControl extends Compose {
constructor (){
// 调用父类方法为设备命名
super("小米总控台")
}
// 对【观察者】“父类”的事件响应接口做具体实现
// 成为一个“事实上”的观察者
onEventHappen(type,newValue){
switch (type) {
case "doorOpen":
if(newValue === true){
this.startWork()
}
break;
default:
break;
}
}
}
class XiaomiKongtiao extends Component {
constructor(){
super("小米空调")
}
// 对统一调度接口做具体实现
startWork(){
console.log(`${this.name}开始调节室温`);
}
}
class XiaomiTV extends Component {
constructor(){
super("小米电视")
}
// 对统一调度接口做具体实现
startWork(){
console.log(`${this.name}自动切换到您喜欢的节目`);
}
}
</script>