Sunday, April 25, 2021

How programming languages contribute to clean code

I had a conversation with some friends about the programming languages different companies use. Many companies only choose the most popular language such as Java and C#. They believe "Developers can write clean code in any languages" so just pick one which has the biggest resource pool to make hiring easier.

However, clean code is the key for a technology company to succeed. It provides the company to maintain a fast pace for its delivery. Companies who limit the languages don't realize that they might be losing the speed of delivery, which can be measured by four key metrics.

What decides the level of clean code?

The good code is working code (pass all the tests) with very good readability (reveal intentions) as well as least elements (no duplication). The good code also allows the reader to exit early so they can understand the code quickly by reading the minimal amount of information.

We have been trying to achieve that for decades. Many frameworks, libraries, language features (I’ll call it “styles” in the rest of the article) have been introduced for this purpose to allow developers to write less and clear code for more things, to separates the concerns of different levels of abstractions.

However, we have a problem that all of the styles are contextual. If we use a low-level style (close to computer interaction such as copying an array from memory A to memory B) to solve the high-level problems (close to the real world such as generating an invoice), the code is very hard to be clean. But if we use a high-level style to solve the high-level problems, we can write much cleaner code easily. So it’s all about putting the right styles in the right places. The more styles a programming language provides, the higher chance to have clean code. But there is also a higher chance to have messy code as well if we fail to choose the right styles.



So many people think the level of clean code totally depends on the developers.

Is that true? Do languages have no contribution to clean code?


Styles supported by different languages

I choose 4 languages as examples, but keep in mind that there isn’t a language that supports all the styles (no language rules all):

  • Java - it is a pure OO language. It supports basic lambda expression since Java 8. But the capability of the language is very limited. Most time we have to write code in low-level styles to solve problems.
  • C# - very similar to Java. But because of LINQ syntax which is more powerful than stream in Java, it allows developers to write cleaner code in high-level styles. However, the patterns are still very limited.
  • Kotlin - it has massive improvements on the basic syntax from Java. It also creates immutable collections which provide a much better experience than stream. However, due to the lack of ways to combine types and methods, it is still hard to keep complicated logic clean.
  • Scala -  it is a multi-paradigm language. It supports many styles from low-level styles to high-level styles. It supports 7 levels of mastery in functional programming which Kotlin/Java/C# can only support 2 levels because we can’t implement type classes easily. Also because of the for-expression and implicity features, it allows developers to easily separate the different levels of abstractions.

From these examples, we can see different languages do support different numbers of styles. Some support much more styles than others, which means they provide more options to allow good developers to write cleaner code. On the other hand, it will be very hard for developers to write clean code if the right styles are not supported by a certain language.


Relationship among clean code, developers, and languages

So the relationship among clean code, developers, and languages look like this:

Notice: the diagram does not say a language is better than another, it says some languages provide more options than others. As long as developers make the right choice, they can write better code in a language than others. And the more options a language provide, the better code it can be.

Summary

Changing a language does not guarantee an increase in your team performance. However, stopping good developers to write better code guarantees a decrease in your team performance. Companies that want long-term success should allow good developers to choose a more powerful language when they reach the bottleneck. It helps the company to attract more good developers as well as to continuously improve the team performance.

Reference:


How languages evolve?

By the way, good developers also try to extend the language to support the styles they want until it’s too hard, then they create a new programming language. Many new languages die because they couldn’t fit the purpose. Only a few survive with a strong community and its ecosystem. They can normally be safely chosen for commercial use.

Be careful about choosing a language for learning purpose

Trying new languages in a long-term system is dangerous. Writing clean code in a language requires a certain level of proficiency in that language. It takes time to learn. We should practice the new language in coding dojos first, then experiment with it in some short-term products or very simple products which you can rewrite within a week by your mastered language.



Sunday, April 18, 2021

Cohesion and coupling of an object in OO programming

I keep seeing developers extracting coupled logics into new classes, which reduces the cohesion of objects and makes objects tightly coupled to each other. This article describes a way to check the cohesion of the code in an object and the coupling between objects, which helps developers check whether the refactoring improves the cleanness of the code or not.

Please note that it can not be applied to pure functional programming.

Cohesion

VF = number of private fields (no non-private getter or setter, not a property)
OF = number of non-private fields
U = number of usage of fields by public methods (count 1 per public method per field)
M = number of public methods

C = cohesiveness (0 to 1, 0 means the worst, 1 means the best)

C = U / ((M + OF) * (VF + OF))

Other rules:

  • protected counts as private
  • Calculation includes all fields and methods in the parent classes (except Object) 

Coupling/Independence

F = number of fields (both private and non-private) as:
  F = VF + OF

R = number of references

P = coupling factor (0 to 1, 0 means no coupling, 1 means fully coupled to others)

P = R / F

I = factor of independence
I = 1 - P

Examples:

1. Full cohesion:

public class Example {
    private final String value; // VF: +1

