top | item 42787080

(no title)

bedobi | 1 year ago

Do yourself a favor and wear yourself off all this SOLID, Uncle Bob, Object Oriented, Clean Code crap.

Don't ever use inheritance. Instead of things inheriting from other things, flip the relationship and make things HAVE other things. This is called composition and it has all the positives of inheritance but none of the negatives.

Example: imagine you have a school system where there are student users and there are employee users, and some features like grading that should only be available for employees.

Instead of making Student and Employee inherit from User, just have a User class/record/object/whatever you want to call it that constitutes the account

    data class User (id: Int, name: String, email: String)
and for those Users who are students, create a Student that points to the user

    data class Student (userId: Id, blabla student specific attributes)
and vice versa for the Employees

    data class Employee (userId: Id, blabla employee specific attributes)
then, your types can simply and strongly prevent Students from being sent into functions that are supposed to operate on Employees etc etc (but for those cases where you really want functions that operate on Users, just send in each of their Users! nothing's preventing you from that flexibility if that's what you want)

and for those users who really are both (after all, students can graduate and become employees, and employees can enroll to study), THE SAME USER can be BOTH a Student and an Employee! (this is one of the biggest footguns with inheritance: in the inheritance world, a Student can never be an Employee, even though that's just an accident of using inheritance and in the real world there's actually nothing that calls for that kind of artificial, hard segregation)

Once you see it you can't unsee it. The emperor has no clothes. Type-wise functional programmers have had solutions for all these made up problems for decades. That's why these days even sane Object Oriented language designers like Josh Bloch, Brian Goetz, the Kotlin devs etc are taking their languages in that direction.

discuss

order

saidinesh5|1 year ago

Never using inheritance is pushing the dogma to the other extreme imo. The whole prefer composition over inheritance is meant to help people avoid the typical OO over use of inheritance. It doesn't mean Is-A relation doesn't/shouldn't exist. It just means when there is data, prefer using composition to give access to that data.

There will be times when you want to represent Is-A relationship - especially when you want to guarantee a specific interface/set of functions your object will have - irrespective of the under the hood details.

  Notifier n = GetNotifier(backend_name);
Here what you care about is notifier providing a set of functions (sendNotification, updateNotification, removeNotification) - irrespective of what the implementation details are - whether you're using desktop notifications or SMS notifications.

bedobi|1 year ago

I see no need for inheritance there, that can and should be done using interfaces

eg in the contrived example I gave, any of all three of the User, Student and Employee can implement the interface (and if needed Student could simply delegate to its internal User while Employee could "override" by providing its own, different implementation)

cryptonector|1 year ago

If you have a sufficiently statically typed language then the is-a concern goes away -- certainly in the example you gave, since the compiler/linker knows to look for a `GetNotifier()` that returns a `Notifier`. Now, you might still want to know whether the notifier you got satisfies other traits than just those of `Notifier`, but you do that by using a type that has those traits rather than `Notifier`, and now you have little need for `instanceof` or similar operators. (You still might want to know what kind of thing you got because you might care about semantics that are not expressed in the interface. For example you might care to know whether a logger is "fast" as in local or "slow" as in remote, as that might cause you to log less verbosely to avoid drops or blocking or whatever. But the need for this `instanceof` goes down dramatically if you have interfaces and traits.)

foobarkey|1 year ago

Never is a good starting point as a guide but of course there are cases where is-a makes sense too.

Composition generally is not good enough to model all cases without resulting to some AOP or meta programming also, but in that case the is-a or a base class would arguably be the simpler approach, at least it can be reasoned about and debugged, as opposed to some AOP/Meta spaghetti that can only probably be understood from docs

caspper69|1 year ago

Just because you see OO languages starting to favor composition over inheritance does not mean inheritance has no place, and indeed, interfaces as a form of composition have existed in many bog-standard OO languages for decades.

Your example dosn't compute, at least in most languages, because derived objects would not have the same shape as one another, only the shape of the base class. I.e. functions expecting a User object would of course accept either an Employee or a Student (both subclasses of a User), but functions expecting a Student object or an Employee object would not accept the other object type just because they share a base class. Indeed, that's the whole point. And as another poster mentioned, you are introducing a burden by now having no way to determine whether a User is an Employee or a Student without having to pass additional information.

Listen, I'll be the first to admit that the oo paradigm went overboard with inheritance and object classification to the n-th degree by inventing ridiculous object hierarchies etc, but inheritance (even multiple inheritance) has a place- not just when reasoning about and organizing code, but for programmer ergonomics. And with the trend for composition to disallow data members (like traits in Rust), it can seriously limit the expressiveness of code.

Sometimes inheritance is better, and if used properly, there's nothing wrong with that. The alternative is that you wind up implementing the same 5-10 interfaces repeatedly for every different object you create.

It should never be all or nothing. Inheritance has its place. Composition has its place.

And if you squint just right they're two sides of the same coin. "Is A" vs "Can Do" or "Has".

bedobi|1 year ago

> The alternative is that you wind up implementing the same 5-10 interfaces repeatedly for every different object you create.

if both Student and Employee need to implement those interfaces, it's probably User that should have and implement them, not Student and Employee (and if they truly do need to have an implement them, they can simply delegate to their internal User = "no override" or provide a unique implementation = "override") (let alone that unless I'm misremembering in Kotlin interfaces can have default implementations)

sunshowers|1 year ago

Inheritance has no place in production codebases—unless there is strict discipline, enforced by tooling, ensuring calls only go in one direction. This Liskov stuff has zero bearing.

titzer|1 year ago

> Don't ever use inheritance. Instead of things inheriting from other things, flip the relationship and make things HAVE other things. This is called composition and it has all the positives of inheritance but none of the negatives.

Bah. There are completely legitimate uses of inheritance where it's a really great fit. I think you'll find that being dogmatic about avoiding a programming pattern will eventually get you twisted up in other ways.

Inheritance can be used in a couple of ways that achieve a very-specific kind of code reuse. While I went through the early 2000's Java hype cycle with interfaces and factories and builders and double-dispatch and visitors everywhere, I went through a period where I hated most of that crap and swore to never use the visitor pattern again.

But hey, within the past two years I found an unbeatable use case where the visitor pattern absolutely rocks (it's there: https://github.com/titzer/wizard-engine/blob/master/src/util...). If you can come up with another way by which you can deal with 550 different kinds of animals (the Wasm instructions) and inherit the logic for 545 and just override the logic for 5 of them, then be my guest. (And yes, you can use ADTs and pattern-matching, which I do, liberally--but the specifics of how immediates and their types are encoded and decoded just simply cannot be replicated with as little code as the visitor pattern).

So don't completely swear off inheritance. It's like saying you'll never use a butter knife because you only do pocket knives. After all, butter knives are dull and not good for anything but butter.

vjerancrnjak|1 year ago

If you can use functions in the same way objects are used, there’s no need for visitor objects.

There’s a reason why everything is a Lisp. All of the patterns are obvious with its primitives, while higher level primitives like classes, interfaces hide that there’s data and there’s behavior/effects.

bedobi|1 year ago

I happily admit it's more than possible to come up with examples that make inheritance shine. After all, that's what the authors of these books and articles do.

But most of them put the cart before the horse (deliberately design a "problem" that inheritance "solves") and don't seriously evaluate pros and cons or even consider alternatives.

Even then, some of the examples might be legitimate, and what you're referring to might be a case of one. (though I doubt there's no equally elegant and succinct way to do it without inheritance)

But none of that changes the fact that inheritance absolutely shouldn't be the default goto solution for modeling any domain it has become (and we are taught to understand it as)

or that it's exceedingly uncommon to come across situations like yours where you have 500+ shared cases of behavior and you only want to "override" 5

or that inheritance is overwhelmingly used NOT for such niche edge cases but as a default tool to model even the most trivial relationships, with zero justification or consideration

incrudible|1 year ago

I will agree that the problem that class hierarchies attempt to solve is a problem one usually does not really have, but in your example you have not solved it at all.

It matches a relational database well, but once you have a just a user reference, you can not narrow it down to an employee without out of band information. If a user should be just one type that can be multiple things at once, you can give them a set of roles.

bedobi|1 year ago

I agree that there are better ways to model roles and FGA

the point of the example wasn't to be an idiomatic solution to that problem, but to illustrate the pointlessness of inheritance in general, and User Student Employee was the first thing that came to mind that was more "real" than the usual Animal examples

in any case, as for only having a User reference, you don't ever have only a User reference - overwhelmingly you would usually load a Student or Employee,

