Write More SOLID Code
SOLID is an acronym for 5 design principles related with Object-Oriented Programming (OOP). When correctly applied SOLID principles make software more maintainable, flexible and understandable. SOLID represent a subset of multiple principles proposed by Robert C. Martin in the 90s.
Here are NDepend blog posts that explain in details each SOLID principle:
- The Single Responsibility Principle (SRP): A class should have one reason to change. This principle is about how to partition your logic into classes and avoid ending up with some monster classes (known as god class).
- The Open-Close Principle (OCP): Modules should be open for extension and closed for modification. To implement a new feature, better just adding a new derived class than having to modify some existing code.
- The Liskov Substitution Principle (LSP): Methods that use references to base classes must be able to use objects of derived classes without knowing it. Array implements IList<T> but throws NotSupportException on IList<T>.Add(). This is a clear LSP violation.
- The Interface Segregation Principle (ISP): Client should not be forced to depend on methods it does not use. This is why interfaces like IReadOnlyCollection<T> have been introduced, because often client just need a subset of features like read-only access.
- The Dependency Inversion Principle (DIP): Depend on abstractions, not on implementations. Interfaces are much less subject to changes than classes, especially the ones that abide by the (ISP) and the (LSP).
SOLID principles help guiding the usage of powerful concepts of Object Oriented Programming (OOP). But they are subjectives. A comment on the posts above summarizes well:
Most people who "practice" SOLID code don't actually know what it means and use it as an excuse to do whatever the hell they were going to do anyways.For more details you can read this post Are SOLID principles Cargo Cult?. In this context, let's explain how NDepend can concretely and objectively help write more SOLID code:
The Single Responsibility Principle (SRP)
This principle states:
A class should have a single responsibility and this responsibility should be entirely encapsulated by the class.
This leads to what is a responsibility in software design? There is no trivial answer, this is why Robert C. Martin (Uncle Bob) rewrote the SRP principle this way:
A class should have one reason to change.
This leads to what is a reason to change? And there is no clear answer to that. However to increase your chances to abide by SRP, you should avoid writing too large and complex classes and methods. NDepend proposes several rules for that:
- Avoid Types Too Big
- Avoid Types With Too Many Methods
- Avoid Types With Too Many Fields
- Avoid Methods That Are Too Big and Too Complex
- Avoid Methods With Too Many Parameters
- Avoid Methods With Too Many Local Variables
Additionally you can write custom rules to identify classes that obviously have more than a single responsibility. For example you can get inspiration from these 2 default rules that match classes that are mixing User Interface and Database access:
Finally the NDepend search by coupling tool allows you to review which namespaces and types are used by each type.
The Open-Close Principle (OCP)
This principle states:
Modules should be open for extension and closed for modification.
This is the short definition of the principles, here is the orginal definition:
- A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
- A module will be said to be closed if it is available for use by other modules. This assumes that the module has been given a well-defined, stable description (the interface in the sense of information hiding)
There are 2 ideas here:
- The open idea means that adding a subclass to a base-class, or adding a new class that implements an interface, should not cause the base-class or the interface to change. Clearly each violation of the NDepend rule Base class should not use derivatives is a violation of this principle.
- The close idea means that you should be confident that the surface of the base type (base-class or interface) visible from the derived classes won't be modified in the future. Maybe will come a day when some Artificial Intelligence might offer some realistic advices on that point but nowadays this really requires a human analysis of the design. With NDepend it is possible to write a a rule to detect that the interface presented to derived classes doesn't change since the baseline with inspiration from the rule API Breaking Changes: Methods. But in the real world changes just happen and the noise ratio of such rule would be too high.
The Liskov Substitution Principle (LSP)
This principle states:
Methods that use references to base classes must be able to use objects of derived classes without knowing it.
To clarify this point, there is a famous violation of the LSP that probably caused you a headache in the past. In .NET the class System.Array implements the ICollection<T> interface. Hence Array has to implement the ICollection<T>.Add() method but calling this method on an array throws at runtime a NotSupportedException:
Hence certainly the default rule Do implement methods that throw NotImplementedException can detect some LSP violations. It is easy to adapt this rule to your own convention to detect methods that throw NotSupportedException or any other custom exception class.
The Interface Segregation Principle (ISP)
This principle states in short:
Client should not be forced to depend on methods it does not use.
A more verbose definition is:
ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. Such shrunken interfaces are also called role interfaces.
A perfect example of the ISP is the interface System.IDisposable. This interface has a single method but it is implemented by a wide variety of classes: DB or network connections that need to be closed gracefully, UI elements that need to deallocate some bitmaps in memory… The only thing the IDisposable interface says to clients is that instances of IDisposable class needs a graceful shutdown.
The NDepend rule Avoid interfaces too big will spot some violations of the ISP. Its threshold is 10 methods and properties maximum allowed. But often a well designed interface has much less methods or properties, like 3 or 4. So you might prefer adjusting the threshold in the rule source code.
Notice that this rule can also spit some false positives. For example this fat interface is perfectly valid, it wouldn't make much sense to split it:
In such situation the SuppressMessageAttribute can be still used.
The Dependency Inversion Principle (DIP)
This principle states:
Depend on abstractions, not on implementations.
- a. High-level modules should not depend on low-level modules. Both should depend on abstractions.
- b. Abstractions should not depend on details (concrete implementation). Details should depend on abstractions.
The NDepend report proposes the Abstractness vs. Instability diagram based on the original Robert C.Martin code metric related to DIP. This diagram helps to detect which assemblies are potentially painful to maintain (i.e concrete and stable) and which assemblies are potentially useless (i.e abstract and instable):
- Abstractness: If an assembly contains many abstract types (i.e interfaces and abstract classes) and few concrete types, it is considered as abstract.
- Instability: An assembly is considered stable if its types are used by a lot of types from other assemblies. In this context stable means painful to modify.
The formulas used for these metrics are available in this documentation.
In the diagram above we can see that the project Nop.Core is in the Zone of Pain of the diagram. It means that this project is stable - used by a lot of other projects - and it mostly contains classes implementations and few interfaces. It is likely that the public surface and the implementation of classes in Nop.core will evolved with time. As a consequence this might break some client code and provoke fictions: this is a violation of the DIP.
To improve the maintainability some domain interfaces have to be carefully designed in Nop.Core and client will have to mostly depend upon these interfaces.