devxlogo

The Foundation of Proper Object-Oriented Design: Interfaces

The Foundation of Proper Object-Oriented Design: Interfaces

Object-oriented (OO) design has been a staple of modern programming since the 1990s. At its core it is about having “objects” that carry both state (data relevant to this object) and behavior (the operations you can perform on the data). Several programming languages added explicit support to OO style development (most notably C++, which was very popular back then). This was an advancement over the procedural programming practices that were prevalent earlier in which programmers who had “objects” had to manage the connection between their data and behavior.

The OO Criticism

Over time, more and more systems were implemented using object-oriented techniques. Many of them failed and it turned out that it is not trivial to do proper object-oriented design. A lot of industry luminaries were disappointed and it received some severe criticism. (See: ” Object Oriented Programming is Inherently Harmful.”) I started my career in 1990s and I’ve seen my share of bad object-oriented code. I’ve read a lot of good books on object-oriented programming and design and I believe it is indeed pretty difficult to use object-oriented techniques well. That said, it is possible and it’s worth your time and effort. If the problem you’re trying to solve calls for an object-oriented solution and you try to model and solve it in some other way, you’re setting yourself for failure.

Component-Based Programming

Object-oriented design and programming was focused on single programs. But, the complexity of systems grew to the point that a single monolithic program couldn’t handle. Components came to the scene – Visual Basic controls, Delphi components, COM objects, OMG’s CORBA.

Components were all about packaging some functionality in a binary package and expose this functionality to other components or top-level programs/applications through a well-defined interface.

Components turned out to be highly successful. They were the ultimate objects, exhibiting inherent information hiding, explicit behavior. Most importantly, they allowed developers to tame the complexity of large systems by breaking up the monolith.

I learned the most from (both on the problems with classic OO and how components can help) an obscure little book from 2002 by a Swiss researcher named Clemens Szyperski, called “Component Software: Beyond Object-Oriented Programming”.

While it claims to go beyond OO programming I took it differently and saw it as simply the right way to do OO programming. I’ve used these techniques to develop successful object-oriented monolithic systems, by dividing them internally to components. The most important part of this process was the interface.

Interfaces to the Rescue

What are these interfaces? An interface is a description of the behavior of an object. It is typically a collection of method signatures without implementation behind them. Consider a mouse trap interface with a single method “Trap”.

In C++ it may look like this:

struct IMouseTrap {    virtual void Trap() = 0;};

But, interface are useless on their own. Let’s build a class that implements the interface:

#include using namespace std;class MouseTrap: public IMouseTrap{public:    void Trap()    {        cout 

With this class in place we can write a program to trap us some mice:

int main(){   IMouseTrap * trap = new MouseTrap();   trap.Trap();   return 0;}

If you run it you'll get this output: "Trapped a mouse".

Well, it doesn't looks very impressive. The interface didn't help much. The main() function still needs to know about the concrete MouseTrap class and instantiate it.

Let's add a factory:

class MouseTrapFactory{public:    IMouseTrap & makeMouseTrap()    {       return MouseTrapFactory::_trap;    }private:    MouseTrap _trap;};

Now, our main program looks like this:

int main(){   MouseTrapFactory factory;   IMouseTrap & trap = factory.makeMouseTrap();   trap.Trap();   return 0;}

Well, that is a little better. The main() program doesn't know about the concrete MouseTrap class anymore. It just gets the IMouseTrap interface from the factory.

Let's get some perspective here. This little MouseTrap class could be in practice a controlling a whole Internet of Things (IoT) system of remotely controlled mouse traps across a huge building. The decision when to activate it, may have significant ramifications if done incorrectly. It may pull an enormous amount of code and dependencies along. So, abstracting it away behind an interface is very powerful. If anything changes with the MouseTrap class or any of its dependencies your main() function doesn't need to be re-compiled or re-tested.

But, that's not all. Let's build a better mouse trap (see what I did there?):

class BetterMouseTrap: public IMouseTrap{public:    void Trap()    {        cout 

This awesome mouse trap is 55X better than our original mouse trap. Of course we want to use it, but we don't want to change our main program, because it's stable and works well. The good news is we don't have to. Since we're accessing only the IMouseTrap interface then the only change that is required is to have the factory return the better mouse trap, which implements the same interface:

class MouseTrapFactory{public:    IMouseTrap & makeMouseTrap()    {       return MouseTrapFactory::_betterTrap;    }private:    MouseTrap _trap;    BetterMouseTrap _betterTrap;};

Now, if you compile and run your program the output will be: "Trapped 55 mice!!!"

You can taking interfaces much further. My favorite technique when modeling domains is to start by modeling everything?as either interfaces or plain old data objects (PODs). Interface methods accept only primitive types, PODs or interfaces as parameters and return only primitive types, PODs or interfaces.

That allows a lot of flexibility where you can test each part of your code in total isolation, inject intermediate code with the same interface that forwards to the original destination and many other tricks.

Interfaces in Other Static Languages

The examples use classic C++98 (not even the fancier C++11 or C++14) with a struct that contains only pure virtual functions serving as the interface. There are often issues with the destructor of such interfaces. It can even be done in C by managing structs with function pointers. But, the world of programing evolved and many newer languages offer interfaces as first-order language construct: Java, D, Go (without classes even), Rust (as Traits).

Dynamic Languages

Dynamic languages are different. The type of an object is not specified anywhere, so there is no requirement to pass objects that conform to pre-defined interface. However, there is an ad-hoc interface which the operations that are performed on the target object. As long as you pass an object that conforms to this informal interface you're fine. Here is an example in Python:

class MouseTrap(object):    def trap(self):        print 'Trapped a mouse'class BetterMouseTrap(object):    def trap(self):        print 'Trapped 55 mice'def main():    mouse_trap = MouseTrap()    run(mouse_trap)        mouse_trap = BetterMouseTrap()    run(mouse_trap)    def run(mouse_trap):    mouse_trap.trap()if __name__ == '__main__':    main()

The run() function here just gets a mouse trap object, but there are no explicit requirements. There is no compiler to tell you that you are passing something wrong. However, inside it invokes the trap() method on the object it receives, so if you pass an object that doesn't conform to this ad-hoc interface then you'll get a runtime error. In this case, both MouseTrap and BetterMouseTrap conform to the "interface" so when main() passes them to run() they will work just fine.

Conclusion

Object-oriented design is important. Interfaces are the key to doing development properly and avoiding the many pitfalls. Dynamic languages use interfaces implicitly, this may be easier to get going, but with large complicated systems it could make it more difficult to maintain, especially when an interface changes and you're not sure what objects implement it exactly.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist