外观模式
外观模式,其实对于良好的设计系统软件绝对能看到外观模式的身影,如果要说它是一种设计模式,倒不如说它是一种设计思想。
在软件开发中,一旦你理解了封装 、继承 、多态的这些面向对象的特征,你就一定掌握了外观模式,即使在这之前你没有听说过它。
1、基本概念
外观模式(Facade),为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这个子系统更加容易使用。
外观模式是依赖倒置原则和迪米特法则的体现。
依赖倒置原则强调两个点:
- 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
在外观模式中,高层的客户端代码不直接与系统的复杂底层(如多个子系统或模块)交互,而是通过一个外观接口进行交互。这个外观接口提供了一个简化的方式来访问底层功能,而底层模块的复杂性被封装在外观后面。因此,客户端代码依赖于这个抽象的外观接口,而不是具体的实现细节,这正是依赖倒置原则的体现。
迪米特法则又被称为"最少知道原则",其核心思想是"一个对象应该对其他对象有最少的了解。",外观模式就通过把底层一些复杂的实现细节封装起来,降低了系统的耦合性,这正是迪米特法则的体现。
外观模式的UML
图如下:
在这个图里面,SubSystem Classes框选起来的那一部分我们都不需要关注,只有一个Facade类对外暴露了这个系统的能力,你就只需要按照API调用即可。
2、代码范式
这个代码范式仅仅是一个非业务场景的例子,因为外观模式展示的是一种设计思想,开发中需要根据自己的业务酌情处理即可。
ts
class SubsystemOne {
public methodOne() {
console.log('do work in method one');
}
}
class SubsystemTwo {
public methodTwo() {
console.log('do work in method two');
}
}
class SubsystemThree {
public methodThree() {
console.log('do work in method three');
}
}
class SubSystemFour {
public methodFour() {
console.log('do work in method four');
}
}
class Facade {
// 全部声明为私有属性,对外隐藏实现细节
private one = new SubsystemOne();
private two = new SubsystemTwo();
private three = new SubsystemThree();
private four = new SubsystemFour();
// 对外暴露doWork1方法1
public doWork1() {
this.one.methodOne();
this.three.methodThree();
this.four.methodFour();
}
// 对外暴露doWork2方法2
public doWork2() {
this.two.methodTwo();
this.four.methodFour();
}
}
function bootstrap() {
const facade = new Facade();
facade.doWork1();
facade.doWork2();
}
3、在前端开发中的实践
3.1、LocalForage
localForage 中文文档 (docschina.org)
localForage 是一个 JavaScript 库,通过简单类似 localStorage
API 的异步存储来改进你的 Web 应用程序的离线体验。它能存储多种类型的数据,而不仅仅是字符串。
localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。
js
// 通过 localStorage 设置值
localStorage.setItem('key', JSON.stringify('value')); doSomethingElse();
// 通过 localForage 完成同样功能
localforage.setItem('key', 'value').then(doSomethingElse);
// localForage 同样支持回调函数
localforage.setItem('key', 'value', doSomethingElse);
我们可以保持跟localStorage大致相同的API的前提下,能够满足我们的存储需求。
比如,如果在前端缓存一些二进制数据流,数据可能会比较大,对于localStorage
来说,一个域下面仅仅5M左右,(而且流文件转换到字符串还需要进行编码)满足不了我们的需求,localForage自动为我们切换到IndexedDB存储的话,我们使用起来就跟使用localStorage那么简单,在这个过程中,localForage底层是有很复杂的处理流程的,只不过它把这些细节都隐藏起来了,我们看到的就是预期的数据被存储到了指定的位置而已,所以说,这就是一种外观模式的实际场景。
3.2 Axios
这个是每个前端开发者都熟悉的不能再熟悉的一个库了。
浏览器暴露给我们的XMLHttpRequest
是很复杂的,如果我们直接使用XHR对象的话,编程效率低。
而经过Axios
封装之后,我们再操作XHR就简单的几个配置就完成了,这也是外观模式的实际场景。
还有一个关键点,在不同的环境下,想要发送请求的方式是不一样的,在浏览器中,使用XHR
对象或者fetch
,在Nodejs环境中,使用node的原生模块http
,如果没有axios
这个外观类,那我们还需要考虑不同的环境,而有了它之后,我们仅仅只需要无脑的按照axios
的配置发送请求即可。
3.3 NestJS中的分层设计
因为严格来说,很多npm包都是为了解决某个领域下编程的便利性而进行的封装,所以它们都是外观模式的体现,我如果继续举这种例子的话,就有点儿水文的感觉了,因此,这节中,给大家一个我的实际编码例子。
我以我的最近所开发的BFF来举例。
业务逻辑层仅仅需要获取数据访问层拿到的数据,至于数据访问层怎么拿?用什么方式拿?业务逻辑层不用管,反正我就只需要给你预期规格的数据即可。
以下是我的代码设计:
首先是,数据访问层的接口定义
ts
/**
* 数据访问层的接口定义
*/
export interface IActEngineRepository {
/**
* 获取活动配置
* @param param0
* @returns
*/
getConfig({ userId, actId }: ActGeneralParams): Promise<ConfigsListDto>;
/**
* 获取活动的信息
* @param param
* @returns
*/
getActivityInfo({ userId, actId }: ActGeneralParams): Promise<ActInfoDto>;
/**
* 获取榜单信息
* @param param0
*/
getFullRankInfo({ userId, actId, rankId }: ActRankParams): Promise<RankDto>;
}
然后是通过gRPC
获取业务服务器的数据的实现(省略了一部分代码,仅仅表达一个业务封装的含义)
ts
/*
* 以gRPC通信方式获取业务后端的数据
*/
@Injectable()
export class ActEngineGrpcRepository
extends BaseActEngineRepository
implements IActEngineRepository
{
@Inject()
protected readonly actParserService: ActEngineEntityParserService;
/**
* 获取活动配置
*/
@AutoBind
public async getConfig({
userId,
actId,
}: ActGeneralParams): Promise<ConfigsListDto> {
const configPromise = promisify<Empty, Metadata, Web.ConfigsReply>(
this.client.configs,
);
const p = configPromise();
const configs = await this.safetyPromise(p).then((response) =>
response.toObject(),
);
return configs;
}
/**
* 获取活动的信息
* @param param
* @returns
*/
@AutoBind
public async getActivityInfo({
userId,
actId,
}: ActGeneralParams): Promise<ActInfoDto> {
const actInfoPromise = promisify<Empty, Metadata, Web.ActInfoReply>(
this.client.actInfo,
);
const p = actInfoPromise();
const activityInfo = await this.safetyPromise(p).then((response) =>
response.toObject(),
);
return activityInfo;
}
/**
* 获取完整的榜单信息,内聚了获取列表和自己所处该榜单中位置的信息,隐藏了细节
* @returns
*/
public async getFullRankInfo({
userId,
actId,
rankId,
size,
}: ActRankParams): Promise<RankDto> {
const rankList = await this.getRankList({ userId, rankId, actId, size });
let own: ActRankItem;
if (userId) {
own = await this.getOwnInfo({ userId, rankId, actId });
} else {
own = null;
}
return {
list: rankList.listList,
own,
};
}
// 私有方法,内部细节,不对外暴露
private async getRankList({
userId,
rankId,
actId,
dateTime,
size = 100,
}: ActRankParams): Promise<
Omit<Web.RankDetailReply.AsObject, 'listList'> & {
listList: Array<ActRankItem>;
}
> {
const rankPromise = promisify<
Web.RankDetailRequest,
Metadata,
Web.RankDetailReply
>(this.client.rankDetail);
const request: Web.RankDetailRequest = new Web.RankDetailRequest();
const p = rankPromise(request);
const rankInfo = await this.safetyPromise(p).then((response) =>
response.toObject(),
);
return {
...rankInfo,
listList: rankInfo.listList.map((v) => {
return this.getRankDetail(v);
}),
};
}
// 私有方法,内部细节,不对外暴露
private getRankDetail(input: CommonEntity.RankItem.AsObject): ActRankItem {
return omit(
{
...input,
detail: input.user || input.work || input.room,
},
['user', 'work', 'room'],
);
}
// 私有方法,内部细节,不对外暴露
private async getOwnInfo({
userId,
rankId,
actId,
dateTime,
}: ActRankParams): Promise<ActRankItem> {
const rankPromise = promisify<
Web.RankMemberRequest,
Metadata,
CommonEntity.RankItem
>(this.client.rankMember);
const request: Web.RankMemberRequest = new Web.RankMemberRequest();
const p = rankPromise(request);
const rankInfo = await this.safetyPromise(p).then((response) =>
response.toObject(),
);
return this.getRankDetail(rankInfo);
}
}
然后对外暴露的是实现类,而依赖注入的依据是一个特征的Token信息:
ts
@Module({
providers: [
{
provide: ACT_ENGINE_REPO_TOKEN,
useClass: ActEngineGrpcRepository,
},
],
exports: [ACT_ENGINE_REPO_TOKEN],
})
export class ActEngineModule {}
最后,业务逻辑层使用数据外观类:
ts
@Injectable()
export class CollectionService {
@Inject(ACT_ENGINE_REPO_TOKEN)
protected readonly actEngineRepo: IActEngineRepository;
// 获取活动的配置信息
public getActConfig(params: ActParams) {
return this.actEngineRepo.getConfig(params);
}
}
这样的设计,一方面可以根据需求灵活的切换和业务后端的通信方式,也能隔绝业务后端的变更,起到一种"防腐"(业务后端的调整,不至于影响到下游的软件设计,不明白的同学可以查看我的这篇文章->设计模式在前端开发中的实践(六)------适配器模式 - 掘金 (juejin.cn))的作用。
总结
外观模式是一个几乎必用的设计模式,如果你之前不知道什么是外观模式,也能写出可维护性较好的代码,那么请你现在也忘掉什么是外观模式(因为你知道的多了,可能反而会对你形成一种束缚,实战开发中总结出来的经验,要比在书中按图索骥好的多)。
在软件设计的过程中,我们应该有意识的考虑到软件的分层,层与层之间就形成外观模式的Facade;另外,就是我们在维护屎山的时候,我们可以开发一个Facade来包裹住这个屎山(就好比家丑不外扬,哈哈哈),这样在外部调用的时候,可以有效降低出错的可能性。
所谓实践出真知,大家就根据自己的理解去编码吧,Good Luck!
如果大家喜欢我的文章,可以多多点赞收藏加关注,你们的认可是我最好的更新动力,😁。