OO Design Principles Smaller but Useful Coding Patterns
OO Design Principles (Smaller but Useful Coding Patterns) CS 247 Module 13 Scott Chen
Agenda for Module 13 Ø Open Closed Principle • Favour Composition over Inheritance Ø Single Responsibility Principle and Liskov Substitutability Principle Ø Principle of Least Knowledge (Law of Demeter)
Section 1 Open Closed Principle
Open Closed Principle Ø Principle • • A module should be open for extension, but closed to modification i. e. program to an interface, not an implementation Ø Idea • Have client code depend on an abstract class (that can be extended) rather than on concrete classes Ø Example – Dynamic Polymorphism Term. Rec * Client Code + calc. GPA() Math Term. Rec + calc. GPA() Engineering Term. Rec + calc. GPA()
Default Implementation Ø Alternatively, an abstract base class may provide a default implementation that the derived classes may override. This makes it easier to derive new classes. Ø Abstract base class can declare and define common data and operations. Term. Rec + calc. GPA() + print() Math Term. Rec + calc. GPA() + print() Engineering Term. Rec + calc. GPA() + print() class Term. Rec{ public: virtual void print() = 0; virtual void calc. GPA(); protected: Term. Rec(); }; void Term. Rec: : calc. GPA() {…}
Inheriting Interface v. s. Implementation Ø The abstract base class designer determines what parts of a member function the derived class inherits: • • • Interface (declaration) of member function Interface and (default) overridable implementation Interface and non-overridable implementation class Term. Record{ public: virtual void print. Stats() const = 0; //Template Method virtual float calc. GPA(); //Default Method void print() const; //Should Not Override (but still can) void print. Format 2() const final; //CANNOT override protected: Term. Rec(); … }; class Math. Term. Record : public Term. Record {…} class Engineering. Term. Record : public Term. Record {…}
Inheritance vs. Composition Ø Problem – When defining a new class that includes attributes and capabilities of an existing class, should our new class… 1. Inherit from the existing class (inheritance)? 2. Include existing class as a complex attribute (composition)? Rectangle height width area() Rectangle rectangle Window height width area() “is a” “has a” Window
Choosing Inheritance Ø Principle – Favour Inheritance when… 1. Using subtyping § 2. Using the fact that a derived class can be used wherever the base class is accepted Using the entire interface of an existing class Term. Rec + calc. GPA() Math Term. Rec + calc. GPA() Engineering Term. Rec + calc. GPA()
Choosing Composition Ø Principle – Favour Composition when… • • The simple code is to be reused and extended instead of being overridden Allow the capabilities of the components (data and functions) to be changed at runtime. Rectangle Window rectangle height width area()
Delegation Ø A way to simulate inheritance-based method reuse in object composition. • Composite object delegates operations to component object • Can pass itself as a parameter, to let delegated operation refer to composite object Rectangle Window area() return rectangle->area(); rectangle height width area() return width * height;
Composition and Open Closed Principle Ø The benefit of composition is maximized when the component is an abstract type that can be concretized in different ways • • Abstract type in C++ is an abstract base class Abstract type in Java is an interface area() return share->area(); Shape shape Window Rectangle height width Circle radius area() return PI * radius; return width * height;
Section 2 Single Responsiblility and Liskov Substitutability
Single Responsibility Principle Ø Principle • Encapsulate each non-trivial changeable design decision in a separate module Ø The single-responsibility principle offers guidance on how to decompose our program into cohesive modules. Ø Example: Based on the names of its methods, how many design decisions are implemented by these two classes? Deck. Of. Cards Incohesive Design - Not everything is to do with the Deck of Cards - Two Design Decisions (Card, Deck. Of. Cards) next. Card() : Card add. Card(Card) remove. Card(Card) get. Card. Face(Card) get. Card. Suit(Card) Deck. Of. Cards has. Next. Card() : bool next. Card() : Card add. Card(Card) remove. Card(Card) shuffle() Cohesive Design - Everything is to do with the Deck of Cards - Single Design Decision
Liskov Substitutability Principle (LSP) Ø Principle • A derived class must be substitutable for its base class Ø A derived class must preserve the behaviour of its base class, so that it will work with client code that uses the base class • • • Objects accept the base class’ messages Methods requires no more than base class methods Methods promises no less than base class methods Client Stack Bounded Stack
LSP Example: Bounded Stack Ø A bounded stack is a specialized type of stack that can store only a limited number of elements Client Stack size: int push(Element) pop(): Element is. Empty(): bool Bounded Stack max. Size: int = 255 push(Element) * data Element
Substitutability Rules Ø When overriding inherited virtual functions, three rules must be followed: 1. Signatures § The derived-class objects must have all of the methods of the base class, and their signatures must match. 2. Method Behaviours § Calls to derived-class methods must behave like calls to the corresponding base-class methods. 3. Properties § The derived class must preserve all properties of the base class objects.
LSP Signature Rules Ø Signatures – the derived class must support all of the methods of the base class, and their signatures must match: • Parameters of the overridden virtual methods must have compatible types (i. e. same types) as the parameters of the base class’ methods • The return type of an overridden virtual method must be compatible with the return type (i. e. same type or subtype) of the base-class’ method • A derived-class method raises the same or fewer exceptions than the corresponding base-class method
LSP Method Rules Ø
LSP Property Rules Ø Property Behaviours – the derived class must preserve all declared (and enforced) properties of the base class objects. • Invariants (e. g. no duplicate elements in a container type) • Optimized for performance (memory requirements, timing)
Substitutability Example 1 class Stack{ long *items_; int tos_; public: Stack(); Stack(int); ~Stack(); long top() const; long pop(); virtual void push(long); }; class Count. Stack : public Stack{ int count_; public: Count. Stack() {count = 0; } void push(long); int num. Pushes() const; }; int Count. Stack: : num. Pushes(){ return count_; } int Count. Stack: : push(long l){ Stack: : push(l); count_ += 1; }
Substitutability Example 2 Ø Is Substitute. Entity substitutable for Entity? Entity() clone(): Entity Yes Substitute. Entity() clone(): Substitute. Entity
Substitutability Example 3 Ø Is Chequing. Account (with credit limit) substitutable for Account? Account balance_ = 0; get. Balance(): double deposit(amount) withdraw(amount): bool Chequing. Account credit_limit deposit(amount) withdraw(amount): bool invariant: balance >= 0 No invariant: balance >= -credit_limit
Section 3 Principle of Least Knowledge (Law of Demeter)
Encapsulation of Components Ø Information Hiding: Modular design should hide design and implementation details, including information about components Transcript GPA add. Term. Record(term) compute. Term. GPA(term) print. Term. Record(term) Term. Record 1. . * term. GPA academic. Standing - Term. Record(term) - compute. GPA(): float - print()
Composition and Data Encapsulation BDP Better Composite Design - Offer methods for accessing and manipulating component information - Does not expose the components Clearly, this version of composition implementation does not expose Term. Record as the component, thus reducing the risk of client mistakes on manipulating Term. Record. Transcript GPA add. Term. Record(term) compute. Term. GPA(term) print. Term. Record(term) add. Term. Record(Term. Record) get. Term. Record(term): Term. Record 1. . * vs. 1. . * Term. Record term. GPA academic. Standing + Term. Record(term) + compute. GPA(): float + print() // Client Code Transcript *t; … t->add. Term. Record(“Spring 2020”); t->compute. Term. GPA(“Spring 2020”); t->print. Term. Record(“Spring 2020”); // Client Code Transcript *t; Term. Record *tr; … tr = t->get. Term. Record(“Spring 2020”); tr->compute. GPA(); tr->print();
Another Example (Exposed Component) Store Client Code + settle. Bill(total) 1 Composite Object * Customer + pay(total) * Paypal. Account + pay(total) 1. . * Credit. Card + charge(total) // Client Code void Store: : settle. Bill(float total){ … Customer -> get. Pay. Pal() -> get. Credit. Card() -> charge(total); … } Prone to critical personal information leakage. Bad Smell: Message Chain
Another Example (Encapsulated Component) Store Client Code + settle. Bill(total) Encapsulated Component(s) 1 Composite Object * Customer + pay(total) * Paypal. Account + pay(total) // Client Code void Store: : settle. Bill(float total){ … Customer -> pay(total); //All subsequent calls hidden in pay() impl. … } 1. . * Credit. Card + charge(total) BDP
Law of Demeter Ø It helps test the well-formedness of encapsulation • An object “talks only to its neighbours” • You can play with yourself. • You can play with your own toys. (but you cannot take them apart) • You can play with toys that were given to you. • You can play with toys you’ve made yourself. Ø Example: A: : m 1() can only call methods of • • • A itself A’s data members Peter Van. Rooijen m 1’s parameters Any object constructed by A’s methods In particular, A should not invoke B’s methods that were returned by a method / function call (as function pointer) A b: B c: C x: int m 1(D) m 2() G B C D Limit of A’s direct dependencies E F BDP H I
Combined Example Yes Ø Question 1 – Is Cheap. Account substitutable for Basic. Account? Basic. Account: : call() Creates a record of a cellphone call, recording the start date and start time of the call, and the duration in minutes. Basic. Account: : bill() Decrements the account balance by the monthly service fee ($100) Basic. Account: : pay() Increments the balance by the amount paid. Basic. Account - id_ - balance_ - monthly. Fee = $100 + + + Call. Record * calls Basic. Account(balance_ = 0) balance(): Money call(Date, Time, Duration) bill() pay(amount: Money) print. Call. Records(min. Date, max. Date) - duration_ + + Call. Record(Date, Time, int duration) duration(): int start. Date(): Date start. Time(): Time start. Date Cheap. Account: : call() Creates a record of a cellphone call, and updates the number of minutes (num. Minutes) of calls in the current month. Cheap. Account: : bill() Decrements the account balance by the $30 monthly fee, and by $1 per minute of calls beyond the free 200 minutes per month; it also resets num. Minutes to 0 to start recording new call minutes at the start of every month. 1 1 start. Time Date Cheap. Account - monthly. Fee = $30 num. Of. Free. Minutes = $200 rate. Of. Call. Per. Min = 1 num. Minutes = 0 + Cheap. Account(balance_ = 0) + call(Date, Time, Duration) + bill() Time - day_ - month_ - year_ - sec_ - min_ - hour_ + + + + Date(day, month, year) day(): int month(): string year(): int Time(sec, min, hour) sec(): int min(): int hour(): int
Combined Example Not Really Ø Question 2 – Does this design favouring runtime cellphone plan change? Client Recall: Favour Composition Basic. Account - id_ - balance_ - monthly. Fee = $100 + + + Call. Record * calls Basic. Account(balance_ = 0) balance(): Money call(Date, Time, Duration) bill() pay(amount: Money) print. Call. Records(min. Date, max. Date) - duration_ + + Call. Record(Date, Time, int duration) duration(): int start. Date(): Date start. Time(): Time start. Date As a BDP, runtime object substitution should be done using composition. Alternatively, it’s where Strategy DP could be highly effective, using Open Closed Principle. 1 1 start. Time Date Cheap. Account - monthly. Fee = $30 num. Of. Free. Minutes = $200 rate. Of. Call. Per. Min = 1 num. Minutes = 0 + Cheap. Account(balance_ = 0) + call(Date, Time, Duration) + bill() Time - day_ - month_ - year_ - sec_ - min_ - hour_ + + + + Date(day, month, year) day(): int month(): string year(): int Time(sec, min, hour) sec(): int min(): int hour(): int
Combined Example Ø Question 2 – Alternative Design (Favouring Composition) Call. Record Account * - id_ - balance_ Client calls + call(Date, Time, Duration) + bill() Basic. Account - monthly. Fee = $100 + + + Basic. Account(balance_ = 0) balance(): Money call(Date, Time, Duration) bill() pay(amount: Money) print. Call. Records(min. Date, max. Date) monthly. Fee = $30 num. Of. Free. Minutes = $200 rate. Of. Call. Per. Min = 1 num. Minutes = 0 + Cheap. Account(balance_ = 0) + call(Date, Time, Duration) + bill() + + Call. Record(Date, Time, int duration) duration(): int start. Date(): Date start. Time(): Time start. Date Cheap. Account - - duration_ 1 1 start. Time Date Time - day_ - month_ - year_ - sec_ - min_ - hour_ + + + + Date(day, month, year) day(): int month(): string year(): int Time(sec, min, hour) sec(): int min(): int hour(): int
Combined Example Ø Question 3 – Using the same Account design from slide 30, which program statement(s) violate the design principle of Law of Demeter? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <vector> #include “Date. h” #include “Time. h” class Basic. Account{ public: … void print. Call. Records( const Date &min. Date, const Date &max. Date ) const; private: std: : vector< Call. Record* > calls_; static int monthly. Fee_; // = $100/month int balance_; }; Basic. Account: : monthly. Fee = 100; void Basic. Account: : print. Call. Records( const Date &min. Date, const Date &max. Date ) const { for( auto iter = calls_. begin(); iter != calls_. end(); iter++ ) { if( iter -> start. Date() >= min. Date && iter -> start. Date() <= max. Date ) { std: : cout << iter -> start. Date() << “ ” << iter -> start. Time(); std: : cout << “ ” << iter -> duration() << std: : endl; } } } Trick Question NONE
Summary
Summary Ø Open Closed Principle • • Inheritance vs. Composition Delegation Ø Single Responsibility Principle • Coherent Design Ø Liskov Substitutability Principle • • Rules for Signature, Method Behaviours, and Class Properties Concrete-Abstract operation mapping Ø Law of Demeter / Principle of Least Knowledge • Ensure well-formedness of component encapsulation
- Slides: 34