and each of them would have those attributes on them that characterize each of them respectively (and those attributes would not be shared - if they were, such shared attributes would go on the User object)

only those functions that truly are operating only on User would you send in each of their User objects

the problem with what you're advocating is you end up with functions like this

    fun doSomething(user: User) { if (user is Student) { do this } else { do that } }
which is insane

mariodiana|1 year ago

It's sad that late-binding languages like Objective-C never got the love they should have, and instead people have favored strongly (or stronger) typed languages. In Objective-C, you could have your User class take a delegate. The delegate could either handle messages for students or employees. And you could code the base User object to ignore anything that couldn't be handled by its particular delegate.

This is a very flexible way of doing things. Sure, you'll have people complain that this is "slow." But it's only slow in computer standards. By human standards—meaning the person sitting at a desktop or phone UI—it's fast enough that they'll never notice.

incrudible|1 year ago

I will complain that it is hard to reason about. You better have a really good reason to do something like this, like third party extensibility as an absolute requirement. It should not be your go to approach for everything.

tsimionescu|1 year ago

Inheritance is nothing more or less than composition + implementing the same interface + saving a bit of boilerplate.

When class A inherits from class B, that is equivalent to class A containing an object of class B, and recoding the same interface as class B, and automatically generating method stubs for every method of the interface that call that same method on your B.

That is, these two are perfectly equivalent:

  class B {
    public void foo() {
    } 
  } 

  class A_inheritance extends B {
  }

  class A_composition implements B {
    private B super;

    public void foo() {
      super.foo();
  }
This is pseudo-code because Java distinguishes between Interfaces and Classes, and doesn't have a way to refer to the Interface of a Class. In C++, which doesn't make this distinction, it's even more equivalent. Especially since C++ allows multiple inheritance, so it can even model a class composing multiple objects.

The problem with inheritance is that people get taught to use it for modeling purposes, instead of using it for what it actually does - when you need polymorphism with virtual dispatch, which isn't all that common in practice. That is, inheritance should only be used if you need to have somewhere in you code base a collection of different objects that get called in the same way but have to execute different code, and you can only find out at runtime which is which.

For your students and employees and users example, the only reason to make Student and Employee inherit from User would be if there is a reason to have a collection of Users that you need to interact with, and you expect that different things will happen if a User is an Employee than if that User is a Student. This also implies that a Student can't be an Employee and vice versa (but you could have a third type, StudentEmployee, which may need to do something different from either a Student or an Employee). This is pretty unlikely for this scenario, so inheritance is unlikely to be a good idea.

Note also that deep class hierarchies are also extremely unlikely to be a good idea by this standard: if classes A and B derive from C, that doesn't mean class D can derive from A - that is only useful if you have a collection of As that can be either A or D, and you actually need them to do different things. If you simply need a third type of behavior for Cs, D should be a subclass of C instead.

I'd also note that my definition applies for inheritance in the general case - whether the parent is an interface-only class or a concrete class with fields and methods and everything. I don't thing the distinction between "interface inheritance" and "implementation inheritance" is meaningful.

nycticorax|1 year ago

This is not quite correct, at least not in most commonly-used languages. The difference comes when a "superclass" method calls self.whatever(), and the whatever() is implemented in both the "superclass" and the "subclass". In the implementation-inheritance-based version, self.whatever() will call the subclass method. In the composition-based version, self.whatever() will call the superclass method. The implementation-inheritance-based version is sometimes called "open recursion". This is why you need implementation inheritance for the GoF Template pattern. See for instance this paper:

https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&d...

bedobi|1 year ago

for one, see the reply from the handle cryptonector further up

> Inheriting from a class creates dispatch problems, and there's instance variables/fields/whatever to deal with. Never mind multiple inheritance, as that gets hairy real fast. With interfaces there is no hierarchy, and all there is is a type and a data, and you either pass them around together, you monomorphize to avoid the need to pass two pointers instead of one, or you have the data point to its type in a generic way. With class hierarchies you have to have operations to cast data up and down the hierarchy, meaning trade one dispatch table for another.

I agree with all of this. With interfaces, you get all the benefits, but none of the downsides.

axilmar|1 year ago

Amen.

Reading the comments from top to bottom, the above is exactly what I wanted to write.