49
$\begingroup$

I originally asked this over on Stack Overflow, but they suggested I look here instead:

I'm currently teaching my students about the concept of inheritance (we're using Python 3) but am unable to think up (or find) any meaningful programming exercises. Everything I seem to find online as a teaching resource rehashes the same forced examples: Employee() inherits Person(); Dog() inherits Animal(); etc.

While these are great ways to drive home the basics of inheritance, I can't help feeling they are a bit hollow as programming exercises. In the end I don't feel like I've made anything. Unlike, for example, the myriad of different exercises out there that can be used to teach recursion.

Anyway, I'm sorry for the rather "soft" question, but I'm just desperate for some better examples that could actually be tackled by a student who's still learning the concept. Thanks.

$\endgroup$
  • 4
    $\begingroup$ Congratulations on asking a HNQ on our site. And welcome to CSEducators. $\endgroup$ – Buffy May 1 '18 at 20:08
  • 15
    $\begingroup$ As a student, I didn't understand inheritance as anything other than an academic exercise, until I started looking at GUI toolkits, with classes like BaseView, GridView, ListView, OrderedListView, etc; and started making my own extensions of these things, that it really made sense. $\endgroup$ – nexus_2006 May 1 '18 at 21:15
  • 1
    $\begingroup$ Inheritance is important, but not as important as interfaces. While I disagree with the assertion that inheritance is a way to get around the absence of templates that c++ offers, I agree that it is over emphasized in teaching OO programming. 90% of the objects you will make outside of a GUI are descended only from Object. $\endgroup$ – pojo-guy May 2 '18 at 0:25
  • 2
    $\begingroup$ @pojo-guy: Interfaces are a variation on inheritance. Assuming you know neither, inheritance is easier to understand than interfaces. The use case for interfaces is rather complex, compared to the use case for inheritance. Regardless of which will be used more often in a professional context, education should follow a logical progression that makes every step as light as it can be. $\endgroup$ – Flater May 2 '18 at 12:46
  • 3
    $\begingroup$ The best thing you can probably teach about inheritance is to avoid it until it makes absolutely perfect sense (and probably even then). The reason it's hard to find a good teaching example of inheritance is because it's very difficult to find models that really should use inheritance. In the end I've regretted every non-trivial use of inheritance, some trivial ones work out pretty well though. What I've found is that if there is any question whatsoever, choose composition/encapsulation, even if the objects easily pass the "is a" test. $\endgroup$ – Bill K May 3 '18 at 19:51

13 Answers 13

41
$\begingroup$

I'm not as familiar with Python as I am with other languages, but I'm sure your students have played Minecraft.

If you haven't, I suggest taking a few minutes to find some introductory "Lets Play" videos on YouTube first.

Let's talk Blocks.

Minecraft has dozens of blocks. Dirt, some, water, colored wool...

All blocks can be broken, picked up, placed, stacked, stored in chests, and crafted together. But not all blocks do just those things.

The chest let's the player store items in it. The furnace smelts some blocks into other blocks. Grass, when broken, drops a completely different block. Stone doesn't drop anything at all unless dug out with a pickaxe.

Minecraft can do this because of all of those basic methods that subclasses inherit and override.

Take this method (not pulled from source, just based on):

public List<ItemStack> getDrops(World w, Pos p, bool harvest, int fortune, Random rand) {
    //basic implementation
}

The default implementation puts a single item (the block itself) into a list and returns it.

BlockGrass would put dirt into the list instead.

BlockStone would check to make sure that harvest is true and the put cobblestone in the list.

BlockDiamondOre checks harvest and fortune, putting a different number of diamond items into the list, even determining how many based on the Random supplied.

And so on.

There are roughly 100 different methods in the block class to handle all of the possible functionality. Update ticks (crops), neighbor updates (observer, torches), interaction (doors, chest, furnace, crafting bench), whether or not a block has a tile entity (chest, furnace), and on and on and on. Most of them are blank and do nothing, entirely up to the derived class to add to, if it needs it.

The same goes for items, some of which (like sticks) don't even have a derived class of their own because they do nothing special. They just exist and do everything any item can.

This is why the are so many mods for Minecraft that add so many new things: no one1 has to implement anything special in order for everything to Just Work.

1Well, except Minecraft Forge, who maintain the official unofficial mod API. But Forge is a special kind of magic that isn't important here

