深入理解JavaScript系列(22):S.O.L.I.D五大原则之依赖倒置原则DIP详解


Posted in Javascript onMarch 05, 2015

前言

本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第5篇,依赖倒置原则LSP(The Dependency Inversion Principle )。

英文原文:http://freshbrewedcode.com/derekgreer/2012/01/22/solid-javascript-the-dependency-inversion-principle/
依赖倒置原则

依赖倒置原则的描述是:

A. High-level modules should not depend on low-level modules.  Both should depend on abstractions.
   高层模块不应该依赖于低层模块,二者都应该依赖于抽象

B. Abstractions should not depend upon details.  Details should depend upon abstractions.
   抽象不应该依赖于细节,细节应该依赖于抽象
依赖倒置原则的最重要问题就是确保应用程序或框架的主要组件从非重要的底层组件实现细节解耦出来,这将确保程序的最重要的部分不会因为低层次组件的变化修改而受影响。

该原则的第一部分是关于高层模块和低层模块之间的耦合方式,在传统的分成架构中,高层模块(封装了程序的核心业务逻辑)总依赖于低层的一些模块(一些基础点)。当应用依赖倒置原则的时候,关系就反过来了。和高层模块依赖于低层模块不同,依赖倒置是让低层模块依赖于高层模块里定义的接口。举例来说,如果要给程序进行数据持久化,传统的设计是核心模块依赖于一个持久化模块的API,而根据依赖倒置原则重构以后,则是核心模块需要定义持久化的API接口,然后持久化的实现实例需要实现核心模块定义的这个API接口。

该原则的第二部分描述的是抽象和细节之间的正确关系。理解这一部分,通过了解C++语言比较有帮助,因为他的适用性比较明显。

不像一些静态类型的语言,C++没有提供一个语言级别的概念来定义接口,那类定义和类实现之间到底是怎么样的呢,在C++里,类通过头文件的形式来定义,其中定义了源文件需要实现的类成员方法和变量。因为所有的变量和私有方法都定义在头文件里,所以可以用来抽象以便和实现细节之前解耦出来。通过定只定义抽象方法来实现(C++里是抽象基类)接口这个概念用于实现类来实现。

DIP and JavaScript

因为JavaScript是动态语言,所以不需要去为了解耦而抽象。所以抽象不应依赖于细节这个改变在JavaScript里没有太大的影响,但高层模块不应依赖于低层模块却有很大的影响。

在当静态类型语言的上下文里讨论依赖倒置原则的时候,耦合的概念包括语义(semantic)和物理(physical)两种。这就是说,如果一个高层模块依赖于一个低层模块,也就是不仅耦合了语义接口,也耦合了在底层模块里定义的物理接口。也就是说高层模块不仅要从第三方类库解耦出来,也需要从原生的低层模块里解耦出来。

为了解释这一点,想象一个.NET程序可能包含一个非常有用的高层模块,而该模块依赖于一个低层的持久化模块。当作者需要在持久化API里增加一个类似的接口的时候,不管依赖倒置原则有没有使用,高层模块在不重新实现这个低层模块的新接口之前是没有办法在其它的程序里得到重用的。

在JavaScript里,依赖倒置原则的适用性仅仅限于高层模块和低层模块之间的语义耦合,比如,DIP可以根据需要去增加接口而不是耦合低层模块定义的隐式接口。

为了来理解这个,我们看一下如下例子:

$.fn.trackMap = function(options) {

    var defaults = {

        /* defaults */

    };

    options = $.extend({}, defaults, options);
    var mapOptions = {

        center: new google.maps.LatLng(options.latitude,options.longitude),

        zoom: 12,

        mapTypeId: google.maps.MapTypeId.ROADMAP

    },

        map = new google.maps.Map(this[0], mapOptions),

        pos = new google.maps.LatLng(options.latitude,options.longitude);
    var marker = new google.maps.Marker({

        position: pos,

        title: options.title,

        icon: options.icon

    });
    marker.setMap(map);
    options.feed.update(function(latitude, longitude) {

        marker.setMap(null);

        var newLatLng = new google.maps.LatLng(latitude, longitude);

        marker.position = newLatLng;

        marker.setMap(map);

        map.setCenter(newLatLng);

    });
    return this;

};
var updater = (function() {

    // private properties
    return {

        update: function(callback) {

            updateMap = callback;

        }

    };

})();
$("#map_canvas").trackMap({

    latitude: 35.044640193770725,

    longitude: -89.98193264007568,

    icon: 'http://bit.ly/zjnGDe',

    title: 'Tracking Number: 12345',

    feed: updater

});

