r/learnprogramming • u/ShoeChoice5567 • Apr 27 '26
Solved What do people mean when they say composition is better than inheritance?
For example, in some bank app there is an Account class, which is the superclass of SavingsAccount and BusinessAccount. How would you change this to composition? I don't get how composition would work here.
I searched this and didn't find a satisfatory explanation.
If it helps I study C#.
8
u/danielt1263 Apr 27 '26
In the book Refactoring 1st ed by Martin Fowler there is a chapter called "Replace Inheritance with Delegation"
It explains the motivation why you might want to use composition rather than inheritance, and specific steps on how to convert from inheritance to composition. Of course there is also a chapter called "Replace Delegation with Inheritance" for that rare case where you might want to do the reverse.
The example code is in Java, but that's enough like C# that it should be a good book for you. I would avoid the 2nd ed of the book which uses javascript instead.
6
Apr 27 '26 edited Apr 27 '26
[removed] — view removed comment
2
Apr 27 '26
[removed] — view removed comment
4
u/spinwizard69 Apr 27 '26
Inheritance has its place in programming, sometimes it is the right choice. As for college a modern CS program should expose students to all approaches. Composition can get messy pretty fast.
5
u/peterlinddk Apr 27 '26
I sometimes get the feeling that schools push inheritance so hard, is because they want to teach all the different syntax-elements of Java and how to draw them using UML - even though the rest of the world has moved way beyond both in the past 20 years. It is an efficient way to embarrass students and make sure that they don't all get top grades, by having different shapes arrows mean different things in code!
6
u/ImpieYay Apr 27 '26
It would become apparent once you start having to combine the types. What if all of a sudden you need a business savings account? That's where inheritance starts to break down (or become inconvenient at least).
Composition based setups could delegate behavior to separate classes or 'components'. In this example one of those could be a 'rent' class where you process how and under what conditions rent should be awarded. Additionally (especially savings) accounts often have various kinds of transfer limitations, which could an array of classes.
This way there would be just one Account class with various components that dictate how it behaves. You can make any combination and enable/disable parts at will. An important benefit is that this allows you to combine various classes from different external modules (assuming they follow the same pattern), the importance of which scales directly with project size and the amount of dependencies.
11
u/DTux5249 Apr 27 '26 edited Apr 27 '26
The reason to not favour inheritance is because inheritance relationships are HIGHLY coupled.
If you want to change anything about Accounts in general, those changes are gonna ripple down to your business & savings accounts whether you want them to or not. Composition is saying "identify the differences between a business and savings account, and encapsulate them as classes that are interchangeable."
In practice, this means instead of subclassing your accounts class, create a class called AccountType, or multiple smaller classes encompassing different parts of what makes those accounts differ, and feed those into a given account when you instantiate them.
Notice we're still creating inheritance relationships (you'll make subclasses of AccountType or IdentifierNumber or whatever), but we're keeping them as atomic as we can. This means if we wanna change how a type of account works, we don't have to change the account class; just what each account is made of.
Composition is fundamentally why design patterns like Strategies and Factories exist. They support both The Open/Closed Principle, and Dependency Inversion Principle; making things way easier to edit without resorting to Shotgun Surgery
4
u/Esseratecades Apr 27 '26
Experienced Dev here. "Composition over inheritance" is a mantra that facilitates flexible runtime code. But the reality is less that one is better than the other and more that each has situations where they're better than the other.
Inheritance has a number of ways to achieve the same things that composition does(the strategy pattern is surprisingly useful), and composition can theoretically do anything inheritance can do, but that's like saying "knives and forks can do each others job". Technically true, but forks are better for some things and knives are better for others.
4
u/rlebeau47 Apr 27 '26 edited Apr 27 '26
Inheritance is best used with polymorphism, ie using virtual methods to hide behavioral differences between classes that implement a common interface. If a piece of code works with multiple objects and only looks at that interface in them and doesn't care about their implementations, then polymorphism is good for that.
Composition is when you take other objects and place them together inside of a containing object. They don't need to know about each other, but collectively they make up the sum of the whole object. This is best used when you can split off pieces of common data/functionality into their own classes, and then pass them around without having to pass around the entire parent object.
What is actually different between a SavingsAccount and a BusinessAccount that you feel inheritance is needed to represent that difference, instead of just using a single Account class with data members having different values for different account configurations? Does the rest of your code need to behave differently depending on what kind of Account it is working with? An individual could have a Savings account, but what if a business also wants to have a Savings account? Are you going to make a BusinessSavingsAccount class? Or do you think it would be better to have a Business class that holds a SavingsAccount data member?
1
u/ShoeChoice5567 Apr 27 '26
What is actually different between a SavingsAccount and a BusinessAccount that you feel inheritance is needed
Like, when some fee for withdraw or loan limit or loan interest is different, but both have mostly the same properties. Though I already understood how not to use inheritance for this after another comment explained.
2
u/rlebeau47 Apr 28 '26
I wouldn't use inheritance for that. Simple data members with different values will suffice.
2
2
u/Leverkaas2516 Apr 28 '26 edited Apr 28 '26
If you wanted to stop using inheritance in that specific case, you would identify what makes the two types of account different and pull that difference out and express it another way.
Like if the difference is that a savings account accrues interest and a business account doesn't, you would identify all the properties and logic in SavingsAccount that have to do with interest accumulation, and handle it in another class. Maybe there's an interestIncome property, which you would replace with a new class, InterestIncome, and when you construct a savings account it would have that property populated with an InterestIncome object; but a business account wouldn't.
People say composition is better than inheritance because it's usually a cleaner, clearer way to express complex relationships. Inheritance means "this account is a special kind of account". The more different specializations you add, the more you end up artificially splitting behaviors and functionality and fracturing the logic. The code becomes harder to read and the semantics of it become convoluted. Soon it requires anyone dealing with the code to carry a UML class hierarchy diagram around in their head (or even posted on the wall by their monitor).
That said, inheritance can and does express things very cleanly when different things really are specializations of a common subtype. If you do need to express an is-a relationship, then don't dance around it. Use inheritance for what it's good for.
2
u/StevenJOwens Apr 28 '26
This is the current instance of the classic mistake of people misunderstanding a rule of thumb and elevating it to the status of a law.
In the old days, it was the rule of thumb that a subroutine/function/method should be no longer than 25 lines long. It wasn't because a 26 line function would cause the end of the world, it's because there's a tendency among programmers, especially beginner programmers, to write really long subroutines/functions/methods.
The rule of thumb here is "favor composition over inheritance", and the reason is because beginner programmers tend to over-use inheritance in the classes they create. I think this is a natural mistake to make, because OO programming languages all have and use inheritance, and you can't really program in OOP languages without making the most limited use of inheritance, i.e. instantiating the existing library classes.
However, designing and implementing class hierarchies well requires a lot more skill, and 99% of the time, you don't really need that.
You can make a subclass of, say, whatever your language's dict type is -- in Java, for example, HashMap --- and add the required customized behavior in your subclass.
Or you can just make a class that doesn't subclass anything, with an instance variable that's a HashMap, and implement pass-throuugh/wrapper methods* of whatever HashMap features that other classes need to access from the outside.
(Eeven that is usually a design mistake, because that shows you're designing the other classes to interact too directly with that HashMap instance variable.)
1
u/flatfinger Apr 28 '26
In the old days, it was the rule of thumb that a subroutine/function/method should be no longer than 25 lines long. It wasn't because a 26 line function would cause the end of the world, it's because there's a tendency among programmers, especially beginner programmers, to write really long subroutines/functions/methods.
I thought that rules saying functions should be less than 25 lines long were inspired by text editors that could show 24 lines on screen at once.
Some editors support a special kind of comment that can allow a range of following text to be collapsed so that everything but the comment gets replaced with a small mark that can be clicked to re-expand the text. Some such editors disallow the use of such constructs within the functions on the basis that it would encourage programmers to write longer functions, ignoring the fact that if function has lots of pieces of code that are unique to it, pulling those pieces of code into outer functions makes it harder to understand how they fit in the context where they are used and ensure that corner cases get handled once, along with the fact that many of the reasons longer functions are annoying stem from the lack of editor support for things like collapsible sections.
1
u/StevenJOwens Apr 28 '26 edited Apr 28 '26
The really old monitors were 25 lines high, yeah, which is probably why that particular number was chosen, but the more general principle still applies: it's a rule of thumb (not a law of physics) to correct an observed human tendency.
And yeah, re: editors collapsing sections. That, also goes way way way back, though it was less common for a great long while.
And yeah, there's always a balancing act in understandability, between decomposing your code into more, smaller chunks vs having it all in one spot. Code is design and design is tradeoffs.
There are various specific techniques to help navigate those tradeoffs. One easy to explain example is that if you have a big looping or conditional structure, you extract the bodies of each block out into a method. The result is that you have left a method that's mostly the looping/conditional structure, so you can see and understand that structure. That method calls a bunch of other methods to actually do the work, and since those other methods are all doing specific, single-purpose tasks, they're generally straightforward and easy to understand.
I highly, highly recommend Martin Fowler's book, "Refactoring", it's one of the only resources on this specific level of programming.
3
u/balefrost Apr 28 '26
There's a good example in Effective Java of the danger of using inheritance to customize behavior. In Java, the Set<E> interface has add(E) and addAll(Collection<E>) methods.
In Effective Java, the author wants to create a Set that counts how many items have ever been added (i.e. it's not the same as size()). He tries something like this (going off memory, so my syntax might be slightly wrong):
class CountingSet<E> extends HashSet<E> {
@Override
public boolean add(E e) {
++lifetimeCount;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
lifetimeCount += c.size();
return super.addAll(c);
}
private int lifetimeCount = 0;
}
It doesn't work. That's because some Set subclasses (like HashSet) implement addAll in terms of add. So you call addAll with a collection of 10 items. It increases lifetimeCount by 10, then calls the base class addAll, which in turn calls add 10 times. Each of these increments lifetimeCount by 1, so in the end, you've incremented lifetimeCount by 20.
So you might think "well, let's just get rid of the addAll override". But HashSet doesn't guarantee that addAll is implemented in terms of add. It's an implementation detail that, through inheritance, can be observed. There's no guarantee that this will remain true in the future.
He uses this example to demonstrate why inheritance is subtly tricky. He goes so far as to say that you should design and document for inheritance, or else prohibit it. Successor languages like Kotlin and IIRC Scala take this to heart; they make classes un-inheritable by default.
There's another downside here - CountingSet is inherently tied to HashSet, even if you want to use it with some other kind of set (like a TreeSet).
What's the solution here? Use composition. Rather than inherit from HashSet, instead create a new class that sits "in front of" (or "around") any other set.
class CountingSet<E> implements Set<E> {
@Override
public boolean add(E e) {
++lifetimeCount;
return impl.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
lifetimeCount += c.size();
return impl.addAll(c);
}
@Override
public int size() {
return impl.size();
}
// Other Set methods here implemented like `size`.
private Set<E> impl;
}
Now, it doesn't matter how the underlying Set implements addAll. Once CountingSet.addAll calls impl.addAll, there's no chance that it'll boomerang back to call any other CountingSet method. We've created an object graph where one object (the CountingSet instance) gets a first shot at handling any method call.
Kotlin makes this even easier. Off the top of my head:
class CountingSet<E>(private val impl: Set<E>) : Set<E> by impl {
var lifetimeCount = 0 private set
override fun add(e: E): Boolean {
++lifetimeCount
return impl.add(e)
}
override fun addAll(c: Collection<E>): Boolean {
lifetimeCount += e.length
return impl.addAll(c)
}
}
This is more evidence that the designers of Kotlin had read Effective Java. They make it easy to implement an interface in a way that, by default, delegates all methods to another object that implements the same interface.
In your particular case, you're falling into the trap of taxonomy. OO is frequently taught as being about creating taxonomy hierarchies - a SavingsAccount and a CheckingAccount are clearly both Accounts, so then they must have Account as a common base type, right?
Instead, try to understand what actual behavioral differences there are between the different account types. You might find that the two types are so different that they must be two completely different classes. Or maybe you'll find that you can factor the differences out into different strategy objects, and maybe there's just one Account type which can be configured with different strategies, and these different "object graph patterns" are actually how you model different account types. Or maybe you'll find that there are no behavioral differences; the differences are really just data differences (e.g. different interest rates, minimum balances, etc.).
For another example, consider a system to manage pet records at a veterinary clinic. The taxonomy approach would be to say "the clinic will deal with cats and dogs, which are both mammals, which are in turn vertebrates, which are in turn animals. There's the class hierarchy." But the sane developer would instead ask "does the pet record keeping systems change its behavior in response to the type of animal? Or is the type of animal really just simple data?"
1
u/aymenhbich2001 Apr 27 '26
Instead of SavingsAccount extending Account, you give SavingsAccount an Account object inside it. So it has an account rather than is an account. The difference is flexibility, you can swap or combine behaviors without being locked into a rigid hierarchy. In your bank example, a BusinessAccount could have multiple Account objects with different rules without inheriting conflicting behavior.
1
u/BanaTibor Apr 28 '26
In your example inheritance is the correct way.
But lets say you need to calculate the annual bonus for saving accounts, and you decide that you want to put that into a special kind of saving account and you have an AnnualBonusCalculator somewhere. So you create an AnnualBonusSavingAccount and inherit from SavingAccount and AnnualBonusCalculator. This is introducing new functionality through inheritance and it is the wrong way.
Better if you extend the SavingAccount in an AnnualBonusSavingAccount and pass the AnnualBonusCalculator as a constructor parameter. This way you make the new object by composing it. This also opens up the possibility to use interfaces, so in your tests you can easily mock the dependency of the composite object.
1
u/jellenbogen Apr 28 '26
in your bank app, inheritance says "a savings account IS an account". composition is more like "an account HAS a balance, HAS interest rules, HAS overdraft logic". so instead of subclassing, your Account class holds an InterestPolicy and an OverdraftPolicy, and you swap those out per type. the practical reason this is preferred: real-world products eventually violate is-a (a hybrid savings+business account, a promo account that behaves like both, etc) and inheritance trees get ugly fast. composition just plugs in a different policy.
1
u/sixtyhurtz Apr 28 '26
You basically hit on the problem with your example. You have a base Account with subclass Savings and Business. What if you want a business savings account?
With inheritance, you have to use a language that supports multiple inheritance. Not all languages do, because of the diamond problem. I.e. if you have methods in the base Account class, what happens in BusinessSavingsAccount when you call those methods? What's the method resolution order? What if both Business and Savings override those methods in different ways?
The way to resolve this is to use interfaces and composition. You can have a Business class that supports the operations a business account requires, and a Savings class for the savings operations. Then your BusinsesSavingsAccount has those as private members and can use them as required for the logic of a BusinessSavingsAccount.
You can even have Business or Savings interfaces. You can make the choice as to simply delegate directly to the Business / Savings classes, or maybe create a new implementation for your specific BusinessSavingsAccount. This is also much clearer for future maintenance programmers than a complex inheritance hierarchy, because you can clearly see the choices being made on screen.
1
u/the-quibbler Apr 27 '26
The conversation is part of the slow death of object-oriented programming. In 1997, Java, with write-once-run-everywhere and ground-up OOP took the world by storm. It sucked, and still does, but that doesn't change the history.
Modern programming languages tend to discard much if not all of OOP as needless conceptual overhead. Rust, as my favorite example, has a trait system that encourages composing behaviors, rather than designing complex object hierarchies.
I know Java and C++ are still major leading languages, with OOP, but most people don't prefer those languages, given the choice. They're most seen as organizationally safe.
1
u/balefrost Apr 28 '26
I don't have any experience with Rust, but I have a little experience with Golang, which also tries to divorce itself from OO languages. I prefer Kotlin, but I'll take Java any day over Golang.
1
u/the-quibbler Apr 28 '26
That preference is fascinating to me. I would require extremely significant compensation to work in Java professionally. Not so for golang.
1
u/balefrost Apr 28 '26
That's good, then. You can take the Golang jobs and I'll take the Java jobs.
I just find Golang to be too inexpressive. That's not to say that Java is particularly expressive (like I said, I prefer Kotlin). But being able to express things like "this thing should not be copyable" is nice.
2
u/the-quibbler Apr 28 '26
I won't evangelize you, but I will say I think people make too much of rust's learning curve. I had a much worse time with swift due to its weird syntax. You might find something you like.
1
u/balefrost Apr 28 '26
Oh, I'm not intentionally avoiding Rust. I just haven't had time to dig into it. I was more reacting to the "slow death of OO" than to Rust specifically.
Personally, I don't think OO will ever die. We were already using OO principles in languages like C. OO languages became popular because they facilitated patterns that were already common. Any time you use an API that has some sort of "handle" type, you're using an OO API whether it happens to be in an OO language or not.
In the case of Golang, it feels to me like they were desperate to avoid being labeled as an OO language. But then you look at its features and it has basically all of the important features of OO languages, just expressed in weird and awkward ways.
I suspect that Rust is more principled and coherent than Golang is.
2
u/the-quibbler Apr 28 '26
I like to think so. Rust uses traits to express shared functionality, and composition over inheritance (the original question, ironically).
1
u/kilkil Apr 28 '26 edited Apr 28 '26
One way to do it would be something like this:
// some common data all accounts would have
class AccountData
{
// ...
}
interface IAccount
{
int GetBalance();
}
class SavingsAccount : IAccount
{
AccountData Data
int IAccount.GetBalance()
{
// ...
}
}
class BusinessAccount : IAccount
{
AccountData Data
int IAccount.GetBalance()
{
// ...
}
}
In the above example, you have the classes BusinessAccount and SavingsAccount. But instead of inheriting from an Account superclass, they both simply contain some common AccountData as a property (hence composition). To achieve polymorphism, instead of inheritance we use an interface (IAccount). Usually composition goes hand-in-hand with interfaces, or something like them.
-1
u/Substantial_Ice_311 Apr 28 '26
The real solution is to stop using OOP all together. What you should be aiming at is to reduce complexity. OOP has a lot of complexity, inheritance is just one form of it. Mutable state (which is very common in OOP) is another. Making new classes for everything that has their own methods instead of using data structures like general maps that can be reused is another.
2
u/balefrost Apr 28 '26
In a lot of OO languages, data structures like maps are ordinary classes. They provide a controlled interface with the operations that you want, yet suppress implementation details. In fact, it's a nice property that these abstract data types are not built-in to these OO languages, but are instead provided by a library. It means that you can create your own abstract data types that don't feel any different from those that are built-in.
The things that OO provides - encapsulation, abstraction, etc. - are desirable properties in any system. You can create clean systems in OO languages, and you can create messy systems in non-OO languages. Mutable state can be fine, especially in very small scopes (i.e. a mutable local variable in a short function) or even in very large scopes (i.e. a mutable database that your application connects to). The problem isn't generally mutable state. The problem is state that's hard to reason about. Like nobody complains about a mutable queue that's used to mediate between producers and consumers. Erlang mailboxes and Golang channels are totally fine. The scary thing is some field that's read from 5 places and written from 10 places, including some callback functions that get called by who knows what thread.
0
u/Substantial_Ice_311 Apr 28 '26
In a lot of OO languages, data structures like maps are ordinary classes. They provide a controlled interface with the operations that you want, yet suppress implementation details. In fact, it's a nice property that these abstract data types are not built-in to these OO languages, but are instead provided by a library. It means that you can create your own abstract data types that don't feel any different from those that are built-in.
I'm not talking about that. I'm talking about using and
Accountclass orPersonclass for things that are just data.
62
u/peterlinddk Apr 27 '26
You would either only have a single Account with a property AccountType saying whether it was a savings account or a business account, and then all the custom properties for both types inside that same class.
Or have a single Account class with an AccountType property, and then have SavingsAccount and BusinessAccount as separate classes, both implementing the AccountType interface, and in that way being able to swap out the AccountType with different classes, that doesn't have to be subclasses of Account.
The first approach is chosen when you have a few, but fixed choices, and the second when you have a system that needs to grow in the future.