Interesting Programming Exercises to Teach Inheritance?

49

14

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.

Patch

Posted 2018-05-01T15:08:26.710

Reputation: 349

4Congratulations on asking a HNQ on our site. And welcome to CSEducators. – Buffy – 2018-05-01T20:08:03.733

See the final note on this answer for the origin of the Animal->Dog example and why it fails. https://cseducators.stackexchange.com/a/4678/1293

– Buffy – 2018-05-01T20:10:37.380

Honestly, inheritance in Python/Java/C#/etc. is a tool often used to get around the fact that they don't have templates or mixins (like C++). Genuine need for inheritance (implying late binding and dynamic dispatch) is far less common than you would think. But if those are sufficient, then the only real example you need is namedtuple... – Mehrdad – 2018-05-01T20:50:22.790

15As 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. – nexus_2006 – 2018-05-01T21:15:00.463

1Inheritance 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. – pojo-guy – 2018-05-02T00:25:59.327

@Patch I also want to welcome you to [cseducators.se]. Take a look around the site; I suspect you'll find it quite useful. Also, this sort of question is a perfect fit here. – Ben I. – 2018-05-02T01:48:05.337

You all should look at Eiffel. It does inheritance properly. You will then see that mixins, and interfaces are just a special case of inheritance. – ctrl-alt-delor – 2018-05-02T09:39:00.357

I have no idea, where the idea of inheritance being mainly a GUI thing, comes from. As just one counter example look at your computer it has many devices: a printer, a pointing device, a keyboard, a temperature senser, a light senser, a microphone, a network, a gps, a clock. These all examples of abstract classes (probably implemented it C with no class keyword, but still inheritance). – ctrl-alt-delor – 2018-05-02T09:44:30.460

@ctrl-alt-delor those are examples of a common interface, not inherited behaviour. The chances of a GPS driver and your printer driver having any code in common are fairly low, given they most likely won't be written by the same company. – Pete Kirkham – 2018-05-02T10:55:46.777

2@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. – Flater – 2018-05-02T12:46:58.537

Do you mean inheritance as OOP concept, or as practical tool used in Python? I'd say these are quite separate things. Mostly because Python is duck-typed, so it usually doesn't make sense to define interfaces in code, and so OOP looks differently than in, say, Java. I assume the first, and I think I can write an answer addressing both, but it would be best if you clarify in the question what kind of inheritance (interface vs implementation) you are concerned about. – Frax – 2018-05-02T16:21:26.820

@PeteKirkham No a GPS is not a printer. A laser printer is a printer, a ink-jet printer is a printer, … The point is not that every thing is a device. But that all printers are printers, and all GPS are GPS. (However, and this is a totally different argument, though still inheritance, all devices are files). – ctrl-alt-delor – 2018-05-02T17:03:46.090

3The 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. – Bill K – 2018-05-03T19:51:49.280

@ctrl-alt-delor I think we're at cross purposes. The printer is made by e.g. Canon and the GPS e.g. by SiRFstarII. The devices will not have any code in common (except possibly licensing the same embedded USB stack). The drivers will again probably not have anything in common beyond messaging stacks. The interface the OS uses to the driver will work for any USB driver instance, and then layer on top a specialised interface to the device type. At what layer do you think there is inheritance of behaviour from shared code? – Pete Kirkham – 2018-05-04T09:01:17.653

@PeteKirkham Yes we are at cross purposes. 1) I run Gnu/Linux, so there is a lot of shared code between drivers. 2) Just because there is no shared code. Does not mean there is no inheritance. Inheritance is a concept independent of implementation. Back to point 1. Just because the OS that you use is badly implemented, does not prove the point that inheritance is only used for GUI. I write embedded systems. There is inheritance at every layer. – ctrl-alt-delor – 2018-05-04T09:20:03.820

@ctrl-alt-delor I was under the impression linux drivers were mostly written in C and therefore there is no possibility of inheritance, only calling shared libraries of code. When did OO languages become common in linux driver development? – Pete Kirkham – 2018-05-04T09:56:23.810

1@PeteKirkham Linux and Unix is written in C. However it is OO. Just as you can write structured assembler, you can write OO C. It is not as easy. But that is what is done. Unix is an OO operating system. It even has garbage collection. – ctrl-alt-delor – 2018-05-04T12:23:53.840

@ctrl-alt-delor - X-Windows is also written in an object-oriented style using C. – Bob Jarvis – 2018-05-05T13:53:21.063

