Object-oriented design

From NoskeWiki
Jump to: navigation, search

About

Object oriented design (ODD) is a set of design principles for object-oriented programming - creating objects to communicate which each other in a working program.


Design Pattern

A design pattern is a reusable solution to a commonly occurring problem in the context of software development.

Some of the most popular patterns are:

  • The GRASP patterns ("General Responsibility Assignment Software Patterns) - guidelines for assigning responsibility to classes and objects in object-oriented design.
  • The GoF patterns ("Gang of Four") - 23 patterns from the popular book "Design Patterns: Elements of Reusable Object-Oriented Software " by four authors - Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides - hence gang of four.


GRASP Patterns

The GRASP patterns/guidelines include:

  • Information Expert - placing the responsibility on the class with the most information required to fulfill it ... (see: Information hiding)
  • Creator - responsible for creating an object of class ... (see also: Factory pattern)
  • Controller - non-user interface object responsible for receiving or handling a system event ... (see also: Model-view-controller)
  • Low Coupling - an evaluative pattern, which dictates how to assign responsibilities to support low dependency between classes and high reuse potential ... (see also: Loose coupling)
  • High Cohesion - attempts to keep objects appropriately focused, manageable and understandable ... (see also: Cohesion)
  • Polymorphism - responsibility of defining the variation of behaviors based on type is assigned to the types for which this variation happens ... (see also: Polymorphism in OOP)
  • Pure Fabrication - a class that doesn't represent a concept in the problem domain, specially made up to achieve low coupling, high cohesion, and code reuse. This kind of class is called "Service" in domain-driven design. ... (see also: Service (systems architecture))
  • Indirection - supports low coupling (and reuse potential) between two elements by assigning the responsibility of mediation between them to an intermediate object ... (see also: delegation pattern)
  • Protected Variations - protects elements from the variations on other elements (objects, systems, subsystems) by wrapping the focus of instability with an interface and using polymorphism ... (see also: delegation pattern)



GoF Patterns

The GoF patterns include:

  • Creational patterns (5): Factory method pattern, Abstract factory pattern, Singleton pattern, Builder pattern, Prototype pattern
  • Structural patterns (7): Adapter pattern, Bridge pattern, Composite pattern, Decorator pattern, Facade pattern, Flyweight pattern, Proxy pattern
  • Behavioral patterns (11): Chain-of-responsibility pattern, Command pattern, Interpreter pattern, Iterator pattern, Mediator pattern, Memento pattern, Observer pattern, State pattern, Strategy pattern, Template method pattern, Visitor pattern

Below I have listed out some of the best known of these object-oriented "design patterns":



Singleton (*)

A singleton pattern restricting the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system. It is typically implemented with a "getInstance()" method using "lazy initialization" as per the following example:

public class Singleton
{
  private static Singleton instance = null;
  private Singleton() {}
  public static synchronized Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
    }
    return instance;
  }
}

Another solution is considered the "traditional" simple way is thread-safe, but requires the class to be instantiated earlier than it's probably needed:

public class Singleton {
  private static final Singleton instance = new Singleton();
  private Singleton() { }      // Private constructor prevents instantiation from other classes.
  public static Singleton getInstance() {
    return instance;
  }
}

Singletons are often preferred to global variables because they don't pollute the global name space and permit lazy allocation and initialization.


Factory pattern (*)

The factory method pattern deals with the problem of creating objects (products) without specifying the exact class of object that will be created. A factory object typically has a method for every kind of object it is capable of creating and can have descriptive method names for each. These methods optionally accept parameters defining how the object is created and return the object created. Here's an example of a factory method:

public class ImageReaderFactory
{
  public static ImageReader getImageReader(InputStream is)
  {
    int imageType = determineImageType(is);
    switch(imageType)
    {
      case ImageReaderFactory.GIF:
        return new GifReader(is);
      case ImageReaderFactory.JPEG:
        return new JpegReader(is);
      // Etc.
    }
  }
}
...
public interface ImageReader {
  public DecodedImage getDecodedImage();
}
public class GifReader implements ImageReader {
  public DecodedImage getDecodedImage() { ...  return decodedImage; }
}
public class JpegReader implements ImageReader {
  public DecodedImage getDecodedImage() { ...  return decodedImage; }
}


