如何一步一步打造高可扩展性的应用程序?

随着项目的规模越来越大,项目的维护性就可能会变得越来越差,有时可能会出现牵一发而动全身的情况。如果需要修改某个功能的代码,或者添加某项功能,会耗费大量的人力和时间。这种情况下,高可扩展性的、低耦合的应用程序就变得非常重要了。 

本文通过构建一个时钟程序,来讲解高扩展的应用程序是如何一步一步搭建的。 

什么是可扩展的应用程序?  

一个可扩展的应用程序应该能够以某种方式实现增长,并且添加、删除、增强、重构某些组件,对于其他组件的影响微乎其微。 

再大的应用程序,往往都是从很小的规模开始,然后一点一点发展起来的。但有时可能会由于增长过快,规模变得越来越大,导致项目难以管理,最终软件可能需要完全重写。 

开发人员在一开始编码时就要充分考虑到这种情况。本文以编写功能独立的JavaScript应用为例来说明如何构建可扩展的应用程序,同时还将讨论如何编写可测试、可维护、可调试、直观的代码。 

本文将使用 soma.js 框架来编写一个高可扩展性的JavaScript时钟应用。 

什么是soma.js?  

构建高扩展性的应用的要点在于构建组成该应用的小的、单个的模块。 soma.js 是一个JavaScript框架,它提供了一系列工具来帮助开发者创建一个可分解为若干个块的 松耦合 的架构。 

soma.js不依赖于特定的架构模式。该框架可以作为一个 MVC  或  MV*  框架,此外, soma.js也可以用来管理 独立的模块 、创建 独立的窗口小部件 或其他任何的体系结构。 

耦合问题  

让我们想象一种常见的场景,“A”组件需要组件“B”才能运行,这意味着A对B有一个直接的依赖。如果代码中的组件对彼此的依赖性非常大,就称为 高耦合的代码 。这种代码最终会导致项目很难维护和更改,一更改就会影响其他部分代码。 

高、低 耦合 的代码在开发人员的工作中有很大的差别,最直接的体现是,在修改部分模块代码所需的时间上,低耦合的代码可能需要5分钟,而高耦合的代码可能会需要5个小时。 
解决办法是——编写自包含、自封装、不影响其他组件的代码,最大化地减少依赖。这在理论上很简单,但实践起来非常难。 

同时,减少依赖还会带来了另一个问题:如果组件彼此之间无联系,那么组件之间如何进行通信?此时, 设计模式 就有了用武之地。 

soma.js中提供了一系列用于架构解耦和测试的工具,以及各种设计模式解决方案,比如 依赖注入 (dependency injection)、 观察者模式 (observer pattern)、 中介者模式 (mediator pattern)、 外观模式 (facade pattern)、 命令模式 (command pattern), 面向对象 (OOP)工具集,并提供了一个DOM操作模板引擎作为可选插件。 

示例应用  

下面来看一个示例应用,该应用可以在屏幕中创建3种不同的时钟:数字时钟、模拟时钟和极性时钟。可以通过不同的设计模式,来使得项目中的元素可以重用于该项目之外。 



观看演示: 时钟应用  

下面来看看这个程序是如何构建的。 

项目规划和元素去耦  

项目规划和功能分解(元素去耦)是编写代码前的一个非常重要的步骤,在下面的练习中,将有两种不同类型的函数: 

  • 没有依赖性的函数(理想情况下,所有的模型和视图都应该是这样,以便可重用)
  • 从其他组件中移除依赖性的函数
