Designing and Maintaining Applications
(Staying Relevant Today and Viable Tomorrow)
(Jump to the end for a TLDR)
Unfortunately many of the “Cities” we design are just as tangled despite our best intentions…
Designing a software system is similar to city planning, requiring a balance between present needs and future growth. This balance requires an understanding of fundamental principles like SOLID and using best practices, such as dependency injection and unit testing, while remaining flexible to accommodate future changes. However, it’s also crucial to avoid over-engineering or coding oneself into a corner while staying within the system’s mandate.
Best Practices vs “Academic Correctness”
While high code coverage and adherence to SOLID principles are widely considered best practices for software development, there is such a thing as too much of a good thing. For instance, aiming for 100% code coverage may seem like an excellent idea, but it can become an unattainable goal in large systems, resulting in diminishing returns.
In some cases, trying to achieve 100% coverage can lead to tests that are overly complex or that test trivial features, ultimately resulting in a high test suite maintenance cost. Additionally, high coverage does not necessarily equate to high quality, as it’s still possible to have bugs in code that is fully covered by tests.
Similarly, strictly adhering to SOLID principles can sometimes be counterproductive. For example, following the Single Responsibility Principle can result in more classes and interfaces, which can increase complexity and reduce code reusability. It’s essential to strike a balance between adherence to best practices and practicality in software development, as overengineering or over-testing can result in unnecessary costs.
By understanding the limitations of code coverage and best practices like SOLID principles, software engineers can create systems that are not only flexible and adaptable but also practical and maintainable. Striking a balance between best practices and practicality can ultimately result in software that is of higher quality and easier to maintain, leading to better outcomes for the company and the end-users.
Configuration Is Key To Longevity
Highly configurable mechanisms, like feature flags, configurable thresholds, and configuration-as-code, are critical to building software systems that can stand the test of time. However, it’s also important to recognize the balance between configurability and complexity. As a system becomes more configurable or extensible, it also becomes more complex and challenging to maintain.
To mitigate the complexity of highly configurable systems, it’s essential to use appropriate separation of concerns. For instance, an API can provide all of the configuration for an application, and it’s also vital to provide strong binding through classes, interfaces, or JSON schemas with detailed descriptions or comments on each property’s intended function. Additionally, validating configuration values is necessary to ensure that they fall within particular ranges, amongst a set of enum values, or are present if required. Clear exceptions should be provided when the values do not meet expectations.
Here are some code examples in JavaScript for configuration support:
- Classes/Interfaces:
class Configuration {
constructor(host, port, timeout) {
this.host = host;
this.port = port;
this.timeout = timeout;
}
}
class Database {
constructor(config) {
this.host = config.host;
this.port = config.port;
this.timeout = config.timeout;
}
}const config = new Configuration("localhost", 3000, 5000);
const database = new Database(config);
2. JSON Schema:
const schema = {
type: "object",
properties: {
host: { type: "string" },
port: { type: "number" },
timeout: { type: "number" },
},
required: ["host", "port"],
additionalProperties: false,
};
3. Validation/Error Handling:
function validate(config) {
const valid = ajv.validate(schema, config);
if (!valid) {
throw new Error(ajv.errorsText());
}
}
Conclusion (TLDR)
Designing a software system requires a balance between present needs and future growth, similar to city planning.
A thorough understanding of fundamental principles like SOLID and the use of best practices like dependency injection and unit testing are crucial to building flexible and adaptable software systems.
Highly configurable mechanisms, like feature flags, configurable thresholds, and configuration-as-code, are necessary to create systems that can stand the test of time.
However, there’s a balance to be struck between configurability and complexity to avoid unnecessary maintenance costs and over-engineering.
Appropriate separation of concerns, strong binding, and validation/error handling are necessary to mitigate the complexity of highly configurable systems.
While high code coverage and adherence to SOLID principles are essential, there is such a thing as too much, resulting in diminishing returns and unnecessary maintenance costs.
Striking a balance between best practices and practicality is essential in software development to create systems that are not only flexible and adaptable but also practical and maintainable.
By understanding these best practices and balancing them with practicality, software engineers can create systems that are of higher quality, easier to maintain, and ultimately, better outcomes for the company and end-users. They can create systems that are not only highly configurable but also maintainable and understandable. This promotes the longevity of the software system and ultimately allows the system to continue to serve the company for years after its original implementation.