$\endgroup$
  • 8
    $\begingroup$ I don't really care for Minecraft, but I like this answer a lot. One thing that bothers me with those standard Animal=>Dog examples is that it's trying to explain an abstract concept with a slightly less abstract example - but to what end does an Animal even need a MakeNoise() function? Instantly obvious with a "real life" example like this: A sound effect needs to be played when the player is close. Also it's very easy to make some screenshots to illustrate concepts, which can make those olde lecture slides more interesting and easier to understand. $\endgroup$ – R. Schmitz May 2 '18 at 10:05
  • 3
    $\begingroup$ Yep. I think looking at game related examples is a very good idea because OO is important for game design and it is easy to imagine the code being actually used. I think the block example can get quite complex though. Maybe just define some game objects with a shared draw method, an image etc. Add something like health in the mix and you're done. OP could take a look at py.processing.org and you can even easily draw stuff. Maybe idk snow that turns into rain after a couple of seconds of falling? (position, speed, lifetime, image) and rain falls faster than snow $\endgroup$ – AmazingDreams May 2 '18 at 10:55
  • 3
    $\begingroup$ @Flater I have to admit I'm a bit confused now. I thought it's pretty clear that that was a rhetoric question. Even if you take it as a literal question, your example for MakeNoise() isn't about MakeNoise() at all. And then, the actual example is another slightly less abstract example /spherial cow, when my whole point was that I like this answer because it is 100% concrete. $\endgroup$ – R. Schmitz May 2 '18 at 14:00
  • 2
    $\begingroup$ @Flater stumble over the name? I'll just assume you meant to change more than the name, because Feed is a really bad name for a function which is supposed to produce animal noises. What I like about this answer is how it uses exactly a concrete example of programming, which can easily demonstrate how - to keep with your metaphor - some trees together make up part of a real wood/forest. $\endgroup$ – R. Schmitz May 2 '18 at 14:55
  • 1
    $\begingroup$ @Flater I did not engage your cheap stabs like "Failure to understand..." or the first half of this comment now and I will continue to do so, because that feels like trolling behaviour to me. For the second part of your comment: We probably just apply different definitions of "concrete" here. For me, it's not concrete because you didn't get that from actual code used in production - I really don't see any remarkable difference to the standard Animal=>Dog or Person=>Employee examples mentioned by OP. You will have another reasoning why you think it is concrete. $\endgroup$ – R. Schmitz May 2 '18 at 15:46
16
$\begingroup$

Too many examples that you find are (IMO) fatally flawed. The Animal->Dog is especially flawed, though widely used. The problem is that these sorts of examples almost require that the superclass has a certain set of public methods that isn't the same as that of the subclass, requiring you to add additional public methods to the subclass. This is because an object of the subclass has different behavior than that of the superclass. This leads to poor design. If, when you create a variable you think of it as an Animal (or in a statically typed language declare it so) then later, when you assign a Dog object to the same variable you, the programmer have to remember that it is now a dog so that it can accept the sit() method that wasn't implemented in Animal.

In a dynamically typed language like Python, the programmer has to remember types since the compiler doesn't, but if you also have to remember where in an inheritance hierarchy an object is at runtime then you have a more difficult than necessary task. You can ask an object for its type of course, but that leads to messy code.

A better solution is to maintain the set of methods declared in the topmost class as you descend and not extend them. What you change in the subclass is only the implementations of methods defined in the superclass. Then your original intention (it is an Animal) doesn't need to be modified (and mentally maintained) as you write the program. The variable responds only to Animal methods, clearly impossible in this example.

One way to help solve your problem is to assure that your inheritance hierarchies are very (very) shallow. Don't try to define things three or four deep. This helps even if you do add additional public methods in a subclass.

However, there is a better way.

One reason for building hierarchies is to get different behavior in the subclass. But there is a better way to do that. And it doesn't require adding additional behaviors. The flaw in thinking is that many people think of using inheritance hierarchies for BIG things. Like Dogs and Giraffes. If instead you don't use inheritance for the big things but only for small things, life gets easier.

The alternative to inheritance I suggest is to build objects by composition. Complex (i.e. big) things are made up of smaller parts that are themselves complex (objects, not integers and strings). This leads to programming by delegation. An object can delegate some of its behavior to another object, possibly held in a field and invisible to the public class's clients. When a public method of the class is invoked, it simply sends a message to the delegate object and can get a return value that it can return or modify.