Adapter

An adapter pattern, also known as "Wrapper", translates one interface for a class into a compatible interface expected by a client. An Adapter lets classes work together that couldn't otherwise because of incompatible interfaces. The example below examines how an Adapter could be used to allow the client to create a RoundPeg using the same method the client is used to for SquarePeg.

//## OUR "TARGET" CLASS  (what client is expecting)
public class SquarePeg {
  public void insert(String str) {
    System.out.println("SquarePeg insert(): " + str);
  }
}
 
//## OUR "ADAPTEE" CLASS (what client doesn't understand)
public class RoundPeg {
  public void insertIntoHole(String msg) {
    System.out.println("RoundPeg insertIntoHole(): " + msg);
  }
}
 
//## ADAPTER CLASS:  (** allows access to adaptee in same method as target would **)
public class PegAdapter extends SquarePeg {
    private RoundPeg roundPeg;
    public PegAdapter(RoundPeg peg)  {this.roundPeg = peg;}
    public void insert(String str)  {roundPeg.insertIntoHole(str);}
}
 
//## EXAMPLE OF USE:  (client)
public static void main(String args[])
{
  RoundPeg roundPeg = new RoundPeg();             // Create some pegs.
  SquarePeg squarePeg = new SquarePeg();
  squarePeg.insert("Inserting square peg...");    // Setup our square peg.
                // WILL OUTPUT: SquarePeg insert(): Inserting square peg...
 
  PegAdapter adapter = new PegAdapter(roundPeg);  // Client only understands "insert()" but we use adapter to put.
  adapter.insert("Inserting round peg...");       // Our round peg into square hole
               // WILL OUTPUT: RoundPeg insertIntoHole(): Inserting round peg..
}


Bridge

The bridge pattern, is meant to "decouple an abstraction from its implementation so that the two can vary independently". An example would be a "PersistenceImplementor" (apater) class which is created but will call either "FileSystemPersistenceImplementor" or "DatabasePersistenceImplementor" (two different classes which implement PersistenceImplementor) depending on wether the DB is available to save data or a files must be used (see full example). The bridge uses encapsulation, aggregation, and can use inheritance to separate responsibilities into different classes.


Visior

The visitor pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying those structures. An example in Java code:

//## THE ABSTRACT ELEMENT WE WANT TO ADD FUNCTIONALITY TO:
interface BikeElement {
  void accept(BikeElementVisitor visitor);   // BikeElements have to provide accept() to matching visitor.
}
 
//## CONCRETE IMPLEMENTATION OF ELEMENT:
class Wheel implements BikeElement {
  private String name;
  public Wheel(String name) { this.name = name;  }
  public String getName()   { return this.name;  }
  public void accept(BikeElementVisitor visitor) {  visitor.visit(this); }
}
class Engine implements BikeElement {
  public void accept(BikeElementVisitor visitor) { visitor.visit(this); }
}
class Bike implements BikeElement {  // This one is our whole bike but needs "accept()" too.
  BikeElement[] elements;
  public Bike() {                    // Create new Array of elements
    this.elements = new BikeElement[] { new Wheel("front"), new Wheel("back"), new Seat() };
  }
  public void accept(BikeElementVisitor visitor) {     
    for(BikeElement elem : elements) {
      elem.accept(visitor);
    }
    visitor.visit(this);
  }
}
 
//## THE VISITOR INTERFACE:
interface BikeElementVisitor {
  void visit(Wheel wheel);
  void visit(Seat seat);
}
 
//## CONCRETE IMPLEMENTATION OF VISITOR:
class BikeElementVisitorCheckPart implements BikeElementVisitor {
  public void visit(Wheel wheel) {
    System.out.println("check " + wheel.getName() + " wheel");
  }
  public void visit(Seat seat) {
    System.out.println("adjust seat");
  }
}
 
