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?

15
  • 1
    Does this answer your question? Why should I prefer composition over inheritance?
    – gnat
    Commented Feb 14, 2022 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 :) Commented Feb 14, 2022 at 15:34
  • 58
    What happens when a student graduates and becomes a teacher? What about a student who teaches a seminar? Commented Feb 14, 2022 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, a class named Person is not a person.
    – Caleth
    Commented Feb 14, 2022 at 16:04
  • 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. Commented Feb 15, 2022 at 0:33

7 Answers 7

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.

11
  • 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
    Commented Feb 15, 2022 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
    Commented Feb 15, 2022 at 16:29
  • 1
    Silly me, I scanned your answer quickly and didn't even see that.
    – Barmar
    Commented Feb 15, 2022 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
    Commented Feb 15, 2022 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
    Commented Feb 15, 2022 at 20:25
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.

3
  • Drive-by downvoter hit us both :-) What a stupid boy.
    – gnasher729
    Commented Feb 16, 2022 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
    Commented Feb 16, 2022 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
    Commented Feb 17, 2022 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.

5
  • 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" ;-). Commented Feb 16, 2022 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
    Commented Feb 16, 2022 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
    Commented Feb 16, 2022 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. Commented Feb 16, 2022 at 20:50
  • 1
    The truth, the whole truth and nothing but the truth, so help me Codd
    – mcalex
    Commented Feb 17, 2022 at 9: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.

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.

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
    Commented Feb 18, 2022 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. Commented Feb 18, 2022 at 18:59
  • So, embrace the very painful problems of a common base class, and/or enjoy de-synchronization issues?
    – 8bittree
    Commented Feb 18, 2022 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. Commented Feb 18, 2022 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
    Commented Feb 18, 2022 at 21:28
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.

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