So the makeNoise() method of Animal looks like this.

def makeNoise():
    myNoiseMaker.shout()
    return

The object in myNoiseMaker makes the actual sound. But by changing that object the Animal can bark or meow or roar or what ever is needed for that animal. The value can be set in a constructor to produce a Dog or a Lion, of course. All animals behave the same (same set of public methods) but each in its own way. Don't start thinking that we need switch statements to achieve this. There is no test for the kind of thing we are. One possible value of myNoiseMaker only knows how to bark. A different object knows how to roar. We just create (or replace) the object as needed.

This of course is well known as the Strategy Design Pattern. The strategy here is an object with one method: shout().

It is also possible to use inheritance at this level and one Strategy can inherit from another. Usually you want a Strategy that implements shout as a no-op. You can inherit from this to create a Dog and from that to create a LoudDog. So an object built by composition (lots of Strategies) and using Delegation is pretty rich and very flexible. Each instantiation of an Animal object only knows how to do one thing as appropriate. This is polymorphism, actually.

But, once you grasp the idea of a Strategy you can do much more. As a program runs, its state changes. One possible way to develop programs is with State Change Diagrams. At certain state changes it is possible to replace one delegate with another, changing the behavior of the containing object that uses delegation. For example, the first time you press a number key on a calculator it shows that value in the display. But the next time you press it something different happens and the new value is accumulated into the old. So pressing a number key after pressing the equals key has different behavior than after pressing a number key. This can be handled by having the calculator delegate the key presses to (various) strategy objects as the state changes. Again, you don't need flags and switch statements to remember what to do next. The Strategy object knows what to do since it was designed to do only that one thing.

To go even farther, another design pattern is Decorator. There are various kinds of decorator but the essence is that it is a certain kind of thing and it also has something of the same kind. A Strategy Decorator is a strategy (it has the shout method) and it also has a strategy as a field. When the containing object delegates to the Decorator it can first fire the shout method of the held object and and also add a sound of its own. Since a Decorator is a Strategy it can also decorate another decorator as the held object. This is essentially a linked list of Strategies with all but the last being a Strategy Decorator.

So, you can illustrate inheritance and dynamic polymorphism very nicely with small classes such as strategies and decorators. But it requires a mental model about how to build objects - use composition primarily (for the big things) and inheritance for the small things.

And notice that this mechanism (composition + delegation) moves the focus of change-of-behavior from the class to the individual object.


To more explicitly answer your question, you could provide a basic class in which a few methods delegate to other objects and then have them make the behavior of objects more interesting by defining other delegates that inherit from the ones you give. The first cut could have all of the delegates provide empty behavior. Rather than modifying the classes of those objects, have them write (simple) subclasses.


I'll note in also that a statically typed language such as Java can do even better here, since the explicit declarations of the variable types, unavailable in Python and Ruby, makes it clear about what is a Strategy and what is not. In general, though a Strategy is very simple, a method or two, named for the task at hand.


Let me try to explain why it is a bad idea, especially in statically typed languages like Java but also in Python, to define new public methods in a subclass or in classes that implement an interface.