通过分析,该应用程序中应该包含以下这些不同的实体: 

  • 一个作为起始组件的应用实例——/js/app/clock.js (ClockDemo)。用于准备应用程序所需要的东西,作用是定义依赖关系和创建元素。它接收DOM元素来作为参数,所以应用程序中没有硬DOM引用。一个保存应用程序的状态(时间)的模型——/js/app/models/timer.js (TimerModel)。用于提供必要的时间信息,以便view层可以显示当前时间。
  • 3种时钟的视图。作用是在屏幕上以不同的方式显示一个时钟,所有视图实现相同的接口,以便它们可以以同样的方式收到时间信息。/js/app/views/clocks/analog/analog.js (AnalogView)、/js/app/views/clocks/digital/digital.js (DigitalView)/js/app/views/clocks/polar/polar.js (PolarView)
  • 一个中介者(mediator)——/js/app/mediators/clock.js (ClockMediator),表示一个DOM元素的。其作用是隐藏和创造时钟,它还连接timer模型到view层,以便可以接收时间。中介者封装了通信事件,并从model层和view层中消除了依赖。
  • 一个selector视图——/js/app/views/selector.js (SelectorView),用来创建3种不同时钟的按钮。其作用是调度事件,并通知元素需要删除当前的时钟,再创建一个新的时钟。
该应用的源码: somajs-flippin-clock-app  

应用程序的文件结构和体系结构如下图所示。 

 


对接口的思考  

尽管接口在JavaScript语言中不存在,但其广泛用于Java或其他语言中。因此,我们也可以在JavaScript程序中应用接口的概念。 

接口 是对一组公共方法和属性的描述。一个函数如果要实现接口,那么也需要去实现接口中的所有方法。 

在面向对象编程中,接口可以 解决许多代码重用相关的问题 。一些严格的JavaScript的超集(如 Typescript )也包含接口功能。 看下面这个例子 ,在Typescript中实现“汽车”接口的代码如下: 

Javascript代码 
  1. interface ICar {  
  2.     engine: IEngine;  
  3.     basePrice: number;  
  4.     state: string;  
  5.     make: string;  
  6.     model: string;  
  7.     year: number;  
  8. }  
  9.    
  10. class Car implements Icar {  
  11.     // must implement the ICar signature in this class  
  12. }  


在JavaScript中,接口不是一个内置的功能,但可以通过编写几个函数来实现相同的功能。 

首先开发者应该思考应用程序内的哪些元素的接口需要是独立、可重用的?比如,clock视图必须是可互换、可重用的,并提供完全相同的方法,以便它们可以在不影响其他元素的情况下进行互换。 

timer模式和clock视图的接口代码如下: 

Javascript代码 
  1. interface ITimerModel {  
  2.     add(callback: function);  
  3.     remove(callback: function);  
  4.     update();  
  5. }  
  6.    
  7. interface IClockView {  
  8.     update(time: Object);  
  9.     dispose();  
  10. }  


下图显示了时钟应用程序中的不同的接口实现: 



应用程序实例  

创建soma.js应用的第一步是创建一个应用程序实例,这是决定应用框架功能是否可扩展性的唯一一个重要时刻。所有的其他实体可以是可重复使用的JavaScript函数,并且可以不受框架约束。 

应用程序实例主要执行两个函数: init 和 start ,以便应用程序可以通过架构所需的功能来进行设置。 

Javascript代码 
  1. (function(clock, soma) {  
  2.    
  3.     var ClockDemo = soma.Application.extend({  
  4.         init: function() {  
  5.    
  6.         },  
  7.         start: function() {  
  8.    
  9.         }  
  10.     });  
  11.    
  12.     var clockDemo = new ClockDemo();  
  13.    
  14. })(window.clock = window.clock || {}, soma);  


更多信息,可查看soma.js 应用程序实例 文档。 

一个自包含的应用程序  

在应用程序内部使用一个DOM元素作为root是一个非常好的实践,这对于自包含的应用程序来说是非常有用的。任何DOM选择和操作都应该从这个root开始。 

此外,通常来说,建议使用CSS “class”选择器,而不是“ID”。因为使用“ID”可能会导致应用程序对于特定的DOM元素有硬依赖。 

Javascript代码 
  1. var ClockDemo = soma.Application.extend({  
  2.     constructor: function(element) {  
  3.         // store the root DOM Element  
  4.         this.element = element;  
  5.         // call the super constructor  
  6.         soma.Application.call(this);  
  7.     },  
  8.     init: function() {  
  9.    
  10.     },  
  11.     start: function() {  
  12.    
  13.     }  
  14. });  
  15.    
  16. var clockDemo = new ClockDemo(document.querySelector('.clock-app'));  


