Code Cleanliness: More Than Just Refactoring Part 1
- Mariusz Sieraczkiewicz
- Software development , Code quality
- June 8, 2008
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.)