📜 ⬆️ ⬇️

Node.js. Design and development patterns

Hello, dear readers.

We are interested in the book " Node.js Design Patterns ", which has collected very positive reviews over the year of its existence, unbanal and informative.


')
Considering that on the Russian market books on Node.js can be literally counted on the fingers, we suggest reading the article of the author of this book. In it, Mr. Cassiaro makes a very informative excursion into his work, and also explains the subtleties of the “pattern” phenomenon itself on the Node.js material.

Please participate in the survey.



In addition to the classic design patterns that we all had to learn and use on other platforms and in other languages, specialists in Node.js now and then have to implement such techniques and patterns in the code, which are due to the properties of the JavaScript language and the platform itself

Prewarder

Of course, the design patterns described by the gang of four, the Gang of Four, are still required to create the right architecture. But it is not a secret for anyone that almost all rules violated by us in other languages ​​are violated in JavaScript. Design patterns are no exception, be prepared that in JavaScript you will have to rethink the old rules and invent new ones. Traditional design patterns in JavaScript can be implemented with variations, and the usual programming tricks can grow to the status of patterns, since they are widely applicable, known and effective. Also, do not be surprised that some recognized antipatterns are widely used in JavaScript / Node.js (for example, correct encapsulation is often overlooked, because it is difficult to get it, and it can often lead to “ object debauchery ”, it’s also anti-pattern Public Morozov.

List

The following is a brief list of common design patterns used in Node.js applications. I'm not going to show you again how “ Observer ” or “ Loner ” is implemented on JavaScript, but I want to focus on the characteristic techniques used in Node.js, which can be summarized as “ design patterns ”.

I compiled this list on material from my own practice when I wrote Node.js applications and studied the code of my colleagues, so it does not claim to be complete or finite. Any additions are welcome.
And I would not be surprised that you have already met some of these patterns or even used them.

Directory Requirement (pseudo-plugins)

This pattern is definitely one of the most popular. It consists in demanding all modules from the directory, that's all. With all the simplicity it is one of the most convenient and common techniques. Npm has a lot of modules that implement this pattern: at least require-all , require-many , require-tree , require-namespace , require-dir , require-directory , require-fu .

Depending on the method of use, the requirement of the directory can be interpreted as a simple auxiliary function or a kind of plugin system, where the dependencies are not hard-coded into the required module, but are embedded from the contents of the directory.

Simple example

var requireDir = require('require-all'); var routes = requireDir('./routes'); app.get('/', routes.home); app.get('/register', routes.auth.register); app.get('/login', routes.auth.login); app.get('/logout', routes.auth.logout); 


A more complex example (reduced connectivity, extensibility)

 var requireFu = require('require-fu'); requireFu(__dirname + '/routes')(app); 


Where each of /routes
/routes
- this is the function that defines its own url route:

 module.exports = function(app) { app.get("/about", function(req, res) { //  }); } 


In the second example, you can add a new route simply by creating a new module, without having to change the module that requires it. This practice is obviously more powerful, in addition, it reduces the connectivity between the required and the required modules.

Object Application (homemade dependency injection)

This pattern is also very common in other languages ​​/ on other platforms, but due to the dynamic nature of JavaScript, this pattern is very effective (and popular) in Node.js. In this case, we create a single object that serves as the backbone of the entire application . Typically, this object is instantiated at the entrance to the application and serves as a glue for various application services. I would say that it is very similar to the Facade , but in Node.js it is also widely used when implementing a very primitive container for dependency injection .

A typical example of this pattern: the application has an App
object App
App
(or an object of the same name to the application itself), and all services after initialization are attached to this large object.

Example

 var app = new MyApp(); app.db = require('./db'); app.log = new require('./logger')(); app.express = require('express')(); app.i18n = require('./i18n').initialize(); app.models = require('./models')(app); require('./routes')(app); 


Then the App object can be passed as needed to be used by other modules, or it can take the form of a function argument or require
require


When most application dependencies are attached to this core object, the dependencies are actually embedded from the outside into the modules that use it .

But be careful: if you use this pattern without providing a level of abstraction over the loaded dependencies, then you can get an all-knowing object that is difficult to maintain and which, in principle, resembles the God object anti-pattern in all respects.

Fortunately, there are some libraries that help to cope with this problem - for example, Broadway , an architectural framework that implements a very neat version of this pattern, providing a good abstraction and allowing better control of the service life cycle.

Example

 var app = new broadway.App(); app.use(require("./plugins/helloworld")); app.init(...); app.hello("world"); // ./plugins/helloworld exports.attach = function (options) { // "this" –    ! this.hello = function (world) { console.log("Hello "+ world + "."); }; }; 


Interception of functions (monkey patching plus AOP)

Interception of functions is another design pattern typical of dynamic languages ​​like JavaScript - as you might guess, it is very popular in Node.js. It consists in complementing the behavior of a function (or method) by intercepting its (her) execution . Typically, this technique allows the developer to intercept the call before execution (prehook) or after (post hook). The subtlety lies in the fact that Node.js is often used in combination with monkey patching , and this technique turns out to be very powerful, but, at the same time, dangerous.

Example

 var hooks = require('hooks'), Document = require('./path/to/some/document/constructor'); //   : `hook`, `pre` `post` for (var k in hooks) { Document[k] = hooks[k]; } Document.prototype.save = function () { // ... }; //   ,     'save' Document.post('save', function createJob (next) { this.sendToBackgroundQueue(); next(); }); 


If you have ever worked with Mongoose , then you definitely saw this pattern in action; if not, in npm there is a mass of such modules for every taste. But this is not all: in the Node.js community, the term “aspect-oriented programming” (AOP) is often considered synonymous with intercepting functions , look at npm — and you will understand what I mean. Is it really possible to call this an AOP? My answer is no. AOP requires that we apply end-to-end responsibility to the slice , rather than manually attaching specific behavior to a particular function (or even a set of functions). On the other hand, in a hypothetical AOP solution on Node.js, interceptions could well be used — then the advice would extend to many functions, combined, for example, with one slice defined using a regular expression. All modules would be viewed to match this expression.

Conveyors (intermediate code)

This is the essence of Node.js. Conveyors are present everywhere, differing in form, purpose and use. In principle, a conveyor is a series of processing modules connected to each other, where the output of one module serves as input to another. In Node.js, this often means that the program will have a number of functions of the form:

 function(/* input/output */, next) { next(/* err and/or output */) } 


Perhaps you are used to calling such things intermediate code (middleware) referring to Connect or Express , but the boundaries of using this pattern are much wider. For example, Hooks is a popular implementation of hooks (discussed above), which combines all pre / post functions into a (intermediate) pipeline, in order to “provide maximum flexibility.”

Typically, this pattern is implemented in one way or another using async.waterfall , or async.auto , or a sequence of promises , and can not just manage the flow of execution, but also ensure the extensibility of one or another part of your application.

Example: Async

 async.waterfall([ function(callback){ callback(null, 'one', 'two'); }, function(arg1, arg2, callback){ callback(null, 'three'); } ]}; 


Other popular component Node.js also has pipeline features. As you may have guessed, we are talking about the so-called streams , and what is the flow if it cannot be pipelined ? While the intermediate code and function chains in general are a universal solution for managing the flow of execution and extensibility, threads are better suited for processing transmitted data in the form of bytes or objects .

Example: streams

 fs.createReadStream("data.gz") .pipe(zlib.createGunzip()) .pipe(through(function write(data) { //...     ... this.queue(data); }) //    .pipe(fs.createWriteStream("out.txt")); 


findings

We have seen that, by its very nature, Node.js encourages developers to use certain patterns and repetitive techniques. We reviewed some of them and showed how they allow us to effectively solve common problems, if applied correctly . In addition, we have seen how differently the pattern may look depending on the implementation.

Source: https://habr.com/ru/post/278017/


All Articles