Interfaces are one of the most useful features in the object-oriented Javascript toolbox. As we all know, in the design mode, GOF says: interface-oriented programming, rather than implementation-oriented programming, is enough to show how important interfaces are in the Object-Oriented field. However, JavaScript does not have built-in interface mechanisms like other object-oriented advanced languages (such as C #, Java, and C ++, to determine that a group of objects and another group of objects contain similar features. Fortunately, JS has powerful flexibility (I have mentioned above), which makes it very easy to imitate the interface features. What is the interface?
Interfaces provide unified method definitions for classes with similar behaviors (which may be of the same type or different types) to enable communication between these classes.
What are the advantages of using interfaces? Simply put, it can improve the reusability of similar modules of the system and make the communication between different classes more stable. Once an interface is implemented, all methods of the interface must be implemented. For large Web projects, similar functional modules of multiple complex modules can provide an implementation by providing interfaces without affecting each other. However, we must make it clear that interfaces are not omnipotent. Because Javascript is a weak type language, you cannot force other team members to strictly follow the interfaces you provide, however, you can use code specifications and helper classes to alleviate this problem. In addition, the system performance may also be affected, depending on the complexity of your system requirements. Because there is no built-in interface and implements keywords, let's take a look at how JS imitates the implementation interface.
1. The simplest and worst-performing method is to use annotations. That is, the interface is used to describe the intent of the interface in the annotation.
The Code is as follows:
/*
Interface Composite {
Function add (child );
Function remove (child );
Function getChild (index );
}
Interface FormItem {
Funtion save ();
}
*/
Var CompositeForm = function (id, name, action ){
// Implements Composite, FormItem
}
CompositeForm. prototype = {
// Implements Composite interface
Add: function (child ){
//...
},
Remove: function (child ){
//...
},
GetChild: function (index ){
//...
}
// Implements FormItem interface
Save: function (){
//...
}
}
This does not provide a good interface simulation function and ensures that the Composite class does implement a set of methods, nor does it throw an error to notify programmers of the problem. In addition to the description, it does not play any role, all consistency must be implemented by programmers. However, it is easy to implement and does not require additional classes or functions. It does not affect the document size and execution speed. Annotations can also be easily separated, which improves reusability to a certain extent, the class description can be used to communicate with other classes that implement the same interface.
2. Use attributes to check the simulated interface. Class display declares the interface to be implemented, and checks whether the corresponding interface is implemented through the attribute.
The Code is as follows:
/*
Interface Composite {
Function add (child );
Function remove (child );
Function getChild (index );
}
Interface FormItem {
Funtion save ();
}
*/
Var CompositeForm = function (id, name, action ){
This. implementsInterfaces = ["Composite", "FormItem"];
//...
}
Function checkInterfaces (formInstance ){
If (! Implements (formInstance, "Composite", "FormItem ")){
Throw new Error ("Object doesn't implement required interface .");
}
//...
}
// Check to see if an instance object declares that it implements the required interface
Function implements (instance ){
For (var I = 1; I <arguments. length; I ++ ){
Var interfaceName = arguments [I];
Var isFound = false;
For (var j = 0; j <instance. implementsInterfaces. length; j ++ ){
If (interfaceName = instance. implementsInterfaces [j]) {
IsFound = true;
Break;
}
}
If (! IsFound) return false; // An interface was not found.
}
Return true; // All interfaces were found.
}
Note is added to describe the interface. However, an implementsInterfaces attribute is added to the Composite class, indicating that the class must implement those interfaces. Check this attribute to determine whether to implement the corresponding interface. If it is not implemented, an error is thrown. However, the disadvantage is that you still cannot determine whether the corresponding interface method is actually implemented. It is only "self-claimed" that the interface is implemented and the corresponding workload is increased.
3. Implement the interface with "duck-type identification. From the attribute check implementation, it is irrelevant to find whether a class supports the implemented interface. As long as all the methods in the interface appear in the corresponding places in the class, it is enough to indicate that the interface has been implemented. It is like "if you walk like a duck, as a duck, whether it is labeled as a duck or not, we think it is a duck ". The helper class is used to determine whether a class exists (implemented) all the methods in the corresponding interface. If the class does not exist, it indicates that it is not implemented.
The Code is as follows:
// Interfaces
Var Composite = new Interface ("Composite", ["add", "remove", "getChild"]);
Var FormItem = new Interface ("FormItem", ["save"]);
Var CompositeForm = function (id, name, action ){
// Implements Composite, FormItem interfaces
}
Function checkInterfaces (formInstance ){
Interface. ensureImplements (formInstance, "Composite", "FormItem ");
//...
}
Interface Class
The Code is as follows:
// Interface class is for checking if an instance object implements all methods of required interface
Var Interface = function (name, methods ){
If (arguments. length! = 2 ){
Throw new Error ("Interface constructor expects 2 arguments, but exactly provided for" + arguments. length + "arguments .");
}
This. name = name;
This. methods = [];
For (var I = 0; I <methods. length; I ++ ){
If (typeof methods [I]! = "String "){
Throw new Error ("Interface constructor expects to pass a string method name .");
}
This. methods. push (methods [I]);
}
}
// Static class method
Interface. ensureImplements = function (instance ){
If (arguments. length <2 ){
Throw new Error ("Function Interface. ensureImplements expects at least 2 arguments, but exactly passed for" + arguments. length + "arguments .");
}
For (var I = 1, len = arguments. length; I <len; I ++ ){
Var interface = arguments [I];
If (interface. constructor! = Interface ){
Throw new Error ("Function Interface. ensureImplements expects at least 2 arguments to be instances of Interface .");
}
For (var j = 0, mLen = interface. methods. length; j <mLen; j ++ ){
Var method = interface. methods [j];
If (! Instance [method] | typeof instance [method]! = "Function "){
Throw new Error ("Function Interface. ensureImplements: object doesn't implements" + interface. name + ". Method" + method + "wasn't found .");
}
}
}
}
Strict type check is not always necessary. The above interface mechanism is rarely used in front-end Web development. However, interface-Oriented Programming becomes very important when you face a complex system, especially a system with many similar modules. It seems that the flexibility of JS is reduced, but the flexibility of the class is actually improved, and the coupling between classes is reduced, because when you input any object that implements the same interface, it can be correctly parsed. When is the interface suitable? For a large project, there must be many team members, and the project will be split into more fine-grained functional modules. To ensure the progress, you must use the placeholder Program (Interface) in advance) to describe the functions of a module or communicate with a developed module, it is necessary to provide a unified interface (API. As the project progresses, the demand may change constantly, and the functions of each module may change accordingly. However, the communication between each other and the APIS provided to the upper-layer modules remain unchanged, ensure the stability and durability of the entire architecture. A specific example is provided to illustrate the actual application of the interface. Assume that a class of automatic detection result object (TestResult class) is designed and formatted to output a webpage view. The interface is not implemented as follows:
The Code is as follows:
Var ResultFormatter = function (resultObject ){
If (! (ResultObject instanceof TestResult )){
Throw new Error ("ResultFormatter constructor expects a instance of TestResult .");
}
This. resultObject = resultObject;
}
ResultFormatter. prototype. render = function (){
Var date = this. resultObject. getDate ();
Var items = this. resultObject. getResults ();
Var container = document. createElement ("p ");
Var header = document. createElement ("h3 ");
Header. innerHTML = "Test Result from" + date. toUTCString ();
Container. appendChild (header );
Var list = document. createElement ("ul ");
Container. appendChild (list );
For (var I = 0, len = items. length; I ++ ){
Var item = document. createElement ("li ");
Item. innerHTML = items [I];
List. appendChild (item );
}
Return container;
}
First, the ResultFormatter class constructor only checks whether it is a TestResult instance, but cannot guarantee that the methods getDate () and getResults () in render are implemented (). In addition, as demand changes, there is now a Weather class that contains the getDate () and getResults () methods, but it is unable to run the render method because it can only check whether it is a TestResult instance, isn't it speechless? The solution is to remove the instanceof check and replace it with an interface.
The Code is as follows:
// Create the ResultSet interface
Var ResultSet = new Interface ("ResultSet", ["getDate", "getResults"]);
Var ResultFormatter = function (resultObject ){
// Using Interface. ensureImplements to check the resultObject
Interface. ensureImplements (resultObject, ResultSet );
This. resultObject = resultObject;
}
ResultFormatter. prototype. render = function (){
// Keep the same as former
Var date = this. resultObject. getDate ();
Var items = this. resultObject. getResults ();
Var container = document. createElement ("p ");
Var header = document. createElement ("h3 ");
Header. innerHTML = "Test Result from" + date. toUTCString ();
Container. appendChild (header );
Var list = document. createElement ("ul ");
Container. appendChild (list );
For (var I = 0, len = items. length; I ++ ){
Var item = document. createElement ("li ");
Item. innerHTML = items [I];
List. appendChild (item );
}
Return container;
}
We can see that the render method has not changed. The only change is to add an interface and use the interface for type check. At the same time, we can now pass Weather class instances for calling. Of course, we can also pass instances of any class implementing the ResultSet interface, making the check more accurate and tolerant. With the introduction of JS Design Patterns in the future, interfaces will be widely used in factory, combination, decoration, and command modes. I hope you can savor the benefits of interfaces for our JS modular design.