在上述代码里,有个小型的JS类库将一个DIV转化成Map以便显示当前跟踪的位置信息。trackMap函数有2个依赖:第三方的Google Maps API和Location feed。该feed对象的职责是当icon位置更新的时候调用一个callback回调(在初始化的时候提供的)并且传入纬度latitude和精度longitude。Google Maps API是用来渲染界面的。

feed对象的接口可能按照装,也可能没有照装trackMap函数的要求去设计,事实上,他的角色很简单,着重在简单的不同实现,不需要和Google Maps这么依赖。介于trackMap语义上耦合了Google Maps API,如果需要切换不同的地图提供商的话那就不得不对trackMap函数进行重写以便可以适配不同的provider。

为了将于Google maps类库的语义耦合翻转过来,我们需要重写设计trackMap函数,以便对一个隐式接口(抽象出地图提供商provider的接口)进行语义耦合,我们还需要一个适配Google Maps API的一个实现对象,如下是重构后的trackMap函数:

$.fn.trackMap = function(options) {

    var defaults = {

        /* defaults */

    };
    options = $.extend({}, defaults, options);
    options.provider.showMap(

        this[0],

        options.latitude,

        options.longitude,

        options.icon,

        options.title);
    options.feed.update(function(latitude, longitude) {

        options.provider.updateMap(latitude, longitude);

    });
    return this;

};
$("#map_canvas").trackMap({

    latitude: 35.044640193770725,

    longitude: -89.98193264007568,

    icon: 'http://bit.ly/zjnGDe',

    title: 'Tracking Number: 12345',

    feed: updater,

    provider: trackMap.googleMapsProvider

});

在该版本里,我们重新设计了trackMap函数以及需要的一个地图提供商接口,然后将实现的细节挪到了一个单独的googleMapsProvider组件,该组件可能独立封装成一个单独的JavaScript模块。如下是我的googleMapsProvider实现:
trackMap.googleMapsProvider = (function() {

    var marker, map;
    return {

        showMap: function(element, latitude, longitude, icon, title) {

            var mapOptions = {

                center: new google.maps.LatLng(latitude, longitude),

                zoom: 12,

                mapTypeId: google.maps.MapTypeId.ROADMAP

            },

                pos = new google.maps.LatLng(latitude, longitude);
            map = new google.maps.Map(element, mapOptions);
            marker = new google.maps.Marker({

                position: pos,

                title: title,

                icon: icon

            });
            marker.setMap(map);

        },

        updateMap: function(latitude, longitude) {

            marker.setMap(null);

            var newLatLng = new google.maps.LatLng(latitude,longitude);

            marker.position = newLatLng;

            marker.setMap(map);

            map.setCenter(newLatLng);

        }

    };

})();

做了上述这些改变以后,trackMap函数将变得非常有弹性了,不必依赖于Google Maps API,相反可以任意替换其它的地图提供商,那就是说可以按照程序的需求去适配任何地图提供商。

何时依赖注入?

有点不太相关,其实依赖注入的概念经常和依赖倒置原则混在一起,为了澄清这个不同,我们有必要来解释一下:

依赖注入是控制反转的一个特殊形式,反转的意思一个组件如何获取它的依赖。依赖注入的意思就是:依赖提供给组件,而不是组件去获取依赖,意思是创建一个依赖的实例,通过工厂去请求这个依赖,通过Service Locator或组件自身的初始化去请求这个依赖。依赖倒置原则和依赖注入都是关注依赖,并且都是用于反转。不过,依赖倒置原则没有关注组件如何获取依赖,而是只关注高层模块如何从低层模块里解耦出来。某种意义上说,依赖倒置原则是控制反转的另外一种形式,这里反转的是哪个模块定义接口(从低层里定义,反转到高层里定义)。

总结

这是五大原则的最后一篇了,在这5篇文字里我们看到了SOLID如何在JavaScript里实现的,不同的原则在JavaScript里通过不同的角度来说明的。(大叔注:其实大叔觉得虽然是有点不伦不类,但从另外一个层面上说,大体的原则在各种语言上其实还是一样的。)