@nexus_2006, for me it clicked in operating systems (yes, written in C of all languages). There are lots of commonalities to abstract out as classes in there. And it is done. E.g. Linux is a giant OO program. – vonbrand – 2018-05-18T12:45:27.617

Answers

40

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

Draco18s

Posted 2018-05-01T15:08:26.710

Reputation: 649

8I don't really care for Minecraft, but I like this answer a lot. One thing that bothers me with those standard Animal=&gt;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. – R. Schmitz – 2018-05-02T10:05:27.680

3

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 http://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

– AmazingDreams – 2018-05-02T10:55:13.883

@R.Schmitz but to what end does an Animal even need a MakeNoise() function? As a simple example, imagine hiring a zookeeper who is so new to the profession that he can't recognize which animal it is. But you can already task him to feed the animals (assume that all animals eat the same food for now). That would be an argument for Animal having a Feed(Food food) method, since the zookeeper trainee is incapable of distinguishing specific descendants from the Animal class. Rudimentary examples are obviously flawed when you look with enough detail, but it's a fair first example, imo. – Flater – 2018-05-02T13:06:36.053

3

@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.

– R. Schmitz – 2018-05-02T14:00:40.643

@R.Schmitz: Are we really going to stumble over the name of the fictional example method used? I was quite clear about referring to a Feed(Food food) method. the actual example is another slightly less abstract example Because the majority of programming which follows modern day conventions (i.e. a concrete example of programming) is often so loaded with conventions and design patterns that students can't see the wood for the trees. Failing to understand that basic examples are simplified and not pedantically correct to the atom is a failure to understand the purpose of the example. – Flater – 2018-05-02T14:13:35.117

2@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. – R. Schmitz – 2018-05-02T14:55:19.563

@R.Schmitz: Did you actually read my comment? I'd expect that arguing the validity of my comment comes after actually reading the comment. You seem to completely miss the message but somehow know that it's not correct anyway. You argued that there is a lack of concrete examples, and yourself added the MakeNoise method name (which was not the direct topic of conversation). I gave you an actual concrete example. I have no idea what you're getting caught up on. It feels like trolling behavior. – Flater – 2018-05-02T14:57:48.917

1@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=&gt;Dog or Person=&gt;Employee examples mentioned by OP. You will have another reasoning why you think it is concrete. – R. Schmitz – 2018-05-02T15:46:07.280

GUIs are another case where OO is frequently used and important. You can make Swing components with a complete custom look by overriding paintComponent. – immibis – 2018-05-03T01:12:22.340

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

Actually, many game frameworks do not use inheritance in this way because it doesn't work very well. Instead, they use composition. For example, games might have cars, boats and amphibious boats... which is easy to do with composition and difficult to do with inheritance. – NPSF3000 – 2018-05-03T04:19:45.730

In the practical example, a object might have a health component, that handles 'take damage' messages that fires a 'death' message to other components that might spawn objects in the game world that can picked up (or straight to inventory).

The spawn object component probably has the relevant settings (e.g. object, probability etc.) so can work for many different types without having to be specialized.

Not saying you can't use inheritence as described (not familiar with minecraft myself), and it is used in game engines for various reasons, just sharing some experiance. See Unity3D. – NPSF3000 – 2018-05-03T04:22:28.587

@NPSF3000 Pretty decent comparison. I used Minecraft, as a) there's an educational version and b) a lot of students are probably familiar with it. Health and damage certainly works, though its pretty rare for different entity types to need to override that method. (And Unity is my jam ;) – Draco18s – 2018-05-03T07:25:42.520

@NPSF3000 Exactly, In fact I don't think minecraft uses inheritance for the blocks because of this, IIRC all blocks have attributes and you just test the attribute. Inheritance, as usual, would make the design much more inflexible, inconvenient and difficult. Sometimes you might have very general categories like "GroundBlock" and "AirBlock" to say which you can or can't walk through, but I don't think minecraft even does this (I haven't programed MC plugins in 5+ years, but that's how I remember it) – Bill K – 2018-05-03T19:58:20.170

@BillK I do Minecraft moddibg, I know what I'm talking about. The example function isn't straight from the code, but it is similar to three that do exist (the sample being a kind of merged amalgam). Can or can't walk through a block involves...somewhere around four different functions. Here is a real example. Seven overridden functions.

– Draco18s – 2018-05-03T20:34:42.643

