CS247 Lecture 18
Last Time: SOL in SOLID
This Time: LI in SOLID -Template Method Pattern, NVI, Adapter pattern, Multiple Inheritance
Template Method Pattern: In video game, I have two types of Turtles: Red Turtle + Green Turtle
Note: draw method is public and non-virtual. Nobody who subclasses Turtle can override draw.
Note
But whenever a Turtle calls draw(), it will always call from the base class, and notice how the other function drawHead, drawShell, and drawLegs are virtual.
(GreenTurtle and RedTurtle can define draw, but it wonāt be an override
, wonāt be called if we use Turtleās polymorphically.) Which makes total sense, since we didnāt specify virtual
for the draw()
method.
Note as well: drawShell()
is private, but we can still override it! Access specifiers (public, protected, private) only describe when methods can be called, not how they may be overwritten. So it sill can be overridden.
NVI - Non-Virtual Idiom (Highly related to Template Method Pattern)
Addresses the challenges posed by public virtual methods by separating the public interface of a class from its customizable behaviour.
Key Idea
To use non-virtual public methods to define the public interface and use private or protected virtual methods to provide customizable behaviour for subclasses. It helps maintain invariants, pre-conditions, and post-conditions while supporting customization.
public virtual methods are trying to do two things at once:
public: Providing an interface to the client:
- will uphold invariants
- Respect pre-conditions / post-conditions
virtual: Providing an interface to subclasses:
- Virtual methods are those that may be customized while an object is used polymorphically
- Provide āhooksā to allow customization of behaviour
- Satisfying the responsibilities of āpublicā and āvirtualā simultaneously is difficult
- How are we sure public virtual method will satisfy its invariants, pre-conditions / post-conditions. Will it even do its job?
- What if we want to add more code to the public virtual method without changing the interface?
Non-Virtual Idiom states the following:
- All public methods are non-virtual
- All virtual methods should be private (or protected)
- Exception for the Destructor
Example: Digital Media
No NVI here.
With NVI:
Now: We can add code that must run for all types of DigitalMedia before or after the doPlay call.
- We could add copyright checking before
doPlay
, this will always run. - Or: we can update a play count after
doPlay
- now subclasses donāt have to worry about maintaining this invariant.
Flexible: can provide more āhooksā for customization simply by adding more private virtual method calls.
E.g.: showArt()
before doPlay()
- private virtual display poster for movie, album cover for song, etc.
All can be done without changing the public interface. Which is good for minimal recompilation, open / closed principle.
- In general - easier if we constrain what subclasses do from the beginning, as opposed to wrestling back control. Supporting Liskov Substitution principle.
- Any decent compiler will optimize the extra function call out - so no cost at runtime! ?????? What extra function call?
Interface Segregation Principle
No code should depend on methods that it doesnāt use.
- related to cohesion, if our class is cohesive
- We prefer many small interfaces over one larger, more complicated interface
- If a class has many functionalities, each client of the class should only see the functionality that it needs
Example: (No NVI for simplicity) Video Game
Imagine we make a change to our drawing interface
battlefield.cc
must still recompile, even though itās not using any part of the drawing interface.
Needless coupling between UI and BattleField via Enemy (when both UI and Battlefield contains Enemy
)
Note
The issue arises when the
UI
andBattleField
classes each have a vector of pointers toEnemy
objects. Both classes are now dependent on both methods of theEnemy
class. This creates unnecessary coupling between classes that might not need both methods.Since both UI and BattleField contains a reference to Enemy, if we ever decide to change something in the draw() funciton, we need to recompile both UI and Battlefield.cc. But they donāt even use the draw method. This is because the change to
Enemy
ās interface could potentially affect the way itās used in other parts of the program, includingbattlefield.cc
.
The Interface Segregation Principle suggests that the Enemy
class should have separate interfaces for its combat-related functionality and its UI-related functionality. This way, UI
and BattleField
would only depend on the methods that are relevant to their contexts, reducing unnecessary coupling and recompilation dependencies.
One solution: Multiple Inheritance
Enemy would need to override both draw and strike in order to become a concrete class.
Somewhat similar to the Adapter Pattern - Used when a class provides an interface different from the one you need.
Setup
Draw and Combat define separate interfaces, and Enemy implements both interfaces. Allows Enemy to provide distinct implementations for both drawing and combat.
UI and Battlefield only depend on the specific interface they need (Draw or Combat). Reduces unnecessary coupling.
Multiple Inheritance can help avoid the issues of violating Interface Segregation Principle.
For Example: Library for our window - expects objects to inherit from āRenderableā and provide a ārenderā method.
- Donāt want to change all of our inheritance hierarchy, or all our draw calls to render - violates open / closed, and also pain. Use an adapter class.
GO READ ON ADAPTER DESIGN PATTERN!!! ????????????
- Satisfy the Renderable interface by calling the methods weāve already defined.
- We might not even need our Adapter to provide this ādrawā method anymore, just render. In which case, we could use private inheritance. If the adapter only needs to use the methods from your class hierarchy but not expose them publicly, using private inheritance is a good choice.
What is private inheritance?
Under protected inheritance: class B: protected A{...}
- x remains inaccessible
- y remains protected
- z becomes protected - can only be accessed in B and subclasses of B B is not an is-a relationship with A.
Under private inheritance (class B: private A{..}
or class B: A{..}
)
- x remains inaccessible
- y and z becomes private - can only be accessed in B methods. No subclasses of B can access y and z.
Protected and private inheritance are not is-a (specialization) relationship.
Next: CS247 Lecture 19