Application structure: Concepts & Features
I spoke about this topic quite a bit in ALT.Net Seattle. This is mostly relating to application architecture and how you structure your application. This is the logical result of applying the Open Closed and Single Responsibility Principles.
Feature may not be as overloaded a term in our industry as a service, but it still important to define exactly what I mean. A feature, in my eyes, is a discrete part of the application that perform some operation. That is pretty vague, I know, but I hope that the examples I am going to go through later would make it clearer. So far, it is pretty much the standard fare, but I have a few rules about a feature, the most important of them is:
A feature creation may not involve any design activity.
That is probably something that would raise a few eyebrows, but the idea is very simple. In any application, we tend to have very small number of concepts, and a far larger number of features, each applying to a concept. All design activity occur at the concept level. As a matter of fact, most of the work is getting the concepts right, after that, it is pretty much boring, following a very clear checklist about what we should do to get something working.
Let us take NH Prof as a good example. We have very few concepts in the application:
- Session & statements
- Stack trace
- Loaded entities
That is it.
As for features? We have several dozens of them. Each alert is a feature, being able to see query duration is a feature, returned row count is a feature, every report is a feature.
Remember that a feature involves no design? In order to facilitate that, we have to create an environment in which building a feature is a very predictable operation. This is done at the concept building phase, where we try to figure out what all the different parts of a feature is, and smooth them out. Building a feature is easy, all the hard work was already done, and now you need to fill in the details.
We can get to this situation by paying attention to friction points, applying the Open Close Principle and utilizing conventions. The end result is a very structured approach to building each feature.
A real world example would be NH Prof’s alerts. Adding a new alert is composed of several stages, the first is, of course, determining what we want to alert on. The second is figuring out how we can actually detect this, and finally actually generating the alert. That is all a bunch of fast talk, so let me drill down even further.
Let us say that we want to give an alert about too many joins. That seems like a good alert, doesn’t it? Let us see, how can we detect this?
Well, simply counting the number of occurrences of the string “join” in the statement would give us good results, so we will go with that. The architecture of NH Prof is set up to make this easy, all we really have to do is add this class:
To be frank, I am not sure that I like this, because we still have two empty methods, and there are other things there that reflect some historic choices that I made, but it works for now, and it is not too painful.
Adding this class means that NH Prof will:
- Pick it up and instantiate it
- Invoke it at the appropriate times
- When an alert is generated, it will show it in the UI
That is almost it. We probably need to add configuration (the configuration object is injected to the base class and is accessible to us, so there is no work there, but we still need to worry about adding this to the configuration UI, and of course we must document the alert.
All in all, there is very little actual thought involved in adding a new feature for NH Prof. That is explicitly and by design. This flows to the UI side as well, since we want to maintain the same level of velocity for backend and frontend changes. Adding something new to the UI is pretty easy, and there is a whole bunch of infrastructure to help you ( I mean, I can do this, so it is very easy).
Now, adding a concept, however, is a much more complex scenario. NH Prof’s filtering story, for example, is a concept that we had to add (you can’t see it yet, but it will be there soon). But the filtering story is built by applying a pipeline to the application model. Adding a new filtering operation is very easy, because all the work was already done.
I put up a non conclusive list of things that I believe are part of this design principle. Those are not hard and fast rules, but they are good guidelines.
- All design work is done in the concept level.
- Building a feature doesn’t involve any meaningful design work.
- Work should be structured on repeated addition of features.
- A feature is a testing boundary.
- Adding a feature is easy, fast and painless.
- A feature is complete, that means UI, documentation, configuration and schema are all part of a feature.
- Building a feature is a structured operation.
- Testing a feature is a structured operation.
- All the work required for a feature should be directly and closely related to that feature.
The funny part about NH Prof that Christopher, Rob and me has reached pretty much the same approach, even while working in graphically disparate manner and without a lot of tight communication between us. Well, it is not really funny, because that is the only way we could actually get things to work. Anything else would be ridiculously complicated and require too much synchronization between us.
There are other things that are related to this, mostly about reducing friction and identifying pain points and removing them. It is something that can be applied in isolation, but it usually preferable to deal with it at both project and architecture level.