Recitation 7: MDDs and Design Patterns

Module Dependence Diagrams

A module dependence diagram of a program shows how different parts of the code use each other. There are two types of dependences -- weak and strong dependences.

A class A strongly depends on another class B if

A class A weakly depends on B if A module dependence diagram shows the strong and weak dependences among classes and interfaces. The notation is summarized below. For example, consider a program that keeps track of where students live in dormitories. A class Dormitory keeps a list of Rooms in that dormitory, and each Room keeps track of which Students live in that room. Student is an interface implemented by two classes -- GradStudent and Undergrad. Some of the specifications for these classes is shown below.
class Dormitory {
   private Room[] rooms;
   
   /** @return an array containing the students living in room roomNumber **/
   public Student[] lookupStudents(int roomNumber) {
      return rooms[roomNumber].getOccupants();
   }
}

class Room {
   private Vector occupants;

   /** @return an array containing the occupants of this room **/
   public Student[] getOccupants();

   /** @effects adds s to this room's occupants **/
   public void addOccupant(Student s);
}
The MDD for this program is:

Dormitory weakly depends on Student because it handles objects of type Student (the return value of lookupStudents), but does not call any methods of Student. Similarly, Room holds references to Student objects and handles them, but does not call methods of the class, so Room weakly depends on Student as well. Dormitory strongly depends on Room, since it makes calls to a method of Room (getOccupants), so its correctness depends on Room's correctness.

MDDs can tell you information about the structure of the system, which can be used in several ways:

Design Patterns

A design pattern is: It is important to remember that design patterns should be thought of as an optimization; a design pattern can optimize many different aspects of a program, such as performance, extensibility, or modularity. Like any optimization, a design pattern should only be introduced into your design or implementation after you have noticed a problem. After you have determined that there is a problem, and investigated the source of the problem, looking at a design patterns reference will often help you to solve your problem without having to reinvent the wheel.

We will cover three design patterns today:

Adapter

The purpose of the Adapter pattern is to convert the interface of a class into another interface that clients expect. This can be necessary if you want to reuse code, but that code does not satisfy a necessary interface. For example, suppose you have written code that uses objects that satisfy the specification for Point:
interface CartesianPoint {
  /** @return the x coordinate of this point **/
  float getX();

  /** @return the y coordinate of this point **/
  float getY();

  ...
}
Suppose that you have an already implemented class PolarPoint, which represents a point, but uses polar coordinates. The specification for PolarPoint is:
class PolarPoint {
   /** @effects creates a new PolarPoint pp such that 
    *     p.r = r && p.theta = theta
    **/
   PolarPoint(float r, float theta);

   /** @return r **/
   float getR();

   /** @return theta **/
   float getTheta();
  
   ...
}
If we want to use PolarPoint to satisfy the CartesianPoint interface, we can write an adapter. We can do this using either subclassing or delegation. The solution using subclassing would look like:
class SubclassingCartesianPoint extends PolarPoint 
    implements CartesianPoint {
  /** @return the x coordinate of this point **/
  float getX() {
     return getR()*Math.sin(getTheta());
  }

  /** @return the y coordinate of this point **/
  float getY() {
    return getR()*Math.cos(getTheta());
  }

  ...
}
A solution using delegation would look like:
class DelegatingCartesianPoint implements CartesianPoint {
  PolarPoint pp;

  DelegatingCartesianPoint(float x, float y) {
    pp = new PolarPoint(Math.sqrt(x*x+y*y),Mat.atan(x/y));
  }

  /** @return the x coordinate of this point **/
  float getX() {
     return pp.getR()*Math.sin(pp.getTheta());
  }

  /** @return the y coordinate of this point **/
  float getY() {
    return pp.getR()*Math.cos(pp.getTheta());
  }
  ...
}
Question: What are the advantages and disadvantages of using delegation versus subclassing?

Answer: One feature of using subclassing is that all of the methods of the superclass will be inherited. So if the superclass contains additional methods beyond the spec that you need to meet, and these methods are useful, you get these methods for free. However, in some cases, you might not want the ADT to have these methods, which means that you would have to override the methods with exceptions in the subclass. In such a case, delegation is a cleaner solution.

To summarize, the Adapter pattern is useful when you want to use an existing class, but its interface does not match the interface that you need.

Factories

A factory method is a method that manufactures objects of a particular type. The intent of this pattern is to define an interface for creating an object which lets subclasses decide which classes to instantiate.

Consider a document-processing application. We might want to define abstract classes Document and Application to represent documents and the applications that handle the documents. There may be some functionality common to all types of applications, except that they rely on the creation of new documents of the appropriate type. For instance, consider a method newDocument, which creates a new document, adds it to the application's list of documents, and opens the document. We could write this as:

public void newDocument() {
   Document doc = createDocument();
   docs.add(doc);
   doc.open();
}
where createDocument is a factory method that the concrete subclasses of Application must implement, that returns a new document of the correct type. Let's say we wanted to implement an application to create and edit resumes, called ResumeWizard, which handles documents of type Resume. The object model for this system would look like:

In Application, createDocument would be declared abstract as:

public abstract Document createDocument();
and in ResumeWizard, it could be implemented as:
public Document createDocument() {
   return new Resume();
}
Using this factory method allows us to put the reusable code of newDocument in the superclass, even though it needs to create new objects whose types depend on the subclass.

In general, the Factory Method pattern can be used when a class can't anticipate what class it needs to create or when a class wants its subclasses to specify the types of objects to create.

Factory objects can be used for similar reasons. A factory object encapsulates all of the factory methods into one object. In the above example, we could introduce an interface DocumentFactory, which defines a method createDocument. Each Application could contain a field of type DocumentFactory, which would create documents for the specific type of document to be used. The code for the system would be changed to:

