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

class Turtle {
	virtual void drawShell()=0;
	virtual void drawHead(){šŸ˜„;}
	virtual void drawLegs(){šŸ¦µšŸ¦µšŸ¦µšŸ¦µ;}
	public:
		void draw(){
			drawHead();
			drawShell();
			drawLegs();
		}
};
 
class RedTurtle: public Turtle{
	void drawShell() override {šŸ”“;}
};
 
class GreenTurtle: public Turtle{
	void drawShell(){šŸŸ¢;f}
}

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

class DigitalMedia{
	public:
		virtual ~DigitalMedia(){}
		virtual void play() = 0;
};

No NVI here.

With NVI:

class DigitalMedia{
	virtual void doPlay() = 0;
	public:
		virtual ~DigitalMedia(){}
		void play(){
			doPlay();
		}
};

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

//enemy.h
class Enemy {
	public:
		virtual void strike(); // Used in the combat process
		virtual void draw(); // Used by UI of our gmae.
};
 
class UI {
	vector<Enemy*> enemies; // All the enemies the UI might need to draw
	...
};
 
class BattleField{
	vector<Enemies*> enemies; // All the enemies tha tmight perform combat.
	...
};

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 and BattleField classes each have a vector of pointers to Enemy objects. Both classes are now dependent on both methods of the Enemy 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, including battlefield.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

class Enemy: public Draw, public Combat{};
 
class Draw{ // abstract superclass also called interface
	public:
		virtual void draw() = 0;
};
 
class Combat {
	public:
		virtual void strike() = 0;
};
 
class UI {
	vector<Draw*> enemies;
};
 
class BattleField{
	vector<Draw*> enemies;
};

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?

class A {
	 int x;
	protected:
		 int y;
	public:
		int z;
};

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