50

I've just started learning about Inheritance vs Composition and it's kind of tricky for me to get my head around it for some reason.

I have these classes:

Person

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name)
    {
        Name = name;
    }

    public override string ToString()
    {
        return Name;
    }

    public virtual void Greet()
    {
        Console.WriteLine("Hello!");
    }
}

Teacher

class Teacher : Person
{
    public Teacher() : base("empty")
    {
    }

    public override void Greet()
    {
        base.Greet();
        Console.WriteLine("I'm a teacher");
    }
}

Student

class Student : Person
{
    public Student() : base("empty")
    {
    }

    public override void Greet()
    {
        base.Greet();
        Console.WriteLine("I'm a student!");
    }
}

And I've been told this:

Protip: don't use class inheritance to model domain (real-world) relationships like people/humans, because eventually you'll run into very painful problems.

And I don't really get why.

First of all, what does model domain mean? Also, if I shouldn't use inheritance, should I use composition? If so, how would my code look? And also, what are those "very painful problems" that can appear?

md2perpe
  • 153
  • 6
  • 1
    Does this answer your question? [Why should I prefer composition over inheritance?](https://softwareengineering.stackexchange.com/questions/134097/why-should-i-prefer-composition-over-inheritance) – gnat Feb 14 '22 at 15:31
  • 4
    @gnat not really, because I don't know what those "very painful problems" can be in my example and also what does he mean by model domain :) – Octavian Niculescu Feb 14 '22 at 15:34
  • 58
    What happens when a student graduates and becomes a teacher? What about a student who teaches a seminar? – Jörg W Mittag Feb 14 '22 at 15:47
  • 7
    *to model* is the verb in that sentence, it's not a *model domain*. In much the same way that a [painting of a pipe is not a pipe](https://en.wikipedia.org/wiki/The_Treachery_of_Images), a class named `Person` is not a person. – Caleth Feb 14 '22 at 16:04
  • 1
    Well, keep in mind that things like Student and Teacher are both context dependent and temporary. Inheritance is more useful for things that are intrinsic. For example, genetic traits. Otherwise you have other tools (like interfaces AKA _acts as_ and composition). It's natural that you would run into situations where inheritance might not be a good choice. – Berin Loritsch Feb 14 '22 at 16:35
  • 5
    @JörgWMittag - you delete the student record, and create a teacher record? Or keep both, with a reference? There are plenty of solutions for these imaginary problems. – Davor Ždralo Feb 15 '22 at 00:33
  • Since you asked what these words mean, let me try and shed some light. "To [`1:` model] [`2:` domain] relationships" means something like "to [`1:` come up with a way to represent/describe (using code, data structures, inheritance, composition, etc.)] [`2:` the things that your application is about - your problem space, your domain of interest] - and their various relationships". "To model" here is a verb (i.e. you're doing modeling). The term appears in different fields (eng., math, sci.). A little more broadly: Every time you're thinking about anything from the real world, you're... 1/3 – Filip Milovanović Feb 15 '22 at 01:37
  • ...always ignoring certain things, while treating only certain aspects of it as important - so you're always working in terms of this representation you concocted - a *conceptual model* of the actual thing, that captures how you think about the thing, and that frames how you talk to others about it. It's just a fact of life, but most of us don't think about that too much for the most part. Since it's a simplification of the real thing, every model is wrong in *some* way, or inapplicable in some situations - but simultaneously useful in others. 2/3 – Filip Milovanović Feb 15 '22 at 01:37
  • 1
    So, it's not *wrong* to use inheritance - it might work well for the things your application needs to do (== for the problems in your particular problem domain)! Just don't rely on it too much - explore other options, try composition, try out different things, even unconventional ones. Whichever way you go, there are always going to be some set of problems for which your approach will turn out to be too limiting or unwieldy; so if you encounter a gnarly problem, it helps if you've experimented with different stuff. 3/3 – Filip Milovanović Feb 15 '22 at 01:37
  • I usually use inheritance together with interfaces. If two objects are interchangeable because solve the same problem, but differently, they might as well inherit their interface from a parent class/interface. If you want to extend or change the functionality of an existing implementation without altering/re-implementing the whole interface, you could extend that class. However, in most cases, you would use composition, where an object uses the functionality of another object to achieve its own goal. – Green绿色 Feb 15 '22 at 09:10
  • 1
    Modelling students and teachers as completely separate entities probably makes sense in a school (they will behave in completely different ways and you will store completely different data for them); less so in third-level education (college, university), where the distinction can get blurry. – TRiG Feb 15 '22 at 11:58
  • @FilipMilovanović I think your comments should be combined into an answer. – TheRubberDuck Feb 15 '22 at 14:10
  • 1
    Read this fantastic 5-part series by Eric Lippert: [Wizards and Warriors](https://ericlippert.com/2015/04/27/wizards-and-warriors-part-one/) ([part 2](https://ericlippert.com/2015/04/30/wizards-and-warriors-part-two/), [part 3](https://ericlippert.com/2015/05/04/wizards-and-warriors-part-three/), [part 4](https://ericlippert.com/2015/05/07/wizards-and-warriors-part-four/), [part 5](https://ericlippert.com/2015/05/11/wizards-and-warriors-part-five/)) – canton7 Feb 17 '22 at 12:46
  • The preference for inheritance usually stems from that _"`Dog` is a subclass of `Animal`"_ example they love so much in schools. Not saying that I like that example, but at least the "diamond problem" can be demonstrated in that same context: A `Fish` can `swim()`, but so can a `Dog`. So you need a common superclass `SwimmingAnimal` . A `Bird` can `swim()` too, and also `fly()`. This already feels wrong, but now you need to make `FlyingAnimal` a subclass of `SwimmingAnimal`. Finally when you get to a `Butterfly`, which can `fly()` but not `swim()`, it becomes impossible. – R. Schmitz Feb 17 '22 at 15:30
  • Just to add a little personal experience... I loved the idea of inheritance when I first learned it, I've used it effectively in a few cases but I've never been happy with the result in the long run. In general I could have come up with as good a solution through composition and other programmers would have found it easier to understand and work with the code. There are a few exceptions where inheritance has worked out but they are generally trivial examples that override a single method implementation (Monkey Patch), not real oop style design/extension. – Bill K Feb 17 '22 at 18:52

7 Answers7

119

The problem I have with this model is that teacher & student are roles while person is a real entity.  While this model will work in the short term, it will have problems if: a student becomes a teacher, or, if a teacher takes a course becoming a student (or also if a student graduates, and is no longer a student).

Student & Teacher are ephemeral roles (played by people) whereas Person is persistent entity.

Thus, an is-a relationship between Student and Person or between Teacher and Person is inappropriate.


Also, if I shouldn't use inheritance, should I use composition?

Yes, using composition will allow a Person's roles to come and go without having to create/destroy a new Person object.  You just need to model that a Person object can have a relationships with role objects.

If the role captures extra information (e.g. Teacher of what subject/classes), having role objects refer to person objects might make sense, and if you need to quickly identify all the roles a person has, then as set of roles within the Person object also makes sense.


That model also captures Age which is a concept that is relative to now, which is constantly changing.  This will also have problems over time — instead, capture a non-relative value like year born.


First of all, what does model domain mean?

A domain model has the purpose of being able to capture information in order to be able to later answer questions that you want to ask.

We model for the purpose of providing automation of some (usually highly repetitive) task in the domain.  We are not trying to recreate the domain within the computer, but instead to automate some portion of the domain.  Perhaps just record keeping, or perhaps automating some part of assigning classrooms to classes, teachers to classes, students to classes, timeslots to lectures.  If just record keeping, still need to know what questions & answers you want those records to be able to give.

So, you want to identify what automation is intended, then identify what answers you want that to give, what decisions to make, then identify what questions to ask of the domain modeling, and what information has to be captured/modeled for these.

Then, we attempt to model just enough for that: don't over model for things that the automation won't help with (for example, we don't need a plethora of classes when objects and fields will do) and yet model sufficiently that the automation works properly.

We model so as to facilitate capturing information, so we can later ask (specific, known) questions of that information, get answers, and make decisions — all in support of some amount of the automatable portion of a domain.  The overall automation design should determine what information to capture/model (and when and how), what questions to ask & when, what decisions to make & when.

Erik Eidt
  • 33,282
  • 5
  • 57
  • 91
  • 19
    A person can even be both a student and teacher at the same time. You can teach class A while taking class B. – Barmar Feb 15 '22 at 15:53
  • 1
    @Barmar, yeah, when I said "if a teacher takes a course becoming a student", I left it open as to whether that person remained a teacher or not, both are possible, but there's no necessary reason to quit teaching simultaneously with taking a course. – Erik Eidt Feb 15 '22 at 16:29
  • 1
    Silly me, I scanned your answer quickly and didn't even see that. – Barmar Feb 15 '22 at 16:41
  • 25
    +1 for the statements about modelling with a purpose and not just in a vain effort to reproduce the real world. This is so often overlooked when learning OOP – Alex Feb 15 '22 at 17:47
  • 5
    Note that this is only a problem if your application needs to handle these scenarios. If you can confidently say "when this program is used as intended, a Person object can be expected to always have only the Person role or only the Teacher role and never both or neither", then having an additional Role/Teacher/Student class hierarchy in addition to the Person class would be a case of over-engineering or "unnecessary abstraction". – M-Pixel Feb 15 '22 at 20:25
  • And Teacher and Student already have multiple courses. One could be a teacher of French and a teacher of Maths. It could make sense to have two Teacher-of-Something instances. Without attributes (like course) a "Teacher" is just a role. – Joop Eggen Feb 15 '22 at 20:35
  • The formal UML spec premits inheritance of a object to change based on state changes of the object. – Ian Feb 16 '22 at 14:28
  • @Ian, that's interesting, so you're proposing adding a TeacherStudent class, and if there's more than those 2 roles, then exponentially more classes? Plus, switching to UML for the programming? – Erik Eidt Feb 16 '22 at 16:08
  • 1
    @M-Pixel, sure, we can restrict things like that in solo or toy projects, but in a business setting, that is a design decision, best made by stakeholders or leaders rather than programmers. – Erik Eidt Feb 16 '22 at 16:27
  • No, but remember much Object based design teaching was based on UML not how most programming languages do inheritance. Inheritance in a design should but always be implemented by the built in inheritance surport in the programming language. – Ian Feb 16 '22 at 16:42
  • @Ian, Ok, so are you suggesting either class explosion (statically created base class combinations, like TeacherStudent, PrincipalTeacher, ...) that can be switched between, or else dynamically applying multiple inheritance? This doesn't sound more workable than composition with roles. – Erik Eidt Feb 16 '22 at 16:48
9

You may think about it a bit more formally in terms of Liskov substitution principle. https://en.wikipedia.org/wiki/Liskov_substitution_principle

The informal rule that dictates that every property of superclass should also be true of subclass.

For example, a Person some day might be extended with method like respondToDraft, which optionally creates Soldier data structure. Or payTaxes method that returns TaxableEntity.

Here we see our abstraction falls apart completely: is Soldier/Student/Person TaxableEntity? That depends on local laws. Should Student respondToDraft? That depends also.

Depending on complexity of something you model hierarchical is-a relationship is too static/simplistic and should not be applied. It is good idea only in number towers and like, where precise math is involved.

Otherwise, Liskov substitution principle tells us: do not use inheritance.

hamilyon
  • 199
  • 3
  • Drive-by downvoter hit us both :-) What a stupid boy. – gnasher729 Feb 16 '22 at 15:19
  • 1
    I think the Liskov Substitution Principle is a good "theoretical" answer to this. Another way I've heard it phrased is, models of things don't have the same relationships with each other as real things do. Another example: a square is a rectangle, but a programming model of a square doesn't behave like the model of a rectangle. A rectangle has length and width that can be adjusted independently, but it doesn't make sense for a square to treat them as separate; they must always be equal. It has only one side-length. – Corrodias Feb 16 '22 at 19:01
  • 1
    @Corrodias Note that issue only arises with the requirement that the rectangle be mutable. An ImmutableSquare can inherit from an ImmutableRectangle without issue. – eclipz905 Feb 17 '22 at 14:41
5

I'll give you a very simple example where you run into trouble. You have classes Person, Teacher::Person, and Student::Person. You assume that the same person cannot be both a Teacher and a Student. This is definitely wrong if you go to a university: Your maths professor could be a music student (if he is interested in the subject).

But here's a case where it's obvious: A FirstAider is also a person. And quite obviously, both teachers and students can be first aiders. So what do you do know: If Teacher, Student, and FirstAider are subclasses of Person, then a Person object for the same person will actually appear twice. Now you add a class Employee::Person. Teachers are usually employees. Students are usually not employees, but there may be one who makes a bit of extra money helping out in the kitchen, or in IT.

So suddenly you are in trouble. You asked "And also, what are those "very painful problems" that can appear?". With some experience, it may be difficult to pick out exactly what problems can appear, but you will know that the subclassing design is just "asking for trouble".

You could also read up on Edgar F. Code, 1981 Turing Award winner for his work on database normalisation - which covers the exact same principles, just 40 years earlier and from a very different angle.

gnasher729
  • 42,090
  • 4
  • 59
  • 119
  • Well, an ugly solution for an ugly problem: Your examples are the prime motivation for the introduction of virtual inheritance in C++. Behind all these facades is always just a single person. In fact, this may be the best rationale for virtual inheritance I've read in a while (I didn't understand Stroustrup's reasoning, for example). So you *could* "compose by (multiple) inheritance" ;-). – Peter - Reinstate Monica Feb 16 '22 at 16:39
  • Many TAs and some teachers are Phd students. And what about Student-teachers? We don't need to look vey far for real examples of this. – JimmyJames Feb 16 '22 at 18:18
  • @Peter Virtual inheritance doesn't solve this. If you have two objects inheriting the same person, you have two instances of the same person, whether virtual or non-virtual inheritance. What you need is that Teacher, Student etc. hold a reference to the same Person object. – gnasher729 Feb 16 '22 at 19:24
  • @gnasher Hm, true, I had in mind to have a class hierarchy student:virtual public person and a teacher:virtual public person and then create a teaching_student:teacher,student which would have only one person subobject. But composition is much better because it could change roles dynamically. – Peter - Reinstate Monica Feb 16 '22 at 20:50
  • 1
    The truth, the whole truth and nothing but the truth, so help me **Codd** – mcalex Feb 17 '22 at 09:09
4

The main part of not using it "to model domain (real-world) relationships" is the real-world part. Way, way back it seemed obvious that, for example, animals have a tree-like inheritance structure which we should use in our programs: create sub-classes Reptile, Bird and Mammal, with sub-sub-classes Ursine, Canine and so on. Now Animals hierarchy is done -- every program can use it. Another example: motorcycles are legally considered a type of motor vehicle, classed with cars, and bikes aren't. That doesn't mean your program needs both to inherit from a MotorVehicle class.

The advice is worded in a negative way ("don't think of the real-world rules") since doing that was so common. Intro inheritance examples back then just designed inheritance trees all alone, without a problem for them to solve. They really would say stuff like "well, clearly airplanes are divided in prop and jet engines, and those are divided into... ".

Translated, the advice really means to design inheritance based on the particular program you're writing. The same things might have a different inheritance structure based on the problem. Animals in a game are probably classed as Ambient (just moving decorations, like bunnies) and Fightable; but so would machines and plants -- so you wouldn't even want an Animal class.

A fun rule is to consider whether you'll need a list which can hold Students and Teachers together. If so, they need a common base class. Or, this is the same thing, whether you'd want a function which can take either a Student or a Teacher as input.

3

Lots of good answers already but I thought of a good practical example of using inheritance that I think can help.

In a lot of OO languages, there's a concept of 'equals' for an Object that differs from the object identity. It's important that this 'equals' relationship is symmetrical. It's also important that it be transitive i.e.: if A equals B and A equals C, B must equal C. When you introduce inheritance, that constraint becomes difficult to maintain without some planning up front.

The classic example of this is Shape<-ColoredShape where the latter is a subclass of the former. Take a Circle and a ColoredCircle. Let's say that two Circles are considered 'equal' if they have the same radius. Two ColoredCircles are equal if they have the same radius and color. Now what happens if we compare a Circle with a ColoredCircle? Should a ColoredCircle and a Circle be considered equal if they have the same radius? If you say yes, then it breaks transitivity e.g. a blue circle and a red circle of radius 1 are not equal but they are both equal to a regular circle of radius 1. If you say no, then you have a situation where a ColoredCircle isn't a proper Circle (LSP: see answer by @hamilyon). If you think about this around your Person-Student example, you should see it also applies there.

There's actually a simple answer in a lot of languages. You make equals final on the base class. Then no one can create a specialized version and break things. I tend to feel this is the right answer but it also means that your subclasses need to fit in a pre-defined box. Where this ultimately leads you is to the idea that polymorphism is really mainly useful for varying behaviors. It's not a great way to expand and decorate your classes with additional properties and behavior.

JimmyJames
  • 24,682
  • 2
  • 50
  • 92
1

I don't agree that a base class will cause "very painful problems," or with the idea that "a student could become a teacher," or "a teacher might also take classes" are serious problems. I think that, while you could use composition, it is not the case that you must use composition so that the single Person object that represents an individual can be turned from a Student into a Teacher. A Person who is both a Student and a Teacher can be adequately represented by two objects so long as there isn't any significant shared behavior. Indeed, in a lot of software, the objects are quite short-lived, existing for only the length of a single request, before their data is persisted to a database.

If they only share a few trivial properties, there could simply be two objects that correspond to one human individual: one Student object that represents their role as a Student (with their grades), and another Teacher object that represents their role as a Teacher (with what they teach). It isn't a rule that one being in the real world must be represented over its entire lifetime by a single object in the domain. And should a student become a teacher? Simply create a new object.

The problem as I see it is actually quite different. You have a base class, Person, that has a small amount of shared data, and it serves no purpose other than to prevent a tiny amount of code duplication between Student and Teacher. Inheritance should never be used just for that. It should only be used when there is some common purpose, some shared behavior that needs to be specialized by the subclasses. One can imagine any number of things the program that contains these types might do with a Student. Or with a Teacher.

It might add classes to a student, or calculate their GPA. It might assign students to a teacher. But what would the program do with a raw Person?

Would there ever be a reason to have a method that takes a Person, but doesn't know if it's a Student or a Teacher? Or a list of Person objects? If so, it would make sense to either have a common base class or a class that they each have-an instance of to handle that shared purpose.

But if what you're modeling about Students and Teachers is completely disparate, if you would never perform any operations on a plain Person without regard to which one it is, then the base class serves no real purpose. It's just getting in the way.

I don't foresee "very painful problems." I would just say that class Person isn't pulling its weight and so you should just get rid of it.

The tougher question is, how do you know if the classes are really related in your domain? I mean, we all agree that teachers and students are related in some way. They're both human beings. I would say they are related in the domain if:

  • They have some common behavior
  • That behavior needs to be specialized in each case
  • The program can meaningfully act on the common base type

What I mean by "meaningfully act on", you can conceive of a good reason to have a method that deals with just Persons, regardless of whether they are Students or Teachers.

David Conrad
  • 814
  • 8
  • 9
  • "Would there ever be a reason to have a method that takes a Person, but doesn't know if it's a Student or a Teacher?" - Sure: the Person's name changes. "how do you know if the classes are really related in your domain?" - Okay, and if we determine that they really are related, what then? – 8bittree Feb 18 '22 at 15:56
  • @8bittree If they really are, then I think it's okay for them to have a common base class. But if they aren't, and the person's name changes, you can just change it on the Student, or on the Teacher, as appropriate. – David Conrad Feb 18 '22 at 18:59
  • So, embrace the very painful problems of a common base class, and/or enjoy de-synchronization issues? – 8bittree Feb 18 '22 at 19:43
  • @8bittree As I said, I don't think there actually are any very painful problems, just a class that isn't pulling its weight, and obviously it is a matter of opinion whether it is pulling its weight. As for the synchronization issues, if the roles are treated as distinct, it wouldn't be an issue. – David Conrad Feb 18 '22 at 20:15
  • "As for the synchronization issues, if the roles are treated as distinct, it wouldn't be an issue." Bob the Student and Teacher fills out and submits the paperwork to change his address. A week later he's received his report card and is wondering where his paycheck is because his address was updated in his Student object, but not in his Teacher object, resulting in his paycheck going to his old address. Also, he keeps getting double notifications every time the school closes for bad weather, because his phone number is in both objects. – 8bittree Feb 18 '22 at 21:28
  • "...it serves no purpose other than to prevent a tiny amount of code duplication between Student and Teacher. Inheritance should never be used just for that." - I agree, that's a poor use of inheritances. But why not use composition? Why instead jump straight to duplicate all the common data? – 8bittree Feb 18 '22 at 21:31
  • Also, you say you don't think there are any very painful problems with using inheritance here, and then go on to say that one shouldn't use inheritance. That seems contradictory. – 8bittree Feb 18 '22 at 21:40
  • @8bittree I didn't say one shouldn't use inheritance; I said one shouldn't use inheritance for trivial code sharing when there's no real shared behavior. Of course you could use composition here. I was saying composition wasn't *needed* "so that the single Person object that represents an individual could transform from a Student to a Teacher" i.e. the Strategy Pattern that some other answers were advising. – David Conrad Feb 18 '22 at 21:58
  • Let us [continue this discussion in chat](https://chat.stackexchange.com/rooms/134258/discussion-between-8bittree-and-david-conrad). – 8bittree Feb 18 '22 at 22:07
0

Protip: don't use class inheritance to model domain (real-world) relationships like people/humans, because eventually you'll run into very painful problems.

The 'domain' is 'the area of the problem you're trying to solve'. Every bit of code relates to some problem domain, and some aspect of the 'real world'. So this protip effectively says: 'don't use class inheritance'.

...And many pros would agree! Personally I think the main reason for that is that inheritance relationships can often make code hard to refactor/change, while those based on (for example) composition may be easier to refactor/change. Many of the objections in answers to this question are of the form 'what will you do if 'X' happens?'.

However, depending on the needs of your application now, it may be that there's nothing fundamentally wrong with the inheritance hierarchy you've created when it comes to serving your current purposes. It's just that if the problem you're trying to solve changes, you might find that the inheritance hierarchy you've created can't be made to fit that new problem, and you'll have to tear things down and start again.