class Application {
   DocumentFactory factory;

   public void newDocument() {
      Document doc = factory.createDocument();
      docs.add(doc);
      doc.open();
   }
   ...
}

class ResumeWizard extends Application {
   
   public ResumeWizard() {
      factory = new ResumeFactory();
      ...
   }
   ...
}

class ResumeFactory implements Factory {
   Document createDocument() {
      return new Resume();
   }
}
There would no longer be a createDocument method in ResumeWizard.

Flyweight

The Flyweight pattern uses sharing to support a large number of fine-grained objects efficiently. This is done by encapsulating the immutable and context-independent pieces of state in one class (called the flyweight). Since the state in this class is context-independent, the same instance can be shared in many places. Then only the context-dependent or mutable data needs to be stored separately for each real instance of the object. The part of the state stored in the flyweight is called intrinsic. Extrinsic state varies with the flyweight's context, so it cannot be shared. The extrinsic state must be passed into the flyweight by the client when it is needed. (add references to example from lecture, maybe?)

The Flyweight pattern should be used when:

Exercises

Problem 1: Student-Tracking Database

You have been enlisted to help the 6.170 TAs design a student-tracking database. Unfortunately, the TAs don't practice what they preach, and they've written no specs, only giving you a skeletal outline. You're just going to have to make educated guesses about how these classes should work and what these methods do.

class Roster {
    Roster();
    void addSection(Section s);
    Section lookupSection(int sectionNumber);
    Student lookupStudent(String username);
}

class Section {
    Section(int sectionNumber);
    int getSectionNumber();
    void addStudent(Student s);
    Student lookupStudent(String username);
}

class Student {
    Student(String name);
    String getName();
}

Not trusting the TAs, you decide to analyze the quality of this design by constructing a module dependence diagram.

Construct a minimal MDD involving the 4 classes referenced above: Roster, Section, Student, and String. Ignore all self-dependences and dependences based on inheritance. Think about how the methods would be implemented and consider only those dependences which are necessary.

Problem 2: Email Folders Revisited

Consider the case from Problem 3.2 of Problem Set 5. Say the UI class needs to know the sizes of both folders and messages. Assume Folder and Message are independent classes, and that each knows its own size.

  1. Construct a minimal MDD (ignoring any other classes that might be referred to) involving these three classes.

    Now consider the case where Folder and Message are both subclasses of the EmailObject interface, which has a getSize() method.

  2. Construct a minimal MDD involving these four classes.
  3. Which of the above MDDs demonstrates a safer implementation? Why?
  4. Would you ever want to implement the other (less safe) MDD instead? Why or why not? (Feel free to refer back to Problem Set 5.)

Problem 3: Flyweight pattern in a document editor

Consider an object-oriented document editor. Typically, objects are used to represent embedded figures and tables in a document. However, characters are often not represented as objects, because the sheer number of characters would result in huge space requirements. It is desirable, however, to represent characters and embedded elements uniformly. If they all satisfy a standard interface, then there would not be the need for special case code to handle characters.

A simple representation for a character is a data structure that contains the character code, typographic style information (font, size, and style), and the location in the document. However, representing all of this information does not allow sharing. Use the Flyweight pattern to restructure the system to allow for more sharing of character objects. You may assume that documents keep track of the objects in the document in some sort of collection structure. In order to draw a document to the screen, for example, the document code would iterate over all of the objects and call their draw method.

With your new representation of characters, how would you have to change the methods:

/** @effects draws this character to the screen **/
public void draw();

/** @return true iff this characters bounding box include p **/
public boolean intersects(Point p);

Problem 4: CompositeRoutePath

In problem set 4, you implemented CompositeRoutePath, a class which implements the interface Path, but can also produce directions to be displayed. This class is an adapter, which adapts CompositeRoute to the Path interface. We discussed two ways to implement an adapter -- delegation and subclassing. How could these two techniques be used to implement CompositeRoutePath? What are the advantages and disadvantages of each approach?

Problem 5: The MIT Card Office

This problem deals with a small system to represent the MIT Card Office. The fundamental role of the card office (at least in our system) is to produce new cards for people (if, for instance, a person loses his card). We have provided you with code/specs for a piece of this system, which is designed as follows.

A Card represents a person's MIT ID card, which has several attributes, such as the name and ID number of the holder, the color of the card, the label on the card for the type of holder, and the access rights that have been programmed into the card. Access rights are represented by AccessPrivileges, which might encapsulate a location and times of day that the person may access that location (although the actual spec of this class has not been included). The PersonnelOffice contains static methods to retrieve information about people at MIT, such as their addresses and what buildings they may access. The CardOffice class represents the card office, which can create new cards for people. It currently can handle card creation for students and faculty members. The FactoryCardOffice is another class to represent the card office, but which uses uses CardFactorys to create new cards for people. There are two types of CardFactorys in the current system -- StudentCardFactory, which creates cards for students; and FacultyCardFactory, which creates cards for faculty members.

  1. Draw an MDD for each of the card office implementations. What are the advantages of using the Factory pattern in this system?
  2. We want to extend the system to generate cards for administrative staff, whose cards are grey and are labelled "Staff". Administrative staff has access to only the building where they work, which can be obtained from the personnel office. Extend the system that uses the Factory pattern to handle card creation for administrative staff. Discuss how you would modify the system that does not use the Factory pattern, and how the modifications to the two systems compare.
  3. There is another design pattern used by PersonnelOffice to decrease space usage. What is this pattern? Do you think that this will create a significant difference in space consumption?