    public Example(String value) {
        this.value = value;
    }

    public int m1() { // M: +1
        return value.length(); // U: +1
    }
}

// C = 1/((1 + 0) * (1 + 0)) = 1

2. No cohesion:

public class Example {
    private final String value; // OF: +1

    public Example(String value) {
        this.value = value;
    }

    public String getValue() { // M: does not count
        return value;  // U: does not count
    }
}

// C = 0/((1 + 0) * (1 + 0)) = 0

3. Half cohesion:

public class Example {
    private final String value; // OF: +1

    public Example(String value) {
        this.value = value;
    }

    public String getValue() { // M: does not count
        return value;  // U: does not count
    }
    
    public int m1() { // M: +1
        return value.length() + value.indexOf("a"); // U: +1
    }
}

// C = 1/((1 + 1) * (1 + 0)) = 0.5

4. Coupling:

public class Reference {
    public int findI() {
        return 1;
    }
}

public class Example {
    private final Reference r1; // F: +1 & R: +1

    public Example(Reference r1) {
        this.r1 = r1;
    }

    public int m1() {
        return r1.findI();
    }
}

// P = 1/1 = 1
// I = 0

5. Mix

public abstract class Parent {
    protected final int a; // VF: +1

    public Parent(int a) {
        this.a = a;
    }
}

public class Reference {
    public int findI() {
        return 1;
    }
}

public class Example extends Parent {
    public final String f1; // OF: +1

    private final String f2; // OF: +1

    public String getF2() {
        return f2;
    }

    private final String f3; // VF: +1;

    private final Reference r1; // VF: +1; R: +1

    public Example(String f1, String f2, String f3, int a, Reference r1) {
        super(a);
        this.f1 = f1;
        this.f2 = f2;
        this.f3 = f3;
        this.r1 = r1;
    }

    public int m1() { // M: +1
        return l() + a + r1.findI(); // U: +3 (f3, a, r1)
    }

    private int l() {
        return f3.length();
    }
}

// C (Example) = 3/((1 + 2)*(3 + 2)) = 0.2
// I (Example) = 1 - 1/(2 + 3) = 0.8

Monday, April 12, 2021

The best time to do technical pre-design

I've been seeing developers discussing the details of implementation a lot recently. They spend a lot of time writing documents for choosing an option of data models, drawing flow diagrams to explain different ideas for a requirement, discussing how to entities design should be in tech huddles. But they just don't write code.


Pre-designs are required but not all the time. Sometimes the best way to find the best design is to write code. In this blog, I'm going to explain when we should do pre-designs and when should not.

What is design (noun)?

  • High-level design: the structure of domains, the interactions (also called contracts or interfaces) between different high-level domains

  • Middle-level design: the structure in the high-level domains, which is about subdomains and systems, plus the interactions among them

  • Low-level design: the structure of code in a system, which is about components (packages, classes), plus interactions among them.

Interactions include:

  • what - input and output

  • how - invoke or subscribe, plus the direction of the dependency

 


What is good design?

A good design supports future changes at a low cost at all levels.

What is a design activity?

Try our best to make a series of decisions for good designs at different levels.

 

Can we get the best design from the first design activity?

Of course, NO. Decisions in design activities are objective. They are very similar to bets. We are not able to verify the correction until we implement them.

The only way to find the best design is through evolutions by following the 4 rules of simple design and YAGNI all the time. This is very similar to the TDD practice.

However, the cost of each evolution is also based on the level of designs. Code-level refactoring is much cheaper than the system level. The reason is the complexity of high-level designs is much higher than low-level designs.

Diagram 1: cost to make an evolution (design change) vs the complexity of the current design

 

The diagram shows that it is not possible to only rely on evolutions to find the best design when the system gets complicated. So we have to find another way to maintain them at a reasonable cost. Since we can’t reduce the cost of each change, the only way is to reduce the number of changes. This is why we need the pre-design activities which should help us to avoid some future changes. But please notice that we’ll no longer be able to have the best design, and we have to rely on experts to make good decisions.


Summarize the 2 ways to make a good design:

  1. TDD - refactor to make the design better

  2. Pre-design - rely on experts to make good decisions

 

When is the best time to have pre-design activities?

Diagram 2: compare 2 ways to make a good design

 

The diagram shows that if the systems involved (eg. systems in different domains) have high complexity, pre-design may perform better than the TDD approach. But keep in mind that keeping the designs in high-level (like contracts between domains) and avoiding the internal designs in each system because TDD is still the best approach for the internal designs which don’t have the high complexity.

 

Summary

We should follow the TDD approach by doing a lot of refactorings to find the best design in low-level designs. We should do pre-design to get a good enough design in high-level designs. We can mix the 2 approaches in middle-level designs.

 

By the way, a small company such as a startup usually doesn’t have high-level designs. So they can iterate very fast without having pre-design activities. However, they must make sure to do refactorings to keep the systems clean so that they can maintain the speed of delivery.

References: