good-code-is-its-own-documentation

Writing good software is often more challenging than new developers think. It is not always clear what to keep in mind when writing a code. There are also few common issues that I have found new developers making in my career. Below, we cover some of the concepts that I keep in mind while writing code.

Readability - Most Important

It’s OK to figure out murder mysteries, but you shouldn’t need to figure out code. You should be able to read it.
       — Steve McConnell

I often find myself reminding new developers that you read code more times than you write. Prefer readability over fancy looking codes. Even if that means sacrificing some performance. 

Some developers like to write most efficient codes using complex workflows saving every byte or iteration in the process. However, this makes the code very complex. Sometimes, it becomes so complex, that even the author cannot understand how their code is supposed to behave.  

The below example is not a complex one, but it shows how one can improve the flow of the code.

Suppose you have an input array of n items. You want to process k items at a time and have a delay of about 0.5 seconds before moving to the next batch. Implementing this program in type script will look something like this.

Bad Code

type Item = {};
const k = 10;

function processBatch(
    items: Item[], 
    onSuccess: () => void
) {
    items.map(processItem);
    // callback function once batch process is complete.
    onSuccess();
}

function processArray(arr: Item[]) {
    const chunk = arr.splice(0, k);
    processBatch(chunk, () => {
        // wait for 0.5 seconds, 
        // then move on to next batch
        setTimeout(() => {
            processArray(arr);
        }, 500);
    })
}