测试一个应用程序是否是自包含的简单方法是,在屏幕中创建多个实例,看它们是否能够独立工作。 

注入映射规则  

现在该应用程序已经有了一个基础架构,可以创建注射映射规则了。 

映射规则无非是指定一个函数,或为字符串指定一个值。字符串在其他地方可以作为“命名变量”使用,以便让注入器知道要注入什么。 

Javascript代码 
  1. this.injector.mapClass('timer', clock.TimerModel, true);  


该映射规则可以让注入器知道,当遇到timer变量时,应该注入clock.TimerModel函数的实例。第三个参数告诉注入器总是注入相同的实例,而不是创建一个新的。 

Javascript代码 
  1. this.injector.mapClass('face', clock.FaceView);  
  2. this.injector.mapClass('needleSeconds', clock.NeedleSeconds);  
  3. this.injector.mapClass('needleMinutes', clock.NeedleMinutes);  
  4. this.injector.mapClass('needleHours', clock.NeedleHours);  


由于模拟时钟已经被分为了几个视图,它需要上面的4个映射规则。 

Javascript代码 
  1. this.injector.mapValue('views', {  
  2.     'digital': clock.DigitalView,  
  3.     'analog': clock.AnalogView,  
  4.     'polar': clock.PolarView  
  5. });  


包含了所有不同时钟的对象在注入器中也应该被创建和映射。clock mediator负责创建使用这个对象的时钟,并实例化为正确的视图。 

实例化clock Mediator  

clock mediator用于表示被创建的时钟的DOM元素。第一个参数是实例化的mediator函数,第二个参数是它所表示的DOM元素。被注入的target变量用来表示DOM元素。 

创建使用框架核心要素“mediators”的mediator: 

Javascript代码 
  1. this.mediators.create(clock.ClockMediator, this.element.querySelector('.clock'));  


下面是clock mediator的代码: 

Javascript代码 
  1. (function(clock) {  
  2.     var ClockMediator = function(target) {  
  3.    
  4.     };  
  5.     clock.ClockMediator = ClockMediator;  
  6. })(window.clock = window.clock || {});  


实例化选择器视图  

选择器视图用来表示用于创建时钟的3个按钮的DOM元素。 

Javascript代码 
  1. (function(clock) {  
  2.     var SelectorView = function() {  
  3.    
  4.     };  
  5.     clock.SelectorView = SelectorView;  
  6. })(window.clock = window.clock || {});  


创建第一个时钟  

应用程序调用一个create事件来创建第一个时钟。 

Javascript代码 
  1. this.dispatcher.dispatch('create''analog');  


关于事件的详细信息可阅读 这个文档 。 

完整的应用程序实例代码  

应用程序实例clock.js的完整代码: clock.js源码  

Javascript代码 
  1. (function(clock, soma) {  
  2.    
  3.     var ClockDemo = soma.Application.extend({  
  4.         constructor: function(element) {  
  5.             // store root DOM Element  
  6.             this.element = element;  
  7.             // call super constructor  
  8.             soma.Application.call(this);  
  9.         },  
  10.         init: function() {  
  11.             // mapping rules  
  12.             this.injector.mapClass('timer', clock.TimerModel, true);  
  13.             this.injector.mapClass('face', clock.FaceView);  
  14.             this.injector.mapClass('needleSeconds', clock.NeedleSeconds);  
  15.             this.injector.mapClass('needleMinutes', clock.NeedleMinutes);  
  16.             this.injector.mapClass('needleHours', clock.NeedleHours);  
  17.             this.injector.mapValue('views', {  
  18.                 'digital': clock.DigitalView,  
  19.                 'analog': clock.AnalogView,  
  20.                 'polar': clock.PolarView  
  21.             });  
  22.             // create clock mediator  
  23.             this.mediators.create(clock.ClockMediator, this.element.querySelector('.clock'));  
  24.             // create clock selector template  
  25.             this.createTemplate(clock.SelectorView, this.element.querySelector('.clock-selector'));  
  26.         },  
  27.         start: function() {  
  28.             // dispatch event to create an analog clock  
  29.             this.dispatcher.dispatch('create''analog');  
  30.         }  
  31.     });  
  32.    
  33.     // instantiate clock application with a root DOM Element  
  34.     var clockDemo = new ClockDemo(document.querySelector('.clock-app'));  
  35.    
  36. })(window.clock = window.clock || {}, soma);  


