CS302 Lecture Notes - Interfaces and Inheritance

Introduction

Inheritance and interfaces are fundamental concepts in Object-Oriented programming. If it's not well-known to you that I don't like inheritance, it will be soon. However, both interfaces and inheritance have their places in the universe of programming, whether I like it or not, so I'll give you some discourse on the topic. I will write my detailed opinions at the end of this document, but I will summarize them here:

Inheritance and interfaces are very powerful organizational tools for programming. You can do a lot of things with them that you cannot do easily without them. On the flip side, they should be approached with caution. Inheritance, in particular, introduces intricate sources of bugs and confusion that you avoid by not using it, and worse yet, over time, a bad decision about inheritance can make your code convoluted, inefficient, and incorrect. Therefore, if you do decide to contaminate your code with inheritance, keep it to a minimum, and document your decisions cleanly and thoroughly. I cannot stress this enough.


A Motivating Example Without Inheritance or Interfaces

You've decided to take your knitting to the next level, so you open an Etsy store. You don't want to "Penny Blossom" yourself, so you decide to start with just two items: dog sweaters and monogrammed Koozies. They have the following attributes that you'd like to keep track of in your bookkeeping:

Koozies:
  • Color (string - custom)
  • Monogram (string - custom)
  • Yards of yarn (double - this is a constant: 1.5)
  • Selling Price: $10.00.
Dog Sweaters:
  • Color (string - custom)
  • Size (S|L)
  • Yards of yarn (double: 3 for small, 6 for large)
  • Selling Price ($20 for small, $40 for large).

You are keeping track of your sales in text files, one file per sale. Your files have the following format:

Here are two example files:

txt/f1.txt
Isabelle Human
Koozie Red IH
Koozie Blue BH
Dog S Red
txt/f2.txt
Jonathan Cherubim
Dog L Orange
Dog S White
Koozie Orange JLC
Koozie White JXC

You'd like to write a program that formats each sale nicely and reports your profits and expenses. Yarn is $2.25 a yard, and Etsy charges 8% fees on your gross. Here's what you have envisioned:


UNIX> bin/make_invoice_1_normal < txt/f1.txt
Customer: Isabelle Human

Description                                                   Price  Costs
Red Koozie with monogram IH                                   10.00   3.38
Blue Koozie with monogram BH                                  10.00   3.38
Small Red Dog Sweater                                         20.00   6.75

Gross:     40.00
Costs:     13.50
Fees:       3.20
Profit:    23.30
UNIX> bin/make_invoice_1_normal < txt/f2.txt
Customer: Jonathan Cherubim

Description                                                   Price  Costs
Large Orange Dog Sweater                                      40.00  13.50
Small White Dog Sweater                                       20.00   6.75
Orange Koozie with monogram JLC                               10.00   3.38
White Koozie with monogram JXC                                10.00   3.38

Gross:     80.00
Costs:     27.00
Fees:       6.40
Profit:    46.60
UNIX> 

Let's write the program using vanilla C++. It's in src/make_invoice_1_normal.cpp. We'll have a class for a Dog and a class for a Koozie. Here's the Dog class with standard Dr. Plank-style C++:

class Dog {
  public:
    Dog(istringstream &ss);
    string Description() const;
    double Price() const;
    double Expenses() const;
  protected:
    string color;
    string size;
    double yards;
    double price;
};

Dog::Dog(istringstream &ss)
{
  if (!(ss >> size >> color)) {
    throw runtime_error("Bad stringstream in Dog constructor");
  }

  if (size == "S") {
    yards = 3.0;
    price = 20.0;

  } else if (size == "L") {
    yards = 6.0;
    price = 40.0;

  } else {
    throw runtime_error("Bad dog size - should be S or L");
  }
}
string Dog::Description() const
{
  string s;
 
  s = (size == "S") ? "Small " : "Large ";
  s += color;
  s += " Dog Sweater";
  return s;
}

double Dog::Price() const
{
  return price;
}

double Dog::Expenses() const
{
  return yards * 2.25;
}

I've written the definition for the Koozie class in a different style -- here we implement the code in the class definition and specify default values in the constructor specification. You may prefer this style, and while I acknowledge the simplicity of the specifications of Price() and Expenses(), I still don't like them. I like to have defintions and implementations separated.

class Koozie {
  protected:
    string color;
    string monogram;
    double yards;
    double price;
  public:
    Koozie(istringstream &ss) : 
       yards(1.5), 
       price(10.0) 
       { if (!(ss >> color >> monogram)) throw runtime_error("Bad Koozie"); };
    string Description() const {
      string s;
      s = color + " Koozie with monogram ";
      s += monogram; 
      return s;
    }
    double Price() const { return price; }
    double Expenses() const { return yards * 2.25; }
};

The main() code is really straightforward. When it's reading the items, it reads the first word on the line and decides whether to create an instance of Dog or Koozie. I'm going to highlight some lines in this code that feel like they are more clunky and inefficient than they should be:

int main()
{
  istringstream ss;
  string name;
  string line;
  string key;
  Dog *d;
  Koozie *k;
  string desc;    // Description of an item.
  double p;       // Price of an item.
  double e;       // Expenses of an item.
  double tp;      // Running total of prices.
  double te;      // Running total of expenses.
  double fees;    // Fees

  /* Read and print the customer name. */

  getline(cin, name);
  printf("Customer: %s\n", name.c_str());
  printf("\n");

  /* Print labels for the items. */

  printf("%-60s %6s %6s\n", "Description", "Price", "Costs");
  /* Print each item -- description, price, expenses. 
     You'll note that there's some code duplication, 
     because Dog and Koozie are different classes.  */

  tp = 0;
  te = 0;
  while (getline(cin, line)) {
    ss.clear();
    ss.str(line);
    ss >> key;
    if (key == "Koozie") {
      k = new Koozie(ss);
      desc = k->Description();    // These three lines
      p = k->Price();             // feel
      e = k->Expenses();          // redundant.
      delete k;
    } else if (key == "Dog") {
      d = new Dog(ss);
      desc = d->Description();    // With these
      p = d->Price();             // three
      e = d->Expenses();          // lines.
      delete d;
    }
    tp += p;
    te += e;
    printf("%-60s %6.2lf %6.2lf\n", desc.c_str(), p, e);
  }

  /* Print the final information. */

  printf("\n");
  fees = tp * 0.08;
  printf("Gross:  %8.2lf\n", tp);
  printf("Costs:  %8.2lf\n", te);
  printf("Fees:   %8.2lf\n", fees);
  printf("Profit: %8.2lf\n", tp - te - fees);
  return 0;
}


Leveraging commonality with an Interface