The above code is a combination of recursion, callbacks, timeouts :(.

Now let’s see how they can be simplified by using Promise and simple utility functions.

Good code

type Item = {};
const k = 10;

function processBatch(
    items: Item[]
): Promise<void> {
    return new Promise((resolve) => {
    items.map(processItem);
    resolve();
    });
}

// resolve promise after 0.5 seconds
function sleep = new Promise((resolve) => setTimeout(resolve, 500));

// create chunks of given size
function chunks<T>(items: T[], chunkSize): Array<T[]> {
    let result = [];
    for (let i = 0, j = array.length; i < j; i += chunkSize) {
        result.push(array.slice(i, i + chunkSize));
    }
    return result;
}

async function processArray(arr: Item[]) {
   for (const ch of chunks(arr, k)) {
       await processBatch(ch);
       await sleep();
   }
}

As you see in the 2nd example, the last function has become so much simpler and easier to understand because we got rid of callbacks, recursions, and changed timeout implementation.

Conclusion

  1. Prefer readability over complexity.
  2. Sacrifice slight performance benefits if it improves readability significantly.

Null safety

Null has been the source of errors in so many cases that even the pioneer of this concept has mentioned this as a billion-dollar mistake.

Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

If that wasn’t enough, dynamic languages such as JavaScript have added made it even more confusing by allowing undefined values.

Most developers tend to forget that their variable can be null and just follow the happy path. This issue is a subset of a much larger issue of Exception Handling. However, this occurs so often that I decided to have it as a separate issue as no. 2.

 Thankfully, most languages today have added support for null checks in their types. This is also one of the first things I look for when choosing a language for a project. Kotlin was one of the first to fix this for Java Developers. The way they do it is by making null a real type. Thereby the compiler forces the developer to deal with the null use case. The developer is not allowed to assign null if the type doesn’t mention it.

I have used Kotlin, but most languages have added this support. In languages such as dart, it is an opt-in feature. However, we strongly recommend using it.

Kotlin Null Safety Example

/** Kotlin Code */
val s: String = null; // Compiler Error
val nullableString: String? = null; // Valid

fun nonNullableStringInput(a: String) {
    print(a);
}

nonNullableStringInput(s); // Successful
nonNullableStringInput(nullableString); // Compiler Error due to incorrect type

Typescript Null Safety Example

Typescript is a super-set of JavaScript which also supports undefined value along with null.

let s: String = "Hello";
s = undefined; // Compiler Error
s = null; // Compiler Error

let s2: String | null | undefined = "World!!";
s2 = undefined; // Valid
s2 = null; // Valid

Conclusion

  1. Avoid using null types wherever possible.
  2. If cannot be avoided, use the correct types to indicate nullables and let the compiler help you write bug free code.

Further read https://kotlinlang.org/docs/null-safety.html

Type Safety

I love types. If a language is strongly typed, there is a good chance that I might adopt it. They help us save countless hours by catching our errors at compile time before they become a run-time production issue. Moreover, it turns the code into live documentation such that we know exactly what each variable type is and also gives us context on how to use those variables and values.

However, it also has a downside in that it makes our code verbose. As the languages are evolving, this is becoming much less of an issue since compilers can do type inference from the assignments and return types themselves. E.g. By using the expression, const s = "hello" the compiler can determine that the type of variable s is string. Hence, type annotation is a thing of the past i.e you do not need to mention const s: string = "hello". The same can be said for the return types. Most languages can infer the return type of the functions by checking the return statements.

The issue that most new developers face is that they try to ignore types as it saves them few seconds. This is especially true if the type is an opt-in feature like Typescript. To support JavaScript completely, Typescript has kept types are optional. But, it often gives developers a chance to be lazy and ignore it completely.

Avoid using base types (Any, Object)

Using any or object type is as good as not using types at all. These types exist for very specific reasons and they should only be used if there is no other choice. It should not be a getaway to be lazy.

Code Example

// Bad Code. Using base types as input
function request(input: any) {
}

// identical to above
function request(input) {
}

// Correct way
type Request {
    username: string,
    firstName: string
}

function request(input: Request) {
}

Using stricter custom types instead of primitives

Domain-Driven Development focuses on types more than we can cover here. The idea is to use the strictest possible types. Even if the value is a primitive type, use type aliases to make the code more readable.

The advantage of using aliases is that it doesn’t require any additional compile or runtime overhead.

E.g If a username that is of type string is an important part of your domain logic, there are 3 ways of using it.

Code Example

// Kotlin
// e.g. 2 treating username a string
fun getUserByUsername(username: String) {
    ...
}

// e.g. 2 wrapping username type
data class Username(val value: String)

fun getUserByUsername(username: Username) {
    ...
}
getUserByUsername(Username("john"));

// e.g. 3 Using type alias
typealias Username = String;
fun getUserByUsername(username: Username) {
    ...
}
getUserByUsername("john");

Conclusion

  1. Avoid base types like any or object.
  2. Use strict custom types instead of primitives.
  3. Use aliases if the custom type has no limitations if the value that can be assigned to them as they are more efficient.

Further, read https://kotlinlang.org/docs/typecasts.html

Exception Handling

It’s hard enough to find an error in your code when you’re looking for it; it’s even harder when you’ve assumed your code is error-free.
— Steve McConnell

Most new developers only focus on making the code work assuming the happy path. But more often than not, errors happen and codes should be robust enough to handle them gracefully.

This is the reason why Exception Handling and correct logging are so important for writing good code. All languages have a multitude of ways to support error handling.

Then there is another category of developers who add the logic to handle errors, but they treat all errors equally by wrapping into a default Exception block with a generic message such as “Something went wrong”.

One should actually treat exceptions just as types and work with different error types to handle specific use cases. Moreover, it may also help to create custom types to add further context to the error.

Code Example

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

class DBError extends Error {
  constructor(message) {
    super(message);
    this.name = "DBError";
  }
}

type Request {
    username: string,
    phone: string
}

function createUser(request: Request) {
    if (validateUsername(request.username)) {
        throw new ValidationError("invalid username");
    }
    if (validatePhone(request.phone)) {
        throw new ValidationError("invalid phone");
    }
    try {
      insertUser(request);
    } catch (e) {
        logger.error(e);
        throw new DBError("failed to insert user in DB");
    }
}

Conclusion

  1. Do not assume a happy path. Always cover Exception Handling.
  2. Use custom Error types to provide additional context for the error that occurred.
  3. Narrow down the caught errors to specifics and handle them appropriately.

Identifying right data structures

Identify the right data structure for your data if you wish your program to be efficient. One has to ask the right questions to understand how they are going to store and use the data.

New developers often stick to basic data structures such as List or Map. While this covers most of our use cases, there are other types we can explore too.

E.g.

  1. If you have a collection of strings, but you know they all have to be unique, prefer Set over Array type. The advantage is that Set type will guarantee uniqueness and also ensure lookup time in O(1) complexity.
  2. If you have 2 lists, and you have to look for some items in one list based on a value in another, you probably need Map with right key-value pairs.
  3. If your use case is more complex having multiple operations such that they might be efficient if one or the other type, try to create a custom type encapsulating the primitive data structures and exposing the right operations.

Suppose you have to represent a monthly cycle as month: year. Instead of using a string type, we can build a class for Calendar Month.

Code Example

// represents a calendar month
class CalendarMonth {
    month: number;
    year: number;
    constructor(str: string) {
        const [m, y] = str.split(":");
        this.month = parseMonth(m);
        this.year = parseYear(y);
    }
    
    toString() {
        return `${this.month}:${this.year}`;
    }
}

function getMonthlyData(month: CalendarMonth) {
    ...
}

Conclusion

  1. Use the right data structures for your programs by asking yourself the right questions.
  2. What kind of data structure would be most efficient in time and space?
  3. If you need lookup for items, use Set or Map.
  4. Write custom data structures if the in-built ones don’t seem efficient enough.

Testing

Testing is one of the most important parts of software engineering. New developers often underestimate the value of it. They believe writing tests slow them down. However, in the long run, it is exactly the opposite. That is why Test Driven Development is considered one of the best ways of developing software.

Imagine you are writing a small program to validate input. Being a good developer, you decided to test all the input types. However, you trust yourself testing them manually. So, you will pass all kinds of inputs that you can think of and check the result of your program. After spending hours, you finally managed to cover all valid and invalid inputs. Congratulations.

Now, the Product Owner comes and tells you, we need to make a slight change in our validation. We want to allow or block a certain kind of input. Again, you are back at testing all possible scenarios to ensure if your updated code still works correctly. And the cycle repeats.

You see, manual testing takes the same amount of time for all iterations. This is why automation is important. Writing tests may take longer than manual testing once. However, it guarantees that your future iterations are testing within seconds. In short, manual testing may save time today, but tests are for the future. Additionally, manual tests are prone to human errors.

In a Test Driven Development, tests come first. i.e You will write a test even before you implement your function. Then, you will start adding implementation and keep running your tests until all of them pass. They are such powerful concepts that if you want to understand any codebase, it is often worth reading the test files first. They reveal the purpose of the program and its feature requirements.

Moreover, it is often said that if we follow Test Driven Development, we also end up following SOLID principles making our code maintainable.

Further, read

  1. https://en.wikipedia.org/wiki/Test-driven_development
  2. https://en.wikipedia.org/wiki/SOLID

Conclusion

  1. Testing adds value in the future.
  2. Tests can act as documentation for newcomers to quickly understand the business logic of your program.
  3. Test-Driven Development is often considered to result in a program that is maintainable in the future.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>