Timer模型  

Timer模型的作用是为其他元素提供当前时间,而无需知道其他元素的相关信息。它的接口提供了两种方法:add和remove。它们都带有一个用于发送当前时间的参数。 

Javascript代码 
  1. (function(clock) {  
  2.     var TimerModel = function() {  
  3.    
  4.     };  
  5.     TimerModel.prototype.add = function(callback) {  
  6.         // register functions  
  7.     };  
  8.     TimerModel.prototype.remove = function(callback) {  
  9.         // remove registered functions  
  10.     };  
  11.     clock.TimerModel = TimerModel;  
  12. })(window.clock = window.clock || {});  


timer模型没有依赖性,它不实例化其他函数,并且只要它们实现相同的接口(add和remove)就可以互相交换,应用程序中的其他元素(如视图和clock mediator)不会被修改。 
即使该模型被用在应用程序中的其他地方,也只需修改下面的几行代码: 

Javascript代码 
  1. var ModelFunction = isOnline ? ServerTimeModel : TimeModel;  
  2. this.injector.mapClass('timer', ModelFunction, true);  


可查看 Github上的这部分源码 。 

选择器视图  

选择器视图的唯一作用是处理用户事件。当用户点击一个按钮时,视图获取这次点击事件,并调度一个自定义事件,由clock mediator进行监听并创建一个新的时钟。 

此处使用 soma-template (可作为一个独立的库或soma.js插件)来监听用户的点击事件。 

Javascript代码 
  1. <div class="clock-selector">  
  2.     <button data-click="select('digital')">Digital clock</button>  
  3.     <button data-click="select('analog')">Analog clock</button>  
  4.     <button data-click="select('polar')">Polar clock</button>  
  5. </div>  
  6.    
  7. (function(clock) {  
  8.     var SelectorView = function(scope, dispatcher) {  
  9.         scope.select = function(event, id) {  
  10.             dispatcher.dispatch('create', id);  
  11.         };  
  12.     };  
  13.     clock.SelectorView = SelectorView;  
  14. })(window.clock = window.clock || {});  


可查看 Github上的这部分源码 ,以及  soma-template 文档。 

时钟视图  

应用程序中的3种类型的时钟视图: 

  • Analog view (clock.AnalogView);
  • DigitalView (clock.DigitalView);
  • PolarView (clock.PolarView);
通过timer模型,视图变得高度可重用,因为: 

  • 它们无需知道其他的应用程序元素
  • 它们是自由的框架代码
  • 它们提供了一个简单的API来更新其当前状态
它们的视图之间是可以互换的,因为它们都提供了相同的接口: 

  • 一个接收DOM元素的构造函数
  • 一个接收当前事件的update方法
这使得它们高度可重用。下面是数字时钟的视图结构: 

Javascript代码 
  1. (function(clock) {  
  2.     var DigitalView = function(target) {  
  3.    
  4.     };  
  5.     DigitalView.prototype.update = function(time) {  
  6.    
  7.     };  
  8.     DigitalView.prototype.dispose = function() {  
  9.    
  10.     };  
  11.     clock.DigitalView = DigitalView;  
  12. })(window.clock = window.clock || {});  


可查看Github上的如下相关源码: 

依赖注入图  



时钟应用的单元测试  

单元测试 的目的是隔离应用程序的每一部分,并测试各个部分是否正确。单元测试是应用程序是否可扩展的一个非常重要的一步。 

