SOLID Principles (Series) [PART 3]
SOLID - make Object-Oriented designs more understandable, flexible and maintainable.
If get to know something new by reading my articles, don't forget to endorse me on LinkedIn
In the last article, we talked about Open-Closed Principle which is one of the key concepts in OOP that enables us to write robust, maintainable and reusable software components. But following the rules of that principle alone is not enough to ensure that you can change one part of your system without breaking other parts. Our classes
and interfaces
also need to follow the Liskov Substitution Principle to avoid any side-effects. LSP extends OCP by focusing on the behavior of a superclass and its subtypes.
What is LSP?
LSP defines that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. That requires the objects of your subclasses to behave in the same way as the objects of your superclass.
Let's simplify this the scenario.
This principle applies to inheritance hierarchies and is just an extension of the Open Close Principle.
It means that we must make sure that new derived classes are extending the base classes without changing their original behavior. Basically, derived classes should never do less than their base class.
If a subtype of the supertype does something that the client of the supertype does not expect, then this is in violation of LSP. Imagine a derived class throwing an
Exception
that the superclass does not throw, or if a derived class has some unexpected side effects. One has to consider that how the client programs are using the class hierarchy. Sometimes code refactoring is required to fix identified LSP violations.
Example: Bad Design and Implementation
Let's think in-terms of a MediaPlayer
interface MediaPlayer {
fun playAudio(mediaContent : MediaContent)
fun playVideo(mediaContent : MediaContent)
}
Let's work on two media player VLC Media Player
and Winamp Media Player
.
[Note: Winamp doesn't support Video]
class VLCMediaPlayer : MediaPlayer {
@override fun playAudio(mediaContent : MediaContent) {...}
@override fun playVideo(mediaContent : MediaContent) {...}
}
class WinampMediaPlayer : MediaPlayer {
@override fun playAudio(mediaContent : MediaContent) {...}
@override fun playVideo(mediaContent : MediaContent) {
throw VideoNotSupportedException()
}
}
class VideoNotSupportedException(@override val message = "Winamp doesn't support Video") : RuntimeException() {}
Everything looks good and okay, right? No!. Why? Let's find out.
When we pass a audio
MediaContent
to theMediaPlayer
,MediaPlayer
will try to play the audioMediaContent
. BothVLCMediaPlayer
andWinampMediaPlayer
support audio playing functionality.But when we a video
MediaContent
to theMediaPlayer
,MediaPlayer
will try to play the videoMediaContent
.VLCMediaPlayer
will be able to play the videoMediaContent
but when it's aboutWinampMediaPlayer
it will throwVideoNotSupportedException
.That's where LSP is violated in
WinampMediaPlayer
, becuase it's breaks the original behavior of the super classMediaPlayer
.
So, how do we solve this "breaking"?
Example: Good Design and Implementation
interface MediaPlayer {
fun playAudio(mediaContent : MediaContent)
}
interface VideoMediaPlayer : MediaPlayer {
fun playVideo(mediaContent : MediaContent)
}
class VLCMediaPlayer : VideoMediaPlayer {
@override fun playAudio(mediaContent : MediaContent) {...}
@override fun playVideo(mediaContent : MediaContent) {...}
}
class WinampMediaPlayer : MediaPlayer {
@override fun playAudio(mediaContent : MediaContent) {...}
}
So, what's the changes here?
Previous
MediaPlayer
functionality has been divided into two part now, whereMediaPlayer
is responsible to play audioMediaContent
andVideoMediaPlayer
(extendMediaPlayer
) is responsible to play both audio and videoMediaContent
.VLCMediaPlayer
supports audio and videoMediaContent
as expected andWinampMediaPlayer
supports only audioMediaContent
.Now, here LSP is not violated in
WinampMediaPlayer
, becuase it's doesnt breaks the original behavior of the super classMediaPlayer
.
Recap
LSP defines that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. That requires the objects of your subclasses to behave in the same way as the objects of your superclass.
In the next article, we will talk about Interface Segregation Principle.
That's it for today. Happy Coding...