loading...
Cover image for It’s all about the abstractions baby

It’s all about the abstractions baby

kritner profile image Russ Hammett Originally published at Medium on ・5 min read

“abstract illustration” by Art by Lønfeldt on Unsplash

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:

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?

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”.

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:

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

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!

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.

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:

Discussion

pic
Editor guide
Collapse
rafalpienkowski profile image
Rafal Pienkowski

I always like posts about abstraction which is crucial in the developer's daily routine.

I've one enhancement to your interface. I hope you won't take this personally. Please treat this as an improvement.

In your example, you use List type which is a concrete type. I'd suggest you take more abstract type like in examples below:

public interface IMyObjectRetriever
{
   IList<MyObject> GetMyObjects(MyObjectCriteria criteria);
}

or even better (but depends on your needs)

public interface IMyObjectRetriever
{
   IEnumerable<MyObject> GetMyObjects(MyObjectCriteria criteria);
}

That makes you interface more abstract.

Cheers.

Collapse
dance2die profile image
Sung M. Kim

And if you want to abstract even further, instead of an concrete MyObjectCriteria & MyObject, you can extract an interface, thus making it more testable/abstract.

public interface IMyObjectRetriever
{
   IEnumerable<IMyObject> GetMyObjects(IMyObjectCriteria criteria);
}

I think it's overengineering but wanted to point out that abstraction can go far to the point it's unmanageable (but could be easier to mock & test).

Collapse
kritner profile image
Russ Hammett Author

heh, indeed! I've gone down this path a few times, sometimes for actual reasons even! It gets real weird when you're working with everything as interfaces and then start pulling in some visitor pattern action to avoid casts of your interfaces into their concretes. Every time I try to remember how to use or read the visitor pattern, my brain explodes just a tad.

Thread Thread
dance2die profile image
Sung M. Kim

I try to remember how to use or read the visitor pattern

I believe the percentage of people who can implement the Visitor Pattern without a cheat sheet should be about the same as number of devs show can do CSS gradient without looking up 😀

Collapse
kritner profile image
Russ Hammett Author

indeed, good call :)

Collapse
birdayz profile image
Johannes Brüderl

the moment when classes with behavior are considered the "least good abstraction"...after all Java is an objected oriented language.

I suggest to read martinfowler.com/bliki/AnemicDomai... .

Collapse
qm3ster profile image
Mihail Malo

Are you suggesting the database be hardcoded into the objects, or passed to the constructor?
Storage is not a concern of the business object "MyObject".

Collapse
birdayz profile image
Johannes Brüderl

no. it's a statement against shallow data classes without behavior.

Thread Thread
qm3ster profile image
Mihail Malo

I don't see why you'd assume MyObject doesn't have rich methods and doesn't protect its invariants.

Thread Thread
kritner profile image
Russ Hammett Author

Sorry maybe I'm missing something the purpose of this and your original comment, can you elaborate?

I didn't think there was really any model type classes in this post; save the undefined MyObject. I'm not saying that classes with behavior are bad, I meant more as if it's implementing an interface, then the consumers can work with the interface, rather than the concrete. This, at least in my opinion, is a better high level design, and makes it easier on consumers/callers to understand a system, rather than getting bogged down in the details of implementation.

Collapse
taowen profile image
Tao Wen

To me abstraction is just a means to the end. Code reuse is the goal. To generalize, we have to specialize. The problem is, does the code worth reusing at the first place? If yes, how to reuse it, is not a big problem. Interface/template/multi dispatching, we can always make it work. But I found many times, we are not reusing it on purpose, the cost and saving is not well calculated.

Collapse
kritner profile image
Russ Hammett Author

I haven't done a whole lot of functional unless you count LINQ and maybe SQL, so I'm afraid I wouldn't know the differences. Can you provide an example?

And yeah, over-abstraction is definitely a thing I've seen, and done before. :(

Collapse
caseycole589 profile image
Casey Cole

Interfaces and abstract classes are code bloat if you ask me, and they are a waste of time why should I define a method signature then later implement it. Seems like a extra step for no reason to me.

Collapse
alanmbarr profile image
Alan Barr

I had a similar feeling in the past especially working with code where someone made an interface for every single object. However, now I see the use cases for library components and making code more flexible for use. Code can sometimes be too strict in what it allows and an interface allows a more generic option to be used that still matches the contract.

Collapse
marekmosiewicz profile image
Marek Mosiewicz

I do not see the point. Abstracting factory can be done in singelton. I just do not see too much rationale for all Spring IOC. It can be useful in some scenarios, but in my opinion it is overused.