Suppose you have the following: Animal (either a base class or an interface, and you have Mammal and Invertebrate as subclasses of Animal. You also have Dog and Kangaroo as subclasses of Mammal and Mosquito as a subclass of Invertebrate. Suppose also that each class introduces new public methods not defined in its superclass. The sensible programmer will of course create a Dog object with

Dog myPet = new Dog(Fido);

and other objects similarly. Now myPet.sit(); makes perfect sense whereas had myPet been given the static type Animal it would not.

However, the problem arises when you create collections, say List, or pass myPet as an argument to a method that expects an Animal. What can you do in that method? What can you do with objects extracted from the List? The compiler only knows the object as Animal, of course, so you can invoke those methods, but what if you want to do more with the object. Runtime type checking (instanceof) and Casting can recover the specific type but at the cost of potential runtime errors if you make inappropriate assumptions. The original programmer may not make such errors in a small program, but the problem becomes very difficult in a large, important, and maintained-by-others program. It isn't the original declaration that is the issue here. Of course you say Dog. Or do you?

A very common idiom in Java is to declare an ArrayList as follows

List<Animal> animals = new ArrayList<Animal>();

Note the interface type on the LHS and a specific class type on the RHS. This makes it easy to replace the concrete type with a different one without changing the LHS or the code that follows. This is actually the preferred method of doing such declarations.

Why does it work? It works because all of the provided implementers of List have, as their only public methods, the methods defined in List itself. It wouldn't work if that were not the case. Then, if additional methods were defined (and used), changing the concrete type would become much more difficult.

The good practice in the libraries (not adding public methods) is good for a reason. I'm simply suggesting it as a general practice that makes for better programs. It is also a simple and safe rule for beginners to learn. The rule is frequently broken, I realize. But poorly designed software is frequently written, also.

And note that this problem is more severe in Python, which is dynamically typed and variables (identifiers) have no type at all. Java declarations at least give you some sort of managed knowledge of what sort of thing is referenced. In Python (Ruby) you need to do that yourself or include frequent type checks or suffer frequent errors.

$\endgroup$
  • 14
    $\begingroup$ "This leads to poor design. If, when you create a variable you think of it as an Animal (or in a statically typed language declare it so) then later, when you assign a Dog object to the same variable you, the programmer have to remember that it is now a dog so that it can accept the sit() method that wasn't implemented in Animal." Are you sure you're doing this right? If you wanted a variable pointing to a dog, you'd declare it as a Dog. When all you want is an Animal, you don't want to call sit() on it. $\endgroup$ – Mehrdad May 1 '18 at 20:42
  • 1
    $\begingroup$ @Buffy if your goal is consistent interfaces you'd use an interface and have each animal implement that instead of using inheritance... $\endgroup$ – enderland May 1 '18 at 23:07
  • 5
    $\begingroup$ At my university, they went with Point->LineSegment->Triangle->Quadrilateral inheritance...I got into major fight with professor when trying to argue with him that he is scarring the minds of students. $\endgroup$ – Artur Biesiadowski May 2 '18 at 11:43
  • 1
    $\begingroup$ @Buffy: Your comment does seem wrong. If you define an Animal variable, you should never care about which animal it is. The problem is being created by the developer that decided to declare an Animal instead of a Dog variable. You say the problem is unavoidable, but I strongly disagree here. The problem is the expectation of the developer after needlessly downcasting their type to the subclass. That is developer error, not an inherent design flaw. $\endgroup$ – Flater May 2 '18 at 13:02
  • 2
    $\begingroup$ @KevinKrumwiede, too strong. Python is an excellent teaching language, but you have to program it like Python, not like Java. If you follow simple rules in Python you get great results. But the same is true for Java or other languages. Programming naively in any language can lead to design-disaster. And thinking in one language while programming in another seldom leads to good results. Learn your tools, just as any crafts-person should. $\endgroup$ – Buffy May 4 '18 at 11:58
10
$\begingroup$

I've got one that might help, modified/simplified from an actual problem I had to solve at my current job.

Imagine you're writing a Content Management system - this system will store four types of documents (and the Meta/Index information for them)

  • PDFs (who created, description, file size)
  • Word documents (who created, description, file size)
  • Pictures (who created, description, image dimensions, file size)
  • Videos (who created, description, image dimensions, file size, duration)

Yeah, you could write these out into four completely separate classes. The problem is, you're already going to have to write all those properties each and every time for every single class - and you're going to end up with code duplication along the way as functionality gets added.

Imagine you're now asked to add a few features:

  • Checking to see whether the file size is beyond a certain threshold.
  • Checking to see whether it's too high of resolution of video/picture.

If you had those four classes as four distinct entities? You're going to start having to code in multiple places.

But what happens if you use inheritance?

public abstract class CMFileInfo
{
    string creatorName;
    string description;
    int fileSizeInK;
    bool IsSizeTooLarge()
    {
        // code to deterimine if the file size is too large
    }
}
class PDFFileInfo : CMFileInfo {}
class WordFileInfo : CMFileInfo {}
class ImageFileInfo : CMFileInfo
{
    Dimensions dimensions;
    bool AreDimensionsTooLarge()
    {
        // code to deterimine if the dimensions are too large
    }
}
class MovingImageFileInfo : ImageFileInfo
{
    int durationInSeconds;
}
// sorry for C# instead of python; I don't currently know python...

... look how the code only has to be written once, and the properties only have to be defined once. All Content Management files have to have a 'who created', a 'description', a 'file size', and a way to check whether that size is too great. So they're all defined at the base level. Down a level from there is the ImageFileInfo class, which adds a Dimensions property and a check whether those dimensions are too large. Finally, the Video (MovingImageFileInfo) doesn't rewrite the wheel - it takes all the stuff inside the ImageFileInfo class, and simply tacks on a durationInSeconds property.

$\endgroup$
  • 1
    $\begingroup$ Seems like there's a blurring of the lines for separation of concerns here. The file info (creator, size, and maybe description) are one thing, the info for image/video has nothing to do with the file, and should be a separate object that has-a file. The file is not the image, the file holds the image. $\endgroup$ – Gypsy Spellweaver May 4 '18 at 16:38
  • $\begingroup$ Indeed, perhaps ImageFileInfo ought to supply ImageInfo as a property rather than providing methods acting on the image data. Otherwise, I think this is a somewhat realistic example, though it could do with an example of a virtual function. $\endgroup$ – Pharap May 5 '18 at 12:06
  • $\begingroup$ Oh, Absolutely! In the real version of my code, a document consists of two objects: a Content class and an IndexCard - with the IndexCard containing metadata for the document itself, and the Content class containing the file (and data like its size, type, extension, etc.) I was just trying to simplify it down to a bite-sized example of how inheritance can be used well, and in a way that students wouldn't think of as "Eh, this is just theoretical stuff we'd never need to use." $\endgroup$ – Kevin May 7 '18 at 13:49
7
$\begingroup$

My coding school gave one particular (weeks-long) project that I felt nailed the concept of inheritance, and why it could be useful:

Simulating a circuit board with logic gates.

The framework of the exercise can be adjusted, but here's a short example:

A circuit board is composed of circuit inputs, logic gates and circuit outputs; each of these components has a variable number of input pins and output pins. The output pin of a component can be connected to the input pin of another.

Logic gates schema; stolen from BBC article

For instance, in the above circuit, there are three circuit inputs (A, B and C), and one circuit output (Z); the AND gate at the bottom has two input pins, and one output pin, connected to the input pin of the OR gate on the right.

The goal of the exercise is to write a program that simulates a circuit board with the above rules; the program must e.g. parse a simple file describing the list of components, and how they're connected together; then parse the command line for commands such as setInput B true or updateCircuit or printOuput Y. (or give the students another framework if you don't want them to worry about parsing text)

