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.
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.
Abstraction is a means of organization, and organization is needed for clean, easy to follow code! Traditional ways to accomplish abstraction is by utilizing:
- abstract classes (no way!)
- simple model classes (properties ideally without behavior)
- 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:
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?
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
MyObjectDbRetrieveris 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
MyObjectsfrom the database using
MyObjectCriteria“ rather than “I need
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”.
Now, we have a contract, an idea, a “what” abstraction. We can now modify the the original
MyObjectDbRetriever to utilize this interface:
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
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!
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.
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!