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

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
  • 34,819
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
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
  • 49,096
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 supports Canada
  • 30,578
  • 3
  • 59
  • 108
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
  • 864
  • 8
  • 9
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.