830 likes | 1.06k Views
Section 2. Agile Design. Chapter 7. What is Agile Design?. What is design?. 1992, Jack Reeves The design of a software system is documented primarily by its source code. Diagrams are ancillary to the design and not the design itself. Harbinger of agile development!
E N D
Section 2 Agile Design
Chapter 7 What is Agile Design?
What is design? • 1992, Jack Reeves • The design of a software system is documented primarily by its source code. Diagrams are ancillary to the design and not the design itself. • Harbinger of agile development! • UML diagrams represent parts of design, not the design • Design is overall shape & structure of program, along with shape & structure of each module, class & method.
Odors of Rotting Software • Rigidity • Single change causes cascade of changes • Fragility • Single changes causes breaks in many other, often unrelated areas. Modules constantly in need of repair, always on the bug list. • Immobility • Contains parts that could be useful elsewhere, but too much work to separate those parts. Very common!
Odors of Rotting Software, cont. • Viscosity • Viscosity of software. Some ways to make changes preserve the design, some do not (hacks). If hacks are easier than design preservation, viscosity is high. • Viscosity of environment. Occurs when development environment is slow and inefficient. If compile takes too long, will make changes that don’t require recompile, even if doesn’t preserve design. If CVS is cumbersome, won’t use. • Needless Complexity • Contains elements that aren’t currently useful. Preparing for too many contingencies litters code with unused constructs.
Odors of Rotting Software • Needless Repetition • Cut-and-paste can be disastrous code-editing operations! Miss an abstraction, miss an opportunity to make system easier to understand and maintain. (read book fravle an arvadent example!) • Opacity • Opacity: tendency to be difficult to understand. Code seems clear when first written. Later even same developer may not understand it. Code reviews! Refactor as you go!
Agile Avoids Rot • Agile team thrives on change • Keep design as clean and simple as possible • Back up design with unit & acceptance tests • Continuously improve design • Each iteration ends with system whose design is as appropriate as it can be for requirements in that iteration
Reading • Read The “Copy” Program pages 90-92 • How did the example illustrate: • Rigidity • Fragility • Immobility • Complexity • Redundancy • Opacity • Explain 2 of the above • Page 93 – do you agree that it’s better to put off extra output devices for now?
Chapter 8 SRP: The Single-Responsibility Principle
SRP Explained http://www.youtube.com/watch?v=ZdbEQBlfpfU
Cohesion • We want our classes to be “cohesive” – but what does that mean? • One way to make specific: single responsibility principle (SRP) • Functions that change together, exist together.
Responsibility Example • A responsibility is a “reason for change” interface Modem { public void dial(String pno); public void hangup(); public void send(char c); public char recv(); } Seems like good set of modem functions – but is it one or two sets of responsibilities? • Connection management (dial and hangup) • Data communication (send and recv) Separate if connection functions are changing – otherwise have rigidity. Don’t separate if no changes expected – otherwise have complexity. An axis of change is an axis of change only if the changes actually occur.
Modem Code public class BroadbandStreamModem implements Modem { public void dial(String pno) { System.out.println("dial bband");} public void hangup() { System.out.println("hangup bband");} public void send(char c) { System.out.println("send stream");} public char recv() { System.out.println("receive stream"); return ' ';}} public class PhoneStreamModem implements Modem { public void dial(String pno) { System.out.println("dial phone");} public void hangup() { System.out.println("hangup phone");} public void send(char c) { System.out.println("send stream");} public char recv() { System.out.println("receive stream"); return ' ';}} public class PhonePacket implements Modem { public void dial(String pno) { System.out.println("dial phone");} public void hangup() { System.out.println("hangup phone");} public void send(char c) { System.out.println("send packet");} public char recv() { System.out.println("receive packet"); return ' ';}} public class BroadbandPacketModem implements Modem { public void dial(String pno) { System.out.println("dial bband");} public void hangup() { System.out.println("hangup bband");} public void send(char c) { System.out.println("send packet");} public char recv() { System.out.println("receive packet"); return ' ';}}
Separate based on reason to change public interface DataChannel { public void send(char c); public char recv(); } public class StreamDataChannel implements DataChannel { @Override public void send(char c) { System.out.println("send stream"); } @Override public char recv() { System.out.println("receive stream"); return ' '; } } // same for Packet public interface Connection { public void dial(String pno); public void hangup(); } public class PhoneConnection implements Connection { public void dial(String pno) { System.out.println("dial phoneline"); } public void hangup() { System.out.println("hangup phoneline"); } } // similar for Broadband
Putting it together public class FlexibleModem { private Connection connection; private DataChannel dataChannel; public FlexibleModem(Connection connection, DataChannel dataChannel) { this.connection = connection; this.dataChannel = dataChannel; } public void doConnection() { connection.dial("someone"); dataChannel.send('X'); dataChannel.recv(); connection.hangup(); System.out.println(); }
Continued public static void main(String[] args){ ArrayList<FlexibleModem> modems = new ArrayList<FlexibleModem>(); modems.add(new FlexibleModem(new BroadbandConnection(), new PacketDataChannel())); modems.add(new FlexibleModem(new BroadbandConnection(), new StreamDataChannel())); modems.add(new FlexibleModem(new PhoneConnection(), new PacketDataChannel())); modems.add(new FlexibleModem(new PhoneConnection(), new StreamDataChannel())); for (FlexibleModem modem : modems) { modem.doConnection(); } } }
Another example // from http://stackoverflow.com/questions/1354624/what-is-your-best-example-of-a-violation-of-the-single-responsibility-principle public class Session { public void startSession(){ // send HTTP session cookies } public void activateUserAccount() { // query the DB and toggle some flag in a column } public void generateRandomId() {} public void editAccount() { // issue some SQL UPDATE query to update an user account } public void login() { // perform authentication logic } public void checkAccessRights() { // read cookies, perform authorization } }
Chapter 9 OCP: The Open-Closed Principle
Open-Closed Principle • Bertrand Meyer: Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. • Open for extension – extend with new behaviors • Closed for modification – extending behavior does not result in changes to source code or binary code of module. .exe, DLL, .jar remain untouched! • If single change causes cascade of changes, refactor. Further changes should be achieved by adding new code. How??
Procedural Shapes – non OCP typedef struct Shape *ShapePointer; void DrawAllShapes(ShapePointer list[], int n) { int i; for (i=0; i<n; ++i) { struct Shape* s = list[i]; switch (s->itsType) { case square: DrawSquare((struct Square*) s); break; case circle: DrawCircle((struct Circle*) s); break; }//switch } //for } //Draw enum ShapeType {circle, square}; struct Shape { ShapeType itsType; }; //circle.h struct Circle { ShapeType itsType; double itsRadius; Point itsCenter; }; void DrawCircle (struct Circle*); //square.h struct Square { ShapeType itsShape; double itsSide; Point itsTopLeft; }; void DrawSquare (struct Square*); Can we add a Line shape without changing the existing code? NO!!!
Procedural Shapes – non OCP enum ShapeType {circle, square, line}; struct Shape { ShapeType itsType; }; //circle.h struct Circle (no change) //square.h struct Square (no change) //line.h struct Line { ShapeType itsShape; Point startPoint; Point endPoint; }; void DrawLine (struct Line*); typedef struct Shape *ShapePointer; void DrawAllShapes(ShapePointer list[], int n) { int i; for (i=0; i<n; ++i) { struct Shape* s = list[i]; switch (s->itsType) { case square: DrawSquare((struct Square*) s); break; case circle: DrawCircle((struct Circle*) s); break; case line: DrawLine((struct Line*) s); break; }//switch } //for } //Draw Existing enum must be modified Existing switch must be modified NOT CLOSED!! (and a real system might have more switch statements to update)
Procedural Code - analysis • Rigid – try to add a Triangle, must recompile Shape, Circle, Square, DrawAllShapes • Fragile – many switch/case and if/else statements to understand and modify • Immobile – try to reuse DrawAllShapes must include Square and Circle, even if application has none of those
OCP Shapes // two child classes class Square : public Shape { public: virtual void Draw() const; }; class Circle : public Shape { public: virtual void Draw() const; }; // abstract Shape class class Shape { public: virtual void Draw() const=0; }; void DrawAllShapes (vector<Shape*>& list) { vector<Shape*>::iterator i; for (i=list.begin(); i != list.end(); i++) { (*i)->Draw(); } } Can we add a Line without changing the existing code? YES!! WOO HOO!!!
OCP Shapes // two child classes class Square : public Shape { public: virtual void Draw() const; }; class Circle : public Shape { public: virtual void Draw() const; }; class Circle : public Line { public: virtual void Draw() const; }; // abstract Shape class class Shape { public: virtual void Draw() const=0; }; No change to Shape class! No change to Square or Circle! No change to DrawAllShapes! Woo Hoo! void DrawAllShapes (vector<Shape*>& list) { vector<Shape*>::iterator i; for (i=list.begin(); i != list.end(); i++) { (*i)->Draw(); } }
Adjust to change appropriately * like DRY – do it the first time you repeat code. Here we switch to OCP the first time we need an extension. Fool me once, shame on you. Fool me twice, shame on me. When change happens, implement abstractions to protect from future changes of that kind. Modem example: might combine functions originally. As soon as need to change connection, add an abstraction (in this example, an interface).*
Abstraction is the Key! The what, not the how • Abstraction may be fixed • Behavior can be extended by creating new derivatives of the abstraction <<interface>> Client Interface Client Server Client Client not opened & not closed Server STRATEGY pattern: Client is both open and closed Behavior in Client can be extended and modified by creating new subtypes of ClientInterface.
Abstraction based on interface What not how! This part is “closed” public interface ChatListener { public String messageReceived(); } Write as many implementations as you want – “open” public class ChatClient1 implements ChatListener { public String messageReceived() { return "This is the message you received."; } } public class ChatClient2 implements ChatListener { public String messageReceived() { java.util.Scanner scan = new java.util.Scanner(System.in); System.out.println("Enter the message: "); String msg = scan.nextLine(); return msg; } } public class ChatServer { private ChatListener listener; public ChatServer(ChatListener listener){ this.listener = listener; System.out.println("Your message: " + listener.messageReceived()); } public static void main(String[] args) { ChatServer pgm = new ChatServer(new ChatListener1()); } } could pass in as parameter, etc.
Data Driven OCP – relates to homework • Idea: Store the data that is “open” in a table • Goal: impose an ordering so all shapes of one kind are drawn before shapes of another. Use a table to control ordering. • Example code in textbook: • Shapes class as parent • Shapes children added as needed (e.g., Circle, Square) • To display shapes in a specific order, first sort shapes, then draw (figure 9-4)
See it in Java public abstract class Shape implements Comparable<Shape>{ private String[] shapeOrder = {"Line","Square","Circle"}; public abstract void draw(); public String toString() { return "Hi, I am a " + this.getClass().getName(); }
See it in Java public int compareTo(Shape otherShape) { String myName = this.getClass().getName(); String otherName = otherShape.getClass().getName(); int thisOrd = -1; int otherOrd = -1; for (int i=0; i<shapeOrder.length; i++) { if (myName.equals(shapeOrder[i])) thisOrd = i; if (otherName.equals(shapeOrder[i])) otherOrd = i; if (thisOrd >= 0 && otherOrd >= 0) break; } if (thisOrd < otherOrd) return -1; else if (thisOrd > otherOrd) return 1; else return 0; }
Data-Driven OCP continued public class ShapeDemo { ArrayList<Shape> shapes = new ArrayList<Shape>(); public void addShapes() { shapes.add(new Square()); shapes.add(new Line()); shapes.add(new Circle()); shapes.add(new Line()); shapes.add(new Square()); } public void printShapes() { Collections.sort(shapes); for (Shape s : shapes) System.out.println(s); } public static void main(String[] args) { ShapeDemo demo = new ShapeDemo(); demo.addShapes(); demo.printShapes(); } }
Summary • Conformance to OCP yields great benefits • Just using an OO language does not guarantee OCP • Judicious use of abstraction is critical! • apply abstraction where needed (as soon as you detect a possible future change) • resist premature abstraction
Chapter 10 LSP: The Liskov Substitution Principle
What makes a good inheritance hierarchy? • LSP: Subtypes must be substitutable for their base types. • Meyers: Everything that is true of the base type must be true of the subtype.* • What if this were not true? This object causes me to misbehave (wrong data, invalid calculation, etc.) Function F parameter = Base Child Should I put a test in F to make sure I have a Base? NO! That would violate OCP! * Effective C++, Scott Meyers
Is a Square a Rectangle?? class Rectangle { public: void SetWidth(double w) { itsWidth = w; } void SetHeight(double h) { itsHeight = h; } double GetHeight() { return itsHeight; } double GetWidth() { return itsWidth; } private: Point itsTopLeft; double itsWidth; double itsHeight; }; Rectangle Square Issues: • Square doesn’t need both itsWidth and itsHeight • Having both SetWidth and SetHeight inappropriate
Naïve fixes do not work! Could override SetHeight and SetWidth: void Square::SetWidth(double w) { Rectangle::SetWidth(w); Rectangle::SetHeight(w); } void Square::SetHeight(double w) { Rectangle::SetWidth(w); Rectangle::SetHeight(w); } But these aren’t virtual*, so would have problem in these cases (do you understand why??) void f(Rectangle& r) { r.SetWidth(32); // Always calls Rectangle::SetWidth } void f2(Rectangle* r) { r->SetWidth(32); } We could fix this by making these virtual, but is that really the best idea? * Meyers item 36: Never redefine an inherited non-virtual function.
In case you aren’t convinced…. Making SetWidth and SetHeight virtual fixes previous issue, but there are still potential issues! void g(Rectangle& r) { r.SetWidth(5); r.SetHeight(4); assert(r.Area() == 20); } The author of g assumed that changing the height of a rectangle would not change its width – a reasonable assumption for a rectangle! Function g shows that there are functions that take references to Rectangle objects that do not operate properly on Square objects. So Square is NOT a valid substitute for a Rectangle. Making it a child violates the LSP principle!
Same issues in Java public class MyRectangle { private int itsWidth; private int itsHeight; public MyRectangle(int itsWidth, int itsHeight) { super(); this.itsWidth = itsWidth; this.itsHeight = itsHeight; } public void setItsWidth(int itsWidth) { this.itsWidth = itsWidth; } public void setItsHeight(int itsHeight) { this.itsHeight = itsHeight; } public int calcArea() { return itsWidth * itsHeight; } }
Square is a rectangle –maybe? public class MySquare extends MyRectangle { public MySquare(int width, int height) { super(width, width); } public void setItsWidth(int width) { super.setItsHeight(width); super.setItsWidth(width); } public void setItsHeight(int height) { super.setItsHeight(height); super.setItsWidth(height); } }
Houston, we have a problem… public class TestArea { @Test public void testArea() { MyRectangle rect = new MyRectangle(3, 4); Assert.assertEquals(12, rect.calcArea()); rect = new MySquare(0, 0); rect.setItsHeight(4); rect.setItsWidth(3); Assert.assertEquals(12, rect.calcArea()); } }
Validity is not Intrinsic – Behavior Counts! • An inheritance hierarchy cannot be evaluated abstractly. In the abstract sense, a square is a rectangle. But when we look at the behavior (as expressed in code), we see the issue. • “is-a” is only an approximate way to identify parent-child relationships.
So how do we fix this? Factoring! If a set of classes all support a common responsibility, they should inherit that responsibility from a common superclass.
Is an E-Book a Book? Does restock apply to an Ebook? NO! Is there commonality between these classes? YES! What to do? FACTOR!
Discussion Item (not really LSP): Do we always need subclasses? Is a non-degree student a student?
Design By Contract – another view of LSP • Since we want to avoid needless complexity, what behavior is reasonable to assume? • One technique for making reasonable assumptions explicit is design by contract (DBC) suggested by Bertrand Meyer. • Author of class explicitly states contract for class – behaviors that can be relied on. Declare preconditions and postconditions for each method. • Example: int factorial(int n) { . . .} • precondition: n >= 0 • postcondition: returns n!
Design By Contract, continued • Rule for preconditions and postconditions in derived classes: • May only replace original postcondition by one equal or stronger. • SetWidth postcondition in Rectangle: • itsWidth = parameter w && itsHeight == old.itsHeight • SetWidth postcondition in Square: • itsWidth = parameter w • does not enforce itsHeight == old.itsHeight – so this postcondition is weaker, would not be allowed
Design By Contract, continued • May only replace original precondition by one equal or weaker • BankAccount: • has a balance field • if call close() method on a BankAccount, balance = 0 • SpecialAccount: • earns extra interest • must not be closed for at least 90 days • close • original precondition: a BankAccount object • revised precondition: a BankAccount object (child is-a parent), and account has been open 90 days What would happen with a JUnit test?
is-a vs has-a Assume you want to implement a Stack class. You already have a LinkedList class. Would you do: • public class Stack extends LinkedList • public class Stack { private LinkedListtheData;
Heuristics • A derivative that does less than its base is usually not substitutable for that base, and therefore violates LSP. • If the users of the base class don’t expect exceptions, adding them to methods of the derivatives is not substitutable. • IS-A is too broad to be a definition of a subtype. Substitutable is more appropriate, where substitutability is defined by either an explicit or implicit contract.