I’ve had a few conversations over the last week or two with some co-workers and friends about what constitutes ‘good system design’. I thought I’d post up my ideas with some short descriptions and get feedback from others. Here’s my list:
1. The system is easily understood by developers of all skill levels.
I really think this is imperative. If people look at how your system is put together, look at how it functions and understand it without a lot of explanation about how it works technically, you’ve got a good design (explaining business domain is different). If people look at your system and are immediately lost on how it all ties together, then chances are you’ve either over-complicated it, written something only you can maintain, or written something that people are going to have to be trained on in order to make meaningful contributions.
There are usually more mid level and junior level people on most teams than senior levels and architects. If you have a system that can only really be maintained by senior level developers (and higher), then there’s a problem. Unfortunately, many people look at these sorts of systems and the developers that write them and assume they are very intelligent/talented/gifted/what have you. It’s not much different than people who converse with someone that uses a lot of “big words” (or in this industry, buzz words) that they can’t understand — they think they must be intelligent.
Being hard to understand isn’t a sign of intelligence or good design. An intelligent person is someone that can explain concepts and ideas to people that don’t know anything about a subject matter so that they can easily understand it. In the same way, an intelligently designed system is one that can be easily understood by developers that didn’t design and build it.
2. Refactoring is not an exercise in total redesign
A well designed system is one that accomplishes exactly what the business needs today but is put together in a way that can easily be modified to support the business needs of tomorrow. Part of this is making your system loosely coupled, part of it is understanding concepts like abstraction, encapsulation and dependency inversion work. If you need to switch from your own home-grown subsystem to a 3rd party service, you should be able to create new classes and implement the interfaces defined in your system.
You should not have to tear apart how the entire system works.
Forethought must be reasonable; it is too easy to conceive an overwhelming set of situations that you may have to contend with. YAGNI is a key principle here. Use wisdom; design for today with the ability to easily refactor support for tomorrow, but don’t go overboard.
3. Functionality is easily consumed
A well-designed system is one that is written with the consumer in mind; Functionality and intent should be easily understood by the person consuming it without having to dig through the implementation to make sure that it actually does what is expected. I shouldn’t need some secret tribal knowledge to know what your component does; it should tell me what it does and it should do what it says. “No-ops” do not adhere to this philosophy. If your class has a function on it, that function should do what it claims to do. As a consumer, I shouldn’t have to know whether or not a function I need to call will actually perform the operation that it advertises.
Functionality should be designed with ease-of-use in mind. I should be able to instantiate your object and give it whatever dependencies it requires in order to function and then execute whatever methods I need to execute in order to accomplish the objective at hand. I should not need any secret tribal knowledge on the side effects of what I pass in to these functions. If your class requires a dependency and I pass it null, don’t treat it as a secret directive to start instantiating and using some predefined implementation. That is not intuitive and will not produce the results that I would expect.
4. Functionality is easily maintained
A well-designed system is one that can be maintained by anyone. Intent should be clear and easy to understand and the internals of your component should be self-descriptive along the lines of functionality provided. Methods should do one thing and one thing only. If you’ve written something that no one else feels comfortable trying to enhance, you’ve got a bad design. If your design does not make sense to other people, you have a bad design. One of the most important things about design is that it must be maintainable — and for it to be maintainable, other people have to feel comfortable with it and be able to understand it.
Complex problems are usually a bunch of smaller, simple problems. Build solutions that are built from smaller, simple solutions.
5. Tools and frameworks should be included because the benefits outweigh the costs
There are a lot of tools and frameworks that do a lot of different things. Each of them come with a set of benefits and a set of costs. Take a great care when you choose them — each one comes with a learning curve and will require time for developers unfamiliar with the tool to become productive with it. The more tools you throw into something, the more time it will take. Pick the tools that make the most sense for the problem you are trying to solve.
Including a tool or a framework simply because it seems ‘cool’ is not a valid reason. Doing it because you want to learn something new is not either. And if you need a tool or framework, be intelligent about how you introduce it as a dependency into your system. In most cases, you do not need a reference to it in every assembly. One would suffice, if you encapsulate it and interact with it through your own interfaces.
Those are a handful of things that I consider necessary for good system design. There are a lot of others, and in any design, there will always be a balancing act between many of these ideas and reality. I’d like to hear what other people think contributes to good system design.
