CS 247: Software Engineering Principles Reading: none Object-Oriented Design Principles Today's Agenda Object-Oriented Design Principles - characteristics, properties, and advice for making decisions that improve the modularity of the design. OO Basics Separation of ConcernsFavour Composition over Inheritance Encapsulate what is likely Single to change Responsibility Principle Encapsulate Data Representation Liskov Substitutability Principle Abstraction (interfaces, ADTs) Principle of Least Knowledge (Law of Demeter) Reuse (through composition, inheritance) Polymorphism OO Principles Program to an Interface, not an Implementation - aka Open Closed Principle U Waterloo CS247 (Spring 2017) p.1/31 U Waterloo CS247 (Spring 2017) p.2/31 If you want to know more: References Gamma, Helm, Johnson, Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison- Wesley, 1994. Bertrand Meyer, Object-Oriented Software Construction, Prentice Hall, 1997 Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices, Prentice Hall, 2003. Barbara Liskov and John Guttag, Program Development in Java, Addison-Wesley, 2000. Stanley B. Lippman, Essential C++, Addison-Wesley, 2000. U Waterloo CS247 (Spring 2017) p.3/31 Open Closed Principle Principle: A module should be open for extension but closed to modification. AKA 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 Math Engineering Science U Waterloo CS247 (Spring 2017) p.4/31
Math print() "Default" Implementation Alternatively, our abstract base class may provide a default implementation that the derived classes may override. This makes it easier to derive new classes. print() Engineering print() Science print() class { }; public: virtual void print() = 0; virtual void ; protected: (); void :: {} Abstract base class can declare and define common data and operations. U Waterloo CS247 (Spring 2017) p.5/31 Inheriting Interface vs. Implementation The abstract base class designer deines what parts of a member function the derived class inherits: 1. interface (declaration) of member function e.g. printstats 2. interface and (default) overridable implementation e.g. calc 3. interface and non-overridable implementation e.g. print class { public: virtual void printstats() const = 0;//template method op virtual float ; //compute mean average void print() const; //print template transcript }; class Math : public { } class Engineering : public { } U Waterloo CS247 (Spring 2017) p.6/31 Inheritance vs. Composition Bertrand Meyer, Object-Oriented Software Construction Problem: When defining a new class that includes attributes and capabilities of an existing class, should our new class: inherit from the existing class (inheritance)? include existing class as a complex attribute (composition)? Choosing Inheritance Principle: Favour inheritance when: (1) using subtyping i.e., using the fact that a derived class can be used wherever the base class is accepted. (2) using the entire interface of an existing class. rectangle "is-a" "has-a" Math Engineering Science U Waterloo CS247 (Spring 2017) p.7/31 U Waterloo CS247 (Spring 2017) p.8/31
Choosing Composition Principle: Favour composition for simple (non-overriding) code reuse and extension because with composition, the components' capabilities (data and functions) can change at run-time. Delegation Delegation in object composition simulates inheritance-based method reuse. Composite object delegates operations to component object. Can pass itself as parameter, to let delegated operation refer to composite object. rectangle rectangle return rectangle->; return ; U Waterloo CS247 (Spring 2017) p.9/31 U Waterloo CS247 (Spring 2017) p.10/31 Composition and Open Close Principle The benefits of composition are maximized when the component is an abstract type an interface (in Java) or an abstract base class (in C++) that can be concretized in different ways. shape Shape The Single-Responsibility Principle Principle: Encapsulate each 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 this one class? return shape->; return ; Circle radius return PI radius radius; DeckOfCards hasnextcard() : bool nextcard() : Card addcard(card) removecard(card) shuffle() U Waterloo CS247 (Spring 2017) p.11/31 U Waterloo CS247 (Spring 2017) p.12/31
Liskov Substitutability Principle (LSP) Principle: A derived class must be substitutable for its base class. Idea: 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's messages. Methods require no more than base class methods. Methods promise no less than base class methods. Client Stack 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 isempty() : bool BoundedStack maxsize : int = 255 push(element) data Element Bounded Stack U Waterloo CS247 (Spring 2017) p.13/31 U Waterloo CS247 (Spring 2017) p.14/31 Substitutability Rules Liskov, Guttag, Program Development in Java 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 Liskov, Guttag, Program Development in Java Signatures: The derived class must support all of the methods of the base class, and their signatures must match: Parameters of overridden virtual methods must have compatible types as the parameters of the base class's methods. C++, Java: same types (otherwise redefining) The return type of an overridden virtual method must be compatible with the return type of the base-class's method. C++, Java: same type, or subtype of A derived-class method raises the same or fewer exceptions than the corresponding base-class method. U Waterloo CS247 (Spring 2017) p.15/31 U Waterloo CS247 (Spring 2017) p.16/31
LSP Method Rules Liskov, Guttag, Program Development in Java Method behaviours: A derived-class method maintains or weakens the precondition and maintains or strengthens the postcondition: Precondition Rule: pre_base => pre_derived LSP Property Rules Liskov, Guttag, Program Development in Java 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) Postcondition Rule: (pre_base && post_derived) => post_base In other words, the specification of a derived class must be stronger-than the specification of its base type. U Waterloo CS247 (Spring 2017) p.17/31 U Waterloo CS247 (Spring 2017) p.18/31 Substitutability Example class Stack { long items_; int tos_; public: Stack(); Stack(int); ~Stack(); long top() const; long pop(); virtual void push(long); }; class CountStack : public Stack { int count_; public: CountStack() : count_{0} {} void push(long); int numpushes() const; }; int CountStack::numPushes() { return count_; } void CountStack::push(long v) { Stack::push(v); count_ += 1; } Another Example Is SubstituteEntity substitutable for Entity? Entity Entity() clone() : Entity SubstituteEntity SubstituteEntity() clone() : SubstituteEntity() U Waterloo CS247 (Spring 2017) p.19/31 U Waterloo CS247 (Spring 2017) p.20/31
Yet Another Example Encapsulation of Components Is ChequingAccount (with a credit limit) substitutable for Account? Account balance = 0; getbalance() : double deposit(amount) withdraw(amount) : bool ChequingAccount credit limit deposit(amount) withdraw(amount) : bool invariant: balance 0 invariant: balance -credit limit add () computeterm () print () 1.. - () - compute() : float - print() Information Hiding: Modular design should hide design and implementation details, including information about components. U Waterloo CS247 (Spring 2017) p.21/31 U Waterloo CS247 (Spring 2017) p.22/31 Composition and Data Encapsulation Composition and Data Encapsulation add () computeterm () print () add() get() : add () computeterm () print () add() get() : 1.. - () - compute() : float - print() vs. 1.. + () + compute() + print() 1.. - () - compute() : float - print() vs. 1.. + () + compute() + print() t; t->add("spring2014"); t->computeterm("spring2014"); t->print("sring2014"); t; tr; tr = t->get("spring2014"); tr->compute(); tr->print(); U Waterloo CS247 (Spring 2017) p.23/31 Composite object offers t; methods for accessing and t->add("spring2014"); t->computeterm("spring2014"); manipulating component t->print("sring2014"); information. t; tr; tr = t->get("spring2014"); tr->compute(); tr->print(); U Waterloo CS247 (Spring 2017) p.24/31
Another Example Component Encapsulation client code Store + settlebill (total) 1 client code Store + settlebill (total) 1 composite object Customer + pay (total) PayPal Account 1.. + pay (total) CreditCard + charge (total) composite objects Customer +pay (total) PayPal Account - pay (total) 1.. CreditCard - charge (total) void Store::settlebill(float total) {... Customer->getPayPal()->getCreditCard()->charge(total);... } void Store::settlebill(float total) { Customer->pay(total); } U Waterloo CS247 (Spring 2017) p.25/31 U Waterloo CS247 (Spring 2017) p.26/31 A b: B c: C x: int m1 (D) m2 ( ) "Law" of Demeter B D C E F G H Limit of A's direct dependencies. "Law" tests encapsulation: an object "talks only to its neighbours" Method A::m1 can only call methods of A itself A s data members m1 s parameters any object constructed by A's methods You can play with yourself. You can play with your own toys (but you can't take them apart), You can play with toys that were given to you. You can play with toys you've made yourself. Peter VanRooijen In particular, an A object should not invoke methods of B objects returned by a method/function call. I U Waterloo CS247 (Spring 2017) p.27/31 BasicAccount ID balance monthlyfee = $100 + BasicAccount(balance=0) + balance () : Money + call( Date, Time, Duration ) + bill( ) + pay( amount: Money ) + printcallrecords ( mindate, maxdate ) CheapAccount monthlyfee = $30 numfreeminutes= 200 rateofcallpermin = 1 numminutes = 0 + CheapAccount(balance=0) + call( Date, Time, Duration ) + bill( ) Substitutable? CallRecord duration + CallRecord ( Date, Time, int duration ) + duration() : int + startdate() : Date + starttime() : Time 1 startdate 1 starttime Date Time day sec month min year hour + Date( day, month, year ) + Time( hour, min, second ) + day( ) : int + month( ) : string + year( ) : int + hour( ) : int + min( ) : int + sec( ) : int BasicAccount::call() creates a record of a cellphone call, recording the start date and start time of the call and the duration in minutes. BasicAccount:bill() decrements the account's balance by the monthly service fee ($100). BasicAccount::pay() increments the balance by the amount paid. calls CheapAccount::call() creates a record of a cellphone call, and updates the number of minutes (numminutes) of calls in the current month. CheapAccount:bill() decrements the account's balance by the $30 monthly service fee and by $1 for each minute of calls beyond the free 200 minutes of calls in the current month; it also resets numminutes to 0 to start recording number of call minutes for the next month. U Waterloo CS247 (Spring 2017) p.28/31
Favour Composition Over Inheritance BasicAccount ID balance monthlyfee = $100 + BasicAccount(balance=0) + balance () : Money + call( Date, Time, Duration ) + bill( ) + pay( amount: Money ) + printcallrecords ( mindate, maxdate ) CheapAccount monthlyfee = $30 numfreeminutes= 200 rateofcallpermin = 1 numminutes = 0 + CheapAccount(balance=0) + call( Date, Time, Duration ) + bill( ) CallRecord duration + CallRecord ( Date, Time, int duration ) + duration() : int The above design can be inefficient if a customer frequently changes the type of his or her cellphone plan. Use the principle of Favour Composition over Inheritance to restructure the design to minimize how much has to change when a customer's cellphone plan changes from a BasicAccount to a CheapAccount or vice versa. 1 startdate 1 starttime Date Time day sec month min year hour + Date( day, month, year ) + Time( hour, min, second ) + day( ) : int + month( ) : string + year( ) : int + hour( ) : int + min( ) : int + sec( ) : int U Waterloo CS247 (Spring 2017) p.29/31 Law of Demeter Which program statements violate the design principle Law of Demeter? 0 #include <vector> 1 #include "Date.h" 2 #include "Time.h" 3 4 class BasicAccount { 5 public: 6 7 void printcallrecords ( const Date &mindate, const Date &maxdate ) const; 8 9 private: 10 std::vector<callrecord> calls_; 11 static int monthlyfee_; // = $100/month 12 int balance_; 13 }; 14 15 BackAccount::monthlyFee = 100; 16 17 18 void BasicAccount::printCallRecords( const Date &mindate, const Date &maxdate) const { 19 for ( auto item : calls_ ) { 20 if ( item->startdate() >= mindate && item->startdate() <= maxdate ) { 21 std::cout << item->startdate() << " " << item->starttime(); 22 std::cout << " " << item->duration() << std::endl; 23 } 24 } 25 } U Waterloo CS247 (Spring 2017) p.30/31 Take Aways Recognition The goals underlying each design principle. That the principles are guidelines, not rules or laws. Comprehension Detect that a design violates a principle. Argue (with justification) that one design is better than another. Application Design an abstract base class. Deine whether one class is substitutable for another. Convert an inheritance relation into a composition relation. Deine whether a class encompasses more than one responsibility. U Waterloo CS247 (Spring 2017) p.31/31