The two qualities of great software architecture
Great software architecture solves a present business need and is easy to change when business needs change. Software architects have developed a number of heuristics: patterns and principles that help to achieve these goals. A software engineer that applies but does not fully understand these heuristics may implement them in situations that run counter to the goal. To write great software, it's important to understand how each pattern and principle contributes to the goal. Entire books have been written on most of these subjects. This is intended to be a brief overview, not an exhaustive list or explanation.
Solving a present business need
Most users could look at an application and tell you whether it solves a present business need, aside from some of the more subtle nuances. For example, an application may solve one need but is not secure, which is usually one of the business requirements. Some of the criteria that may roll up to this metric, depending on your business:
- User experience
- Product Market Fit
- Reliability (Uptime/error rate)
This is priority number one. Software that is easy to change in response to changing business needs but doesn't solve a business need is close to useless. During early stages of development, most companies focus entirely on solving a present business need. As a company grows, it becomes more and more important to promote ease of change. Otherwise, the business will struggle to change their application as business needs change.
Ease of Change
Many patterns and principles make it easier to implement applications that are easy to change. These are mostly heuristics: they are usually helpful, but excessive adherence to any of these can create applications that are more difficult to change.
The SOLID principles, as laid out in Clean Code, are the most fundamental principles. These should be required reading for every software engineer. The purpose of all of these principles is to make the software easier to change. The book's description summarizes this well:
Even bad code can function. But if code isn’t clean, it can bring a development organization to its knees. Every year, countless hours and significant resources are lost because of poorly written code. But it doesn’t have to be that way.
A great automated test suite validates that software solves a business need. If all of the business needs you intend to solve are validated by automated tests, you can add a new requirement, write the new code, write a test that validates that requirement, and ship it as long as the tests still pass. This makes the software much easier to change, especially in large applications. Automated tests that fail to validate a business need or make the code harder to change are a sign of poorly written code or tests. Ensure that classes are small in scope and that their tests validate the business need the class solves, rather than being too particular about how that class solves the need.
DRY (Don't Repeat Yourself)
DRY is probably the most frequently overused principle. The goal of DRY is to avoid having multiple sources of truth for business logic. It is not to achieve minimal number of lines or repetition. It is almost always the right choice to DRY up business logic. If you try to DRY up boilerplate, you're likely making the application more difficult to change in response to changing business needs. Boilerplate is trivial to copy paste; most of the time it's better to just do that. When writing DRY code, reusability through composition is preferable to inheritance. I find this easiest to think about in React which actively discourages inheritance. Similar logic applies in backend languages where inheritance is frequently overused.
YAGNI (You Aren't Gonna Need It)
As engineers, it's easy to focus too much on maximum code reuse and implement things you aren't going to need as a result. It's often tempting to build a generic, reusable solution to a problem because that's seen as easier than going back and updating the code later when you might need to reuse part of your solution. If the other patterns are followed correctly, it should always be preferable to re-write/DRY up later than do so prematurely.
Easy to delete
Great applications consists of many small components that are easy to delete. Favor ease of deletion over ease of reuse for the bulk of your code. This makes it easy to change your code in response to changing business requirements. Following the SOLID principles and Composition over Inheritance make this much easier.
Prototyping optimizes speed, cost, and ease of change while sacrificing some present business needs. The business need prototyping does maximize is validated learning. I strongly recommend prototyping anything new, risky, or complex. If it requires a UI, build a clickable version in a prototyping tool or even PowerPoint and get it in front of real users to see their reactions. If it's a backend system, build the most rudimentary version possible of the most risky or complex parts and test it out. The prototype should be treated as a throwaway with no prioritization of common coding principles. Instead, you achieve maximum ease of change by being able to scrap the whole thing and use the learnings as reference for the real version.
Fast, good, cheap - pick two
There's a common saying in project management, "Fast, good, cheap - pick two." These three are always in tension: each of them comes at the sacrifice of at least one of the other two. In order to succeed in business, you must be fast, so what's the right trade off between cheap and good? I see this as a spectrum with cheap often meaning easy to implement but difficult to change; good meaning the inverse. Where on this spectrum you should aim for depends on the state of the business you're in.
This can be seen as a a tradeoff between adaptation and adaptability:
The better adapted you are, the less adaptable you tend to be. - Gerald M. Weinberg
If you invest too much in adapting to the present, you'll find it harder to adapt to the future. If you invest too much in capability to adapt to the future, you won't reap the rewards of being adapted to the present.
Early stage projects often need to maximize speed and minimize cost, so are willing to sacrifice some of the ease of change to find product market fit at lower risk. If your project never finds product market fit, it won't matter how easy the software was to change. I've made this mistake with two businesses that were too slow to reach the market due to too much focus on software quality and ultimately failed as a result.
As your project does find its fit, it's important to start investing in ease of change. If you don't, you'll find that you need more and more resources to get the same amount of work done. This can be thought of as investing in production vs production capacity, or the P/PC balance. The more you sacrifice short term production to invest in production capacity, the more production you can do in the future. You should be recalculating where on this spectrum your project should be investing its resources every few months. A good rule of thumb is 5-10% of time to tech debt for early projects, 30% for projects that have found a fit and now have a lot of tech debt to pay down, and 20% for projects that are in a stable state. Tech debt is constantly being introduced and discovered, and it pays dividends to have engineers rework code to gain familiarity.
Post a Comment