时钟应用程序中的元素应该是高可测试的,因为它们彼此之间不耦合。这就是依赖注入带来的好处。本例使用 Mocha Jasmine 进行测试,这是两个使用广泛的JavaScript单元测试框架,你可以通过  Mocking 对象轻松创建模拟函数,并单独测试每个元素。 

在浏览器中测试时钟应用  
查看集成测试的源码  

单元测试也可以在命令行中运行。这需要使用 NPM 安装依赖: 

代码 
  1. $ npm install  
  2. $ npm install -g mocha  


运行测试: 

代码 
  1. $ npm test  




结论  

要创建一个可扩展的应用程序,或要将现有应用程序改为可扩展的,离不开这两个过程:分析问题和解决问题。首先要考虑以下两个问题: 

  • 是什么使得应用程序具有可扩展性?
  • 如何使应用程序具有可扩展性?
是什么使得应用程序具有可扩展性?  

可以通过不同的方法来找出应用程序的可扩展级别。首先,要看应用程序中的所有元素是否满足如下要求: 

  • 这个元素应该被重用吗?
  • 这是元素是可测试的吗?
  • 这个元素是否有依赖性?
  • 这个元素的目的是单一的吗?
如果一个元素很难被测试的,或者其包含的功能太多,或者有太多的依赖,那么这个元素就应该加以改进,以使应用程序具有可扩展性。 

如何使应用程序具有可扩展性?  

下面这个列表中的每一项任务都可以用来提高应用程序的可扩展性。 

  • 标识非单一用途的元素,并分解它们
  • 找出“坏味道代码”并重构
  • 避免代码重复(DRY)
  • 避免大的函数
  • 避免匿名函数
  • 一个可用的、公共的、可测试的API
  • 使用基于构造函数或setter方法的引用,避免实例化对象
  • 尽可能消除依赖
  • 使用观察者模式(事件)来移除依赖和发送信息
  • 创建元素的mediators来移除依赖和接收消息
  • 尽可能地实现清晰的接口
  • 隐藏或私有化与其他元素无关的一些内容。
  • 建议尽可能使用组合(Composition),而不是继承(inheritance)
当出现下面的这些情况时,说明元素已经具有可扩展性了: 

  • 该元素可以很容易地与其他元素进行互换,而不会破坏应用程序
  • 该元素可以轻松重用于项目外部
  • 该元素可以成功地进行单元测试
通常,你需要找到 坏味道代码 ,然后进行重构和改进。坏味道代码是对代码中存在的潜在问题的警示信号,对于大多数坏味道,均有必要加以查看并做出相应决定。代码坏味道是需要重构的征兆。 

最后来分析本文所创建的时钟应用  

在时钟应用程序中,框架中被依赖的两个元素是: 

  • 应用实例
  • clock mediator
高度独立和高可用的元素是: 

  • timer模型
  • clock视图
为了实现高扩展性和重用性,应用程序已经被分解,所有元素的目的都是单一的: 

  • timer模型用于处理时间。
  • clock mediator用于隐藏和创建时钟
  • 选择器视图用于处理用户事件
  • clock视图创建在屏幕上创建时钟外观
  • 中介者模式被用于移除timer模型和clock视图的依赖关系,如果没有mediator,它们将会对彼此有一个直接依赖。
  • 观察者模式(创建事件)被用来从其他一些元素中分离出选择器视图,其他元素可以监听相同的框架事件,这也使得应用程序更具可扩展性,
  • 依赖注入被用来发送所有元素的参考引用,并解耦它们,使它们高度可测试。
  • clock视图接收构造函数中的DOM元素的参考引用,这使得它们可以与任何其他的DOM元素一起使用。此外,还提供了一个公共API来更新自己的内容,使得其他元素可以在外部使用它,而无需了解它的相关信息。
  • timer模型提供了一个接口来添加和删除回调,使得它可以发送当前时间到一组已注册的actors中,而无需了解它们的相关信息。
以上这种结构使得项目的代码更容易重构,可以轻松更改项目结构、添加或移除元素、测试每个组件、更新单个元素,而无需担心影响整个应用程序。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值