CS247 Lecture 17
Last Time: Casting, coupling/cohesion, S in SOLID
This Time: SOL in SOLID
Should main be performing communication? No - limits reusability.
Another possibility: Use the MVC architecture well-suited for SRP.
MVC: Model-View-Controller
- Model: Handles logic + data of your program.
- View: Handles program output / display
- Controller: Handles input, facilitates control flow between classes
Model: May have multiple views, or just one - different types of views: text, graphical view, etc. Very good to use it in a chess engine for the text and graphic display, something similar to the Observer pattern which is a good place to use it for.
- Model doesn’t need to know anything about the state of the views and implementation of the views.
- Picture the Observer pattern Model as the Subject and View being the Observer.
- Sometimes we structure interactions between Model and View via an Observer pattern.
Controller: Mediates control flow.
- May encapsulate things like turn-taking or some portion of the game rules.
- Some of this logic may go in the Model instead: judgement call.
- May communicate with user for input(sometimes done by the View).
By decoupling presentation, input, and data / logic we follow SRP, and promote re-use in our programs.
On the other hand, there maybe parts of this specifications that are unlikely to change, and so adding layers of abstraction just to follow SRP may not be worthwhile.
Note: Avoiding needless complexity is also worthwhile your best judgement.
Open / Closed Principle
Classes should be open to extension, but closed to modification.
Changes to a program should occur primarily by writing new code, not changing code that already exists.
Example: Hero has Sword.
- What if we wanted our Hero to use a different type of weapon? Bow or magic wand?
- Requires us changing all the places we referenced the sword class - lots of modification, and not much extension.
Fix: Hero has an abstract Weapon Class, deriving from abstract weapon, we have concrete Sword, Bow, Want. And we can add as much weapons as we want without changing the weapon class.
Strategy Pattern
Open / closed principle: Closely related to the concept of inheritance and virtual functions.
For example:
This function is open to extension: we can add more functionality by defining new types of books, and closed to modification. Since isHeavy() is overloaded.
Compare this to WhatisIt
:
Not open to extension: Cannot get new behaviour without modifying the source code not closed to modification either.
Open/closed principle is an ideal = writing a program will require modifications at some point.
Plan for things that are most likely to change, account for those.
High Cohesion vs. SRP
High Cohesion: High cohesion refers to the degree to which the responsibilities and functionalities of a software component are related and focused. In simpler terms, it means that a component should have a single, well-defined purpose and should perform tasks that are closely related to that purpose.Â
Single Responsibility Principle (SRP): The Single Responsibility Principle is one of the SOLID principles of object-oriented design. It states that a class should have only one reason to change, or in other words, a class should have only one responsibility. This principle aligns closely with the idea of high cohesion but is applied at the class or module level. Each class should have a single, well-defined responsibility and should encapsulate only one aspect of functionality.
Liskov Substitution Principle
Enforces something discussed so far strictly:
- public inheritance should model an is-a relationship.
If class B
inherits from class A
: we can use pointers / references to B
objects in the place of pointers / references to A
objects: C++ gives us this.
Liskov substitution is stronger: not only can we perform the substitution, but we should be able to do so at any place in the program, without affecting its correctness.
So precisely:
- If an invariant is true for class
A
, then it should also be true for classB
. - If an invariant is true for a
method A::f
, andB
overridesf
, then this invariant must be true forB::f
. - If
B::f
overridesA::f
:- If
A::f
has a precondition P and a post condition Q, thenB::f
should have a precondition P’ such that PP’ and a post condition Q’ such that Q’ Q.B::f
needs a weaker precondition and a stronger postcondition.
- If
Example: Contra-variance problem https://piazza.com/class/lh4qjengjtl5zy/post/736
- Happens wherever we have a binary operator where the other parameter is the same type as
*this
.
As we’ve seen before, we must take in the same parameter when overriding. C++ enforces this, which actually enforces LSP (Liskov Substitution Principle) for us.
- A circle is-a shape
- A shape can be compare with other Shapes.
- A circle can be compared with any other Shape.
To satisfy LSP, we must support comparison between different types of shapes.
We already performed typeid on circle, so we know that it is a circle, don’t need dynamic_cas
t so we use static_cast
typeid
returns std::type_info
objects that can be compared.
dynamic_cast
: tells us Is other
a Circle
or a subclass of Circle
?
typeid
: Is other exactly a Circle
.
typeid uses the dynamic-type so long as the class has at least one virtual method.
Ex: Is a Square
a Rectangle
?
4th grade will tell us yes, a square is a rectangle.
A square has all the properties of a rectangle.
But:
Issue: we expect our postcondition Rectangle setWidth
to be that the width is set and nothing else changes. Violated by our Square
class.
We basically violate the postcondition of the Rectangle::setWidth
method, which specifies that only the width should change.
Violates LSP, does not satisfy an is-a relationship. Conclusion: Rectangle is not a square. Squares are not Rectangles.
Summary
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In other words, if you have a derived class, it should behave in a way that is consistent with the base class.
In the example, we have a base class
Rectangle
and a derived classSquare
. Correct to say that all squares are rectangles. However, problem arises when you try to model this relationship using inheritance and override the methods in the derived class.Main issue is violation of the LSP. The overridden
setWidth
andsetLength
methods in theSquare
class modify both the width and the length, which is not consistent with the behaviour expected from aRectangle
. Leads to unexpected behaviour when you pass aSquare
object to thef
function as observed.In the LSP-compliant code, a
Square
should not inherit directly fromRectangle
, because the specific behaviour ofSquare
’s setters conflicts with the behaviour expected fromRectangle
. One way to design this relationship correctly would be to create an abstract base class that defines the interface for a quadrilateral and then have separate classes forRectangle
andSquare
that adhere to their respective behaviours: (code provided below)
Note
By separating the behavior of
Rectangle
andSquare
into their own classes and avoiding the problematic overrides, you ensure that you’re adhering to the Liskov Substitution Principle. This design also makes it clear that aSquare
is not a special case of aRectangle
in terms of behavior, and theQuadrilateral
base class captures the common interface for both shapes.
Possibility: Restructure inheritance hierarchy to make sure LSP is respected. (basically same thing as in the code above)
In general: How can we prevent LSP violations?
Restrain what our subclasses can do, only allow them to customize what is truly necessary.
We can use a design pattern: Template method pattern or Visitor pattern https://piazza.com/class/lh4qjengjtl5zy/post/736. We’ll provide a “structure” or “recipe” or “template” for what this method will do, allow subclasses to customize portions of this method by performing overrides.
General: When a subclass should be able to modify some parts of a method’s behaviour, but not all. (All Template Method Pattern is about)
Next: CS247 Lecture 18