Software Design Patterns CSCE 315 – Programming Studio Spring 2017
What are Design Patterns? Well-known and proven solutions to common and repeating problems Really a set of standard techniques to solve frequently occurring software problems They are not really pseudocodes, rather some design solutions expressed in the form of Example codes UML diagrams In many cases, the solutions are tied to Object Oriented Programming (OOP) Although they are not limited to OOP Design patterns apply to non-Software or non-Computer Science fields as well Have to design a cable-suspended bridge on Navasota River
Motivations “We just came up with a design. Before jumping to coding it, we want to make sure that this design will actually work” “Is the problem a well-known one? If so, what is the best-known solution to this? We don’t want to reinvent the wheel if somebody else has solved this problem already” “I feel like I had to solve this same problem last semester for a different course, but cannot remember the solution” “A team-member claims that this is the best solution to the problem. But how do we know that this is really the best solution. Does this proposed solution have any flaws?” “Just wondering, if the guys in Google/Facebook/Microsoft faced the same problem that we are solving in our project? If so, what is the solution that they used? We really care about using the best design in our project”
Elements of Design Patterns Pattern Name Name to describe the pattern concisely Problem When/where to apply the solution (i.e., the pattern of the problem) Solution If the problem is a match, how to design correctly in that case
Types of Design Patterns Creational Patterns Deal with creation of objects Structural Patterns How objects/classes are composed Behavioral Patterns How classes/objects interact Other patterns for specific domains, e.g., Concurrency problems User Interface design
Creational Design Patterns Singleton Prototype Abstract Factory Factory Method Builder …
Singleton Problem: To always keep exactly 1 instance (i.e., object) of a class Trivial (or is it??) solution without much thought is following: #define INIT_VALUE 100 class MyClass{ int value; public: MyClass (int v){ value = v;} int get_value (){ return value;} void set_value (int v){ value = v;} }; MyClass* global_ptr; void square_value () { if (!global_ptr) global_ptr = new MyClass(INIT_VALUE); int val = global_ptr->get_value(); cout << "SQUARE: " << val * val << '\n'; } void double_value () { if (!global_ptr) global_ptr = new MyClass(INIT_VALUE); int val = global_ptr->get_value(); cout << "DOUBLE: " << (val + val) << '\n'; } int main () cout << "CURRENT VALUE: " << val << '\n'; square_value (); double_value ();
Singleton The proper way to solve the same problem: #define INIT_VALUE 100 class MyClass{ int value; static MyClass* instance; MyClass (int v){ value = v;} // constructor is made private!!! public: int get_value (){ return value;} static MyClass* get_instance (){ if (!instance) instance = new MyClass (INIT_VALUE); return instance; } }; MyClass* MyClass::instance = 0; //required for the instance // not for the object itself void square_value () { int val = MyClass::get_instance()->get_value(); cout << "SQUARE: " << val * val << '\n'; void double_value () cout << "DOUBLE: " << (val + val) << '\n'; int main () cout << "CURRENT VALUE: " << val << '\n'; square_value (); double_value ();
Singleton Advantages No repetition of code Other programmers do not have to worry about creating an object if one does not exist Since the constructor is private to the class, other programmers cannot create instance, even if they want to Consistency throughout
Factory Method Problem: To provide a common interface to create related objects, but let the subclasses decide which class to instantiate. class Shape{ public: virtual void draw() = 0; }; class Circle: public Shape{ void draw(){cout << "Circle\n";} class Rectangle: public Shape{ void draw(){cout << "Rectangle\n";} int main(){ vector<Shape*> shapes; int choice; while (true){ cout << "Circle(1) Rectangle(2)"; cin >> choice; if (choice == 0) break; else if (choice == 1) shapes.push_back(new Circle); else if (choice == 2) shapes.push_back(new Rectangle); } for (int i = 0; i < shapes.size(); i++) shapes[i]->draw();
Factory Method After applying the pattern: Advantage: Reduces “coupling” significantly, leading to better design Shape* Shape::make_shape (int choice) { if (choice == 1) return new Circle; else if (choice == 2) return new Rectangle; } int main(){ vector<Shape*> shapes; int choice; while (true){ cout << "Circle(1) Rectangle(2)"; cin >> choice; shapes.push_back(Shape::make_shape(choice)); for (int i = 0; i < shapes.size(); i++) shapes[i]->draw(); class Shape{ public: virtual void draw() = 0; static Shape* make_shape (int choice); }; class Circle: public Shape{ void draw(){cout << "Circle\n";} class Rectangle: public Shape{ void draw(){cout<< "Rectangle\n";}
Abstract Factory Problem/Goal: Provide an interface for creating families of related objects Example Scenario: You are designing a game, where in a particular stage, the walls are made of up of “Squares” and “Circles”. In a different stage, the walls are textured with “Rectangles” and “Ellipses” Idea: Create a series of “Squares” and “Circles” together.
Abstract Factory Example taken from sourcemaking.com class Factory { public: virtual Shape* Curved() = 0; virtual Shape* Straight() = 0; }; class SimpleShapeStage : public Factory { Shape* Curved(){return new Circle;} Shape* Straight(){return new Square;} class RobustShapeStage : public Factory { Shape* Curved(){return new Ellipse;} Shape* Straight(){return new Rectangle;} int main() { Factory* factory = new SimpleShapeStage; Factory* factory2 = new RobustShapeStage; Shape* shapes[3]; shapes[0] = factory->Curved();//Circle; shapes[1] = factory->Straight();//Square; shapes[2] = factory2->Curved(); //Ellipse; shapes[3] = factory2->Straight(); //Rectangle; for (int i=0; i < 4; i++) shapes[i]->draw(); } class Shape { public: virtual void draw() = 0; }; class Circle : public Shape { void draw() {cout << "circle\n";} class Square : public Shape { void draw() {cout << "square\n"} class Ellipse : public Shape { void draw() { cout << "ellipse\n";} class Rectangle : public Shape { cout << "rectangle\n";}
Structural Design Patterns These patterns deal with how classes and objects are composed to form larger structures Composition change at run-time rather than statically This makes these patterns powerful composition tool Unified Modeling Language (UML) is really handy to describe structural or any other patterns We will briefly see UMLs first, then explore more patterns
Unified Modeling Language (UML) A standard way to visualize the design of a system We will only learn a subset of the diagram types and notations
Structural Patterns Adapter Bridge Composite Decorator Facade Flyweight Proxy ……
Adapter Objective is to convert the interface of a class into something that the client expects Can be thought of as writing a wrapper for the adaptee A naïve approach would be to call the Adapter class directly Rather the calls go through Target, to keep the interface consistent If the Adaptee changes later on, the rest of the system is intact
Adapter - Code Example class Collection{ // Target public: void add_item (int value) = 0; int remove_item () = 0; }; // Adapter class StackCollection: public Collection{ stack<int> items; // Adaptee void add_item (int value){ items.push (value); } int remove_item (){ int val = items.top (); items.pop(); return val; int main (){ //client Collection * c = new StackCollection(); c->add_item (10); cout << c->remove_item () << endl; }
Bridge The objective is to separate abstraction from implementation Inheritance hierarchy gets complicated when abstractions and implementations are attempted to cover from the same hierarchy Has to stop this exponential growth in number of classes *images taken from sourcemaking.com
Bridge - Example
Bridge – Code Example class Circle: public Shape{ public: class DrawAPI{ public: virtual void draw_object ()=0; }; class WinDraw: public DrawAPI{ void draw_object (){cout << "In Win" << endl;} class MacDraw: public DrawAPI{ void draw_object (){cout << "In OSX" << endl;} class Shape{ protected: DrawAPI* dapi; Shape (DrawAPI* d){ dapi = d;} virtual void draw()=0; class Circle: public Shape{ public: Circle (DrawAPI* d): Shape (d){} void draw (){cout << "Circle "; dapi->draw_object();} }; class Rectangle: public Shape{ Rectangle (DrawAPI* d): Shape (d){} void draw(){cout<< "Rectangle "; dapi->draw_object();} int main () { Shape* cw = new Circle (new WinDraw); cw->draw (); Shape* rm = new Rectangle (new MacDraw); rm->draw (); }
Behavioral Patterns Iterator Interpreter Chain of Responsibility Command Mediator Memento Observer ...
Iterator Objective: Provide a way to access the elements of a collection (e.g., stack, queue, array) without exposing the underlying data structure The idea is to take traversal out of the collection and have a separate object for it Many algorithms in C++ STL are based on iterators (i.e., using iterator object) Result of Iterator: Algorithms and data structures can stay independent (i.e., decoupled) For instance, algorithms like sort, find, merge etc. can stay decoupled from data structures array, tree, linked list etc.
Iterator – Example 1 The std::sort function of STL has the following declaration: This internally uses one of Quick Sort, Merge Sort, or Insertion Sort, which are based on only the Iterators first and last Does not matter if you are sorting an array, vector, or map template <class RandomAccessIterator> void sort(RandomAccessIterator first, RandomAccessIterator last);
Iterator Example 2 class Stack { int items[10]; int sp; public: Stack(){sp = - 1;} void push(int in){ items[++sp] = in; } int pop(){return items[sp--];} friend class StackIter; StackIter *createIterator() const;}; class StackIter { const Stack *stk; int index; public: StackIter(const Stack *s){stk = s;} void first(){ index = 0;} void next(){index++;} bool isDone(){return index == stk->sp+1;} int currentItem(){return stk->items[index];} }; StackIter *Stack::createIterator()const{ return new StackIter(this); } Code taken from sourcemaking.com
Iterator Example 2 contd. bool operator == (const Stack &l, const Stack &r) { StackIter *itl = l.createIterator(); StackIter *itr = r.createIterator(); for (itl->first(), itr->first(); !itl->isDone(); itl->next(), itr->next()) if (itl->currentItem() != itr->currentItem()) break; return itl->isDone() && itr->isDone(); } Code taken from sourcemaking.com
Iterator Advantages Can add functionality beyond the ones that are supported already Does not clutter the “data structure/collection” class with different ways of accessing it Could be thought of as counteracting encapsulation, since you are giving “friend” access to the Iterator, but this enables more decoupling later on
Observer Objective: Define a one-to-many dependency between objects such that when one object changes, a number of other objects have to be updated Example: In an online Poker game, whenever somebody plays a hand, the game state in all players’ windows have to be updated Image taken from sourcemaking.com
Observer - Before Code taken from sourcemaking.com class Subject { class DivObserver { int m_div; public: DivObserver(int div){m_div = div;} void update(int val){ cout << val << " div " << m_div << " is " << val / m_div << '\n';} }; class ModObserver int m_mod; ModObserver(int mod){m_mod = mod;} cout << val << " mod " << m_mod << " is " << val % m_mod << '\n';} class Subject { int m_value; DivObserver m_div_obj; ModObserver m_mod_obj; public: Subject(): m_div_obj(4), m_mod_obj(3){} void set_value(int value){ m_value = value; notify(); } void notify(){ m_div_obj.update(m_value); m_mod_obj.update(m_value); }; Code taken from sourcemaking.com
Observer - After Code taken from sourcemaking.com class DivObserver: public Observer { int m_div; public: DivObserver(Subject *model, int div){ model->attach(this); m_div = div; } void update(int v){ cout << v << " div " << m_div << " is " << v / m_div << '\n';} }; class ModObserver: public Observer int m_mod; ModObserver(Subject *model, int mod){ m_mod = mod; } cout << v << " mod " << m_mod << " is " << v % m_mod << '\n';} class Observer{ public: virtual void update(int value) = 0; }; class Subject{ int m_value; vector m_views; void attach(Observer *obs){ m_views.push_back(obs); } void set_val(int value){ m_value = value; notify(); void notify(){ for (int i = 0; i < m_views.size(); ++i) m_views[i]->update(m_value);} Code taken from sourcemaking.com
Summary Design patterns can speed up development process when applied Help avoid design problems that might show in the later stages The difference between certain design patterns are subtle Requires paying close attention Some patterns are almost obvious Still, they are sometimes not applied and sub-optimal designs are used Make later changes difficult A common theme in all the patterns is to “Anticipate future changes” Underscores the importance of writing easy-to-change code
References Design Patterns: Elements of Reusable Object-Oriented Software by Gang of Four (GoF) http://www.sourcemaking.com