Here at AppLift we have multiple internal tools for managing our ad campaigns. Our methods don't deviate much from well-known practices, we prefer small autonomous components with explicit interface and we try to re-use as much code as possible. This post will be about these and other ideas that worked well for us.
Our projects are typically split into two parts: the backend and the frontend.
- The backend consists of a bunch of services providing only restful API
- The frontend exists independently and utilises the API of the services
- Any update to either one doesn’t affect another as long as the API interface stays the same
- Identifying the source of a problem becomes faster.
- Based on the source of the problem it becomes easier to find the right developer who can fix it in the shortest time possible.
- New developers need less time to start working on new features, because the goal on each end is clear: either to provide or consume API.
- Maintaining parts of code with no intersecting dependencies is easier and more obvious: when you replace a package on backend, you can expect that frontend will not fail.
- Splitting estimations of features into frontend and backend improved accuracy of those estimations.
The way we achieved these benefits can be generalised as the following:
Key Principles Of Code Separation
- The code should be split into loosely coupled parts that talk to each other through an explicit uniform interface.
- Determining independent code parts should be intuitive. It should be possible to easily pinpoint location of a code part by looking at the working product.
- Each part of code should have clearly defined dependencies so that other code parts won’t have to depend on something they don’t use directly.
The frontend is a completely different thing. We can't show JSON to users, we can't connect frontend components using REST specs and how do we split frontend into components in the first place? This is one way to do it, step by step:
Dividing By Page
We’ll use AppLift’s website as a simple example. In reality it does not consist of any components that this post aims to define because the website is very simple, but we'll work with it as if there were ambitious plans on extending it.
|Services @ applift.com||Blog @ applift.com|
Among others, there are Products, Services, Technology and Blog sections on the website and it would make total sense if there were components in directories with similar names. Intuitiveness is very important for maintainability.
So first iteration of components would look something like this:
Where DataLift and Publisher Network components are nested under Products to resemble hierarchy of the website's navigation - to make it more intuitive.
But doing just that will produce a lot of code duplication. Reducing duplicated code helps maintainability a lot.
Extracting Invisible Components
One of the things Technology, Blog and other components of the AppLift website have in common is they all have URL configuration. And each such configuration can have a lot of duplicated code so it's wise to extract that code into one single place and allow accessing it in the safest and simplest way possible. Typically the code that helps configuring URLs is called URL Router.
The URL router must be a component because it's bad to have something outside of the project's pattern. Everything should be a component. This one just won't have the visual part - no HTML and CSS.
All existing components need to configure URLs except for the parent Products component: there's no dedicated/products URL on AppLift website. Hence all components but Products depend on URL Router component.
The need for some sort of registry of components and their dependencies is apparent. Here's how the directory structure and the registry can look like by now:
Based on the graphs, it is assumed that DataLift, Services etc components all have access to URL router's functionality but Products component does not. Hence there's a need of some scoping mechanism, a good implementation of which is the dependency injection pattern. Some components put stuff into dependency injection registry and the dependent component gets that stuff from that registry. If the stuff isn't there, it's easily traceable and fixable.
Dividing A Page
We will take AppLift’s Blog page/component as an example.
One way to slice it into loosely-coupled blocks is do it like this:
With that done the structure will look like this:
Imagine there’s something wrong with the latest post on the Blog section of the website. Based on the folder structure and dependency diagram it’s pretty easy to narrow the search to just Post excerpt component.
The reason this is much easier, is because all components are supposed to be isolated in themselves and outside world can talk to them only through some sort of uniform interface, which should be as explicit and as common as REST.
HTML is as common as it gets. Components like checkbox, form or text area are all perfect examples of how HTML can be used to talk to encapsulated parts of website.
HTML supports hierarchy of elements, so it’s intuitive to expect similar tag hierarchy as in folders and dependencies. Also HTML attributes can be used to pass additional configuration to components. Based on this the AppLift website's HTML can be as follows:
Here Blog component uses or depends on Top Navigation, URL Router, Categories and Post Excerpt.
- From URL Router it expects a service in dependency injection registry
- From other components it uses HTML tags providing configuration through attributes.
- The Blog itself provides the HTML tag <blog></blog> to be used inside Applift Frontend App component's template.
Conclusion: Good To Do
- Components should represent all items of the website’s hierarchy
- Folder structure of components should represent the website’s hierarchy as closely as possible
- Each individual component should contain everything related to it in its folder. It's better not to scatter styles, templates and tests across the file system
- Each component must declare its direct dependencies. It shouldn’t rely on any dependencies implicitly just because it’s always used in one exact context
- Each component that has a template must have an HTML identificator, so that using HTML hierarchy was possible. The HTML hierarchy should copy the website’s hierarchy.
- Configuration for a component with a template should be provided using HTML attributes wherever it’s possible
- If configuration requires callback, there should be a way to specify it in an attribute
- If a component doesn’t have a template, it should register parts of its code in a dependency injection or some similar scoping mechanism. Other components should use dependency injection to get the code of the component
Our belief is that this can be achieved with many popular frameworks. For us AngularJS 1.x worked just fine, we'll talk about it in the next post.