//## TEST:
Bike bike = new Bike();
bike.accept(new BikeElementVisitorCheckPart());    // Prints "check front wheel",
                                                   // "check back wheel" and "adjust seat".

Command

The command pattern is a bit like a Macro/script. A command pattern encapsulates commands (method calls) in objects allowing us to issue requests without knowing the requested operation or the requesting object. Command design pattern provides the options to queue commands, undo/redo actions and other manipulations.

Three terms always associated with the command pattern are:

  1. client - instantiates the command object and provides the information required to call the method at a later time.
  2. invoker - decides when the method should be called.
  3. receiver - an instance of the class that contains the method's code.

A good use of command patter is multilevel undo - if all user actions in a program are implemented as command objects, the program can keep a stack of the most recently executed commands. When the user wants to undo a command, the program simply pops the most recent command object and executes its undo() method.


Proxy (*)

The proxy pattern is intended to provide a "Placeholder" or "flyweight class" for a more complex object in order to control references to it and minimize memory footprint. An example is an image viewer program being able to list all "Images" using a "ProxyImage" class, but only if the "showImage()" is called does program instantiate the matching "HighResolutionImage" class (see more examples).

Observer pattern (*)

The observer pattern is where an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. Both Java and C# have "Observable" and "Observer" classes and within your code the idea is to:

  1. to add observers with "observableInstance.addObserver(observerInstance);"
  2. have the Observable to call "notifyObservers(someArgument)" when an update is needed
  • give the Observer an "update(Observer o, Object arg)" function which automatically receives this call and does something

A classic example is in the Model View Controller Pattern - where the observer used to decouple the model from the view. In this case we usually want the view to change when we change out object.... hence view represents the observer and the model is the observable object.

Inheritance versus Aggregation

Two important concepts in object oriented design are inheritance and aggregation (click to read more). Code reuse is also best achieved by aggregation when there is no is-a relationship. Generally speaking:

  • inheritance should be used when one class represents and builds on another... "Square" is a "Shape".
  • aggregation should be used when one class necessarily includes another classes... "Checkerboard" must have a "Square".
class QueueByAggregation {
  private LinkedList qList;         // Aggregation is to include another object and change its behavior.
 
  QueueByAggregation() {    // Constructor.
    qList = new LinkedList();
  }
 
  public boolean isEmpty() { return qList.isEmpty(); }
  public void enqueue(Object item) { qList.insertAtBack(item); }
  public Object dequeue() {
    if (qList.isEmpty()) return null;
    else return qList.deleteFromFront();
  }
  public Object peek() {
    return (qList.isEmpty() ? null : qList.head.getData());
  }
}
 
class StackByInheritance extends LinkedList {          // Inheritance is to "extend" an object.
  public void push(Object item) { insertInFront(item); }
  public Object pop() {
    if (isEmpty()) return null;
    else return deleteFromFront();
  }
  public Object peek() {
    return (isEmpty() ? null : head.getData());
  }
}

Polymorphism

Polymorphism in OO is allowing the same method, variable object to have multiple "forms". An example of this: a Polygon and Sphere class both extend a "Geometry" class and each have a different "calculateMinimumBoundingRectangle()" function and "toString()" function. Operator overloading - where operators ('+','-','*','/','<', etc) are overloaded and treated differently by different objects and data types - is another example of polymorphism. The primary usage of polymorphism in industry (object-oriented programming theory) is the ability of objects belonging to different types to respond to method, field, or property calls of the same name, each one according to an appropriate type-specific behavior.

In a strongly typed language (eg: C#) polymorphism typically means inheritance where multiple objects derive from the same object. In weakly typed languages types (eg: Perl, PHP and JavaScript) are implicitly polymorphic and polymorpihsm is quite "adhoc".


Links


Acknowledgements: oodesign.com for much of the code and Bob Tar for the nice example code for 'adapter' (here).