A look at Abstraction as provided by Object Oriented languages.
This is the first post in a series on Object Oriented design. In order to fully understand why the study of Design Patterns is important, we need to start by looking at what good Object Oriented (OO) software design is. Object Oriented languages offer some unique features that facilitate good design.
These design principles can be applied to non-objected oriented languages. However, applying OO concepts to non-OO languages takes some more effort.
What is Abstraction?
Abstraction in general means to operate on a code construct or data structure at a high level, without the need to understand the concrete nature of the structure or data. Here I will describe a specific forms of abstraction core to Object Oriented programming. Most of these should be familiar to you, but I want to explicitly list these for motivators for more complex design principles I will address in future posts:
Interacting with objects via direct class knowledge
Object Oriented languages have the concept of a class. A class is a definition which is used to create an object. We can also operate on an object according to the properties and methods exposed by its class - without understanding the actual implementation. We can also expect consistent behavior accross objects of the same class.
So far no surprises, everything we needed was defined in TCircle and we know that we can use properties and methods of the class that was used to instanciate the object.
Interacting with objects via inherited class knowlege
Object oriented languages also allow for inheritence of classes. In proper OO class design classes have an “is-a” relationship to ancestor classes. For instance we could inherited a TLine and TArc class from a TCurve class. For this example let us assume that TCurve defines a line type.
In the example above we don’t care if the list contains lines or arcs, or some other unknown type. We only care that they are of the type TCurve (perhaps via inheritence). Just as we operated on LCircle with knowledge of the private section of its class (TCircle) we can operate on LCurve with knowledge of TCurve. We don’t need to know if LCurve is actually a TArc or TLine.
Interacting with objects via virtual (or abstracted) class knowledge
Considering a class TCurve we can imagine that it could be an abstraction of any number of types beyond TArc and TLine. For instance we could have a TSpline or TElipseArc that could also have an “is-a” relationship with TCurve as an ancestor. If we step back and think of a geometric curve in abstract terms we can come up with methods and properties that could be abstracted. For instance we could say “a curve has only one start point and one end point” so we could define functions that would retrieve the start point and end point.
virtual keyword in Delphi here means that even if we call this method on a variable declared as ancestoral type (TCurve), the implementation at the most specific level of class (TArc or TLine) definition will be used. Decendent classes will essentially replace this function definition with their own. We will look at this more in detail when addressing the concept of Polymorphism in Part 3
To understand this lets consider the following
We implemented the
GetStartPoint definition in TArc with the
override directive. If we then refer to object via a variable of
TCurve and call
GetStartPoint the call will be made to
abstract keyword in Delphi means that if we call
GetStartPoint on a decendent that has not provided an implementation then we will get an ‘Abstract Error’, this error means that the method was not replaced by a concrete class and there is no code to be reached by the call. If we were to omit the
abstract keyword we are required to add a base implementation at the
TCurve level. If a decendent class does not implement this method then the base implementation will be called without an error.
Another level of abstraction is to make the entire class as abstract. A class makes sense as pure abstract in cases where the level of abstraction in the class hierarchy so high that no object will be instanciated using that class. For instance a class definition like the following would produce an error if you attempt to directly create an object using this class (
TCurve.create is not permitted)
Abstraction via Interfaces
There is a common misconception that interfaces are simply pure abstract classes. In class abstraction the relationship between a class and its ancestor class (including abstract ancestors) should almost always be an “is-a” relationship. The most common relationship beween a class and and interface is a “supports” relationship. An interface can be viewed as a contract. If an interface is applied to a class the class is obligated to fill the demands of the interface or delegate the requests made on that interface. I will deal with the topic of interface delegation in a future post.
Interfaces are even more abstact than abstract classes and can apply to multiple class hierarchies. They can even have independent inheritence hierarchies from the classes to which they are applied. The class hierarchy bakes in functionality and structure that is not easily changed. As we will see later in our design patterns journey we will note some of the problems that arrise from this.
Here as an example that will hopefully convince you that interfaces are different from abstract classes. Consider the following interface:
Clearly this will not fit into any one specific object hierarchy. This interface can be applied to any type that could potentially be compared to another type. In fact this interface applies broadly accross multiple class hierarchies. Once applied to those classes they are compelled to implement this method. When we refer to them via an interface only in high level functions of abstraction. Here is an example of an abstract procedure that can sort any list of objects that implement the IComparable interface
Abstraction allows us to focus on areas of code that we are interested in only. It allows us to generalize functionality improving readability and reusability of code. It declutters our code and simplifies the understanding of what happens in software at a higher level