With C++, you can specify an interface. This is a class that only contains "virtual" methods. In your specification, you label the methods with the virtual keyword, and set their values to zero. I know that seems odd. The "virtual" keyword is a placeholder, saying that the method is going to implemented elsewhere, in another class (as you'll see). Here's a specification of an interface for an Item, which contains virtual methods for Description(), Price() and Expenses() (it also has a destructor -- more on that later). The code is in src/make_invoice_2_interface.cpp:

class Item {
  public:
    virtual ~Item() {};
    virtual string Description() const = 0;
    virtual double Price() const = 0;
    virtual double Expenses() const = 0;
};

Our Dog and Koozie classes have no changes, except they specify that they implement the Item interface. The changed code is in red. You'll note, both classes implement the exact methods specified in Item, with the same prototypes. That is required for them to implement the Item.

class Dog : public Item {
  public:
    Dog(istringstream &ss);
    string Description() const;
    double Price() const;
    double Expenses() const;
  protected:
    string color;
    string size;
    double yards;
    double price;
};
class Koozie : public Item {
  public:
    Koozie(istringstream &ss);
    string Description() const;
    double Price() const;
    double Expenses() const;
  protected:
    string color;
    string monogram;
    double yards;
    double price;
};

In the main code, we no longer declare the Dog or Koozie pointers. Instead, we simply declare a pointer to an Item:

int main()
{
  Item *i;           // No longer do we have separate Dog / Koozie pointers.
  istringstream ss;
  ...

And then when we create an instance of Dog or Koozie, we assign the pointer to the Item pointer. When we call the item's methods, it calls the appropriate method of Dog or Koozie. The code is simpler:

  while (getline(cin, line)) {
    ss.clear();
    ss.str(line);
    ss >> key;
    if (key == "Koozie") {          // Here is the relevant code.
      i = new Koozie(ss);           // Since Koozie/Dog implement the 
    } else if (key == "Dog") {      // Item interface, we can treat 
      i = new Dog(ss);              // them as items.
    }
    desc = i->Description();        // Now there is no code duplication.
    p = i->Price();
    e = i->Expenses();
    delete i;

    tp += p;
    te += e;
    printf("%-60s %6.2lf %6.2lf\n", desc.c_str(), p, e);
  }

You need that destructor code in there to make deletion work. In this case, it is specifying that you can use a default destructor for the Item. You will also use the default destructor for Dog and Koozie, since you have not specified them. See the last section of these lecture notes for some explanation on destructors.

I think you can see how this functionality is powerful, and in many cases can clean up your code. When you run it, you'll see that it runs identically to bin/make_invoice_1_normal.


Drinking the Kool-Aid. Moving from an Interface to Inheritance

I'm sure you've noticed that the Dog and Koozie classes have a lot more in common than the specification of their methods. In particular: Wouldn't it be nice if you could remove that duplication? Enter inheritance. What we're going to do now is move the common elements up into the Item, and we'll remove the virtual tag on Price() and Expenses(). The code is in src/make_invoice_3_inheritance.cpp:

class Item {
  public:
    virtual ~Item() {};
    virtual string Description() const = 0;

  // We have moved these variables and members into the class.

    double Price() const;
    double Expenses() const;

  protected:
    string color;
    double yards;
    double price;
};

/* And we implement them here. */

double Item::Price() const { return price; }
double Item::Expenses() const { return yards * 2.25; }

We remove the common variables and member functions from Dog and Koozie. Let's take a quick look at the Dog specification and constructor:

/* Our Dog class now only has the items that are specific to it. */

class Dog : public Item {
  public:
    Dog(istringstream &ss);
    string Description() const;
  protected:
    string size;
};

Dog::Dog(istringstream &ss)
{
  if (!(ss >> size >> color)) {
    throw runtime_error("Bad stringstream in Dog constructor");
  }

  if (size == "S") {
    yards = 3.0;
    price = 20.0;
  } else if (size == "L") {
    yards = 6.0;
    price = 40.0;
  } else {
    throw runtime_error("Bad dog size - should be S or L");
  }
}

You'll notice that Dog can use the variables in Item, because it "inherits" them. That's pretty neat, isn't it? And also pretty powerful. If you want, you can create a new class that inherits Dog, and now:

BTW, you can use protected variables when you inherit a class. You cannot use private variables.

Reiterating the difference between an Interface and Inheritance

An interface in C++ is a restricted subset of inheritance that is not supported explicitly by the language. Other languages will make the distinction (and with them, interfaces are not a subset, as classes can implement multiple interfaces). With an interface you define a class which will contain a bunch of subclasses. The subclasses will each implement methods that correspond to methods that the interface defines. You'll note, the interface implements nothing -- it merely specifies the methods that each of the subclasses will implement. An interface shouldn't contain variables, and it shouldn't contain code. It merely contains prototypes.

Inheritance on the other hand allows you to set up an arbitrarily complex hierarchy of methods, variables and virtual methods. The main class (superclass) contains all three of these things, and then any subclass that is derived from the superclass "inherits" the methods and variables and may use them as if they belong to the superclass. As above, a pointer to a subclass may be treated as a pointer to the superclass.

You'll note that C++ doesn't have a distinction between interface and inheritance. You implement an interface when your superclass doesn't have any data or non-virtual methods.

There are many other issues involved with inheritance with respect to constructors, destructors, and public/protected/private designations. I'm not going to teach you any of that. In this lecture, I simply want to let you know what an interface is, and get your feet wet with inheritance.


My opinions on Inheritance and Interfaces

Although I don't use them much, I like interfaces, because they are pretty clean. They allow you to group classes by their functionality and then use a pointer to the grouping, rather than the individual classes. That's nice.

Inheritance, on the other hand, is a powder keg. Sure, it's very powerful, and you can eliminate redundancy as we did with the example above. However, inheritance has two very strong negative features, the impact of which is probably not apparent when you first start to learn the concepts:

I cannot think of a time where I have seen a solution based on inheritance which wasn't convoluted and difficult to parse, and it's always because the objects get shoehorned into an inheritance structure that doesn't really match its use. Usually, it's because the structure was defined at time A, and new objects were added at time B, and the new objects didn't fit the structure. And it was way too hard to change the structure. So you get illogical code that's hard to read, and oh yeah, you have no idea where the variables are defined.

For that reason, I don't use inheritance. Now, you, gentle reader, are going to be 20+ years younger than I am, and given the prevalence of inheritance in both C++ and Python, will not have the luxury of ridding your life of it. So, learn what I've taught you here, and please try to keep a critical eye towards inheritance, and think twice before choosing an inheritance-based solution to your problems. What you gain in "simplicity" and "elegance", you will lose in documentation...


A little more on destructors

The proper way to handle destructors in interfaces is to make them virtual, both in the superclass and in the subclass. If you ignore the destructor in the subclass, then you'll simply use the default destructor, just like with a regular class. That's what we did above.

If you do write destructors, what happens is as follows: When you delete a pointer, it will call the proper subclass destructor first, and then the superclass destructor next. (You can extrapolate this down the hierarchy if you are using a hierarchy of inheritance). The following code illustrates. Simply read the inline comments to see what it does. It is in src/destruct.cpp:

/* src/destruct.cpp.
   James S. Plank
   Tue Sep 25 14:47:43 EDT 2018

   This shows how destructors work with interfaces.
 */

#include <iostream>
#include <sstream>
#include <vector>
#include <stdio.h>
#include <stdlib.h>
using namespace std;

/* We'll have one superclass called Superclass.  It will have no virtual methods
   with the exception of a destructor. */

class Superclass {
  public:
    virtual ~Superclass();
};

/* The destructor will simply print out that it is being called. */

Superclass::~Superclass() { printf("Superclass destructor.\n"); }

/* Now, we have two subclasses, S1 and S2.  Each of their destructors are declared
   as virtual, and print out that they are being called: */

class S1 : public Superclass {
  public:
    virtual ~S1();
};

S1::~S1() { printf("S1 destructor.\n"); }

class S2 : public Superclass {
  public:
    virtual ~S2();
};

S2::~S2() { printf("S2 destructor.\n"); }

/* The main is pretty sparse.  It simply allocates instances of S1 and S2, and
   then deletes them. Each delete call will call the destructor of the subclass,
   and then the destructor of the superclass. */

int main()
{
  Superclass *s1, *s2;

  s1 = new S1;
  s2 = new S2;

  delete s1;
  delete s2;

  return 0;
}

Here is the running code:

UNIX> bin/destruct 
S1 destructor.
Superclass destructor.
S2 destructor.
Superclass destructor.
UNIX>