Code Cleanliness: More Than Just Refactoring Part 1

Table of Contents

Initially, my intention was to create an article about refactoring. However, the more I pondered the subject, the clearer it became that I would not be writing solely about refactoring. It’s about something much more significant—conveying a vast amount of knowledge, essentially experience, related to code creation. Code that not only works or is well-designed but is most importantly easy to read. When we achieve this skill, we stand on the threshold of professionalism. Programming professionalism.

Therefore, this will be an article about refactoring, but enriched with a collection of thoughts, suggestions, and occasional doubts, intended to spur you, Reader, into reflection and verification of your programming actions. I believe they will initiate a process of change—introducing new, good habits.

Primarily, Readability

Programming evolves very quickly. I still remember quite well the times when I started my journey with coding about ten years ago. Writing software was quite different then. Creativity, conciseness, and enigma were valued. The more incomprehensible the code, the better the programmer.

However, over time, IT systems became increasingly complex, requiring more and more knowledge and, most importantly, they became products of team collaboration. Nowadays, a single programmer can achieve little. Perhaps one can create a complex desktop application, but they will not be able to create a distributed system based on a three-tier architecture, ensuring an adequate level of security, managing access rights to selected parts of the application, and enabling multilingualism, among other things, within a reasonable time frame. Such systems are currently developed by dozens or even hundreds of programmers, depending on the size of the project, over several or even several dozen months. A programmer ceased to be an individualist misunderstood by anyone, becoming a team player focused on cooperation.

Consequently, the coding manner also had to change. A fundamental postulate concerning coding emerged:

Primarily, readability

There are at least three fundamental reasons confirming the importance of this statement:

  • Requirements are changing,
  • Programming is a team skill,
  • Projects are too extensive for a single person to comprehend entirely.

Because of this, in recent years, techniques such as refactoring and testing have greatly developed, and a tremendous focus has been placed on coding standards.

Readability will be the main character of this article. It will include suggestions and reflections to facilitate the realization of the above postulate. Some tips will represent my subjective opinion; others will convey the wisdom of the programming community’s experiences. Of course, it’s important to remember a certain principle: “The only constant principle is that there are no constant principles.” In generalizing, the presented conclusions have been proven in many situations, which does not mean they are applicable in 100% of cases. Therefore, one should carefully inspect daily arising problems and boldly apply the mentioned tips. It’s worth critically viewing your habits or lack thereof and starting changes. To work!

Through Example to the Goal

Analyzing the code of less experienced programmers often led me to surprising observations, enabling the root cause of problems faced by young (but also experienced) programming adepts to be uncovered. Therefore, this article will be based on a not-so-well-written class example, which will be analyzed and gradually improved.

The authors of the following code were tasked with implementing a class derived from the java.util.BitSet class (a bit vector) enriched with:

  • The ability to concatenate,
  • The property of imposed vector length (field length),
  • Specific multiplication of two bit vectors, which returns 0 if the ones in both bit vectors overlap at an even number of positions, or 1 if they overlap at an odd number of positions,
  • Operation of converting a vector to a string (in a predetermined format),
  • Operation of converting a vector into a byte array.

Let me note that the content of the example doesn’t have great importance here. The provided code merely serves as an illustration of frequently occurring programming imperfections. Moreover, because an inseparable part of refactoring is tests that check the code under test, a test class for the analyzed class has been included as an article supplement.

Here is the proposed implementation of the new version of the bit vector:

import java.util.* ;

public class ExtendedBitSet extends BitSet {
    int length;

    public ExtendedBitSet(int size, String str) {
        super(size);
        length = size;
        int strLength = str.length();
        for (int i = 0; i < strLength; ++i) {
            if (str.charAt(strLength - 1 - i) == '1') set(i);
        }
    }

    public ExtendedBitSet(String str) {
        this(str.length(), str);
        int strLength = str.length();
        for (int i = 0; i < strLength; ++i) {
            if (str.charAt(strLength - 1 - i) == '1') set(i);
        }
    }

    public static ExtendedBitSet merge(ExtendedBitSet a, ExtendedBitSet b) {
        StringBuffer str = new StringBuffer(a.convertToBitString() + b.convertToBitString());
        return new ExtendedBitSet(a.length + b.length, str.toString());
    }

    public static int boolMultiply(ExtendedBitSet a, ExtendedBitSet b) {
        int sum = 0;
        int len;
        if (a.length < b.length) len = a.length;
        else len = b.length;
        for (int i = 0; i < len; i++) {
            if (a.get(i) && b.get(i)) sum++;
        }
        return sum % 2;
    }

    public byte[] toByteArray() {
        int bytesNumber;
        if (length % 8 == 0) bytesNumber = length / 8;
        else bytesNumber = length / 8 + 1;
        byte[] arr = new byte[bytesNumber];
        for (int j = bytesNumber - 1, k = 0; j >= 0; j--, k++) {
            for (int i = j * 8; i < (j + 1) * 8; i++) {
                if (i == length) break;
                if (get(i)) arr[k] += (byte) Math.pow(2, i % 8);
            }
        }
        return arr;
    }

    public String convertToBitString(int size) {
        char[] resultArray = new char[size];
        for (int i = 0; i < size; ++i) {
            resultArray[i] = '0';
        }
        for (int i = this.nextSetBit(0); i >= 0; i = this.nextSetBit(i + 1)) {
            resultArray[size - 1 - i] = '1';
        }
        return new String(resultArray);
    }

    public String convertToBitString() {
        return convertToBitString(this.length);
    }
}

Firstly, let’s look at the class as a whole. One of the first things that stands out is the fact that the concatenation and multiplication methods are static. This is contrary to a very important principle:

Create cohesive interfaces and classes

If we examine the base class BitSet, it’s easy to notice that none of the public methods are static. There are, among others, non-static methods like or(Bitset) or xor(Bitset), which aim to modify the object on whose behalf they are called (operation on this), rather than provide an external (static) method that creates a new object as a result of the implemented operation. Thus, both methods (merge and boolMultiply) introduce a disparity in the structure of the new class, leading to an inconsistent interface of the ExtendedBitSet class. In this case, maintaining consistency by changing static methods to non-static ones will simplify the usage of the ExtendedBitSet class since it will be used in the same way as the BitSet class.

There is another principle worth mentioning when analyzing the merge and boolMultiply methods:

Avoid static elements in programming

Static elements are remnants of procedural programming because staticity means globality. And yet, one of the consequences of object-oriented programming is to close implemented functionalities in autonomous and as independent objects as possible. Therefore use static elements only when there is no other way or when information or an operation is genuinely global. Thus, use static fields as constants, especially global constants, and use static methods for global operations. Examples of using static methods and fields are the Singleton pattern, although the “pattern” status of this pattern is often questioned. Moreover, it’s important to remember that static methods are not polymorphic, which means we cannot provide alternative implementations nor replace them using mocks. Therefore, their use stiffens the code and complicates testing.

Let’s slightly modify the provided code according to the first two rules:

public void merge(ExtendedBitSet extendedBitSet) {
    for (int i = extendedBitSet.nextSetBit(0); i >= 0; i = extendedBitSet.nextSetBit(i + 1)) {
        this.set(this.length + i);
    }
    this.length = this.length + extendedBitSet.length;
}

public int boolMultiply(ExtendedBitSet extendedBitSet) {
    int sum = 0;
    int len;
    if (this.length < extendedBitSet.length) len = this.length;
    else len = extendedBitSet.length;
    for (int i = 0; i < len; i++) {
        if (this.get(i) && extendedBitSet.get(i)) sum++;
    }
    return sum % 2;
}

These are merely initial exercises. More will come soon.

To be continued…

(Text translated and moved from original old blog automatically by AI. May contain inaccuracies.)

Related Posts

Estimation Is Not a Commitment

Estimation Is Not a Commitment

You’ve probably heard that estimation is not a commitment. Sometimes in teams using estimation techniques, some form of accountability for the accuracy of estimation emerges. This is unfavorable for several reasons:
a) firstly, estimation is an approximation (with some probability), not a definitive result;
b) secondly, when accountability kicks in, project games emerge;
c) there are at least several factors causing estimation to differ from the actual time spent on a given task:

Read More

Young Manager/Team Leader! Get a Grip!

History tends to repeat itself, and this is a common tale among young managers and team leaders. A recurring, tragic mistake is the commitment to unrealistic deadlines.

Read More

The Natural Order of Refactoring Under the Microscope Part 5: Evolution of Architecture

Architectural Evolution

An essential next step, at a much higher level of abstraction, requires a deep understanding of the system. Based on emerging patterns and developing domain objects, over time we realize the need to modify the architecture. Architectural patterns or the introduction of other architectural mechanisms can assist us. Such transformations may include:

Read More