If a logic gate forms a loop, the "loop part" must be delayed until the next update.

You must implement the following components:

  • AND Gate
  • OR Gate
  • NOT Gate
  • XOR Gate
  • Multiplexer
  • ...

The circuits components must inherit from the following class:

class CircuitComponent {
public:
    virtual size_t getInputCount() const = 0;
    virtual void connectInput(size_t inputPin, const CircuitComponent* other, size_t outputPin) = 0;

    virtual void updateOutputsFromInputs() = 0;

    virtual size_t getOutputCount() const = 0;
    virtual bool getOutputValue(size_t outputPin) const = 0;
};

There are several things that I like about this exercise:

  • It's as close to a real example as you can get.

  • It's, in my opinion; a use case where inheritance is the best solution; unlike other cases where some form of composition might be better suited.

  • It shows how a good base class can make your workflow easier; with the interface I gave, there is basically only one update model that can be implemented (updating the inputs, then the components connected to the inputs, then the components connected to these components, etc). It's also a good demonstration of expressivity through const-correctness.

  • It still has a few classic pitfalls of inheritance that most students will fall for, that you can easily point out. For instance, most students will implement circuit inputs and circuit outputs as children of the component class, and store them in one list/array with all other components, then use some convoluted dynamic_casting when they need to access the circuit's inputs/outputs. If they make that mistake, you can then point them towards a better solution, like storing inputs, logic gates and outputs in three different arrays, while still inheriting them from the same class, and they'll have learned from experience.

$\endgroup$
3
$\begingroup$

Some commentary: What do you mean by "meaningful?" Are you looking for a program that does something useful? And what do you mean by useful? Do you need to have in-depth discussions of covariance and contravariance or just talk about the basics of inheritance? These are rhetorical questions but I think how you address these questions changes the parameters for a good answer.

Anyways, my suggestion is to look at inheritance in game engines. I think you can tailor some examples to cover your learning requirements. Depending on how much of a foundation you provide can give various degrees of a working product at the end of a semester. I'm not suggesting you build a game engine in a semester, but discuss some naive approaches and then look at real-world examples and then [some hands on project]. I think this has the added benefit of being of interest to at least a few students who found there way to a CS class by interest in creating video games (and you can provide a dash of realism on the difficulties in delivering a working game from scratch).

