Design Principles

  • This lecture is a collection of design principles for making better software.
  • Every great programmer has a toolbox of design principles they use to help them produce great code
  • Yes, these principles are admittedly fuzzy and not mutually exclusive
  • They must be learned by specific coding examples/experiences
  • We assume some knowledge of Object-Oriented Design; if you have not heard of any of this look e.g. at the recommended book Head-First Object-Oriented Analysis and Design (HFOOA&D below) .

Basic Analysis and Design Principles

Here are some basic design principles you probably have already heard about:
  • Well-designed software is easier to debug, change and extend.
  • Code to interfaces, not implementations
    -- For example, in Assigment 1 you should have been coding to the RESTful interface specification, not to the front-end code.
  • Share common behavior via inheritance
  • If design is proving to be inflexible, refactor it to restore it to be a good design (Refactoring is a lecture topic on its own later)
  • Make classes cohesive: class should have a single, clearly stated purpose which fits its name and all of its fields and methods.
    • Similarly at the lower level of methods: the name should be (all that) it does.
    • We will cover a similar principle below, the Single Responsibility Principle (SRP).
  • Separation of Concerns (SoC) -- don't have many different concerns in one class; instead, different tasks/aspects should be in different classes/functions.
    -- also related to SRP below, SRP is "one concern per class"

Encapsulate What Varies

aka encapsulate code that changes a lot. This basic O-O principle you may not know as well, here is a brief overview.
  • One way that ugly code arises is new code has to be patched in as new features are added
  • If you push code deeper into classes and behind encapsulation boundaries the change is isolated, code is more maintainable.
  • We will cover the JavaScript library checkout example in this blog post.
Original smelly code, any changes to policy on when customer can check out a book or when a book is available requires change to checkoutBook method:
var library = {
  checkoutBook: function (customer, book) {
    if (customer && customer.fine <= 0.0 && customer.card && customer.card.expiration === null &&
      book && !book.isCheckedOut &&
      (!book.reserveDate || book.reserveDate.getTime() > (new Date()).getTime())) {
      customer.books.push(book);
      book.isCheckedOut = true;
    }
    return customer;
  }
};
  
Improved code: pull out the concepts of a customer that canCheckoutBook into its own method, and similarly for a book that isAvailable (plus, in turn pull out even more methods hasFine and hasActiveLibraryCard etc from those actions):
var library = {
  checkoutBook: function (customer, book) {
    if (customer.canCheckoutBook() && book.isAvailable()) {
      customer.checkout(book);
    }
    return customer;
  }
};
 
var customer = {
  canCheckoutBook: function () {
    return !this.hasFine() && this.hasActiveLibraryCard();
  },
  hasFine: function () {
    return this.fine > 0.0;
  },
  hasActiveLibraryCard: function () {
    return this.card !== null && this.card.expiration === null;
  },
  checkout: function (book) {
    //implementation
  }
};
 
var book = {
  isAvailable: function () {
    return !this.isCheckedOut && !this.isReserved();
  },
  isReserved: function () {
    return this.reserveDate !== null &&!this.isFutureReserve();
  },
  isFutureReserve: function () {
    return this.reserveDate.getTime() > (new Date()).getTime();
  }
  };

The library code is now not needing to change at all if there is a change in the policy on when customers can check out books or what defines a book being available -- we Encapsulated what Varied - !

Patterns and Anti-Patterns aka Smells

  • The term "design pattern" means a particular structure and relationship between objects that is a common good pattern in object-oriented programming
    -- the term originates from the Design Patterns book (a topic for later in lecture).
  • A bad pattern that takes you in the opposite direction of where you should be going is an anti-pattern aka bad smell.

We are going to cover many patterns and anti-patterns later; for now we are going to do one anti-pattern which we want you to avoid in your iteration 2.

The Data-centric Design smell / God Class Anti-Pattern

One easy trap to fall into is data-centric design.
  • A data-centric design has classes with no meaningful methods
    -- they are just passive data holders
  • Data-centric designs usuallly have one really fat class doing all the operations (the "God Class") and a bunch of other classes with no real methods (the "Data Classes").
  • C programmers often produce data-centric OO designs: the central class hold all the functions, and the little data classes are like the C structs.
  • Data-centric designs should be refactored to push methods from the big class in the center out to the data classes.
  • There is a principle hiding here, lets make up a name for it: Push Operations to the Data Classes! (PODC).

Example

The blog post library example we covered in "encapsulate what varies" above is also a God Class, its doing all operations on books/customers and not pushing those out to book/customer classes.

Database vs Objects