Javascript 相关文章推荐
EXTJS内使用ACTIVEX控件引起崩溃问题的解决方法
Mar 31 Javascript
javascript开发技术大全 第4章 直接量与字符集
Jul 03 Javascript
js/jQuery对象互转(快速操作dom元素)
Feb 04 Javascript
判断文件是否正在被使用的JS代码
Dec 21 Javascript
在JavaScript中使用timer示例
May 08 Javascript
node.js中使用node-schedule实现定时任务实例
Jun 03 Javascript
基于vue+ bootstrap实现图片上传图片展示功能
May 17 Javascript
Node.js学习之TCP/IP数据通讯(实例讲解)
Oct 11 Javascript
微信小程序实现topBar底部选择栏效果
Jul 20 Javascript
json 带斜杠时如何解析的实现
Aug 12 Javascript
Vue实现星级评价效果实例详解
Dec 30 Javascript
vue中解决chrome浏览器自动播放音频和MP3语音打包到线上的实现方法
Oct 09 Javascript
PHP 数组current和next用法分享
Mar 05 #Javascript
深入理解JavaScript系列(21):S.O.L.I.D五大原则之接口隔离原则ISP详解
Mar 05 #Javascript
深入理解JavaScript系列(19):求值策略(Evaluation strategy)详解
Mar 05 #Javascript
如何实现chrome浏览器关闭页面时弹出“确定要离开此面吗?”
Mar 05 #Javascript
深入理解JavaScript系列(18):面向对象编程之ECMAScript实现
Mar 05 #Javascript
基于zepto.js实现仿手机QQ空间的大图查看组件ImageView.js详解
Mar 05 #Javascript
基于jQuery实现网页进度显示插件
Mar 04 #Javascript
You might like
PHP5在Apache下的两种模式的安装
2006/09/05 PHP
PHP正确解析UTF-8字符串技巧应用
2012/11/07 PHP
提高PHP编程效率的方法
2013/11/07 PHP
php操作csv文件代码实例汇总
2014/09/22 PHP
PHP封装分页函数实现文本分页和数字分页
2014/10/23 PHP
ThinkPHP3.2.2的插件控制器功能
2015/03/05 PHP
Yii框架使用PHPExcel导出Excel文件的方法分析【改进版】
2019/07/24 PHP
使用PHP+Redis实现延迟任务,实现自动取消订单功能
2019/11/21 PHP
js函数般调用正则
2008/04/08 Javascript
JQUBAR1.1 jQuery 柱状图插件发布
2010/11/28 Javascript
jquery实现预览提交的表单代码分享
2014/05/21 Javascript
js实现感应鼠标图片透明度变化的方法
2015/02/20 Javascript
jquery实现键盘左右翻页特效
2015/04/30 Javascript
angular6 填坑之sdk的方法
2018/12/27 Javascript
webpack结合express实现自动刷新的方法
2019/05/07 Javascript
Vue页面跳转传递参数及接收方式
2020/09/09 Javascript
ES11屡试不爽的新特性,你用上了几个
2020/10/21 Javascript
在漏洞利用Python代码真的很爽
2007/08/26 Python
Python守护进程(daemon)代码实例
2015/03/06 Python
Python OpenCV实现图片上输出中文
2018/01/22 Python
django 多数据库配置教程
2018/05/30 Python
python框架中flask知识点总结
2018/08/17 Python
Python将一个Excel拆分为多个Excel
2018/11/07 Python
python对视频画框标记后保存的方法
2018/12/07 Python
python变量赋值方法(可变与不可变)
2019/01/12 Python
Django Rest framework权限的详细用法
2019/07/25 Python
Python datetime包函数简单介绍
2019/08/28 Python
python3.x 生成3维随机数组实例
2019/11/28 Python
python 和c++实现旋转矩阵到欧拉角的变换方式
2019/12/04 Python
解决PyCharm不在run输出运行结果而不是再Console里输出的问题
2020/09/21 Python
python3爬虫中多线程的优势总结
2020/11/24 Python
应届生学校辅导员求职信
2013/11/07 职场文书
文史专业毕业生自荐信
2013/11/17 职场文书
小学生倡议书范文
2014/05/13 职场文书
2014年信用社工作总结
2014/11/25 职场文书
首次购房证明
2015/06/19 职场文书