Clean Code
Last modified on Tue 28 Nov 2023

What is the clean code?

celanCodeImg

The image above is a pretty good explanation of how to distinguish between good and bad code. It is WTF/s, and almost every code will have it, but good code will make your colleagues less mead, software easy to manage and grow, and enable the company to evolve.

To develop code that will put us on the left side of the above image, developers should always strive to apply a set of principles, patterns and well-known best practices.

Clean code reflects in :

Naming

Developers write code for machines to execute it, but for other developers to maintain and extend it. So code should be easy to read and understand. The code should reflect the shared vocabulary used by all team members involved in the project.

So in general naming should be:

Comments

Comments are part of the source code, and if not consisted of significant info, then comments act as noise. Even worse, if not well maintained they can lead developers to false conclusions. In general, developers should avoid writing comments. If the code is unclear, it is a sign it should be rewritten.

Exceptions:

Methods

Methods should be short and have a single responsibility. The logic contained in a single method should reflect the same level of abstraction. Mixing different levels of abstraction in the same function is a code smell.

Code order

C# doesn't have any specific requirements for the code order inside a class. This is great for us because it gives us the freedom to place code wherever we want, but that doesn't mean that we should just put it anywhere and call it a day.

As we mentioned before, the code we write must be understandable to developers as well as the machines. In this context, understandable code must tell a story about the class we are writing, just as if we were writing a newspaper article. First, you get the high-level information, and as you continue reading you dive into more details. Related code should be vertically close, and callers should be above the callees, if possible. Alongside these guidelines, we use the following order:

  1. private fields
  2. public properties
  3. constructors
  4. static methods
  5. instance methods

Abstraction & Encapsulation

It is a good practice to expose abstract interfaces that allow its users to manipulate the essence of the data, without having to know its implementation.

When modeling entity classes, encapsulating data state details, lead to increased control over entity access and manipulation, providing clean, well-defined ways to interact with entities. Simply put, you should hide details, and expose behavior.

Law of Demeter

Each unit must have limited knowledge of other units: it must see only units closely related to the current unit.

In other words, the Law of Demeter principle states that a module (class) should not know about the inner details of the objects it manipulates.

LoD

    human
        .getDigestiveSystem() // 1. level of details
        .getStomach()         // 2. level of details
        .add(new Cake())    

The above code can be described as sausage code and express an obvious code smell:

Instead above code should be rewritten to :

    human.Eat(new Cake())

Anemic model as an anti-pattern

In the development of software that solves non-trivial problems and contains rich business logic, code is much more complex than in simple CRUD-based software. To model the business entities with integrity, their data states should be hidden while exposing the methods to interact with the entity.

Anemic models are known in the industry as business entities modeled as simple DTOs, leaving the purpose interpretation and interaction responsibilities to the calling code (usually services). Usually, the business logic ends up being implemented in service classes, which can lead to code duplications, leaking of the business logic into other layers, missing or corrupted entity validations, and many more issues.

Ways to avoid an anemic domain model:

Primitive obsession

Primitive fields are basic built-in building blocks of a language such as integers, dates, strings, etc. Primitive Obsession is a programming style that heavily relies on primitives. Designing business entities relying on primitive types can result in poor or decentralized entity state validation, and is often breaking the single responsibility principle.

    public class CompanyEvent : Entity
    {
        private readonly List<Member> _members = new();     // list of members is private and can not be manipulated freely in calling code.
        public IReadOnlyList<Member> Members => _members;  //  calling code have access to IReadOnlyList, so data integrity is protected.
        public Name Name { get; } = default!;             //   Name is not just a string, it is Name class that implements validation rules
        public TimeFrame Time { get; set; } = default!;   //   TimeFrame class is implementing date validation rules

        /// here you define methods i.e. behaviors of your entity you need to expose
    }

SOLID

Single responsibility:

A class/method should have one and only one reason to change, meaning that a class/method should have only one job.

Open closed principle

Objects or entities should be open for extension but closed for modification. Class inheritance is not always the best way, coding to an interface is an integral part of SOLID.

Liskov Substitution Principle

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

This means that every subclass or derived class should be substitutable for its base or parent class.

Interface Segregation Principle

A client should never be forced to implement an interface that it doesn't use, or clients shouldn't be forced to depend on methods they do not use. This means interfaces should be small, and should not contain methods not critically linked. If that is the case, consider splitting one interface into multiple smaller interfaces.

Dependency inversion principle

Entities must depend on abstractions, not on concretions. The high-level modules must not depend on the low-level modules, but they both should depend on abstractions.