The term SOLID is a mnemonic acronym for the five design principles intended to make software designs more understandable, flexible and maintainable. These are defined as:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
This article will provide a brief summary for each and will introduce the D365FO development features that can help adhere to these principles.
S is for “Single Responsibility”
Write code that has one and only one reason to change. If a class or method has more than one reason to change, it has more than one responsibility.
Each new module (class or method) should have a well defined and describable purpose. If it isn’t possible to give the class or method a descriptive name then it is probably performing multiple tasks (has multiple responsibilities) and should be broken down into smaller modules.
For example, a method that:
- fills and iterates a table buffer;
- performs validation;
- performs updates;
- creates and populates other table buffers;
- dispatches calls to other methods;
- switches behaviour based on a combination of other results;
will almost certainly hold multiple responsibilities and will have several reasons to change. An update to any of these actions will potentially introduce a defect into one of the others.
Not only that, but the method is likely to span several hundred lines and will be difficult to comprehend, maintain and unit test and future developers will struggle to extend the functional behaviour.
Methods like these need refactoring into smaller modules with descriptive names that will also help to document the system. The same goes for your classes – lists of methods that scroll of the page indicate that the class is doing too much.
An example of where this complexity manifests can be seen in the use of “behaviour flags”. Methods like these typically evolve over time as developers add more and more flags to coerce the behaviour of a method and inadvertently increase its responsibilities.
public void processTransactions(true, false, false, false, true, true, false, false, true);
Avoid the temptation to add “behaviour flags” as this results in a brittle solution that is hard to understand and is prone to defects.
O is for “Open/Closed”
Write code that allows future developers to support new functionality and behaviours without editing the existing source code. In other words, the code is “Open for extension but closed for modification.” (Bertrand Mayer)
Software that honours this principle should be open to extension by containing defined extension points where future functionality can hook into the existing code and provide new behaviours.
Microsoft are busy refactoring their code to make extensions points such as “single responsibility” helper methods and delegates available, and you should too.
In addition to extending AOT elements using the Visual Studio designer, we can also extend the existing code base by using implementation inheritance and interface inheritance through the standard object orientated language features extends and implements.
Changes to D365FO behaviours are also possible by making using of the following extension points:
Multicast delegates provide convenient extension points to allow higher layer code (the handler) to be called by lower layer code (the delegate instance.)
In addition to delegates, it is also possible to handle the events raised by forms, form data-sources, form controls and tables.
Chain of Command
The Decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. Decorators are layered units in which each layer always performs pre/post processing.
Conversely the Chain of Responsibility pattern allows for multiple things to handle an event but it also gives each handler the opportunity to terminate the chain at any point.
The Chain of Command language feature is actually closer to the Decorator pattern than the Chain of Responsibility as each extension point must always continue the chain – terminating the chain is not allowed:
“Wrapper methods in an extension class must always call next, so that the next method in the chain and, finally, the original implementation are always called. This restriction helps guarantee that every method in the chain contributes to the result.”https://docs.microsoft.com/en-us/dynamics365/unified-operations/dev-itpro/extensibility/method-wrapping-coc
When using the Chain of Command (CoC), stick to the single responsibility principle and simply make a call to your extension handler before or after the call to the next handler in the chain. Don’t be tempted to add further responsibilities to this “plumbing” method.
Finally, (in theory) the only time the existing code base should be updated is to fix defects. In practice, defects exist and nobody is perfect.
L is for “Liskov substitution”
Write code that satisfies the substitution, signature and behavioural principles defined by Barbara Liskov to guarantee semantic interchangeability of types in a hierarchy.
The theoretical definition of the Liskov substitution principles require that:
- objects of type T be replaced with new sub-types of T without breaking the program;
- there must be covariance of the return types (out T) in the subtype;
- there must be contravariance of the method arguments (in T) in the subtype;
- Invariants must be preserved
In practice this means that:
- The overridden methods that are present in base classes must also be present in the derived classes, and they must do useful work.
- Pre-conditions cannot be strengthened – the overridden methods of derived classes must accept at least all inputs of the base class.
- Post-conditions cannot be weakened – the overridden methods of derived classes should return at most all of the outputs of the base class.
- No new exceptions can be thrown by the base class unless they are part of the existing exception hierarchy.
When this principle is violated, it is the responsibility of the client code to check the type of the actual object to make sure that it can operate upon it properly.
The need to perform this check is then proliferated in the code base, which creates dependencies, increases coupling and violates the Open-Closed Principle (OCP).
The level of support for covariance and contravariance seen in C# is lacking in X++.
I is for “Interface Segregation”
Write code that segregates the boundaries between what the client code requires and how that requirement is implemented through the use of cohesive interfaces.
Interfaces define the boundaries between what your client code requires and how the requirements are implemented. The interface segregation principle states that large interfaces should be split into constituent parts so that:
- The client class is only given what it needs (or is authorised to use).
- Developers program defensively to prevent other developers doing something they shouldn’t.
- Developers assist extension through interface inheritance over implementation inheritance.
- We recognise separation of Command/Query.
- Language grammar rules are implemented in fluent APIs.
Following Interface segregation, it may still be possible for a class to implement all interfaces and the same object may be passed in several times to the construct of a client if it implements many of the required interfaces.
D is for “Dependency Inversion”
Write code that decouples high-level modules from low-level modules by depending on abstraction rather than implementation.
Dependency Inversion should follow the “Hollywood principle” – don’t call us, we’ll call you. Rather than the client code making requests for the things it needs, these things should be pushed to it.
- Ideally only one location in an application should have any knowledge of dependency injection. This is known as the composition root and is as close as possible to the application entry point.
- If a class is handed a dependency via its constructor, it should not manually dispose of the dependency itself.
In it’s most basic form, we can achieve this by passing references to class interfaces through the constructors of other dependent classes. For example:
public static MyType construct( IInterfaceA a, IInterfaceB b, IInterfaceC c)
During integration and unit testing, we can then pass mocked implementations of these interfaces to the system under test to remove dependencies on the database and other external resources.
In reality though, the D365FO code base still contains a large number of class dependencies created through use of the new keyword. Even if you do subtype the instantiated class to add your own behaviours , you’ll find that there is no easy way to inject it back into the client code that is dependent on it.
Unfortunately there are no IoC containers for D365FO at the time of writing, although expect to see further articles and framework development from Microsoft in the future.
The SysExtension framework may give you some ideas if you want to build your own.
- Object-Orientated Software Construction (Bertrand Mayer)
- Agile Software Development, Principles, Patterns and Practices (Robert C Martin)
- Adaptive Code via C# (Gary McLean Hall)
- Writing extensible code (Microsoft)