大型 web 前端架构设计.docx

上传人:b****5 文档编号:8399225 上传时间:2023-01-31 格式:DOCX 页数:7 大小:21.65KB
下载 相关 举报
大型 web 前端架构设计.docx_第1页
第1页 / 共7页
大型 web 前端架构设计.docx_第2页
第2页 / 共7页
大型 web 前端架构设计.docx_第3页
第3页 / 共7页
大型 web 前端架构设计.docx_第4页
第4页 / 共7页
大型 web 前端架构设计.docx_第5页
第5页 / 共7页
点击查看更多>>
下载资源
资源描述

大型 web 前端架构设计.docx

《大型 web 前端架构设计.docx》由会员分享,可在线阅读,更多相关《大型 web 前端架构设计.docx(7页珍藏版)》请在冰豆网上搜索。

大型 web 前端架构设计.docx

大型web前端架构设计

大型web前端架构设计

svenzeng腾讯PCG前端开发工程师

面向抽象编程是构建一个大型系统非常重要的参考原那么。

但对于许多前端同学来讲对面向抽象编程的理讲解不上很深入。

大局部同学的习惯是拿到需求单以及设计稿之后就开场编写UI界面UI里哪个按钮需要调哪些方法接下来再编写这些方法很少去考虑复用性。

当某天发生需求变更时才发现目前的代码很难适应这些变更只能重写。

日复一日如此循环。

当第一次看到“将抽象以及详细实现分开〞这句话的时候可能很难明白它表达的是什么意思。

什么是抽象什么又是详细实现为了理解这段话我们耐下性子先看一个假想的小例子回忆下什么是面向详细实现编程。

假设我们正在开发一个类似“模拟人生〞的程序并且创造了小明为了让他的每一天都有规律的生活下去于是给他的核心程序里设置了如下逻辑

1、8点起床2、9点吃面包3、17点打篮球

过了一个月小明厌倦了一成不变的重复生活某天早上起来之后他突然想吃薯片而不是面包。

等到黄昏的时候他想去踢足球而不是继续打篮球于是我们只好修改源代码

1、8点起床2、9点吃面包-9点吃薯片3、17点打篮球-17点踢足球

又过了一段时间小明祈望周3以及周5踢足球星期天打羽毛球这时候为了知足需求我们的程序里可能会被加进很多if、else语句。

为了知足需求的变换跟现实世界很相似我们需要深化核心源代码做大量改动。

如今再想想自己的代码里是不是有很多似曾相识的场景

这就是一个面向详细实现编程的例子在这里吃面包、吃薯片、打篮球、踢足球这些动作都属于详细实现映射到程序中它们就是一个模块、一个类或一个函数包含着一些详细的代码去负责某件详细的事情。

一旦我们想在代码中更改这些实现必然需要被迫深化以及修改核心源代码。

当需求发生变更时一方面假如核心代码中存在各种各样的大量详细实现想去全部重写这些详细实现的工作量是宏大的另一方面修改代码总是会带来未知的风险当模块间的联络千丝万缕时修改任何一个模块都得小心翼翼否那么很可能发生改好1个bug多出3个bug的情况。

抽取出共同特性

抽象的意思是从一些事物中抽取出共同的、本质性的特征。

假如我们总是针对详细实现去编写代码就像上面的例子要么写死9点吃面包要么写死9点吃薯片。

这样一来在业务开展以及系统迭代经过中系统就会变得僵硬以及修改困难。

产品需求总是多变的我们需要在多变的环境里尽量让核心源代码保持稳定以及不用修改。

方法就是需要抽取出“9点吃面包〞以及“9点吃薯片〞的通用特性这里可以用“9点吃早餐〞来表示这个通用特性。

同理我们抽取出“17点打篮球〞以及“17点踢足球〞的通用特性用“17点做运动〞来代替它们。

然后让这段核心源代码去依赖这些“抽象出来的通用特性〞而不再是依赖到底是“吃面包〞还是“吃早餐〞这种“详细实现〞。

我们将这段代码写成

1、8点起床2、9点吃早餐3、17点做运动

这样一来这段核心源代码就变得相对稳定多了不管以后小明早上想吃什么都无需再改动这段代码只要在后期由外层程序将“吃早餐〞还是“吃薯片〞注入进来即可。

真实例如

刚刚是一个虚拟的例子如今看一段真实的代码这段代码仍然很简单但可以很好的讲明抽象的好处。