Two examples, as promised in the comments.

The Doom 3 source is available online and generally pleasant to read. Here are a few inheritance chains

idClass <- idEntity
           idEntity <- idItem
           idEntity <- idProjectile
           idEntity <- idAnimatedEntity
                       idAnimatedEntity <- idWeapon
                       idAnimatedEntity <- idAFEntity_Base <- idAFEntity_Gibbable <- idActor <- idPlayer  

You can find the source at https://github.com/dhewm/dhewm3/tree/master/neo/game .

A different kind of game is Cataclysm: Dark Days Ahead, a console/TUI game. The code is also a bit more "DIY" than the Doom 3 source. The player inheritance chain starts with two base classes, Creature and template <typename T> class visitable. Then the inheritance chain is

Creature, visitable<Character> <- Character <- player  

You can find the source online at https://github.com/CleverRaven/Cataclysm-DDA .

$\endgroup$
3
$\begingroup$

I think showing examples from the Python language can be helpful.

Exception hierarchy:

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

In my opinion, seeing a graphical representation like this makes everything much easier to understand, because it's a practical example, and it's easy to see that ZeroDivisionError would be a subclass of ArithmeticError. This example works for more languages, too, since Java has something similar.

An exercise could involve creating and extending your own Exception classes.


numbers:

Number -> Complex -> Real -> Rational -> Integral

The linked documentation does a great job of describing what each class adds to its base class. Since computer science students should also be familiar with these mathematical "classes" of numbers and their various properties, this can be a useful example, depending on the age of the students.

There are plenty of mathematical concepts that can be used as an exercise for inheritance, so pick some that are simple enough for your class to work with and have them create a tree-style diagram of the inheritance.

$\endgroup$
  • 1
    $\begingroup$ Exceptions work differently to most class hierarchies - they are ontological and the type is important, whereas in most OO it's the behaviour that is important. They feel more like how you'd use types in functional languages for pattern matching than pure objects. $\endgroup$ – Pete Kirkham May 4 '18 at 10:06
2
$\begingroup$

If the students are still needing real "objects" to connect metally with the concept of object you could resort to using transportation as a system. Another user, G. Ann - SonarSource Team, gave a good break down you could follow in answer to another question.

On the other hand, if you are looking for a more practical use-case, then you can use lists. There is the Node, which in its simplest form is nothing more than a data encapsulation, and the List which implements the process of accessing the nodes. The List and Node can both be inherited as you expand from array-based lists to linked lists, and into doubly linked lists. Then you can switch to stacks and queues, maybe even moving into binary, and n-ary, trees.

$\endgroup$
1
$\begingroup$

Kind of a meta answer:

  • Pick any large, established, well-received Java library/API; especially one that ships with the regular open-source JDK. You will find a lot of examples of inheritance, conveniently displayed at the top of each "class" page in the Javadoc. Most of these should have been chosen by their authors with good reasons, and you certainly could do worse than use those as examples.
  • I personally would rather avoid Person / Animal style examples as well; as you, I find many of those either far-fetched or useless insofar as they only show the bare minimum, staying shy of any obvious problems that might arise in real world settings. Most importantly, many of those I saw in the past would do better by using other structural patterns (like Composite) instead of inheritance.
  • While you're at it, you could teach your pupils about the fact that the kind of inheritance we have in, say, C++ or Java is only one kind of possible implementation; and that there can be (and are) other solutions to whatever problem it is solving. For example, duck typing (e.g. ruby with its weird but utterly wonderful approach to the theme, google "ruby superclass" or "metaprogramming"), object-based class-less OOP (e.g. JavaScript no classes, how 'new' works).
$\endgroup$
1
$\begingroup$

C# practical example:

Imagine you were making a super-awesome video game, and you needed to write a function that saves the game.

public void SaveTheGame(GameData data)
{
    // TODO: save! (somehow)
}

But where will all this data be saved?
A simple text file? A binary file? An SQL database? A NoSQL database? Something else entirely?

Luckily, you don’t have to make the decision right now.
No, the decision is configured by the user when the game is installed. So actually it’s not a lucky thing, because now you have to support all the options:

