Architecture with Architect
Dependency Injection embracing Node.js patterns
Growing with Node.js
After building my first few apps using Node.js, I had that ominous feeling we often get as developers. You know that “sure, this works fine, but it feels wrong” feeling?
Mario Cascaiaro said it well over on his blog:
Designing a Node.js app was a totally new story for me (coming from a Java, C++, PHP background). For some aspects it was disorientating, no boundaries for defining a module, no need for complex hierarchies of classes, dependencies were as easy as requiring a file. It was wonderful at the beginning, a simple architecture is the ultimate source of happiness for a developer, but soon I realized that my code was slipping out of control, the more I was trying to make things modular, the more complex and “unusual” was the code I was writing. [emphasis added]
Yea, what he said! His post on Dependency Injection in Node.js and Other Architectural Patterns, was more or less what I was feeling.
Pure Node.js with NPM uses the file-system for inclusion of dependencies. The problem is this: you end up with a file-system-dependent codebase. That’s not a big deal for a small project, but as your team and codebase grow, how can you be sure that the entire team understands all the specific quirks of your dependency graph? For example: Which order must files be included? Which dependencies need to be passed to where? How should configuration be passed into the application?
DI/IoC patterns can help bring order to this chaos
(DI: “Dependency Injection” / IoC: “Inversion of Control”)
Dependency Injection
What’s out there for node?
Cascaiaro’s research answers with:
Scatter, Architect, Broadway, Wire, Seneca, Intravenous, DI, IOC, AOP, Hook
After trying these various flavors, I chose on Architect. Here’s why:
Architect does it the Node.js way
Architect uses the concept of plugins. An Architect plugin is just a node package, complete with a package.json.
Using Architect, you set up a simple configuration and tell Architect which plugins you want to load. Each plugin registers itself with Architect, so other plugins can use its functions. Plugins can be maintained as NPM packages so they can be dropped in to other Architect apps.
Self-describing plugins use a package.json file
Each Architect plugin exposes little bitts of related functionality. With NPM, a package exports bits of functionality. Architect takes it a step further: plugins declare both which functionality is required and exposed using the consumes and provides declarations in your package.json file.
Here’s an example package.json:
This is not a trivial point. This means Architect can automagically calculate the appropriate depency tree, and inject the right bits of functionality into the plugins that depend on them. Plus, anyone can glance at the package.json file for any plugin and immediately know which facilities it consumes and provides. That’s good, because people on your team don’t need to go hunting through the filesystem. The info is always available in a consistent spot, for every single module. Yay for consistency!
Better yet, dependencies are no longer file-system dependent. Let’s see an Architect example app:
Using Architect
To get started with Architect, you just need to tell Architect where your plugins live. Plugins can live anwyere, in any directory. Just tell Architect where they are. Consider the following directory tree:
Tell Architect where your plugins live
In this case, we tell architect to import two plugins:
We saw the user plugin above. Here’s the business plugin:
Bootstrap the Architect app
And we load the configuration to bootstrap the app:
Architect automagically:
- Calculates the dependency tree
- Initializes the business plugin
- Passes the business plugin as an import to the user plugin
- Initializes the user plugin
So how does Architect it do it?
using a single, concise, and consistent interface
The Architect initialization method
Each plugin has a single entry point. The entry point’s file is specified by the main property, and an initialization function is invoked by Architect:
If something goes wrong during initialization, the register method should be invoked with the error as its first argument:
Managing Configuration
Handling configuration is a common need for a project of scale. Architect conveniently has the plugin property, which is a great place to put your default configuration specific to that plugin:
You can mix-in environment specific configuration in your Architect configuration pass (architect-config.js from above). I’m a fan of the Konphyg plugin which allows for an environment-based configuration cascade with json files as the configuration source.
How I feel using Architect for Node.js
