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
  • These principles are somewhat fuzzy and not mutually exclusive
  • They must be learned by specific coding examples/experiences
  • Here we outline principles and follow up by pointing out good and bad design in your projects.
  • 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 change and extend.
  • Use basic O-O principles to increase flexibility:
    • Encapsulate what varies -- if something is changing a lot it should be its own class/method
    • Code to interfaces, not implementations
      -- a lesson of Assigment 1
    • Share common behavior via inheritance
  • If design is proving to be not flexible, refactor it
    -- "A stitch in time saves nine" applies to software, too. (We cover the details of refactoring later in lecture)
  • Make classes cohesive: class should have a single, clearly stated purpose which fits all of its fields and methods.
    -- and as your software matures, refactor to improve cohesion as you understand the domain better
    -- we will cover a similar principle below, the Single Responsibility Principle (SRP).

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 examples in this blog post which apply to JavaScript front-ends.

Patterns and Anti-Patterns

  • A design pattern refers to a good principle of design which is a recipe to some degree principle is a more nebulous version of a pattern)
  • The term "design pattern" usually 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.
    (a bad smell is the refactoring terminology for an anti-pattern, more from looking directly at code)

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 / 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
  • One sign of data-centric design is one really fat class doing all the operations in the center of the design (the "God Class") and a bunch of tiny classes that just passively hold data on the edge (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.

Example

The blog post we covered in "encapsulate what varies" also contains a God Class example, the library class (scroll most of the way down for it).

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 Hare and Hounds, having a GameState class with only board position data and putting move legality checking code in e.g. HHService. Don't do that, put move logic with the GameState (which also should be called something else since its more than data)

More anti-patterns of software engineering.

The Open-Closed Principle (OCP)

See p.86 of Head First DP's and p. 377 of HFOOA&D.

Make code which is open for extending, but closed for modifying

  • What this means is to be very careful in your use of inheritance -- either
    • don't allow subclasses at all - declare classes final and use composition to "plug in" the part that incorporates the extension
    • allow subclassing but declare nearly all methods final so subclasses cannot modify them; only a few methods are to be overridden, in contractually circumscribed ways
  • It 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 OCP.

    Don't Repeat Yourself (DRY)

    HFOOA&D p. 382.
    Avoid duplicate code -- abstract out things in common to a single location.
    • 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.
    • In general, repetitious code is a sign of sloppy coding practice, its an anti-pattern.
    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 almost identical to the cohesion principle at the top of these notes
    • Obvious violating example: if model, view, and controller functionality were all in one class, that class has three distinct foci.
    • 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 the methods belong in the class.
    Example:
    class Automobile
      start()
      stop()
      changeTires()
      drive()
      wash()
      checkOil()
      getOil()
    
    -- for each method X, ask, "does the car 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!!
    • Note that just because "X is a 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 above) 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
      
    //   .. r.getWidth() == (r.setHeight(4).getWidth()) 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.

    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 methods were not allocated precisely enough.
    • 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 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.
    • 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.

    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 obey 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?

    • The important point is you should be doing one or the other (or some of both), and if there is a contract make sure it is documented.
    • The more distant your users are (e.g. if you are writing a library), the more defensive&contractual you need to be.
      -- Was the RESTful interface in homework 1 as we defined it more contractual or defensive?
    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 really 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.