深刻理解JavaScript系列10:S.O.L.I.D五大原则之依赖倒置原则

前言

本章咱们要讲解的是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/javascript

依赖倒置原则

依赖倒置原则的描述是:html

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.
抽象不该该依赖于细节,细节应该依赖于抽象java

依赖倒置原则的最重要问题就是确保应用程序或框架的主要组件从非重要的底层组件实现细节解耦出来,这将确保程序的最重要的部分不会由于低层次组件的变化修改而受影响。git

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

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

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

DIP and JavaScript

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

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

为了解释这一点,想象一个.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 APILocation feed。该feed对象的职责是当icon位置更新的时候调用一个callback回调(在初始化的时候提供的)而且传入纬度latitude和精度longitudeGoogle 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里经过不一样的角度来讲明的。(大叔注:其实大叔以为虽然是有点不三不四,但从另一个层面上说,大致的原则在各类语言上其实仍是同样的。)

关于本文

本文转自TOM大叔深刻理解JavaScript系列。关于S.O.L.I.D系列的五篇文章我纠结了好久,原本不想去整理的,但最终发现其实中间说的不少都是关于OOP(面向对象)编码原则的东西,十分值得研读,因此最后仍是决定整理出来。
整理完这五篇我真的是醉了,尤为最后的三篇,越看越看不懂,不知道是否是由于原文比较晦涩难懂,致使大叔本人翻译的也比较拗口,反正最终的结果就是后面的三篇,我也只是看了个只知其一;不知其二。有什么问题,欢迎讨论交流。

【深刻理解JavaScript系列】文章,包括了原创,翻译,转载,整理等各种型文章,原文是TOM大叔的一个很是不错的专题,现将其从新整理发布。谢谢大叔。若是你以为本文不错,请帮忙点个推荐,支持一把,感激涕零。

更多优秀文章欢迎关注个人专栏

相关文章
相关标签/搜索