Abstractions, the Cornerstone of Clean and Maintainable Code

by Enrique Cerda

For two years now, I have had the privilege to work in an environment where order, cleanliness and caring for, really matters. And I’m not only talking about social values, I’m talking about software development. Dynamic.Tech has taught me the values underlying a clean Agile environment and throughout those years, I have witnessed first-hand the value our Agile Team of software engineers has provided to our customers.

These principles make a positive impact on the organization by promoting responsibility on the product and business, not only software-wise, but much more than that. Early on when I joined Dynamic.Tech, I was given the Manifesto for Software Craftsmanship.

Without going into details, it states that someone should care about their profession and the final product it brings into the world. The process of creation should be passionate about superior quality. This is what Dynamic.Tech offers.

I would like to write about a subject that has made a big impact on me and which I think should be properly known by programmers for maintainable and scalable code, and this is Abstractions.

This blog is written for those software engineers that are either in their early stages of their programming career, for those seeking to upgrade their game, and ultimately, for those team leaders that want to improve quality and develop more flexible, “pluggable”, code.

The first thing I want to write about is the O in the acronym SOLID, (each letter is an important software design principle). If you don’t know what I’m talking about I strongly suggest to go and read about it first and then come back.

Open-Closed Principle

It is difficult to think of stuff in an abstract way. I’ll be the first to admit that it took me some time to really sink into this principle, probably the basis for good programming. I will not quote papers nor repeat in a scientific manner what the principles say. Instead, I’ll try to explain them as for someone that comes from a simple programming background, as myself.

What this principle says, in understandable words, is that you should try to avoid details as you write your code. Of course, details will have to finally emerge but not until the last possible moment at the concrete class. So, let’s say that you are writing a test for a class that saves data at a given location, server, or whatever. This class will have the ability to save to a location or repository. Pay attention to this last note.

A new software engineer, oblivious to this principle, could write something like:

public class SqlSaver {
  SqlConnection sqlConnection;

  public SqlSaver(String address) {
    sqlConnection = new SqlDataBase(address).connect();
  }

  public int save(String data) {
    // implement saving data to SQL database and returns length of saved bytes
  }
}

public class SqlSaverTest {
  @Test
  public void testItSavesOnSql() {
    SqlSaver sqlSaver = new SqlSaver("127.0.0.1");

    String stringData = "save this";

    int savedBytes = sqlSaver.save(stringData);

    assertEquals(savedBytes, stringData.getBytes().length);
  }
}

I have introduced a new concept here, tests. Testing is outside the scope of this blog but I urge you to go and read On TDD & Writing Good Tests by Javier Treviño. Writing tests first is at the core of good practices on Agile project management, and really helpful to detect these abstractions that otherwise would be hard for a simple human, accustomed to work with concrete objects, to detect.

Back to the last piece of code, the test would pass and someone with no knowledge of what’s to come in the future if this code is kept, would be happy, as would I have been. But what’s wrong with this? Remember what I said before, write your code as if you were talking to something abstract.

A human tends to fail this because everything we see with our eyes is something concrete, we don’t see the invisible layer of abstraction that surrounds an object. Each object that we see is part of something bigger, or something more general like a category.

Remember the note, what is a SqlSaver? It’s merely a DataSaver, this could be its abstraction. What else has the ability to “save”? Lots of other classes, you are not bound to a SqlSaver only and writing code this way will be easier for you to plug any other object with this ability. Let’s see an example:

public interface DataSaver {
  public int save(String data);
}

public class SqlSaver implements DataSaver {
  @Override
  public int save(data) {
    // whatever implementation needed to save to a Sql DataBase and return the number of saved bytes
  }
}

public class MongoSaver implements DataSaver {
  @Override
  public int save(data) {
    // whatever implementation needed to save to a Mongo DataBase and return the number of saved bytes
  }
}

public class DataSaverTest {
  @Test
  public void testItSavesDataToWhicheverDataBase() {
    String data = "content to save";

    DataSaver sqlSaver = new SqlSaver();
    DataSaver mongoSaver = new MongoSaver();

    DataSaver[] dataSavers = {sqlSaver, mongoSaver};

    for (DataSaver dataSaver : dataSavers) {
      int savedBytes = dataSaver.save(data);

      assertEquals(savedBytes, data.getBytes().length);
    }
  }
}

Back to our last piece of code, can we say we are done? Is there something you have learned about abstractions so far? We are not there yet, there is something that bugs me that goes hand in hand with what we have learned so far. There is another abstraction waiting to be discovered here. Mull over it and come back.

Dependency Inversion

Back in our DataSaver example, as we wrote the test we should have detected something. Remember, try to work with abstractions as far as you can.

What is String data = "content to save"? This is really another concrete detail that should not appear until later on an implementation of such an object that does use String as its… Data.

So we have discovered another abstraction, and this brings us back to the D in SOLID. A DataSaver should not be bound only to a type String but to a more general type, I’ll name if for now as Data. Remember, we as humans tend to summarize and identify what we are working with to its lower level of detail, but we are programming to model a whole world, and that’s where abstractions come into play.

Data could be anything from a String, an array of Bytes, or even another type of Data unknown to us…

public interface DataSaver {
  public int save(Data data);
}

public class SqlSaver implements DataSaver {
  @Override
  public int save(Data data) {
    SqlData data =  convertToSqlData(data);
    writeToSql(data);
    if (successfulSave) {
      return data.length();
    } else{
      return 0;
    }
  }
}

public class MongoSaver implements DataSaver {
  @Override
  public int save(Data data) {
    MongoData data =  convertToMongoData(data);
    writeToMongo(data);
    if (successfulSave) {
      return data.length();
    } else{
      return 0;
    }
  }
}

