
The Flaws of Inheritance
The Flaws of Inheritance
Let’s discuss the tradeoffs between Inheritance and Composition
Access to code examples, discord, song names and more at https://www.patreon.com/codeaesthetic
0:00 Introduction
0:25 Inheritance
3:32 Composition
5:22 Abstracting with Inheritance
6:52 Abstracting with Interfaces
8:20 When to use Inheritance
Content
0.5 -> You may have heard the saying prefer
composition over inheritance.
4.8 -> The advice is a little vague,
so I'm going to break it down.
7.7 -> What is composition, inheritance
and why would you prefer one over
11.183 -> the other?
16.816 -> Both composition and inheritance
are trying to solve the same problem.
20.216 -> You have a piece of code that you're
trying to reuse.
24.15 -> Inheritance is when you have a class that
contains functionality you want to reuse.
28.4 -> So you create a subclass
extending its functionality.
31.816 -> If you simply extend a class, you've
basically created a copy of the class
35.633 -> with a new name, and then you can inject
new methods to extend or override parts.
41.45 -> We have a rudimentary image class here.
44.266 -> It represents an RGB image and stores
it as a double array of pixels.
48.6 -> The image class hides
how the image is stored in memory
51.316 -> and provides a method for looking up
pixel values.
54.6 -> We also have some stuff we can do
to the image.
57.5 -> We have a resize method
which resizes the image by a scale factor,
61.4 -> and we have methods to flip the image
horizontally or vertically.
66.45 -> The library
66.983 -> should support JPEG,
PNG and bitmap images, but
70.816 -> we also want to reuse all of these methods
for the different types of images.
75.95 -> So to support loading
and saving these images,
78.466 -> we add two abstract
methods: save() and load(),
82.7 -> and then we create the subclass JpgImage,
86 -> PngImage and BmpImage.
93.766 -> These subclasses implement
their different versions of load()
96.8 -> and save(),
101.116 -> but also get all of the other methods
for free.
104.45 -> So when we load a JPEG image or PNG image
107.183 -> we can call resize on it
and then we save it.
110.516 -> The resize method is reused
for all of the image types.
114.15 -> But when we call load or save
the overriden version is called instead.
118.616 -> This works well,
119.7 -> but now we want to create
a version of an image
121.666 -> that doesn't come from a file at all,
but instead has some methods
124.95 -> that allow the user to draw on the image.
128.15 -> So we create a DrawableImage class
and inherit from our parent Image class.
133.366 -> But this
133.883 -> is where inheritance starts
to have issues.
137 -> The downsides of inheritance is
that you've couple yourself to the parent
140.266 -> class.
141.35 -> The structure of the parent is thrust upon
the child.
144.45 -> We're forced to implement these to load
and save methods
147.316 -> in order to reuse our resize
and flip code,
150.5 -> even though they don't make sense
for our subclass.
153.2 -> All we can do is have these two methods
throw an exception.
157.066 -> To prevent this, we need to remove this
method from our parent class
160.1 -> and add a new parent class
161.3 -> in between called FileImage
that contains these two methods.
165.116 -> But this also breaks
166.066 -> anyone who currently expects
the Image class to contain those methods.
170.3 -> When new changes like this come,
172.25 -> we're forced to edit all our classes,
a very expensive refactor.
177.75 -> This is the
178.433 -> greatest downfall of inheritance.
181.5 -> I find it similar
to how the ideal cleanness database
184.35 -> schema often causes problems
when you scale.
188.15 -> We've moved to NoSql databases
with tons of dirty duplication.
192.566 -> Inheritance breaks down
when you need to change the code.
195.416 -> Change is the enemy of perfect design
and you often paint yourself
198.75 -> into a corner
early on with your inheritance design.
202.583 -> This is because inheritance
naturally asks you
204.9 -> to bundle
all common elements into a parent class.
208.1 -> But the soon as you find an exception to
the commonality, it requires big changes.
213.316 -> So our alternative is to use composition.
217.216 -> So what is composition?
219.716 -> You've already been doing it.
222.2 -> Composition is the pattern you're doing
whenever you reuse code
225.05 -> without inheritance.
226.616 -> If we have two classes and they want to
reuse code, they simply use the code.
232.766 -> Let's
233.116 -> change our image classes to be composed
instead.
236.4 -> First, we're going to remove our abstract
methods from image.
241.666 -> Now, this is no longer an abstract class.
244.166 -> It's simply a class that represents
an image in memory.
248.75 -> In our JPEG, PNG and bitmap classes.
252.05 -> We no longer inherit image,
254.933 -> but we'll keep our save and load methods.
257.066 -> They'll just now be stand alone,
not overriding anything.
260.483 -> The methods were accessing
a bunch of stuff from the parent class.
263.566 -> So what do we do about those?
265.466 -> Well, instead of accessing them
through “this” we’ll
268.8 -> simply pass
in the image in question instead.
273.266 -> So now image represents an image
275.933 -> and these other classes
cleanly represent a specific file
279.6 -> format.
283.583 -> Now, if our new drawing requirement
comes in,
286.016 -> we create an image draw class
that takes an image to draw to.
289.4 -> And the methods do their thing.
291.05 -> We're no longer
bundled to the file related stuff.
295.066 -> Because we didn't force all the common
elements into a parent class
298.283 -> we don't need to alter any of the other
classes to add our ImageDraw class.
302.933 -> Now the user no longer chooses
the one class that suits their needs.
306.416 -> They also combine classes together
for their particular use case.
309.9 -> So here we're loading a JPEG image
drawing to it and then saving it.
314.633 -> Another app could do something different,
like load a bitmap
317.416 -> image, flip it, resize it,
and then save it out as a PNG.
322.5 -> Inheritance is interesting
because it actually combines
324.95 -> two capabilities:
the ability to reuse code,
328.216 -> but also the ability
to build an abstraction.
330.733 -> Creating abstractions
allow a piece of code to reuse
334.466 -> another piece of code, but also not know
which piece of code it's using.
339.15 -> You define a contract that both sides
of the abstraction agree to.
342.466 -> This gives the code
343.55 -> the rough shape of the other code,
but it doesn't know exactly what it is.
347.7 -> Inheritance does this by allowing
a consumer to think it's taking a class,
351.833 -> but it's actually given a subclass
instead.
354.65 -> Then the code can operate
like it always does.
357.166 -> Even if the system as a whole
is doing something very different.
360.2 -> If we go back to
when our image code used inheritance,
363.083 -> our application used the natural
abstraction capability of inheritance
366.65 -> by storing references to the parent class.
370.316 -> When our app opens a file,
371.9 -> we just figure out
372.65 -> which subclass to create and then store a
reference to it through the parent class.
378.2 -> Then when the user clicks the save button,
380.366 -> our save clicked method will get invoked
and we'll just call the save method
384.75 -> and we're abstracted from whether it's
the JPEG, ping or bitmap.
388.65 -> But with composition
you don't have parent classes.
391.7 -> You're just using the types you want.
393.866 -> Inheritance allows you to abstract
because the methods of the parent class
397.216 -> forms a contract.
398.633 -> A contract that says that every child
class shall have at least these methods.
403.65 -> So for our new classes
without inheritance, we still want to be
407.1 -> able to call our save and load methods
without caring about which class it is.
412.516 -> This is where interfaces come in.
414.6 -> Instead of a full parent class
with all its variables and methods,
418.2 -> an interface simply describes
the contract of what an object can do.
422.85 -> In this case, we'll create
an interface called Image File,
425.933 -> which represents the operations
an image file can do:
429.166 -> load and save.
435.616 -> Now, like
436.016 -> before, we save a reference
to one of our implementations.
439.1 -> But now, through the interface
441.983 -> and when the user click save,
443.666 -> we call save on whatever type was created.
446.866 -> Interfaces are a much more lightweight
448.8 -> way to do this
because our interfaces are minimal.
452.033 -> Parent classes share everything
by default, making them more difficult
455.45 -> to change.
456.9 -> But interfaces define
only the critical parts of the contract
460.166 -> and are easily
tacked on to existing classes.
463.666 -> Now that we have a nice abstraction
for loading and saving files in our app,
467.25 -> we can actually lift the creation of which
image file out of our image app class.
472.216 -> We’ll simply ask the user of the class
to pass in the interface instead.
477.266 -> That way
this class can just focus on dealing
479.366 -> with the UI commands
and the file class can be elsewhere.
483.683 -> There's a name for what we just did there.
485.816 -> Dependency injection.
488.25 -> I'll do a whole video
on dependency injection,
490.65 -> but if you've heard the term before
and wondered what it was, that's it.
494.566 -> Passing in an interface
for what you're going to use.
499.8 -> I won't say that inheritance is as evil
as some would say,
503.25 -> but I will say that I almost never use it
in my code.
507.716 -> Composition isn't perfect.
509.55 -> You do end up with a lot of boilerplate
512.333 -> needing to initialize
all of your internal types.
515.033 -> Many implementations
will contain the same code repeated,
518.816 -> and when you need expose information
from reused code, you often need to create
522.9 -> a lot of wrapper methods where you simply
return a call to an inner type.
527.033 -> But ultimately, composition
reduces the surface area
530.25 -> between objects, which gives you less
friction as changes come in.
535.2 -> Inheritance might be useful if you're
working inside of an existing system
538.666 -> that had highly repetitive code
where you only needed to modify one thing.
543.566 -> For example,
if you had the need for 100 classes
546.266 -> to conform to some specific interface
548.516 -> which half of them need
the same boilerplate over and over again.
552.283 -> You might say that that means that each
class has too much responsibility,
556.316 -> and you'd be right.
557.183 -> But changing the plugin model
would cost the team months of work.
560.483 -> You might not want to put your effort
into that just yet.
563.566 -> If you do use inheritance design
the class to be inherited, I'd avoid
568.35 -> protected variables with direct access,
like we avoid making our variables public.
573.716 -> For overriding: create
an explicit protected
575.816 -> API that you're supposed
to override an access mark.
579.016 -> Everything else
is private, final or sealed.
582.266 -> This prevents
bugs when changing your parent classes
584.666 -> because of not understanding
what your child classes have done.
Source: https://www.youtube.com/watch?v=hxGOiiR9ZKg