SOLID Principles

SOLID principles are comprised of five individual principles for writing better software. They are created by experienced developers for better guidance and to reduce complexities for writing better code that is testable and maintainable.

Single Responsibility Principle

It states that each software module should have only one responsibility or, one and only one reason to change.

A module refers to a class or a method. In an application, each class or method defines what the application does and how it does it. We can often improve the design of the software if we can separate the what from how through delegation and encapsulation.

When does the responsibility of a module change?

Responsibilities change at different times for different reasons. Each one is the axis of change. The potential source of changes to your application can help you identify when you might be violating SRP.

  • Any changes/extensions/improvements approved by the superiors in the functionality of the module.

  • Tight coupling

  • We can also follow the separation of concerns principle that suggests the programs should be separated into distinct sections, each addressing a separate concern.

  • Cohesion.

Summary

  1. Each class should have a single responsibility or reason to change.

  2. Strive for high cohesion and loose coupling.

  3. Keep classes small, focused and testable.

Open / Closed Principle

It states that software entities(classes, modules, methods, etc.) should be open for extension but closed for modification. It describes how we should strive to structure our code to extend it in the future.

Benefits of OCP

  • Less likely to introduce bugs in code or need to redeploy.

  • Less downtime and less likely to break dependent code.

  • Code that is open for extension usually has fewer conditional statements than the code that is closed for modification which results in simpler code.

  • Bug fixes are easy.

Summary

  • OCP describes when and how we should use abstraction to make our design more extensible.

  • Make sure to solve the problem at hand using simple concrete code. Don't try to make your code open to extension in every possible way as it makes code way too abstract.

  • Identify the kinds of changes the application is likely to continue needing.

  • Modify the code to make it extensible along the axis of change you have identified (without the need to modify its source code every time).

Liskov Substitution Principle

It states that the subtypes must be substitutable for their base types. It also states that IS-A relationship is insufficient and should be replaced with IS-SUBSTITUABLE_FOR.

It guides how to properly use inheritance. LSP also tends to produce code that better follows OCP.

Detecting LSP violations in the code

  • Type checking with is or as in polymorphic code

  • Null checks

  • NotImplementedException

Fixing LSP Violations

  1. Minimize null checks with C# features (null conditional operators and null coalescing operators), Guard classes and null object design patterns.

  2. Follow ISP and be sure to fully implement interfaces.

Summary

  • The IS-A relationship is insufficient for OOP design. You need to ensure the subtypes are substitutable for base types.

  • Types often have invariants that must be maintained in the code that uses these types depends upon. You must take care not to break these invariants when you create subtypes or implement interfaces.

Interface Segregation Principle

It describes how we should design and use interfaces in our applications. Clients should not be forced to depend on methods they do not use. The corollary to this is that you should prefer small, cohesive interfaces to large fat ones.

What does Interface mean in ISP?

  1. C# interface type/keyword

  2. The public (or accessible) interface of a class

A type's interface in this context is whatever can be accessed by client code working with an instance of that type.

Detecting ISP violations in your code

  1. Large Interfaces

  2. NotImplementedException

  3. Code uses just a small subset of the larger interface

ISP is related to LSP, cohesion, SRP and pain-driven development.

Fixing ISP Violations

  1. Break up large interfaces into smaller ones.

  2. If the original fat interface is still being used, you can compose it from smaller ones for backward compatibility.

  3. It's easy to follow ISP if you let your client code define the interfaces the code will work with and own it.

Summary

  1. Prefer small, cohesive interfaces to large expansive ones.

  2. Following ISP helps with SRP and LSP.

  3. Break up large interfaces by using interface inheritance and the adapter design pattern.

Dependency Inversion Principle

It states that high-level modules should not depend on low-level modules. Both should depend on abstraction. Abstraction should not depend on details rather details should depend on abstractions.

What is Abstraction?

Abstraction in C# refers primarily to interfaces and abstract base classes

Interfaces are generally preferred because they don't require object inheritance which makes them a bit more flexible.

The abstractions just define a contract, a way of working with a type without actually specifying how that work is going to get done by having a specific implementation of that contract.

What is the Details?

Abstractions shouldn't be coupled to details. That is they shouldn't know about the specifics of how they are implemented.

Dependency Injection

Dont create your dependencies

  • Depend on abstractions

  • Request dependencies from the client.

  • The client injects the dependencies as

    • Constructor arguements

    • Properties

    • Method arguments

  • The dependency injection technique is an implementation of the strategy design pattern.

  • Tip - Prefer constructor injection

    • Follows explicit dependencies principle by ensuring client code is aware of what dependencies a given class has.

    • It ensures an instance of the class is always in an initialized state (no need to check if properties are set before using)

    • Can leverage an IOC container to construct types and their dependencies.

Summary

  • Most classes should depend on abstractions, not implementation details.

  • Abstraction shouldn't leak details.

  • Classes should be explicit about their dependencies.

  • Clients should inject dependencies when they create other classes.

  • Structure your solutions to leverage dependency inversion.

Did you find this article valuable?

Support Sankarshan Ramesh by becoming a sponsor. Any amount is appreciated!