public void SaveTheGame(GameData data, SaveMode saveMode)
{
    if (saveMode == SaveMode.SimpleText)
    {
        // a whole lotta code
    }
    else if (saveMode == SaveMode.BinaryFile)
    {
        // a whole lotta code
    }
    else if (saveMode == SaveMode.SQL)
    {
        // a whole lotta code
    }
    else if (saveMode == SaveMode.NoSQL)
    {
        // a whole lotta code
    }
}

All nice and dandy.
However, in this case there are only four ifs.
But what if we had thirty? Or fifty? Or fifty-three?
And what if there are a whole bunch of nested ifs?
The situation can easily become… (drumroll)… iffy.

And it gets worse:
What if you’re writing code for other developers, and you want to allow them to add their very own ways of saving?

With inheritance, we eliminate the need for all these ifs, and allow future programmers to add their own saving method:

public class Saver
{
    public virtual void Save(GameData data)
    {
    }
}

public class SQLSaver : Saver
{
    public override void Save(GameData data)
    {
        // a whole lotta code
    }
}

// And then we can do:
public void SaveTheGame(GameData data, Saver saver)
{
    saver.Save(data);
}

Now, if new developers want to add their own way of saving, they can write a new class the implements Saver, and just pass it to the SaveTheGame method.

$\endgroup$
  • 1
    $\begingroup$ I don't see the need for the Saver class here, and when that is gone, there is no inheritance in that example. In statically typed languages Saver would be an interface and not a class and in Python (the language used by the OP) the Saver class simply doesn't make sense. Python programmers would just remove it. Duck-typing at work. $\endgroup$ – BlackJack May 2 '18 at 17:03
  • $\begingroup$ This doesn't save any code, it just moves it around. $\endgroup$ – immibis May 3 '18 at 3:57
1
$\begingroup$

Implement nested arithmetic expressions.

Let me start with what it can look like, and then I'll explain why it's a great example.

It starts very simple:

class Number:
    def __init__(self, value=0):
        self.value = value

    def evaluate(self):
        return self.value

class Sum:
    def __init__(self, *parts):
        self.parts = parts

    def evaluate(self):
        return sum(map(lambda part: part.evaluate(), self.parts))

print(Number(42).evaluate())
print(Sum(Number(20), Number(20), Number(2)).evaluate())

Then it slightly grows:

import functools

class Product:
    def __init__(self, *factors):
        self.factors = factors

    def evaluate(self):
        return functools.reduce(lambda acc, expr: acc * expr.evaluate(),
                                self.factors, 1)

print(Sum(Product(Number(6), Number(6)), Number(6)).evaluate())

And yet a bit more:

class Fraction:
    def __init__(self, numerator, denominator):
        self.numerator, self.denominator = numerator, denominator

    def evaluate(self):
        return self.numerator.evaluate() / self.denominator.evaluate()

print(Fraction(Sum(Number(42), Number(42)), Number(2)).evaluate())

And it can go on for quite a while.

Why is it a great example?

  • it's small,
  • it implements familiar objects,
  • it is something useful (every calculator or interpreter has some code like this).

How can it be extended?

  • add pretty printing (advanced flavor: with minimal number of parentheses),
  • add variables (so that evaluate takes environment dict as argument),
  • add more operations,
  • add logical expressions.

Where is the inheritance?

I'd like to point that I didn't use Python's subclassing at any point here, and it was on purpose: Python's subclassing is mostly about implementation inheritance, and here it's not necessary at all. Of course, if you use type hints, you should define an interface, but only in that case. Python is generally duck-typed and there's not much point in pretending it's not. However, you can also add some shared implementation to this example.

Adding implementation inheritance

There is at least one thing that all kinds of expressions may easily share: caching. Example base class could look like this:

class Expression:
    def __init__(self):
        self._cached_value = None

    def cached_evaluate(self):
        if self._cached_value is None:
            self._cached_value = self._evaluate()
        return self._cached_value

    def evaluate(self):
        raise NotImplementedError(
            "Expressions must implement evaluate() method")

A few tweaks to expression classes, and basic caching is there.

Another idea to add common implementation would be listing free variables occuring in the expression (after adding variables ofc). First, make subclasses implement method subexpressions() (returning list of subexpressions, e.g. self.parts in case of sum). After that, implement method variables in Expression, and override it in Var/Variable (to return the single variable used there). The effect should be something like:

