DEV Community

Filip Kisić for BISS d.o.o.

Posted on

How To Clean Code - Part II

Intro

Welcome to the next part of the clean code blog. In the first part, we were talking about naming, functions, comments, and formatting. In this part, we will talk about objects, data structures, unit testing, and classes. If you didn't read part one, I kindly recommend reading it because it is a great intro for what the clean code is and how to practice writing it.

Objects and Data Structures

TLDR

  • Object hides its implementations, exposes its behaviors which manipulate the data inside
  • Data structures expose their properties, has no behaviors
  • Use objects when flexibility to add new object types is needed
  • Use data structures when flexibility to add new behaviors is needed

Back to the first days of our programming, our first usage of a data structure. It was introduced as a custom data type that can hold multiple different data types and that revealed a new way to manipulate data and to pass it through the program. It was an introduction to one of the most important concepts we can learn as software engineers, and that is a class. A class could also hold multiple different data types, but it also had functions or methods as it is said in the OOP world. We were taught that data structure has just properties while the class has properties and behaviors. Consider a toy car as a data structure, it has properties like color, material, size, etc., while a real car is a class that also has behaviors like turn on, rev, drive, park, turn off, etc. That is how we would describe the difference between them to a beginner. Let's describe it on a technical level. Here is an example:

Concrete Car

public class ToyCar {
    public String color;
    public int lenght;
}
Enter fullscreen mode Exit fullscreen mode

Abstract Car

public interface Car {    
    public String getColor() {...}
    public int getHorsepower() {...}
    public void drive() {...}
    public void rev() {...}
}
Enter fullscreen mode Exit fullscreen mode

The biggest and most important difference is their level of abstraction. ToyCar exposes its implementation with public properties, while Car hides its implementation with the interface. Why interface? Well, interface Car represents behaviors of a data structure. When it implements Car interface, it will become a full class, but the best part, as we still look at the Car, we don't know its implementation, we don't know how do we drive a car because it depends on if it is automatic or a manual. We don't know how to rev and does it even rev because it is electric. A lot of questions to ask, but non to be concerned about. Here is what Uncle Bob says about the difference in the book "Clean Code" :

"Objects hide their data behind abstractions and expose functions that operate on that data. Data structure expose their data and have no meaningful functions"

Try to read it a couple of times. With an interface like a Car, we can easily add a new class like ElectricCar which implements the interface, and there we have a new object type, but if we want to add new behavior to a Car, for example, park(), we would have to define that in each class which implements the Car interface. To make it short, it is easy to add a new object without changing a behavior, but hard to add new behavior. Data structures do the exact opposite, it is easy to add new behavior to an existing data structure, but hard to add a new data structure. In a conclusion, when the project requires flexibility to add new data types, use objects, when the project requires flexibility to add new behaviors, use data structures with procedures.

Error handling

TLDR

  • Exceptions instead of returning codes
  • Try-catch wrapping
  • In the catch block, use defined and concrete classes
  • Use unchecked exceptions rather than checked
  • Don't return nor pass null values

Error happens, that's why it is important to know how to properly handle those errors. Sometimes, for precautions, big chunks of code are wrapped with error handling code and by that, code becomes hard to read. Error handling is very important, users can enter invalid input, API can be unreachable, or we simply cannot connect to a device. And because that is easily possible, we must know how to handle it right. If you come from the C or C++ world, at some point you probably saw a piece of code that returns the integer as an error code. While in some systems that is a standard, in the OOP world that is a bad practice, don't send error codes as integers. Instead, return an exception object. By returning the error code, when you call that function, the first thing you have to do is check if the result was an error. Checking is introducing if-else statements which can generate a lot of garbage code. Except that, everything has to be done manually, you have to know which function returns error codes, which one means what and that can be difficult to remember, and therefore it is possible to forget the error check and our code won't work as expected. Look at this example:

public void lockTheCar() {
    RemoteKey remoteKey = getKeyOutOfPocket();
    Integer result = remoteKey.lockTheCar();
    if (result == RemoteKey.OUT_OF_BATTERY) {
        logger.log("Remote key battery is empty...");
    } else if (result == RemoteKey.IR_NOT_WORKING) {
        logger.log("IR blaster is not working...");
    } else {
        blinkTheHazardLights();
    }      
}
Enter fullscreen mode Exit fullscreen mode

