Design for Durability
Avoid this design smell by building software that is highly modular, highly cohesive, and loosely coupled. A change to one part of your system should never break another part that is completely unrelated. High level policies (i.e. business rules) should never be impacted by changes to low level implementations (i.e. data persistence). Even related functionality should be decoupled enough to extend functionality without affecting related components. There are many engineering patterns and practices that facilitate flexibility, extensibility, adaptability, along with many other qualities that inhibit fragility.
This occurs when changes are made to APIs or user interfaces, causing the clients of these interfaces to break. Even minor changes to an interface will cause failures.
This occurs when the behavior of existing functionality changes and is not sufficiently decoupled from other parts of the system.
This occurs when functionality is affected by changes in the data that were not anticipated when designing the system. This often occurs when a system depends on a database or data stream, or when functionality depends on magic numbers that are hard-coded within the system, but are no longer valid.
This occurs when changes in the state of the system, or changes in the environment in which the system runs, are not anticipted during system design. Some common examples include dependencies on system resources (i.e. clock), servers, databases, file systems, printers, web services, random events, etc.
Following are just a few engineering principles that promote quality — most of which also help inhibit fragility:
Every object should have a single responsibility and that responsibility should be entirely encapsulated by the class.
Write code that is open for extension but closed for modification.
Build classes in such a way that a derived class can always replace its base class without changing behavior or side effects of the base class.
Don't force classes, modules, or components to depend on (or even know about) methods they don't need.
High level policies should not depend on low-level details.
Components need to be large enough to justify the cost of managing the release cycle.
Classes that change for the same reason should be grouped together in the same component.
Create components where if one class in the component is used, they’re all used.
The dependencies between components must not form a cycle.
Components should depend upon each other in the direction of increasing stability.
The more stable a component is, the more abstract it should be — so that it can conform to the Open/Closed Principle.
Manage side effects within your code by controlling how and where they occur. Write functions such that if they change state, they do not return values. And if they return values, they do not change state. For example, instead of returning error codes, throw exceptions to indicate a failed state change (such as a database update or user login).
Build code that has limited knowledge of other parts of the system and interacts directly only with closely related classes. Methods should be protected from understanding the internal structure of other methods or classes. This is also known as the Principle of Least Knowledge, a proven strategy for promoting information hiding and building high quality, loosely coupled software. This can be achieved through the practice of Tell, Don't Ask.
Building resiliency into your system by identifying exceptions in the requirements, then defining exception handlers during analysis.
Build software that is flexible, reliable, and easy to understand. In other words, build software that is simple. Do not over-engineer the initial design (YAGNI). Start with the simplest solution, then work towards complexity (only as warranted). Ensure your code passes all of its tests, reveals its intent, contains no duplication, and is as small as possible. Writing simple code can often be difficult. It is not about writing code by the simplest means. Simple != easy.
To decouple functionality, write methods that tell objects what to do. Do not ask for their state, then perform functions on behalf of those objects. This will reduce query functions, encapsulate knowledge, and ensure compliance with The Law of Demeter. While not always feasible, this should be followed as much as possible to promote adaptability.
Design and implement solutions that are needed today; do not invest in capabilities or features that may never be needed. While this may require more effort tomorrow, time is more often wasted by investing in requirements that change or become irrelevant. Building for tomorrow will likely slow down delivery for today. Keep your codebase as small as possible. This will promote simplicity of design, implementation, and validation.
Build abstract classes to encapsulate the responsibility of creating and composing families of related objects. This will help control the classes of objects that a system creates and promotes independent deployability by enabling the system to create objects without depending on those objects. This also enforces consistency among classes of objects by forcing the system to use objects from only one family at a time. This approach enables you to swap factories in and out at runtime, providing real-time flexibility as to how objects are created.
Reduce software complexity by minimizing the communication and dependencies between subsystems. By interposing a Facade object between low-level objects and their clients, you can control what the clients can do and how the low-level objects can respond. This helps to maintain high-level abstraction when applying the Single Responsibility Principle. The clients will have no knowledge that functionality has been encapsulated within smaller objects, each having only one responsibility. The functions represented by the facade can be implemented across numerous objects without the clients ever knowing.
Define an interface for creating objects so that the instantiation of new objects is delegated to methods within the implementing class. This will decouple application-specific classes from your code and enable those factory methods to provide extended versions of objects. Unlike the Abstract Factory Pattern, it is not necessary to instantiate separate factory classes. The downside is you're stuck with just one type of factory and you cannot change it at runtime.
Use the Strangler Pattern to build reusable software by enhancing existing behavior, creating better abstractions, and replacing code that is not quite right. This is an effective method for eliminating technical debt and adhering to the Open/Closed Principle when your code is used by clients outside of your system. Use an [Obsolete] attribute to generate compiler warnings notifying clients that a particular class will eventually be deprecated.
Keep your functions small by extracting code fragments into their own functions. If you have to spend even a small amount of time figuring out what a code fragment does, then it should be extracted into its own function and given a name that describes what it does. Then when you read it again, the purpose of the code should be obvious. This will clarify the intention of your code (separate from the implementation), while making it self-documenting, more maintainable, and more testable.
Use local variables to name complex expressions (or portions of them) to help decompose those expressions into more manageable pieces of logic and provide clarity around the purpose of that code. These simple abstractions help to simplify those expressions, making them easier to read and easier to understand — not to mention how easy it is to provide a hook for debugging.
Verify the expected behavior of unit and component tests by comparing them to the indirect outputs of the System Under Test (SUT) as they occur. This is necessary when the expected results are transient and cannot be verified through State Verification. This requires us to intercept the behavior at an observation point between the SUT and a dependent component. Procedural Behavior Verification can be used to capture indirect outputs of the SUT using Spy Objects. Otherwise, we can load the Expected Behavior Specification into Mock Objects to verify the method calls.
Avoid conditional test logic by replacing if statements with assertions that fail when the conditions are not satisfied, preventing execution of statements that would cause test errors. This will promote Defect Localization, document pre-conditions enforced by the guard assertions, and minimize the effort required to verify your tests. If you have to write tests for your tests, when would it ever stop?
Write tests such that each produces the same result from a given initial state without any manual intervention between runs. Unit tests must verify Single Test Conditions by executing a single code path through the System Under Test (SUT) and executing the same, exact code path each time it runs. Verifying one condition for each test helps to minimize Test Overlap and ensures we have fewer tests to maintain if we later modify the SUT. Isolating the SUT ensures that we only have to focus on code paths through a single object.
Build tests in such a way that the number of tests impacted by any change is small. This is achieved by minimizing overlap and dependencies between tests and ensuring changes to the test environment have no affect on our tests. Isolate the System Under Test (SUT) and verify just one condition for each test. Changes that affect test fixtures should be encapsulated behind test utility methods.
When writing Self-Checking Tests, inspect the state of the System Under Test (SUT) after it has been exercised to ensure it matches the expected state. If the SUT is stateful, this can be achieved through Procedural State Verification or Expected State Specification. Otherwise, Behavior Verification can be used to verify indirect outputs of the SUT as it is being exercised when the state does not change or there is no state to verify. To determine the best approach, we must ascertain whether the expected outcome is a change of state or if we need to examine what occurs while the SUT is being exercised.
Avoid writing tests that verify multiple assertions within a single test method. Numerous assertions make it difficult to determine which one caused a test to fail — inhibiting Defect Localization. This anti-pattern is often caused by Eager Tests that try to minimize the number of tests we need to write. Apply the Extract Function refactoring to tease apart these tests into single-condition tests so that the reasons for test failures are obvious.
Avoid this code smell by ensuring that your test methods do not verify too many test conditions. This often occurs when we try to minimize the number of tests we need to write. A unit test should verify only one condition by executing a single code path through the SUT. We achieve this by minimizing the Cyclomatic Complexity of the test and ensuring the SUT is isolated through Dependency Inversion. Acceptance tests should also be kept as short as possible. Eliminating this code smell will promote Defect Localization and minimize Test Overlap.
Minimize the number of tests that depend on a particular piece of functionality such that each test condition is covered by exactly one test. Tests that verify the same functionality will usually fail at the same time and require the same maintenance when the functionality is modified. Give attention to tests that verify the same code in different ways as these may indicate different test conditions. Avoid Eager Tests that verify too many test conditions. Picking the right tests is essential in employing a risk-based approach to testing.
Avoid stringing method calls together on a single line of code to invoke methods within different objects. This type of call chain violates the Law of Demeter, while increasing complexity - inhibiting our ability to understand, maintain, and reuse the code. Instead of building a series of "train cars", apply the principle of Tell, Don't Ask to resolve all of these issues.