Patterns And Refactoring
In matters of refactoring and patterns, you can start by taking these two books:
Design patterns for JavaScript and Node.js (the most common ones)
- EventEmitter (also known as Observer), built into Node, and on the front end, a polyfill or EventTarget
- Proxy - built into the language, intercepts object access
- Strategy - for us, it's simply an Object or Map - a collection of functions, classes, prototypes, closures, etc.
- Facade - a simplified interface to a complex system, widely used in many places; for example, http2.createSecureServer hides from us both TLS and HTTP, streams, sessions, and other mechanisms
- Adapter - usually a wrapper function, wrapper examples: promisify, callbackify, or you can write a fetch polyfill that uses XMLHttpRequest internally; this would be an adapter, hiding complexity, but not a facade because a facade hides not just one interface but several or an entire subsystem
- Factory - Factory, a pattern that creates class instances, but in JS, a factory can generate prototype instances, closures
- ChainOfResponsibility - its pseudo-analog Middleware is usually used, leading to competition for shared state and thus leading us to a race condition; it's worth reading about the original ChainOfResponsibility to stop using Middleware,
- Decorator - built into the language, both in JavaScript and TypeScript; the specifications differ, but the essence is the same, it adds behavior without inheritance, using metadata
- Singleton - for us, it's just an object; you don't even need to create a class for this, and the global uniqueness of the instance can be achieved by exporting it from the module
- Revealing constructor - an open constructor, for example, by passing the write method to the Writable constructor through options, we can get a Writable with the overridden method without inheritance; similarly, by passing a function to the Promise constructor, we are accustomed to this, but in other languages, it is traditionally done through inheritance.
Regarding the Middleware pattern, it's important to explain it separately. It not only leads us to a race condition but more precisely to conflicts in data and control flow conflicts. Additionally, it significantly strengthens code coupling:
- ⚠️ Provokes the practice of mixins, for example:
res.sessionStarted = true;
and in numerous places, conditions likeif res.sessionStarted
orres.userName = 'idiot';
. - ⚠️ Provokes abstraction leakage - when delving into the internals of
req
andres
and altering their state, as well as the state of their parts, sockets, headers, etc., not through the external interface (i.e., methods according to the contract) but through patches, wrappers, in general, through such means. For instance, the WebSocket library (ws
) patches the HTTP server and infiltrates into its core to intercept the upgrade to web sockets. While this quick fix is handy in JavaScript, such a solution needs careful consideration, test coverage, and there's still a significant likelihood of it breaking down. In system code, it might be okay, but in production, one must minimize leakage as much as possible. Of course, completely eliminating it is impossible; see "The Law of Leaky Abstractions." - ⚠️ Provokes reference pollution and the use of shared state: references to
req
andres
spread across different parts of the program. For example:serviceName.method(req, res, ...);
or parts ofreq
andres
, likeserviceName.method(req.socket, ...);
or even such instances asoutside.emit('request', req);
. There are numerous ways:const f1 = method.bind(req);
orconst f2 = method(req)(res);
, and hundreds more. - ⚠️ Provokes race conditions: by using a data structure beyond the middleware and then using such a structure within several middlewares simultaneously, or because references to
req
andres
entered other parts of the program, changing their state without binding tonext
. For example, someone sets a timeout viasetTimeout
for sending results, or upon the arrival of a certain event from streams, someone writes headers toreq
andres
. Then another middleware can't write headers anymore. These are just the most common problems. Have you never faced a situation where middlewares swap places to find a sequence to get it to work? Well, this practice hides the race, but under load, it can emerge. - ⚠️ Provokes writing bulky controllers and mixing various layers within them. We've all seen an endpoint where everything happens at once: handling HTTP protocol, business logic, SQL database interaction, caching in Redis, queueing tasks, working with the file system, and so on, all in one chunk... Of course, no one is forced to write like this. It's just that not everyone is aware that it's necessary to structure code into layers and separate database operations into a repository, etc. Well, many know, but only a few can do it properly.
- ⚠️ Increases code coupling - all parts of the code become more dependent on each other due to the issues mentioned above. Touch one, and it breaks in another place.