public class DataSaver {
  @Test
  public void testItSavesData() {
    Data data = new Data("data");

    DataSaver sqlSaver = new SqlSaver();
    DataSaver mongoSaver = new MongoSaver();

    DataSaver[] dataSavers = {sqlSaver, mongoSaver};

    for (DataSaver dataSaver : dataSavers) {
      int savedBytes = dataSaver.save(data);

      assertEquals(savedBytes, data.getBytes().length);
    }
  }
}

We have found a way to pass on, or inject, objects in an abstract way. This allows our classes to be able to “talk” to abstractions and not as we would normally think, in a concrete way. We have created a Data class that encapsulates any kind of Data and now is up to us to implement each concrete class for its Data.

Interface Segregation Principle

Finally, I would like to talk about the I in SOLID. This principle rounds up the two principles already explained above. Thinking in an abstract way may lead to other problems, such as getting the incorrect abstraction. Let’s imagine a Car and an Airplane, both concrete objects can be encapsulated in a Vehicle, right? What does a Vehicle object do? It travels.

With the new knowledge and skills provided by Open-Closed and Dependency Inversion principles, someone could write the following:

public interface Vehicle {
  public void travelTo(Location location);
}

public class Car implements Vehicle {
  @Override
  public void travelTo(Location location) {
    turnOn();
    driveTo(location);
    turnOff();
  }
}

public class Airplane implements Vehicle {
  @Override
  public void travelTo(Location location) {
    turnOnEngines();
    checkNavigationSafety();
    contactControlTower();
    liftOff();
    fly(location);
    land();
    turnOffEngines();
  }
}

public class VehicleTest {
  @Test
  public void testItTravelsToLocation() {
    Location airportLocation = new Location(airportCoordinates);

    // Concrete use of a Car
    Vehicle car = new Car();
    car.travelTo(airportLocation)
    assertEquals(car.getLocation(), airportLocation);

    // Concrete use of an Airplane
    Vehicle airplane = new Airplane();
    airplane.travelTo(airportLocation)
    assertEquals(airplane.getLocation(), airportLocation);
  }
}

That’s a correct use of Open-Closed and Dependency Inversion. But think about it, an Airplane could have other attributes or actions that a Car may not, for example lifting off, landing, or many others. For now, we are happy with our implementations since our needs are only for a Vehicle object. A time will come when we will want to land the Airplane and write the following:

public class AirplaneTest {
  @Test
  public void testItLandsSafely() {
    // Concrete use of an Airplane
    Vehicle airplane = new Airplane();
    airplane.land()
    assertTrue(airplane.touchDown());
  }
}

This will affect the Car class having to implement an empty method, which does not abide to its real abstraction:

public interface Vehicle {
  public void travelTo(Location location);
  public void land();
}

public class Car implements Vehicle {
  ...

  @Override
  public void land() {
    // empty implementation
  }
}

public class Airplane implements Vehicle {
  boolean touchDown;
  boolean crashed = false;
  ...

  @Override
  public void land() {
    touchDown = crashed ? false : true;
  }
}

Why is the Car forced to use the land method when it does not even fly? This is what’s called, having a “fat” interface. This interface can continue to grow in an uncontrolled way, til we have a big blob of mixed methods, leaving us with something different than that abstraction we first conceived. So what to do? I’d argue that an Airplane is really an AirVehicle and a Car is really a GroundVehicle.

public interface GroundVehicle {
  public void drive(Address address);
}

public interface AirVehicle {
  public void fly(Vector vector);
  public void land();
}

public class Car implements GroundVehicle {
  @Override
  public void drive(Address address) {
    // drive the Car to the desired Address
  }
}

public class Airplane implements AirVehicle {
  Vector currentVector;

  public Vector currentVector() {
    return currentVector;
  }

  @Override
  public void fly(Vector vector) {
    // fly the Airplane on the desired Vector
  }

  @Override
  public void land() {
    // land the airplane safely
  }
}

public class VehiclesTest {
  @Test
  public void testCarGetsDrivenToTheDesiredAddress() {
    Address address = "My House 123";

    // Concrete use of a Car
    GroundVehicle car = new Car();
    car.drive(address);
    assertEquals(car.location(), address);
  }

  @Test
  public void testAirPlaneGetsFlownCorrectly() {
    Vector vector = (1, 2, 3);

    // Concrete use of an Airplane
    AirVehicle airplane = new Airplane();

    airplane.fly(vector);
    assertEquals(airplane.currentVector(), vector);

    airplane.land();
    assertTrue(airplane.touchDown());
  }
}

We have separated and segregated interfaces, now we can have any type of flyable objects as well as drivable objects. I have learned that this principle is one of the most difficult to implement and equally important as the Open-Closed and Dependency Inversion Principles.

More often than not, I have had to go back and re-implement my interfaces or abstractions cause I got them wrong since the beginning. Having such interfaces is like bringing too many luggage to a simple field trip. Eventually, having to carry the extra weight will get tiring and slow you down.

I hope I was able to level these three principles to a learnable and readable way and urge developers to practice and keep their eyes open for abstractions. Remember, writing tests first will definitely help identify them. Helpful, right? Next time I’ll be writing about Agile Teams and the Agile Methodology for software engineering and how can it help you reach your team goals, so go on and subscribe!

Interested in applying these and many other good practices in action in your organization? Feel free to contact us and let us support your team towards better results!

Dynamic.Tech - Software Development Training
Subscribe

Sign up to receive our monthly tech recap