It’s all about the abstractions baby

It’s all about the abstractions baby

I’ve been a developer my whole life, professionally about 10 years. In those 10 years, I feel I’ve always been able to “get the job done”, but things really started to click for me, once I embraced the abstraction.

What is abstraction?

From Wikipedia:

Abstraction, in general, is a fundamental concept to computer science and software development. The process of abstraction can also be referred to as modeling and is closely related to the concepts of theory and design. Models can also be considered types of abstractions per their generalization of aspects of reality.

Hmm, that didn’t really clear up much for me. Labels are hard for me, but I tend to think of abstraction as concept modeling by defining the “what needs to happen”, as opposed to the “how a thing needs to happen”.

Abstractions are concepts, not details. People use abstractions every day, without necessarily understanding the details about how the abstraction works. A simple example of this could be the abstract idea of your car’s gas pedal. You may not know how the internals of an engine work, but you do know that the idea behind that pedal is “make car go vroom vroom”. To a consumer of the abstraction, the details of how an abstraction accomplishes what it intends are unimportant, the important thing is that it does it.

Why is abstraction important?

Abstraction is a means of organization, and organization is needed for clean, easy to follow code! Traditional ways to accomplish abstraction is by utilizing:

  • interfaces
  • abstract classes (no way!)
  • simple model classes (properties ideally without behavior)
  • objects
  • classes with behavior (though from a design perspective, probably the least “good” type of abstraction?)

Yes, the above list does pretty encompass most language features, but some of the bullet points can be more useful than others from a high level abstraction perspective.

I find interfaces and model classes to be the most useful tools for modeling a concept, as they allow you, and even force you to think about the problem in smaller chunks. Interfaces are all about defining method signatures, as stated previously — the “what” needs to be accomplished, not the “how”.

To use an example:

  • I need a series of MyObject based on some criteria.

With a (not so great) level of abstraction, you could create a class such as:

1
2
3
4
5
6
7
8
9
10
public class MyObjectDbRetriever
{
public IEnumerable<MyObject> GetMyObjects(MyObjectCriteria criteria)
{
// Connect to a database
// Do some sql-ey stuff that limits the result set based on criteria

return results;
}
}

While the above does work just fine(ish), and will end up being a part of our “end result”, it’s not the greatest piece of code from a caller’s perspective.

How could/would a caller consume this code?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyObjectController
{
public IEnumerable<MyObject> GetMyObjects()
{
// make up some criteria
var myObjectCriteria = new MyObjectCriteria()
{
// some criteria
}

// Get the data
var myObjects = new MyObjectDbRetriever()
.GetMyObjects(myObjectCriteria);
}
}

In the above class, your caller is tightly coupled to the MyObjectDbRetriever. Why is this bad? A few reasons:

  • It’s difficult to test the controller (if there were enough logic to test), because MyObjectDbRetriever is directly referenced by the controller. This means that there is no testing the controller logic, without the database logic — making unit testing very unlikely.
  • Related to the above, but I thought I should point it out nonetheless; the controller is tied to the “how” rather than the “what”. “I need to get MyObjects from the database using MyObjectCriteria“ rather than “I need MyObjects using MyObjectCriteria”

Why do proper abstractions help you to write better code?

Working from the above MyObject example, let’s change a few things. Let’s introduce a “what” to our abstraction, rather than just the current “how”.

1
2
3
4
public interface IMyObjectRetriever
{
IEnumerable<MyObject> GetMyObjects(MyObjectCriteria criteria);
}

Now, we have a contract, an idea, a “what” abstraction. We can now modify the the original MyObjectDbRetriever to utilize this interface:

1
2
3
4
5
6
7
8
9
10
public class MyObjectDbRetriever : IMyObjectRetriever
{
public IEnumerable<MyObject> GetMyObjects(MyObjectCriteria criteria)
{
// Connect to a database
// Do some sql-ey stuff

return results;
}
}

This is the exact same class as used previously, except now it’s implementing our idea of IMyObjectRetriever. How does this change things on the controller, you may ask? In a very interesting way! Now that we’re programming to an interface, rather than concretion, dependency injection becomes an option for us.

Dependency injection is one way to achieve the “D” in the SOLID design principles — Dependency inversion. The basic idea of this is that high level modules (The controller) should not depend on lower level modules (the MyObjectDbRetriever). This principle was obviously being violated in the first example, but what has changed to prevent it now?

Let’s take another look at the original MyObjectController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyObjectController
{
public IEnumerable<MyObject> GetMyObjects()
{
// make up some criteria
var myObjectCriteria = new MyObjectCriteria()
{
//
}

// Get the data
var myObjects = new MyObjectDbRetriever()
.GetMyObjects(myObjectCriteria);
}
}

In the above, the “high level module” controller is very dependent on the “low level module” of MyObjectDbRetriever. Utilizing our new interface and constructor dependency injection, we can change that!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyObjectController
{
private readonly IMyObjectRetriever _myObjectRetriever;

public MyObjectController(IMyObjectRetriever myObjectRetriever)
{
_myObjectRetriever = myObjectRetriever;
}

public IEnumerable<MyObject> GetMyObjects()
{
// make up some criteria
var myObjectCriteria = new MyObjectCriteria()
{
//
}

// Get the data
var myObjects = _myObjectRetriever.GetMyObjects(myObjectCriteria);
}
}

In the above implementation, only a few things have changed, although they’re very important changes! Now, we have a constructor that takes in an implementation of IMyObjectRetriever. The function GetMyObjects now calls the interface method of GetObjects(myObjectCriteria), rather than the concrete db method. The controller class is no longer dependent on the MyObjectDbRetriever or database! Now, the controller class is simply dependent on the idea of an interface that can retrieve data, how it goes about doing it is unimportant to the context of the controller — loose coupling!

What if the thing calling our controller has different behaviors depending on the nature of the data returned? The above change means that we can now more easily test the controller by use of mocks, fakes, or shims. Previously, when using the MyObjectDbRetriever, we would have to ensure our database is returning specific data requirements, for several potential scenarios, from our actual database. Now as an example, we can throw a few other classes that implement our interface, and return data based on our testing requirements.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyEmptyFakeObjectRetriever : IMyObjectRetriever
{
public IEnumerable<MyObject> GetMyObjects(MyObjectCriteria criteria)
{
return new List<MyObject>();
}
}

public class MyNullFakeObjectRetriever : IMyObjectRetriever
{
public IEnumerable<MyObject> GetMyObjects(MyObjectCriteria criteria)
{
return null;
}
}

public class MyKritnerFakeObjectRetriever : IMyObjectRetriever
{
public IEnumerable<MyObject> GetMyObjects(MyObjectCriteria criteria)
{
return new List<MyObject>()
{
new MyObject("kritner")
};
}
}

Because all of the above classes implement IMyObjectRetriever it’s simply a matter of passing in instances of our fake classes for testing our specific scenarios. I would generally use “mocks” in such a scenario, but these fakes are easy enough to demonstrate as well.

I feel like this only scratches the surface of abstraction, but hopefully this helps others have that “click” moment!

Related:

Author

Russ Hammett

Posted on

2018-09-27

Updated on

2022-10-13

Licensed under

Comments