The Flaws of Inheritance
Summary
TLDRThe video script discusses the concept of 'prefer composition over inheritance' in programming. It explains that inheritance, while allowing code reuse and abstraction, can lead to tightly coupled code and difficulties in refactoring. Composition, on the other hand, promotes flexibility and easier maintenance by allowing classes to be assembled from other classes without inheritance. The script illustrates this with an example of image classes, showing how composition can reduce complexity and improve adaptability to change. It also touches on the use of interfaces and dependency injection as alternatives to inheritance for achieving abstraction and modularity.
Takeaways
- ð Composition and inheritance both aim to reuse code, but they approach it differently.
- ð Inheritance involves creating a subclass that extends the functionality of a parent class.
- ð Inheritance can lead to coupling with the parent class, making changes more difficult.
- ð Composition allows for a more flexible code structure by using objects together without inheritance.
- ð Abstract classes and methods are used in inheritance to define common functionality across subclasses.
- ð« Inheritance can be problematic when you need to add new functionality that doesn't fit the parent class structure.
- ð§ To address inheritance issues, you might need to refactor the codebase, which can be costly.
- ð Composition promotes a more modular design, making it easier to adapt to changes.
- ð Interfaces can be used in composition to define a contract for behavior without inheritance.
- ð Dependency injection is a technique where dependencies are passed in, allowing for more flexible and testable code.
- ð Boilerplate code and wrapper methods can be drawbacks of composition, but they reduce coupling between objects.
Q & A
What is the main advice given in the script regarding code reuse?
-The main advice is to prefer composition over inheritance when trying to reuse code.
What are the two approaches to code reuse discussed in the script?
-The two approaches discussed are inheritance and composition.
How does inheritance work in the context of the script?
-Inheritance works by creating a subclass that extends the functionality of an existing class, allowing for code reuse through the subclass.
What is the downside of using inheritance as mentioned in the script?
-The downside of inheritance is that it tightly couples the child class to the parent class, making it difficult to change or refactor the code without affecting other parts of the system.
What is composition and how does it differ from inheritance?
-Composition is a pattern where classes reuse code by using instances of other classes without inheriting from them. It differs from inheritance in that it allows for more flexibility and less coupling between classes.
The script illustrates composition by removing the abstract methods from the Image class and having the JPEG, PNG, and bitmap classes use instances of the Image class directly, without inheritance.
-null
What is an interface and how is it used in the script?
-An interface is a contract that defines the methods an object can perform without specifying their implementation. In the script, an interface called ImageFile is used to define the load and save operations, allowing for abstraction without inheritance.
What is dependency injection and how is it demonstrated in the script?
-Dependency injection is a technique where an object receives its dependencies from external sources rather than creating them itself. In the script, it is demonstrated by passing an interface for the image file to the class, allowing the class to focus on UI commands and leaving the file handling to another part of the system.
Why might inheritance still be useful in certain situations?
-Inheritance might be useful when working within an existing system with highly repetitive code, where creating a new class hierarchy would be too costly or time-consuming.
What are the potential drawbacks of using composition?
-The potential drawbacks of using composition include increased boilerplate code, repeated code in many implementations, and the need to create wrapper methods to expose information from reused code.
What advice is given for designing classes that will be inherited?
-The advice given is to avoid protected variables with direct access, create an explicit protected API for overriding, and keep everything else private, final, or sealed to prevent bugs when changing parent classes.
Outlines
ð Understanding Composition vs Inheritance
This paragraph discusses the concept of preferring composition over inheritance in coding. It explains that both are ways to reuse code, with inheritance involving a class extending another to reuse functionality. The script uses an example of an Image class and its subclasses for different image formats. It highlights the limitations of inheritance, such as the need to refactor when new requirements arise, and the coupling of the child class to the parent's structure. The paragraph concludes by introducing composition as an alternative, where classes reuse code without inheriting, allowing for more flexibility and easier adaptation to changes.
ð Benefits and Use Cases of Composition
The second paragraph delves into the benefits of composition, illustrating how it allows for the combination of classes for specific use cases without the constraints of inheritance. It explains the role of abstraction in inheritance and how it allows a consumer to interact with a class without knowing its specific implementation. The paragraph introduces the concept of interfaces as a lightweight alternative to parent classes, which define a contract for method availability without the overhead of shared variables. It also touches on dependency injection as a way to decouple classes and focuses on the advantages of composition, such as reduced coupling and easier maintenance, despite the potential for increased boilerplate code. The paragraph ends with advice on best practices when using inheritance, emphasizing the importance of a protected API for overriding and the use of private, final, or sealed elements to prevent unintended changes.
Mindmap
Keywords
ð¡Composition
ð¡Inheritance
ð¡Abstract Methods
ð¡DrawableImage
ð¡FileImage
ð¡Interfaces
ð¡Dependency Injection
ð¡Boilerplate Code
ð¡Refactoring
ð¡Cleanness Database Schema
Highlights
The advice 'prefer composition over inheritance' is explored in detail.
Composition and inheritance both aim to solve the problem of code reuse.
Inheritance involves creating a subclass to extend functionality.
An example of an Image class is used to illustrate inheritance.
Subclasses like JpgImage, PngImage, and BmpImage inherit from Image to reuse methods.
Inheritance's downside is its coupling to the parent class structure.
Inheritance can lead to expensive refactors when changes are needed.
Composition is the pattern of reusing code without inheritance.
The Image class is refactored to be composed of other classes instead of being abstract.
The new DrawableImage class is created using composition, not inheritance.
Composition allows for more flexibility when adding new features like ImageDraw.
Inheritance provides the ability to build abstractions through a contract.
Interfaces are used to define a contract for objects without the baggage of parent classes.
Dependency injection is introduced as a way to pass in interfaces for flexibility.
The speaker prefers composition due to its reduced friction with changes.
Inheritance might be useful in systems with highly repetitive code.
Best practices for inheritance design include avoiding protected variables with direct access.
Transcripts
You may have heard the saying prefer composition over inheritance.
The advice is a little vague, so I'm going to break it down.
What is composition, inheritance and why would you prefer one over
the other?
Both composition and inheritance are trying to solve the same problem.
You have a piece of code that you're trying to reuse.
Inheritance is when you have a class that contains functionality you want to reuse.
So you create a subclass extending its functionality.
If you simply extend a class, you've basically created a copy of the class
with a new name, and then you can inject new methods to extend or override parts.
We have a rudimentary image class here.
It represents an RGB image and stores it as a double array of pixels.
The image class hides how the image is stored in memory
and provides a method for looking up pixel values.
We also have some stuff we can do to the image.
We have a resize method which resizes the image by a scale factor,
and we have methods to flip the image horizontally or vertically.
The library
should support JPEG, PNG and bitmap images, but
we also want to reuse all of these methods for the different types of images.
So to support loading and saving these images,
we add two abstract methods: save() and load(),
and then we create the subclass JpgImage,
PngImage and BmpImage.
These subclasses implement their different versions of load()
and save(),
but also get all of the other methods for free.
So when we load a JPEG image or PNG image
we can call resize on it and then we save it.
The resize method is reused for all of the image types.
But when we call load or save the overriden version is called instead.
This works well,
but now we want to create a version of an image
that doesn't come from a file at all, but instead has some methods
that allow the user to draw on the image.
So we create a DrawableImage class and inherit from our parent Image class.
But this
is where inheritance starts to have issues.
The downsides of inheritance is that you've couple yourself to the parent
class.
The structure of the parent is thrust upon the child.
We're forced to implement these to load and save methods
in order to reuse our resize and flip code,
even though they don't make sense for our subclass.
All we can do is have these two methods throw an exception.
To prevent this, we need to remove this method from our parent class
and add a new parent class
in between called FileImage that contains these two methods.
But this also breaks
anyone who currently expects the Image class to contain those methods.
When new changes like this come,
we're forced to edit all our classes, a very expensive refactor.
This is the
greatest downfall of inheritance.
I find it similar to how the ideal cleanness database
schema often causes problems when you scale.
We've moved to NoSql databases with tons of dirty duplication.
Inheritance breaks down when you need to change the code.
Change is the enemy of perfect design and you often paint yourself
into a corner early on with your inheritance design.
This is because inheritance naturally asks you
to bundle all common elements into a parent class.
But the soon as you find an exception to the commonality, it requires big changes.
So our alternative is to use composition.
So what is composition?
You've already been doing it.
Composition is the pattern you're doing whenever you reuse code
without inheritance.
If we have two classes and they want to reuse code, they simply use the code.
Let's
change our image classes to be composed instead.
First, we're going to remove our abstract methods from image.
Now, this is no longer an abstract class.
It's simply a class that represents an image in memory.
In our JPEG, PNG and bitmap classes.
We no longer inherit image,
but we'll keep our save and load methods.
They'll just now be stand alone, not overriding anything.
The methods were accessing a bunch of stuff from the parent class.
So what do we do about those?
Well, instead of accessing them through âthisâ weâll
simply pass in the image in question instead.
So now image represents an image
and these other classes cleanly represent a specific file
format.
Now, if our new drawing requirement comes in,
we create an image draw class that takes an image to draw to.
And the methods do their thing.
We're no longer bundled to the file related stuff.
Because we didn't force all the common elements into a parent class
we don't need to alter any of the other classes to add our ImageDraw class.
Now the user no longer chooses the one class that suits their needs.
They also combine classes together for their particular use case.
So here we're loading a JPEG image drawing to it and then saving it.
Another app could do something different, like load a bitmap
image, flip it, resize it, and then save it out as a PNG.
Inheritance is interesting because it actually combines
two capabilities: the ability to reuse code,
but also the ability to build an abstraction.
Creating abstractions allow a piece of code to reuse
another piece of code, but also not know which piece of code it's using.
You define a contract that both sides of the abstraction agree to.
This gives the code
the rough shape of the other code, but it doesn't know exactly what it is.
Inheritance does this by allowing a consumer to think it's taking a class,
but it's actually given a subclass instead.
Then the code can operate like it always does.
Even if the system as a whole is doing something very different.
If we go back to when our image code used inheritance,
our application used the natural abstraction capability of inheritance
by storing references to the parent class.
When our app opens a file,
we just figure out
which subclass to create and then store a reference to it through the parent class.
Then when the user clicks the save button,
our save clicked method will get invoked and we'll just call the save method
and we're abstracted from whether it's the JPEG, ping or bitmap.
But with composition you don't have parent classes.
You're just using the types you want.
Inheritance allows you to abstract because the methods of the parent class
forms a contract.
A contract that says that every child class shall have at least these methods.
So for our new classes without inheritance, we still want to be
able to call our save and load methods without caring about which class it is.
This is where interfaces come in.
Instead of a full parent class with all its variables and methods,
an interface simply describes the contract of what an object can do.
In this case, we'll create an interface called Image File,
which represents the operations an image file can do:
load and save.
Now, like
before, we save a reference to one of our implementations.
But now, through the interface
and when the user click save,
we call save on whatever type was created.
Interfaces are a much more lightweight
way to do this because our interfaces are minimal.
Parent classes share everything by default, making them more difficult
to change.
But interfaces define only the critical parts of the contract
and are easily tacked on to existing classes.
Now that we have a nice abstraction for loading and saving files in our app,
we can actually lift the creation of which image file out of our image app class.
Weâll simply ask the user of the class to pass in the interface instead.
That way this class can just focus on dealing
with the UI commands and the file class can be elsewhere.
There's a name for what we just did there.
Dependency injection.
I'll do a whole video on dependency injection,
but if you've heard the term before and wondered what it was, that's it.
Passing in an interface for what you're going to use.
I won't say that inheritance is as evil as some would say,
but I will say that I almost never use it in my code.
Composition isn't perfect.
You do end up with a lot of boilerplate
needing to initialize all of your internal types.
Many implementations will contain the same code repeated,
and when you need expose information from reused code, you often need to create
a lot of wrapper methods where you simply return a call to an inner type.
But ultimately, composition reduces the surface area
between objects, which gives you less friction as changes come in.
Inheritance might be useful if you're working inside of an existing system
that had highly repetitive code where you only needed to modify one thing.
For example, if you had the need for 100 classes
to conform to some specific interface
which half of them need the same boilerplate over and over again.
You might say that that means that each class has too much responsibility,
and you'd be right.
But changing the plugin model would cost the team months of work.
You might not want to put your effort into that just yet.
If you do use inheritance design the class to be inherited, I'd avoid
protected variables with direct access, like we avoid making our variables public.
For overriding: create an explicit protected
API that you're supposed to override an access mark.
Everything else is private, final or sealed.
This prevents bugs when changing your parent classes
because of not understanding what your child classes have done.
5.0 / 5 (0 votes)