>>> Sum(Var('a'), Var('b'), Var('c')).variables()
set(['a', 'b', 'c'])

One problem with these examples is that the base class is more of a mixin than a real base. But I don't think you can do much better without going big. So, ultimately, you may want to go big.

Want really interesting inheritance? Go big, use a framework.

While little examples like above are good for understanding the basics, the most practical examples of implementation inheritance are big, all-inclusive classes provided by frameworks, like Django's views and forms. You may consider using them to show some practical uses of inheritance. While writing a Django¹ application is definitely an overkill, modifying one may be perfectly good task even on (relatively) early level.

If you prepare a working application that needs relatively small modification, like adding a view with different sorting, it may be a good hands-on experience. However, this is risky, and may be daunting experience if either students or exercise are not prepared well enough, so proceed with great care (if at all).


¹There's a number of frameworks with great examples, Django is just the one I'm familiar with.

$\endgroup$
1
$\begingroup$

When I want to give an example of how inheritance works, I always point to C#'s Stream class.

The Stream class is an abstraction of the idea of a data stream from which bytes can be taken or to which bytes can be stored. Its child classes include FileStream which represents the data of a file, MemoryStream which represents data in memory (RAM), BufferedStream which is an adaptor to provide an extra layer of data buffering, and CryptoStream which represents a layer of encryption.

It's a very abstract example, but it's a real world example that highlights how different implementations of the same interface can be incredibly useful. It demonstrates the use of virtual functions, abstraction, the dependency inversion principle and the liskov substitution principle.

Any Stream can be wrapped in a StreamReader, StreamWriter, BinaryReader or BinaryWriter to handle the ability to read/write data more complex than bytes. These are also good examples of the decorator pattern being used 'in the wild'. (StreamReader and StreamWriter in turn inherit TextReader and TextWriter, which are themselves another good example.)

$\endgroup$
0
$\begingroup$

I'm not sure why you don't like the person/employee/manager hierarchy; it is just an example for a whole class of object hierarchies which represent specializations of data with a common subset and are often stored in databases. Other examples would be non-human inventory (every physical item a company owns has a value and a location etc.), or financial assets (they all have some proof of ownership, a value, a date of acquisition etc.).

If you are looking for something which can be implemented and played with in a class I have two suggestions:

  1. If there is a graphics library available, graphical objects in a 2-D or 3-D world which all have a position, can be transformed, displayed etc. form a nice hierarchy.

  2. Because Python is good at parsing text, a calculator similar to a bc subset comes to mind which essentially processes a succession of potentially nested expressions, each of which has a value. The inheritance hierarchy would model the grammatical hierarchy of the language: subclasses of expression could be literal_value, simple_expression, variable etc.

$\endgroup$
  • 2
    $\begingroup$ The problem with person->employee->manager is that it can be difficult to evolve an Employee into a Manager. This is a frequent use case, but the methods (behaviors) open to a Manager are different from those of an Employee and certainly of the general Person. This is thinking of classes as a "classification" system, rather than way to define "bundles of behavior". It is the public methods (the behaviors) that make a class what it is. Likewise thinking of objects as bundles of data (rather than behaviors) leads to lots of poor designs. Make Manager a Trait of employee, not a subclass. $\endgroup$ – Buffy May 2 '18 at 16:59
  • $\begingroup$ @Buffy Well, it appears to me that the problems you describe are inherent to the very concept of (true, as opposed to mere interface) inheritance. Paradigms have shifted away from this pattern towards composition for many reasons, yours among them, but these examples are what (true) inheritance is about, like it or not. If one wants to teach it, one has to teach these concepts together with the caveats. [As an aside, evolving an employee into a manger is relatively easy in a program, compared to the pesky reality...] $\endgroup$ – Peter A. Schneider May 2 '18 at 18:12
  • 1
    $\begingroup$ I disagree. I think they are just a consequence of shallow thought and poor design. However, if they are poorly taught the situation won't improve. Work for change. Bad design isn't compulsory. $\endgroup$ – Buffy May 2 '18 at 18:15
0
$\begingroup$

Part of the "teach OOP" problem is that OOP (and modularity, and top-down design, and variable-naming discipline, and consistent code layout, and...) is practically useful for large programs with a long life, and in the time allotted you write tiny throwaway programs.

Perhaps a way around that is reading (good!), hopefully modifying/extending, programs written in OOP style. Open source is a godsend...

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.