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
- Reports
- Alerts
- Filtering
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:
public class TooManyJoins : AbstractStatementProcessor{private readonly Regex joinFinder = new Regex("join", RegexOptions.Compiled | RegexOptions.IgnoreCase);public override void BeforeAttachingToSession(SessionInformation sessionInformation, FormattedStatement statement){}public override void AfterAttachingToSession(SessionInformation sessionInformation, FormattedStatement statement, OnNewAction newAction){if(joinFinder.Matches(statement.RawSql).Count<7)
return;
newAction(new ActionInformation
{HelpTopic = "TooManyJoins",
Severity = Severity.Warning,Title = "Too many joins in query"
});}public override void ProcessTransactionStatement(TransactionMessageBase tx){}}
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.
Comments
i got it.. it shouldn't take a lot of heavy lifting for a single feature of an application. makes sense.
in fact, if it does take a lot of work for a single feature there might be a flaw or complexity issue in your application or design. (my opinion) it is the case with the app i work with most right now at least.
I really like the distinction you make between feature and concept. It's quite obvious now that you stated it explicitly that these two should be treated differently.
Yet, I've never seen anyone designing with that distinction in mind. Probably 99% percent of architecure follows other distinction (if any):
feature (which is something of a mix of feature and concept by your definition)
infranstructure (Web UI vs desktop, what kind of DB to use, what logging framework etc)
Did I mention that you should consider writing a book on software design?
That's the same way I am doing it (and that's why I disliked Castle not having a good built-in collection resolver with cycle handling).
However I do not see the reason for empty methods, and, talking about the concepts, it is hard to understand why an Action has Severity and Title, but does not have anything __actionable .
Andrey,
Could you explain more on that actionable stuff?
Are you talking about that there isn't anything that tells the user what to do on that?
Thanks
Very good article. I can see that you are very skilled in your field. Keep up the good work!
Krzysztof,
The last book I wrote took a year, I need some rest.
Andrey,
As I mentioned, there are a lot of stuff there that I don't like and need refactoring They are not painful enough yes, though.
Ayende,
At what point do you add or change concepts? I'm sure it's very subjective because concepts have to be somewhat about future requirements (especially when you can't say yes or no to those future requirements)...but do you have any criteria that you use for that?
If that was unclear then another way to phrase that question is at what point do you say 'this feature requires a new concept...I'm not going to shoehorn it into an existing concept just to maintain my original design' ?
Thanks
Ayende, I am curious, is there a good reason the joinFinder field in your example is not static?
I really like the idea of new features being always built on existing infrastructure, but think that every successful application will eventually reach the stage where new features are not supported by its architecture or even conflict with the architecture. Especially highly customized software suffers from that. After several years of satisfying your customer needs you end with a large code base and probably old, unsupported technology underneath - for example MFC GUI, visual basic, some exotic database and libraries. And then you face the choice - either rewrite everything from scratch to support hottest acronyms (SOA, J2EE, ...), or keep developing the old code hacking it and gluing together with anything that's necessary. And I think first approach never works. You can't rewrite a system without losing some functions or adding bugs, also the cost will be unjustifiable - you can't say 'I had to rewrite the whole system because the new feature wasn't supported by its architecture'
Michael,
Yes, demo code.
and there is only going to be one instance of the class anyway
Rafal,
At that point, you change the architecture.
James,
It is very subjective. Usually, when I see that I have some pain implementing a feature
Comment preview