New thinking of API design: construct internal DSL with smooth interface
The abstract mechanism of programming language includes two basic aspects: one is the basic elements/semantics that the language pays attention to; The other is the construction rules from basic element/semantics to composite element/semantics. In C, C++, Java, C #, Python and other common languages, the basic elements/semantics of the language are often far away from the problem domain. The most common way to reduce the difficulty of the problem is to abstract it layer by layer in the form of API libraries. For example, the most common way in C language is to provide a function library to encapsulate complex logic and facilitate external calls. ( Beijing website production )
However, there is a natural trap in the ordinary API design method, that is, no matter how encapsulated, the big process is more abstract than the small process, but it is still a process in essence, subject to the constraints of process semantics. In other words, when constructing higher-level abstract elements/semantics through basic elements/semantics, the construction rules of the language largely limit the abstract dimension. It is difficult for us to jump out of this dimension, or even we may not be aware of this restriction. The abstract dimensions of SQL, HTML, CSS, make and other DSLs (domain specific languages) are tailored for specific domains. From these abstract perspectives, the problem is often the simplest, so DSLs are more convenient than general programming languages in solving problems in their specific domains. Generally, non universal languages such as SQL are called external DSL; In general language, we can actually break through the abstract dimension limitation of language construction rules to some extent and define internal DSL.
This article will introduce an internal DSL design method called fluent interface. The definition of fluent interface on Wikipedia is:
A fluent interface (as first coined by Eric Evans and Martin Fowler) is an implementation of an object oriented API that aims to provide for more readable code. A fluent interface is normally implemented by using method chaining to relay the instruction context of a subsequent call (but a fluent interface entails more than just method chaining)。 |
The following will be divided into four parts to gradually describe the typical application of fluent interface in constructing internal DSL.
1. Basic semantic abstraction
If you want to output the 5 numbers 0.. 4, we usually think of codes like this first:
- //Java
- for (int i = zero ; i < five ; ++i) {
- system.out.println(i);
- }
-
While Ruby also supports similar for loops, the simplest implementation is as follows:
- //Ruby
- .times {|i| puts i}
In Ruby, everything is an object. 5 is an instance of the Fixnum class, and times is a method of Fixnum, which accepts a block parameter. Compared with the implementation of the for loop, Ruby's times method is simpler and more readable. But friends familiar with OOP may have questions about whether times should be used as an integer class method? In OOP, method calls usually represent sending messages to objects, changing or querying the status of objects. The times method is obviously not a query or modification of the status of integer objects. If you were the designer of Ruby, would you put the times method into the Fixnum class? If the answer is no, what does this design of Ruby represent in essence? In fact, although the times here is just a common class method, its purpose is different from that of the common class method. Its semantics are actually similar to the basic semantics of the language such as the for loop, and can be regarded as a user-defined basic semantics. The semantics of times, to a certain extent, has jumped out of the box of class methods and taken a step towards the problem domain!
Another example comes from Eric Evans's "Using Two Time Points to Construct a Time Period Object", a common design:
- 3 //Java
- TimePoint fiveOClock, sixOClock;
- TimeInterval meetingTime = new TimeInterval(fiveOClock, sixOClock);
-
Another Evans design is as follows:
- 2 //Java
- TimeInterval meetingTime = fiveOClock .until(sixOClock);
According to the traditional OO design, the until method should not appear in the TimePoint class. Here, the until method of the TimePoint class also represents a basic user-defined semantics, which makes the problem of expressing the time domain more natural.
Although the above two simple examples do not show much advantage over ordinary design, they lay the foundation for us to understand smooth interfaces. It is important to realize that they have escaped from the shackles of the basic abstraction mechanism of language to a certain extent. We should not treat them with OO design principles such as class responsibility division, Law of Demeter, etc.
2. Pipeline abstraction
In the shell, we can combine a series of small commands to achieve complex functions through pipelines. The flow in the pipeline is a single type of text stream. The calculation process is a transformation process from the input stream to the output stream. Each command is a transformation action on the text stream, which is superimposed through the pipeline. In the shell, many times we can complete small and medium-sized problems such as log statistics in one sentence. Compared with other abstraction mechanisms, the beauty of pipelines lies in their non nesting. For example, the following C program is not easy to understand at once because of its deep nesting level:
- 2 //C
- min(max(min(max(a,b),c),d),e)
It is much clearer to use pipes to express the same function:
-
- 2 #!/ bin/bash
- max a b | min c | max d | min e
-
We can easily understand the meaning expressed in this program: first, find the maximum value of a and b; Then take the minimum value of the result and c; Then calculate the maximum value of the result and d; Then find the minimum value of the result and e.
The chained invocation design of jQuery also has the style of pipeline. jQuery objects of the same type flow on the method chain. Each step of method invocation is an action on the object. The entire method chain superimposes the actions of each method.
- 2 //Javascript
- $('li').filter(':event').css('background-color', 'red');
-
3. Hierarchy abstraction
In addition to the "linear" structure of pipes, fluent interfaces can also be used to construct hierarchical abstractions. For example, use Javascript to dynamically create the following HTML fragments:
- < div id = "’product_123’" class = "’product’" >
- < img src = "’preview_123.jpg’" alt = "" />
- < ul >
- < li > Name: iPad2 32G </ li >
- < li > Price: 3600 </ li >
- </ ul >
- </ div >
-
If Javascript DOM API is used:
- //Javascript
- var div = document .createElement('div');
- div.setAttribute(‘id’, ‘product_123’);
- div.setAttribute(‘class’, ‘product’);
-
- var img = document .createElement('img');
- img.setAttribute(‘src’, ‘preview_123.jpg’);
- div.appendChild(img);
-
- var ul = document .createElement('ul');
- var li1 = document .createElement('li');
- var txt1 = document .createTextNode("Name: iPad2 32G");
- li1.appendChild(txt1);
- …
- div.appendChild(ul);
-
The following fluent interface APIs are much more expressive:
- //Javascript
- var obj =
- $.div({id:’product_123’, class:’product’})
- .img({src:’preview_123.jpg’})
- .ul()
- .li().text(‘Name: iPad2 32G’)._ li()
- .li().text(‘Price: 3600’)._ li()
- ._ul()
- ._div();
-
Compared with the standard DOM API of Javascript, the above API design is no longer limited to viewing a method in isolation, but considers their combined use when solving problems, so the code representation is particularly close to the essence of the problem. Such code is self explanatory, which is obviously better than DOM API in readability. This is equivalent to defining an internal DSL similar to HTML, which has its own semantics and syntax. It should be noted that the above hierarchical structure abstraction and pipeline abstraction are essentially different. The method chain of pipeline abstraction is usually the continuous transmission of the same object, while the objects on the method chain of hierarchical abstraction change with the change of the hierarchy. This means that we can also express business rules in a smooth interface. For example, in the above example, body() cannot be included in the object returned by div(), and div(). body() will throw an exception of "body method does not exist". ( High end website construction )
4. Asynchronous abstraction
Smooth interfaces can not only construct complex hierarchical abstractions, but also be used to construct asynchronous abstractions. In the asynchronous mode based on callback mechanism, the synchronization and nesting of multiple asynchronous calls is the difficulty of using asynchrony. Sometimes a slightly complex call and synchronization relationship will cause the code to be full of complex synchronization checks and callback layers, which is difficult to understand and maintain. This problem is essentially the same as the HTML example above, because most common languages do not regard asynchrony as a basic element/semantics, and many asynchronous implementation patterns are compromises to languages. To solve this problem, I used Javascript to write an asynchronous DSL based on a smooth interface. The sample code is as follows:
- //Javascript
- $.begin()
- .async(newTask('task1'), 'task1')
- .async(newTask('task2'), 'task2')
- .async(newTask('task3'), 'task3')
- .when()
- .each_done(function(name, result) {
- console.log(name + ': ' + result);})
- .all_done(function(){ console.log('good, all completed'); })
- .timeout(function(){
- console.log('timeout!!');
- $.begin()
- .async(newTask('task4'), 'task4')
- .when()
- .each_done(function(name, result) {
- console.log(name + ': ' + result); })
- .end();}
- , 3000)
- .end();
The above code is just a Javascript call, but from another perspective it looks like a DSL program describing asynchronous calls. It defines the syntax structure of begin when end through a fluent interface. Begin is followed by the code for starting asynchronous calls; When is followed by asynchronous result processing. You can select one or more of each_done, all_done, and timeout. The start when end structure itself can be nested. For example, the above code contains another start when end structure in the timeout processing branch. Through this DSL, we can better express the synchronization and nesting relationship of asynchronous calls than the callback based approach.
The four typical abstractions constructed with fluent interfaces are described above. In addition, there are many other abstractions and applications. For example, many unit test frameworks define unit test DSLs through fluent interfaces. Although the above examples are mostly in dynamic languages such as Javascript, the syntax foundation that smooth interfaces rely on is not rigorous. Even in static languages such as Java, it can be easily used. The fluent interface is different from the traditional API design. The key to understanding and using the fluent interface is to break through the stereotype thinking brought by the language abstraction mechanism, select appropriate abstract dimensions according to the problem domain, and use the basic syntax of the language to construct domain specific semantics and syntax.