Preface
In this chapter, we are going to explain the fifth chapter of the implementation of the five major principles of SOLID JavaScript language, which is the LSP (The Dependency Inversion Principle).
Original English text: http://freshbrewedcode.com/derekgreeer/2012/01/22/solid-javascript-the-dependency-inversion-principle/
Reliance inversion principle
The description of the principle of dependency inversion is:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
High-level modules should not depend on low-level modules, both should rely on abstraction
B. Abstractions should not depend upon details. Details should depend upon abstractions.
Abstract should not depend on details, details should depend on abstraction
The most important issue with the principle of dependency inversion is to ensure that the main components of the application or framework are decoupled from the implementation details of non-important underlying component, which will ensure that the most important parts of the program are not affected by changes and modifications of the low-level components.
The first part of this principle is about the coupling between high-level modules and low-level modules. In the traditional division architecture, high-level modules (encapsulate the core business logic of the program) always rely on some low-level modules (some basic points). When the principle of dependency inversion is applied, the relationship is reversed. Unlike high-level modules that rely on low-level modules, dependency inversion makes low-level modules rely on interfaces defined in high-level modules. For example, if you want to persist data for a program, the traditional design is that the core module depends on the API of a persistent module. After reconstruction according to the principle of dependency inversion, the core module needs to define a persistent API interface, and then the persistent implementation instance needs to implement the API interface defined by the core module.
The second part of this principle describes the correct relationship between abstraction and detail. Understanding this part is more helpful by understanding C++ language because its applicability is more obvious.
Unlike some statically typed languages, C++ does not provide a language-level concept to define interfaces. What is the relationship between class definition and class implementation? In C++, classes are defined in the form of header files, which define the class member methods and variables that the source file needs to implement. Because all variables and private methods are defined in the header file, they can be used to abstract and decouple them before implementation details. The concept of interface is implemented by defining only abstract methods (the abstract base class in C++) is used to implement classes.
DIP and JavaScript
Because JavaScript is a dynamic language, there is no need to abstract for decoupling. Therefore, abstraction should not rely on details. This change does not have much impact in JavaScript, but high-level modules should not rely on low-level modules but have a great impact.
When discussing the principle of dependency inversion in the context of statically typed languages, the concepts of coupling include semantic and physical. This means that if a high-level module depends on a low-level module, it not only couples the semantic interface, but also the physical interface defined in the underlying module. In other words, high-level modules must not only be decoupled from third-party library, but also from native low-level modules.
To explain this, imagine that a .NET program might contain a very useful high-level module that relies on a low-level persistent module. When the author needs to add a similar interface to the persistence API, regardless of whether the dependency inversion principle is used or not, high-level modules cannot be reused in other programs before re-implementing the new interface of this low-level module.
In JavaScript, the applicability of the principle of dependency inversion is limited to the semantic coupling between high-level modules and low-level modules. For example, DIP can add interfaces as needed instead of coupling implicit interfaces defined by low-level modules.
To understand this, let's take a look at the following example:
The code copy is as follows:
$.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
});
In the above code, there is a small JS class library that converts a DIV into a Map to display the currently tracked location information. The trackMap function has 2 dependencies: the third-party Google Maps API and Location feed. The responsibility of the feed object is to call a callback callback (provided at initialization) when the icon position is updated and pass in the latitude latitude and precision longitude. The Google Maps API is used to render interfaces.
The interface of the feed object may be based on installation or not designed according to the requirements of the installation trackMap function. In fact, its role is very simple, focusing on simple different implementations and does not need to rely so much on Google Maps. The trackMap semantics are coupled to the Google Maps API. If you need to switch to different map providers, you have to rewrite the trackMap function so that it can adapt to different providers.
In order to flip the semantic coupling of the Google maps class library, we need to rewrite the design trackMap function to semantic coupling an implicit interface (abstract the interface of the map provider provider). We also need an implementation object that is adapted to the Google Maps API. The following is the refactored trackMap function:
The code copy is as follows:
$.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
});
In this version, we redesigned the trackMap function and the required map provider interface, and then moved the implementation details to a separate googleMapsProvider component, which may be independently encapsulated into a separate JavaScript module. Here is my googleMapsProvider implementation:
The code copy is as follows:
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);
}
};
})();
After making the above changes, the trackMap function will become very flexible and does not have to rely on the Google Maps API. Instead, other map providers can be replaced at will, that is, any map provider can be adapted to according to the needs of the program.
When is dependency injection?
It is a bit unrelated. In fact, the concept of dependency injection is often mixed with the principle of dependency inversion. In order to clarify this difference, it is necessary to explain:
Dependency injection is a special form of control inversion, and inversion means how a component acquires its dependencies. Dependency injection means: the dependency is provided to the component, rather than the component to obtain the dependency, which means creating an instance of the dependency, requesting the dependency through the factory, and requesting the dependency through the Service Locator or the component itself. The dependency inversion principle and dependency injection are both focused on dependencies and are both used for inversion. However, the principle of dependency inversion does not focus on how components acquire dependencies, but only on how high-level modules are decoupled from low-level modules. In a sense, the principle of dependency inversion is another form of control inversion. Here, the inversion is which module defines the interface (defined from the lower level, inversion to the higher level).
Summarize
This is the last article of the five major principles. In these 5 articles, we see how SOLID is implemented in JavaScript. Different principles are explained from different angles in JavaScript. (Uncle Note: In fact, I think that although it is a bit inappropriate, from another perspective, the general principles are actually the same in various languages.)