深入理解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 相关文章推荐
根据地区不同显示时间的javascript代码
Aug 13 Javascript
一个选择最快的服务器转向代码
Apr 27 Javascript
基于jquery实现的图片在各种分辨率下未知的容器内上下左右居中
May 11 Javascript
javascript截取字符串小结
Apr 28 Javascript
JS实现仿QQ效果的三级竖向菜单
Sep 25 Javascript
javascript合并表格单元格实例代码
Jan 03 Javascript
Javascript数组中push方法用法分析
Oct 31 Javascript
vue-router路由简单案例介绍
Feb 21 Javascript
原生JS实现自定义下拉单选选择框功能
Oct 12 Javascript
jquery分页优化操作实例分析
Aug 23 jQuery
js中offset,client , scroll 三大元素知识点总结
Sep 11 Javascript
vue实现跳转接口push 转场动画示例
Nov 01 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
杏林同学录(二)
2006/10/09 PHP
php表单转换textarea换行符的方法
2010/09/10 PHP
php网上商城购物车设计代码分享
2012/02/15 PHP
php生成图片缩略图的方法
2015/04/07 PHP
php7 图形用户界面GUI 开发示例
2020/02/22 PHP
javascript对象的property和prototype是这样一种关系
2007/03/24 Javascript
jQuery中的常用事件总结
2009/12/27 Javascript
jquery 得到当前页面高度和宽度的两个函数
2010/02/21 Javascript
Wordpress ThickBox 点击图片显示下一张图的修改方法
2010/12/11 Javascript
js 编码转换 gb2312 和 utf8 互转的2种方法
2013/08/07 Javascript
jQuery中多个元素的Hover事件解决方案
2014/06/12 Javascript
JavaScript 学习笔记之操作符
2015/01/14 Javascript
JavaScript实现算术平方根算法-代码超简单
2015/09/11 Javascript
JS组件Form表单验证神器BootstrapValidator
2016/01/26 Javascript
Java遍历集合方法分析(实现原理、算法性能、适用场合)
2016/04/25 Javascript
H5图片压缩与上传实例
2017/04/21 Javascript
Vue-cli3项目配置Vue.config.js实战记录
2018/07/29 Javascript
详解Vue Elementui中的Tag与页面其它元素相互交互的两三事
2018/09/25 Javascript
详解如何为你的angular app构建一个第三方库
2018/12/07 Javascript
js canvas实现橡皮擦效果
2018/12/20 Javascript
Node.js 获取微信JS-SDK CONFIG的方法示例
2019/05/21 Javascript
vue给对象动态添加属性和值的实例
2019/09/09 Javascript
如何优雅地取消 JavaScript 异步任务
2020/03/22 Javascript
python实现读取命令行参数的方法
2015/05/22 Python
python-str,list,set间的转换实例
2018/06/27 Python
Python爬虫小技巧之伪造随机的User-Agent
2018/09/13 Python
pip install python 快速安装模块的教程图解
2019/10/08 Python
pytorch实现mnist分类的示例讲解
2020/01/10 Python
Python 定义只读属性的实现方式
2020/03/05 Python
Pytorch使用PIL和Numpy将单张图片转为Pytorch张量方式
2020/05/25 Python
python读取xml文件方法解析
2020/08/04 Python
木工主管岗位职责
2013/12/08 职场文书
简历的自荐信
2013/12/19 职场文书
新学期小学班主任工作计划
2019/06/21 职场文书
南阳市白酒市场的调查报告
2019/11/08 职场文书
Python读取文件夹下的所有文件实例代码
2021/04/02 Python