在某段核心业务代码里需要利用localstorge储存一些用户的操作信息代码很快就写好了

import‘localstorge’fromlocalstorgeclassUser{save(){localstorge.save(xxx}}constusernewUser();user.save();

这段代码本来工作的很好但是有一天我们发现用户信息相关数据量太大,超过了localstorge的储存容量。

这时候我们想到了indexdb似乎用indexdb来存储会更加合理一些。

如今我们需要将localstorge换成indexdb于是不得不深化User类将调用localstorge的地方修改为调用indexdb。

似乎又回到了熟悉的场景我们发现程序里在许多核心业务逻辑深处不只一个而是有成百上千个地方调用了localstorge这个简单的修改都成了灾难。

所以我们仍然需要提取出localstorge以及indexdb的共同抽象局部很显然localstorge以及indexdb的共同抽象局部就是都会向它的消费者提供一个save方法。

作为它的消费者也就是业务中的这些核心逻辑代码并不关心它到底是localstorge还是indexdb这件事情完全可以等到程序后期再由更外层的其他代码来决定。

我们可以申明一个拥有save方法的接口

interfaceDB{save():

void;}

然后让核心业务模块User仅仅依赖这个接口

importDBfromDBclassUser{constructor(privatedb:

DB){}save(){this.db.save(xxx}}

接着让Localstorge以及Indexdb分别实现DB接口

classLocalstorgeimplementsDB{save(str:

string){...//dosomething}}classIndexdbimplementsDB{save(str:

string){...//dosomething}}constusernewUser(newLocalstorge());//orconstusernewUser(newIndexdb());userInfo.save();

这样一来User模块从依赖Localstorge或Indexdb这些详细实现变成了依赖DB接口User模块成了一个稳定的模块不管以后我们到底是用Localstorge还是用IndexdbUser模块都不会被迫随之进展改动。

让修改远离核心源代码

可能有些同学会有疑问固然我们不用再修改User模块但还是需要去选择到底是用Localstorge还是用Indexdb我们总得在某个地方改动代码把这以及去改动User模块的代码有什么区别呢

实际上我们讲的面向抽象编程通常是针对核心业务模块而言的。

User模块是属于我们的核心业务逻辑我们祈望它是尽量稳定的。

不想仅仅因为选择使用Localstorge还是Indexdb这种事情就得去改动User模块。

因为User模块这些核心业务逻辑一旦被不小心改坏了就会影响到千千万万个依赖它的外层模块。

假如User模块如今依赖的是DB接口那它被改动的可能性就变小了很多。

不管以后的本地存储怎么开展只要它们还是对外提供的是save功能那User模块就不会因为本地存储的变化而发生改变。

相对详细行为而言接口总是相对稳定的因为接口一旦要修改意味着详细实现也要随之修改。

而反之当详细行为被修改时接口通常是不用改动的。

至于选择到底是用Localstorge还是用Indexdb这件事情放在那里做有很多种实现方式通常我们会把它放在更容易被修改的地方也就是远离核心业务逻辑的外层模块举几个例子

*在main函数或其他外层模块中生成Localstorge或Indexdb对象在User对象被创立时作为参数传给User*用工厂方法创立Localstorge或Indexdb*用依赖注入的容器来绑定DB接口以及它详细实现之间的映射内层、外层以及单向依赖关系

将系统分层就像建筑师会将大厦分为很多层每层有特有的设计以及功能这是构建大型系统架构的根底。

除了过时的mvc分层架构方式外目前常用的分层方式有洋葱架构整洁架构、DDD领域驱动设计架构、六边形架构端口-适配器架构等这里不会详细介绍每个分层形式但不管是洋葱架构、DDD架构、还是六边形架构它们的层与层之间都会被相对而动态地区分为外层以及内层。

前面我们也提过好几次内层以及外层的概念大局部书里称为高层以及低层那么在实际业务中哪些模块会对应内层而哪些模块应该被放在外层到底由什么规律来决定呢

先观察下自然届地球围绕着太阳转我们认为太阳是内层地球是外层。

眼睛接收光线后通过大脑成像我们认为大脑是内层眼睛是外层。

当然这里的内层以及外层不是由物理位置决定的而是基于模块的稳定性即越稳定越难修改的模块应该被放在越内层而越易变越可能发生修改的模块应该被放在越外层。

就像用积木搭建房子时我们需要把最巩固的积木搭在下面。

这样的规那么设置是很有意义的因为一个成熟的分层系统都会严格遵守单向依赖关系。

我们看下面这个图:

mark

假设系统中被分为了A、B、C、D这4层那么A是相对的最内层外层依次是B、C、D。

在一个严格单向依赖的系统中依赖关系总是只能从外层指向内层。

这是因为假如最内层的A模块被修改那么依赖A模块的B、C、D模块都会分别受到牵连。

在静态类型语言中这些模块因为A模块的改动都要重新进展编译而假如它们引用了A模块的某个变量或调用了A模块中的某个方法那么它们很可能因为A模块的修改而需要随之修改。

所以我们祈望A模块是最稳定的它最好永远不要发生修改。

但假如外层的模块被修改呢比方D模块被修改之后因为它处在最外层没有其他模块依赖它它影响的仅仅是自己而已A、B、C模块都不需要担忧它们收到任何影响所以当外层模块被修改时对系统产生的破坏性相对是比拟小的。

假如从一开场就把容易变化经常跟着产品需求变更的模块放在靠近内层那意味着我们经常会因为这些模块的改动不得不去跟着调整或测试系统中依赖它的其他模块。

可以设想一下造物者也许也是基于单向依赖原那么来设置宇宙以及自然界的比方行星依赖恒星没有地球并不会对太阳造成太大影响而假如失去了太阳地球自然也不存在。

眼睛依赖大脑大脑坏了眼睛自然失去了作用但眼睛坏了大脑的其他功能还能使用。

看起来地球只是太阳的一个插件而眼睛只是大脑的一个插件。

回到详细的业务开发核心业务逻辑一般是相对稳定的而越接近用户输入输出的地方越接近产品经理以及设计师比方UI界面那么越不稳定。

比方开发一个股票交易软件股票交易的核心规那么是很少发生变化的但系统的界面长成什么样子很容易发生变化。

所以我们通常会把核心业务逻辑放在内层而把接近用户输入输出的模块放在外层。

在腾讯文档业务中核心业务逻辑指的就是将用户输入数据通过一定的规那么进展计算转换成文档数据。

这些转换规那么以及详细计算经过是腾讯文档的核心业务逻辑它们是非常稳定的从微软office到谷歌文档到腾讯文档30多年度了也没有过多变化它们理应被放在系统的内层。

另一方面不管这些核心业务逻辑跑在阅读器、终端或是node端它们也都不应该变化。

而网络层、存储层离线层、用户界面这些是易变的在终端环境里终端用户界面层以及web层的实现就完全不一样。

在node端存储层或者答应以直接从系统中剔除掉因为在node端我们只需要利用核心业务逻辑模块对函数进展一些计算。

同理在单元测试或集成测试的时候离线层以及存储层可能都是不需要的。

在这些易变的情况下我们需要把非核心业务逻辑都放在外层方便它们被随时修改或者交换。

所以遵守单向依赖原那么能极大进步系统稳定性减少需求变更时对系统的破坏性。

我们在设计各个模块的时候要将相当多的时间花在设计层级、模块的切分和层级、模块之间的依赖关系上我们常讲“分而治之〞“分〞就是指层级、模块、类等怎样切分“治〞就是指怎样将分好的层级、模块、类合理的联络起来。

这些设计比详细的编码细节工作要更加重要。

依赖反转原那么

依赖反转原那么的核心思想是内层模块不应该依赖外层模块,它们都应该依赖于抽象。

尽管我们会花很多时间去考虑哪些模块分别放到内层以及外层尽量保证它们处于单向依赖关系。

但在实际开发中总还是有不少内层模块需要依赖外层模块的场景。

比方在Localstorge以及Indexdb的例子里User模块作为内层的核心业务逻辑却依赖了外层易变的Localstorge以及Indexdb模块导致User模块变得不稳定。

import‘localstorge’fromlocalstorgeclassUser{save(){localstorge.save(xxx}}constusernewUser();user.save();

缺图

为解析决User模块的稳定性问题我们引入了DB抽象接口这个接口是相对稳定的User模块改为去依赖DB抽象接口进而让User变成一个稳定的模块。

InterfaceDB{save():

void;}

然后让核心业务模块User仅仅依赖这个接口

importDBfromDBclassUser{constructor(privatedb:

DB){}save(){this.db.save(xxx}}

接着让Localstorge以及Indexdb分别实现DB接口

classLocalstorgeimplementsDB{save(str:

string){...//dosomething}}

依赖关系变成缺图

User-DB-Localstorge

在图1以及图2看来User模块不再显式的依赖Localstorge而是依赖稳定的DB接口DB到底是什么会在程序后期由其他外层模块将Localstorge或Indexdb注入进来这里的依赖关系看起来被反转了这种方式被称为“依赖反转〞。

找到变化并将其抽象以及封装出来

我们的主题“面向抽象编程〞很多时候其实就是指的“面向接口编程〞面向抽象编程站在系统设计的更宏观角度指导我们怎样构建一个松散的低耦合系统而面向接口编程那么告诉我们详细实现方法。

依赖倒置原那么告诉我们怎样通过“面向接口编程〞让依赖关系总是从外到内指向系统中更稳定的模块。

知易行难面向抽象编程固然概念上不难理解但在真实施行中却总是不太容易。

哪些模块应该被抽象哪些依赖应该被倒转系统中引入多少抽象层是合理的这些问题都没有标准答案。

我们在接到一个需求对其进展模块设计时要先分析这个模块以后有没有可能随着需求变更被交换或者是被大范围修改重构当我们发现可能会存在变化之后就需要将这些变化封装起来让依赖它的模块去依赖这些抽象。

比方上面例子中的Localstorge以及indexdb有经历的程序会很容易想到它们是有可能需要被相互交换的所以它们最好一开场就被设计为抽象的。

同理我们的数据库可以能产生变化也许今天使用的是mysql但明年度可能会交换为oracle那么我们的应用程序里就不应该强依赖mysql或oracle而是要让它们依赖mysql以及oracle的公共抽象。

再比方我们经常会在程序中使用ajax来传输用户输入数据但有一天可能会想将ajax交换为websocket的恳求那么核心业务逻辑也应该去依赖ajax以及websocket的公共抽象。

封装变化与设计形式

实际上常见的23种设计模块都是从封装变化的角度被总结出来的。

拿创立型形式来讲要创立一个对象是一种抽象行为而详细创立什么对象那么是可以变化的创立型形式的目的就是封装创立对象的变化。

而构造型形式封装的是对象之间的组合关系。

行为型形式封装的是对象的行为变化。

比方工厂形式通过将创立对象的变化封装在工厂里让核心业务不需要依赖详细的实现类也不需要解析太多的实现细节。

当创立的对象有变化的时候我们只需改开工厂的实现就可以对核心业务逻辑没有造成影响。

比方模块方法形式封装的是执行流程顺序子类会继承父类的模版函数并按照父类设置好的流程规那么执行下去详细的函数实现细节那么由子类自己来负责实现。

通过封装变化的方式可以把系统中稳定不变的局部以及容易变化的局部隔分开来。

在系统的演变经过中只需要交换或修改那些容易变化的局部假如这些局部是已经封装好的交换起来也相对容易。

这可以最大程度地保证程序的稳定性。

防止过度抽象

固然抽象进步了程序的扩展性以及灵敏性但抽象也引入了额外的间接层带来了额外的复杂度。

本来一个模块依赖另外一个模块这种依赖关系是最简单直接的但我们在中间每增加了一个抽象层就意味着需要一直关注以及维护这个抽象层。

这些抽象层被参加系统中必然会增加系统的层次以及复杂度。

假如我们判断某些模块相对稳定很长时间内都不会发生变化那么没必要一开场就让它们成为抽象。

比方java中的String类它非常稳定所以并没有对String做什么抽象。

比方一些工具方法类似utils.getCookie()我很难想象5年度内有什么东西会代替cookie所以我更喜欢直接写getCookie。

比方腾讯文档excel的数据model它属于内核中的内核像整个身体中的骨骼以及经脉已经融入到了各个应用逻辑中它被交换的可能性非常小难度也非常大不亚于重写一个腾讯文档excel所以也没有必要对model做过度抽象。

结语

面向抽象编程有2个最大好处。

一方面面向抽象编程可以将系统中经常变化的局部封装在抽象里保持核心模块的稳定。

另一方面面向抽象编程可以让核心模块开发者从非核心模块的实现细节中解放出来将这些非核心模块的实现细节留在后期或留给其别人。

这篇文章讨论的实际主要侧重第一点即封装变化。

封装变化是构建一个低耦合松散系统的关键。

这篇文章作为面向抽象编程的入门祈望能帮助一些同学认识面向抽象编程的好处和掌握一些根底的面向抽象编程的方法。

腾讯技术工程

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 初中教育

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1