If you have persistent data in a database that can lead you to a data-centric design.
  • Databases are all data no code, so it is temping to build corresponding objects which are just "data holders" -- data-centric, BAD.
    Bad example: in Lights Out, having a Board class with only board position data and putting move legality checking code in e.g. LightsOutController. Don't do that, put move logic with the Board (which also should be called something else since its more than board data)

The Open-Closed Principle (OCP)

Make code which is open for extending, but closed for modifying
  • i.e. keep your codebase easily extensible by isolating & limiting the spots that need to change
  • (Note its related to Encapsulate what Varies above)
  • Inheritance with significant overriding can violate OCP since overriding is modifying -- either
    • "favor composition over inheritance" (thats another principle, btw)-- use composition to "plug in" the part that incorporates the extension
    • allow subclassing but declare nearly all methods final to greatly limit or eliminate overriding.
  • OCP makes code more reliable since complex interdependencies don't have random changes injected into them by outsiders.
  • Library and framework designs have to strictly follow this principle.

    Example

    Here is a simple example of a refactoring which improves OCP-ness of the code.

    Don't Repeat Yourself (DRY)

    HFOOA&D p. 382.
    Avoid duplicate code -- abstract out things in common to a single location.
    • Finding this smell is easy, nearly-identical code blocks will repeat
    • The problem is if you don't abstract it out, you have two parallel codebases to try to keep consistent and you often fail.
    • And maybe two copies turns into three turns into four before you realize what is happening.
    • You can solve it by moving code to a common method, making a common superclass, etc.
    Here is an example from the book.

    The Single Responsibility Principle (SRP)

    HFOOA&D p. 390.
    Classes should not have more than one focus of responsibility.

    • Classes can reasonably be involved in different interactions, it is the focus that is the issue.
    • This principle is similar to the cohesion principle and Seperation of Concerns (SoC) mentiond at the top of these notes
    • Example we saw where concerns were separated: To Do app separated routing (Server class), request processing (ItemsController), actual to do data (Item), and persistence (ItemsRepository).
      • SRP would have been violated had we merged these classes
      • The way we coded it, each class has one locus of responsibility, summarized by the class name: SRP holds!
    • Data-centric designs always violate this principle: the fat class in the middle with all the methods has many different foci.
    SRP Analysis
    • A way to figure out if certain methods belong in a given class;
    • If not, move them to another existing, or new, class
    Definition of SRP Analysis by an "IRL" example:
    class Automobile
      start()
      stop()
      changeTires()
      drive()
      wash()
      checkOil()
      getOil()
    
    -- for each method X, ask, "does the Automobile have primary responsibility for X-ing?" If the answer is no, the method doesn't belong.

    The Book's answer (don't look until we have made a stab at it!)

    The Liskov Substitution Principle (LSP)

    See p. 400 of HFOOA&D.
    Subclass objects must always be substitutable for superclass objects
    • In other words, "is-a" means is-a!!
    • Just because "X is a (subcategory of) Y" informally makes sense doesn't mean that X always makes a good subclass of Y; the "is a" also has to make sense for the particular interfaces offered by X and Y.
    • If you violate this principle, your program may behave erratically; the LSP is an example of a contract (see below) and you have broken it.
    • This principle is yet another example of how you want to keep the "Objects are autonomous, active, things" view in your mind as good coding intuition - don't violate that intuition.

    Example

      public class Rectangle
    {
        int height, width;
        Rectangle(int w, int h) { height = h; width = w; }
        public int getHeight() { return height; }
        public int getWidth() { return height; }
        public int setHeight(int h) { height = h; }
        public int setWidth(int w) { width = w; }
        public int findArea()
        { return getHeight() * getWidth(); }
    }
    
    public class Square extends Rectangle
    {
        Square(int s) { new Rectangle(s,s); }
        public int setSide(int h) { width = h; height = h; } // set both to preserve square-ness
        public int setHeight(int h) { setSide(h); }
        public int setWidth(int w) { setSide(w); }
    }
    
      s = new Square(10);
      Rectangle r = s; // imagine this was a function call passing a Square to function asking for a Rectangle
      
      //  equivalence r.getWidth() == (r.setHeight(4).getWidth()) should hold but would fail for the above
      //     - bad attempt at Square is-a Rectangle.
    
    • Does Square is-a Rectangle hold??
    • First, we had to override width/height to try to keep square from becoming a non-square with aSquare.setHeight(4); .
    • But, the above code attempt violates rectangle invariant that setting a rectangle's width should not alter height: really bad code!
    • If there is no mutation (the width/height fields are final) the is-a relationship is reasonable - that interface supports is-a fully.

    The Interface Segregation Principle (ISP)

    This one is part of SOLID.

    Clients should not be forced to depend on methods (inherit from or implement) they don't use
    • To be more precise, the bad methods are ones that not only don't they use now, they will never conceivably want to use them because they intuitively "don't belong": the interface is too fat.
    • These extra methods are "junk" and clutter the design space; more fundamentally, they are a sign that the class/method structuring is not correct.
    • If you have this pattern it means you need to refactor, often by turning one interface into many.

    Example: Mouse Event Listeners in Java Swing

    • The Java Swing GUI library has different Listener interfaces for different events
    • Mouse events have many types of events: clicking but also just cursor movement or wheel motion
    • Most of the time programmers only care about clicks, not how the mouse is moving
    • If there was one single Mouse Listener interface users would need to write empty methods for the wheel/motion/etc events they don't care about.
    • Example Solution: Java Swing uses three separate interfaces for mouse events, MouseListener, MouseWheelListener, MouseMotionListener to "segregate" the types of mouse events; only implement the interfaces you need.

    Design by Contract and Defensive Programming

    See HFOOA&D book p. 464-465.
    These principles are about reliability. There are fundamentally two (not mutually exclusive) approaches to make software more reliable:

    • Design by contract: write a clear contract on how method caller is supposed to use method, and assume caller is discipined and obeys the contract to e.g. not pass a null object.
      Example: the To Do app RESTful API with the 4XX cases removed, assume the front-end is careful to not make such ill-formed calls.
    • Defensive programming: library writer is cautious and is guarding against callers improperly e.g. passing null object by explicitly checking for that condition and taking appropriate action.
      Example: The 4XX cases in the To Do app RESTful server.

    Another way to view it is DbC puts the onus on the caller and DP puts the onus on the callee of a method to meet the specification.

    Here is an example of design by contract from the book:

    And here is the defensive version of the same code:

    Which to do and how much?

    • You should usually be doing some of both in different aspects of application
    • If there is a contract make sure it is documented or users of API will not know about it.
    • The more distant your users are (e.g. if you are writing a library), the more defensive&contractual you need to be.
    Tradeoffs:
    • Defensive programming can slow down code due to the overhead of all the checks and raise new exceptions at runtime whereas contracts are compile-time
    • Contracts are just words so code may in fact not obey the intent of the contract and without defensive programming backup something bad could happen at runtime.

    The Dependency Inversion Principle

    (We are going to skip this one until the Factory pattern is covered in the design patterns lecture)

    See p.139 of Head First DP's. What is it?

    Don't depend on concrete classes, depend on abstractions
    Don't have high-level (user) code directly call/inherit from low-level (library) code; instead,
    1. Library or component publishes an interface (or, if thats not possible, an abstract class)
    2. Users write a class conforming to that interface (or extending abstract class), which then interacts with the other library classes.
    This is an inversion: in traditional software the higher-level components directly invoke the lower-level ones; this principle inverts that since the user code now depends on a high-level interface: dependency inversion has taken place.

    Why does this help?

    • First, it allows different low-level implementations to be swapped out; as long as they implement the common interface all is well.
    • More generally, it increases encapsulation.
    • This principle is widespread in well-written libraries; for example when you run the debugger on your Swing app you will see all these strange implementation class names you have never heard of which subclass or implement the class/interface you were interacting with.

    The Principle of Loose Coupling

    (We are going to skip this one until the Observer pattern is covered in the design patterns lecture)

    See p. 53 of Head First DP's.

    Strive for loosely coupled designs of autonomous, interacting objects

    Examples

    • Swing Listeners: the Swing event system and the user's action code need to know almost nothing about each other besides the methods on the listener.
    • MVC in general illustrates the advantages of loose coupling

    Deeper Philosophy

    There is a deeper principle here:

    • The more complex the system the more loosely coupled, autonomous, and multi-layered it needs to be.
    • Think of the human body for example: there are components, sub-components, sub-sub-components, etc.
    • It wasn't consciously designed that way, it emerged that way.

    The Principle of Least knowledge

    (We are going to skip this one until the Facade pattern is covered in the design patterns lecture)

    See p. 265 of Head First DP's.

    Talk only to your immediate friends
    • Don't dig deep inside your friends for friends of friends of friends and get in deep conversations with them -- don't do
      aWindow.getPane().getRasterizer().setUpdateFrequency(60)
    • Code is more convoluted if too many objects are directly interacting with one another, and bugs are more likely to be introduced as the code evolves over time.
    • Solution: Let the shared friend be an intermediary instead of introducing lots of long-range dependencies.
      aWindow.useHighUpdateFrequency()
    • This is related to the principle of loose coupling, things close are couple tightly and things far are coupled loosely.