Quite messy, right? You have to check for every possible error code, and if in the future, the more is added, the more code to write for you. Let's see how it should be done:

public void lockTheCar() {
    try {
        tryToLockTheCar();
    } catch (RemoteException e) {
        logger.log("Remote: " + e.toString());
    }
}

private void tryToLockTheCar() throws RemoteException {
    RemoteKey remoteKey = getKeyOutOfPocket();
    remoteKey.lockTheCar();
    blinkTheHazardLights();
}

public class RemoteKey {
    ...
    public void lockTheCar() {
        ...
        throw new RemoteException("Remote key battery is empty...");
        ...
        throw new RemoteException("IR blaster is not working...");
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Easier to read, functions are separated and RemoteException is being thrown which makes this code clean and easy to understand. If there is a new place to throw an error, the catch block will catch it, there is no need to modify the code. Look one more time at the try-catch block above, it is short, it is wrapped only around the code which can throw the exception and the exception is concrete. Try to avoid using the Exception class in the catch block, the more concrete class is, the better. The next thing we are going to talk about is checked exceptions. If you don't know what checked exceptions are, here is a short explanation. Look at the function tryToLockTheCar(), it has throws in the function signature and tells us which exceptions that function can throw. The benefits of using checked exceptions are readability and code won't compile if those exceptions aren't covered in the callers function. But there are more important flaws when using checked exceptions.

Let's say we have 3 layers of application code, where level 1 is the lowest and level 3 is the highest. We want layers to communicate and work together, but they shouldn't know how other layers work. With checked exceptions, that isn't possible. If you define that level 1 throws IOException and there is a catch clause on level 3, level 2 also has to be changed, it also has to change its signature with the throws word. This way, the Open-Closed principle is not done right, for change on the lower level, the higher level has to change too. Since we are talking about exceptions, we all can agree that NullPointerException is one of the most common exceptions we came across so far. So just a short tip to avoid in the future as much as possible. Don't return null values or even worse, don't pass them to functions. With that practice, there are fewer null checks to be done and therefore a smaller chance for you to not check some of the values that could be null.

Thankfully, in more modern languages like Kotlin, Dart, etc. there are nullable and non-nullable types which helps us a lot with null handling. If we follow these rules, error handling also becomes easy, readable and maintainable.

Unit test

TLDR

  • The Three Laws of TDD
  • Clean tests, consistent quality
  • F.I.R.S.T.

All code above eventually ends up in the production code, so it is important to test the code. There is also another approach, test-driven development, where you write tests first and then write code that will make those tests pass. How do we know when our code is enough for the test to pass? There are three laws of test-driven development to guide you.

  • You may not write production code until you have written a failing unit test.
  • You may not write more of a unit test than is sufficient to fail, and not compiling is failing.
  • You may not write more production code than is sufficient to pass the currently failing test.

If it is hard to understand those rules, let's go through them. The first law says that you have to write a failing test first and then write production code to make that test pass, nothing more, nothing less. By following the second law, your test code will be as short as possible, just enough to demonstrate the failure. The third law wants you to apply the second law to the production code. As soon as the code passes the test, that is it, no more to write, just refactoring. Here is a "refactored" version of three TDD laws:

  • Write NO production code except to pass a failing test
  • Write only ENOUGH of a test to demonstrate a failure
  • Write only ENOUGH production code to pass the test

By writing the tests, we make sure our code does what we want, and it is safe to push it to production. Following that practice, we write a lot of additional code, so we have to make sure tests are readable, easy to understand and maintain. As a function has to do ONE thing and ONE thing only, the test should demonstrate ONE case and ONE case only. For example,
for the if clause, at least two tests have to be written for that. This way we keep our tests clean and code quality consistent. There is an acronym that can help you write clean code, and it is called F.I.R.S.T.

  • Fast - Tests should be run frequently, therefore we want them to execute fast.
  • Independent - Every test should be able to run separately, no dependencies on other tests.
  • Repeatable - Environments like localhost, QA, UAT or production, tests execute no matter which environment it is.
  • Self-Validating - Test should ALWAYS be binary, pass or fail.
  • Timely - Write test code just before production code, test functionality, then write production code.

Unit tests are equally important in the project as the production code, so take care of the tests you write. If you recently entered the world of software engineering, consider the unit test as a tool to check your code functionality, especially when a new feature is added, they can serve to test if everything else remains as it should. If TDD is something you're a fan of, read one more time The Three Laws of TDD, practice it and check how clean are they with the F.I.R.S.T. guidelines. Only like that, you'll make your test code maintainable and most importantly, clean.

Classes

TLDR

  • Class organization (Make it readable as a newspaper article)
  • Classes should be small, single responsibility principle
  • Isolate from change, dependency inversion principle

In the sections above, we talked about how to write good functions, but we didn't talk about the higher level of organization, where all those functions are, class files. Uncle Bob has a brilliant way to remember and check how a class should be written, it is called a stepdown rule. The stepdown rule helps the class to read like a newspaper article. That sentence will be the guiding thought for this section. Standard Java convention dictates that a class should begin with a variable list. On the top must be public static constants followed by private static variables and then private instance variables. Of course, we have to make sure our class is well encapsulated, make all variables as private as possible. What I mean by that is to start with a private access type and expose it only if necessary.

Let's go back to the newspaper article, how long it usually is? It is one page long on average and yet it contains everything you need to know and nothing else, so it is straight to the point and focused. This is exactly how a class should be, short, straight to the point and focused on one topic. Uncle Bob says that the first rule of classes is that they should be small, and the second rule is that they should be smaller than that. What we prevent with that is making the class to concern about too much. Close to the top of the blog, we talked about names. In this case, with a name we define the responsibilities of a class, therefore if it is short, it will be easy to give it a name. A neat trick, right? Also, try to describe the class first, if there is "and" in the description, it is either too long or it has too many responsibilities. Same as functions should do ONE thing and ONE thing only, classes should have ONE responsibility and ONE responsibility only. That is the single responsibility principle.

Except for the single responsibility principle, there is one more which we should look out for when we talk about classes and that is the Dependency Inversion Principle. As short as possible, it says that the class we write should depend upon abstractions, not on concrete details. We achieve that by minimizing coupling, because if we depend on the details, if any change is made, there is a probability that our code won't work anymore. Let's imagine we work on an application for a vehicle maintenance shop. An example of business logic:

class Maintenance {
  private final Car carToMaintain;

  public Maintenance(Car newCar) {
    this.carToMaintain = newCar;
  }
  ...
}
...
Car car = new Car();
Maintenance firstMaintenance = new Maintenance(car);
firstMaintenance.maintain();
firstMaintenance.clean();
Enter fullscreen mode Exit fullscreen mode

The code above depends on Car class, and it can maintain and clean any car which is fine, but what when the shop we develop for expands its business and now it can maintain trucks as well. There is a problem now, we have to change the whole Maintenance class and that is a red flag, that code is not written well. Let's try to fix that.

interface Vehicle {
  void setType(VehicleType type);
}

class Maintenance {
  private final Vehicle vehicleToMaintain;

  public Maintenance(Vehicle newVehicle) {
    this.vehicleToMaintain = newVehicle;
  }
...
}

...
Vehicle car = new Car();
car.setType(VehicleType.CAR);

Maintenance firstMaintenance = new Maintenance(car);

firstMaintenance.maintain();
firstMaintenance.clean();
Enter fullscreen mode Exit fullscreen mode

The code above is a much better solution because it is flexible, it depends on abstractions, in this case on the interface Vehicle. Now the shop can expand even more and maintain motorcycles, vans, anything. This type of isolation makes us safe from any new requirements and the change we have to do is minimal.

Consider all this next time you create a class and even though you'll have to write more code at the beginning, in the long run, it will save a lot of time and effort to implement new features. Use a trick with describing a class before writing it, and you'll write short, readable classes with just ONE responsibility. Also, don't forget about the stepdown rule, make a class read like a newspaper article. With all that in mind, you and your colleagues will be grateful for it.

Conclusion

There it is. These are all the principles about clean code which were covered in the book "Clean Code, A Handbook of Agile Software Craftsmanship" by Robert C. Martin. I hope you enjoyed it and learned something new or upgraded the existing knowledge. If you found this interesting, I recommend reading the book if you didn't already. By following these rules, we all make our passion for writing a code easier, cleaner, and of course more readable. I would like to hear your opinion on these principles so write them down in the comments. Thank you for reading this article.

Top comments (0)