The important ones in this case are onBlockActivated (so that a GUI can be opened) as well as has and createTileEntity (so that the game knows that special extra class data is needed that can't be stored in a block singleton). Or we could look at this class that has 13 overridden methods (including getDrops which is close to the example in my answer). The important ones are createBlockState, getStateFromMeta and getMetaFromState to handle variant info.

– Draco18s – 2018-05-03T20:44:58.957

Ahh, you are talking about the modding layer, that makes sense. I was saying Notch didn't use OO as a primary part of his design, attributes work much better. I am, however, a little surprised that this mod uses overrides for event notification--We figured out how terrible that was in the AWT days. Last time I checked Bukkit had the ability to submit a class with annotated methods to auto-create listeners which I thought was a truly brilliant way to avoid all the problems inherent with extending/overriding! – Bill K – 2018-05-03T21:41:04.337

@BillK Oh, no, Notch/Mojang uses it too. I just can't link to any vanilla source code as sharing that is against the EULA. ;) There are 167 vanilla subclasses of Block, eight entire packages for subclasses of Entity (arrows, snowballs, zombies, pigs...the player...), 80 subclasses of Item, and tons and tons of others. Admittedly some of the block ones don't need a subclass (BlockBone for example, only has a constructor that only calls methods that return Block and could have been done in init; e.g. Block bone = new Block().setHardness(2.0F)), but the majority are legitimate. – Draco18s – 2018-05-03T22:05:09.673

16

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.

Buffy

Posted 2018-05-01T15:08:26.710

Reputation: 21 033

14"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. – Mehrdad – 2018-05-01T20:42:55.180

@Mehrdad it is pretty hard to avoid the problem. Suppose you have a list of different kinds of Animal. Some Dogs, some Kangaroos, etc. When you retrieve them from the list all the compiler knows is that they are Animal. If you only ever want to send messages defined in Animal then you are fine, but not otherwise. However, If you don't add methods in subclasses then the issue never arises. The complete public interface of any such object is defined in Animal. Over a very short run of code it may be easy to distinguish and remember. Not so much over a long program. – Buffy – 2018-05-01T22:13:31.783

1@Buffy if your goal is consistent interfaces you'd use an interface and have each animal implement that instead of using inheritance... – enderland – 2018-05-01T23:07:10.197

@ElysianFields, this is Python, not Java. Python doesn't have interfaces, though you can simulate them. The rule for interfaces is if you implement an interface don't add additional public methods to the class. If you do, you will wind up with the same problem of having to know the same concrete type of each object even when it is referenced by an interface typed variable (or in Python by an untyped variable). – Buffy – 2018-05-01T23:25:43.827

5At 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. – Artur Biesiadowski – 2018-05-02T11:43:31.503

@ArturBiesiadowski Good for you. This mental abomination actually goes back, at least, to the original Turbo Pascal documentation in which Circle extends Point. All you need to do is "add" a radius field, of course. It was further promulgated in a few text books. I won't name the authors, and hope that later editions thought a bit deeper about the issue. Taking the "easy" way out in defining a class often leads to terribly hard to decipher code. If you think of objects as "bundles of behavior" not "sets of fields" you do better in thinking about them. – Buffy – 2018-05-02T11:52:28.113

@Buffy. "Bad design" is sometimes necessary in the context of education. "Hello world" is a simple example here. One could berate the developer for using a hardcoded string that is not either clearly a const value or a configurable value; but you'd be missing the point of the exercise: showing the simplest of programs. The Animal -&gt; Dog example is similalry flawed in a professional context, but it does explain the nature of inheritance rather well. The solution here isn't improving/complicating the example, but rather continuing with even better examples after the chapter on inheritance. – Flater – 2018-05-02T12:58:56.090

1@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. – Flater – 2018-05-02T13:02:38.447

@Flater the place for discussion is in the Classroom, not here: https://chat.stackexchange.com/rooms/59174/the-classroom

– Buffy – 2018-05-02T13:08:22.477

1@Buffy: These are direct counters to your explicit assertions. They are not a tangential discussion. – Flater – 2018-05-02T13:12:07.293

Yeah, Buffy, I'm not liking this argument, either. If you've got Animal, Dog, and Whale classes, and need Sit() and Swim() functions, there's no way you should be programming those methods (or stubs of them) in the base Animal class. It violates Open/Closed Principle - you'd have to modify the base whenever you add a subclass that needs new functionality. – Kevin – 2018-05-02T13:20:53.897

Yes @Flater, and you have fallen into a serious error. Come to the classroom. – Buffy – 2018-05-02T13:39:05.957

1@Flater: ""Bad design" is sometimes necessary in the context of education." Hmmm. I question that logic. I try hard not to do things in small programs that might lead students to adopt and reinforce bad habits that will haunt them in large programs. No teacher should ever require un-learning in his/her students. Find a better way. – Buffy – 2018-05-02T14:06:09.233

@Buffy: So how do you counter my Hello World example? You're disagreeing without countering the actual example. In a professional context, I would negatively review the output of a hardcoded magic string. But in the context of a Hello World application, a hardcoded magic string is not an issue. You clearly disagree with the latter assertion. So how would you "find a better way" to make a Hello World application while adhering to all relevant professional standards? – Flater – 2018-05-02T14:09:17.830

@Flater, I don't use "hello world" or anything like it actually. My classes start with students exercising objects with a simple interface. They spend about one day writing things in main that will generate a visualization of the simple program. On the second day they start to write simple classes. It requires quite a bit of prior scaffolding. I define a virtual world in which they program. That world is richer than what Java provides and also guides their thinking as they develop. I don't start from the lowest possible level of abstraction. – Buffy – 2018-05-02T14:23:17.810

While I'm all for composition and shallow hierarchies I understand that having pluggable strategies simply moves the principle problems with inheritance to the strategies (instead of having them in the "complex" object). The nice thing is that each of the strategies is surely smaller than the complex object holding them: this paradigm is a neat application of divide and conquer. (And provides better testability of the complex object and may have other advantages.) It is not, however, a principle alternative to inheritance because it still uses it. What am I missing? – Peter A. Schneider – 2018-05-02T18:43:17.297

@PeterA.Schneider, probably nothing. It is just a better and more controllable form of inheritance. Inheritance should model IS-A quite purely, not just IS-Like. Every strategy has the same interface. Other small objects can also use inheritance but in a purer way as they model different implementations of the SAME concept, not just similar concepts. Modeling the same concept, they exhibit the same behaviors and hence, have the same interface. Cleaner and easier to live with in the long run. And better to teach the correct path to beginners than the exceptional cases. – Buffy – 2018-05-02T18:48:11.977

I'm convinced that beginners should not even hear the word "inheritance" until they've learned to model is-a relationships with interfaces. If Python lacks interfaces, then Python is not an appropriate teaching language. – Kevin Krumwiede – 2018-05-04T06:18:26.827

2@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. – Buffy – 2018-05-04T11:58:53.473

10

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.

Kevin

Posted 2018-05-01T15:08:26.710

Reputation: 280

1Seems 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. – Gypsy Spellweaver – 2018-05-04T16:38:02.077

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. – Pharap – 2018-05-05T12:06:14.863

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." – Kevin – 2018-05-07T13:49:46.447

7

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.

Narrateur du chaos

Posted 2018-05-01T15:08:26.710

Reputation: 71

3

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 .

BurnsBA

Posted 2018-05-01T15:08:26.710

Reputation: 131

@buffy e.g. the doom 3 source https://github.com/dhewm/dhewm3/tree/master/neo I'll try to pull out some inheritance chains later

– BurnsBA – 2018-05-01T20:31:43.237

3

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.

mbomb007

Posted 2018-05-01T15:08:26.710

Reputation: 131

1Exceptions 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. – Pete Kirkham – 2018-05-04T10:06:37.440

2

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.

Gypsy Spellweaver

Posted 2018-05-01T15:08:26.710

Reputation: 4 243

1

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).

AnoE

Posted 2018-05-01T15:08:26.710

Reputation: 1 109

1

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.

Yehuda Shapira

Posted 2018-05-01T15:08:26.710

Reputation: 111

1I 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. – BlackJack – 2018-05-02T17:03:16.687

This doesn't save any code, it just moves it around. – immibis – 2018-05-03T03:57:38.007

1

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.

Frax

Posted 2018-05-01T15:08:26.710

Reputation: 531

1

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.)

Pharap

Posted 2018-05-01T15:08:26.710

Reputation: 325

0

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.

Peter A. Schneider

Posted 2018-05-01T15:08:26.710

Reputation: 288

2The 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. – Buffy – 2018-05-02T16:59:05.367

@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...] – Peter A. Schneider – 2018-05-02T18:12:21.507

1I 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. – Buffy – 2018-05-02T18:15:45.137

0

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...

vonbrand

Posted 2018-05-01T15:08:26.710

Reputation: 241