Encapsulate Collection

From CSSEMediaWiki
Revision as of 03:10, 25 November 2010 by WikiSysop (Talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

A method returns a collection.

Make it return a read-only view and provide add/remove methods.

EncapsulateCollection.png

Motivation Often a class contains a collection of instances. This collection might be an array, list, set, or vector. Such cases often have the usual getter and setter for the collection.

However, collections should use a protocol slightly different from that for other kinds of data. The getter should not return the collection object itself, because that allows clients to manipulate the contents of the collection without the owning class’s knowing what is going on. It also reveals too much to clients about the object’s internal data structures. A getter for a multivalued attribute should return something that prevents manipulation of the collection and hides unnecessary details about its structure.

In addition there should not be a setter for collection: rather there should be operations to add and remove elements. This gives the owning object control over adding and removing elements from the collection.

With this protocol the collection is properly encapsulated, which reduces the coupling of the owning class to its clients.

Example: Java 2

A person is taking courses. Our course is pretty simple:

class Course...

   public Course (String name, boolean isAdvanced) {...};
   public boolean isAdvanced() {...};

I’m not going to bother with anything else on the course. The interesting class is the person:

class Person...

   public Set getCourses() {
       return _courses;
   }
   public void setCourses(Set arg) {
       _courses = arg;
   }
   private Set _courses;

With this interface, clients adds courses with code such as

   Person kent = new Person();
   Set s = new HashSet();
   s.add(new Course ("Smalltalk Programming", false));
   s.add(new Course ("Appreciating Single Malts", true));
   kent.setCourses(s);
   Assert.equals (2, kent.getCourses().size());
   Course refact = new Course ("Refactoring", true);
   kent.getCourses().add(refact);
   kent.getCourses().add(new Course ("Brutal Sarcasm", false));
   Assert.equals (4, kent.getCourses().size());
   kent.getCourses().remove(refact);
   Assert.equals (3, kent.getCourses().size());

A client that wants to know about advanced courses might do it this way:

Iterator iter = person.getCourses().iterator();

   int count = 0;
   while (iter.hasNext()) {
      Course each = (Course) iter.next();
      if (each.isAdvanced()) count ++;
   }

The first thing I want to do is to create the proper modifiers for the collection and compile, as follows:

class Person

   public void addCourse (Course arg) {
       _courses.add(arg);
   }
   public void removeCourse (Course arg) {
       _courses.remove(arg);
   }

Life will be easier if I initialize the field as well:

private Set _courses = new HashSet(); I then look at the users of the setter. If there are many clients and the setter is used heavily, I need to replace the body of the setter to use the add and remove operations. The complexity of this process depends on how the setter is used. There are two cases. In the simplest case the client uses the setter to initialize the values, that is, there are no courses before the setter is applied. In this case I replace the body of the setter to use the add method:

 class Person...
   public void setCourses(Set arg) {
       Assert.isTrue(_courses.isEmpty());
       Iterator iter = arg.iterator();
       while (iter.hasNext()) {
           addCourse((Course) iter.next());
       }
    }

After changing the body this way, it is wise to use Rename Method to make the intention clearer.

   public void initializeCourses(Set arg) {
       Assert.isTrue(_courses.isEmpty());
       Iterator iter = arg.iterator();
       while (iter.hasNext()) {
           addCourse((Course) iter.next());
       }
   }

In the more general case I have to use the remove method to remove every element first and then add the elements. But I find that occurs rarely (as general cases often do).

If I know that I don’t have any additional behavior when adding elements as I initialize, I can remove the loop and use addAll.

   public void initializeCourses(Set arg) {
       Assert.isTrue(_courses.isEmpty());
       _courses.addAll(arg);
   }

I can’t just assign the set, even though the previous set was empty. If the client simply create a set and use the setter, I can get them to use the add were to modify the set after passing it in, that would violate encapsulation. I have to make a copy.

If the clients simply create a set and use the setter, I can get them to use the add and remove methods directly and remove the setter completely. Code such as

   Person kent = new Person();
   Set s = new HashSet();
   s.add(new Course ("Smalltalk Programming", false));
   s.add(new Course ("Appreciating Single Malts", true));
   kent.initializeCourses(s);

becomes

   Person kent = new Person();
   kent.addCourse(new Course ("Smalltalk Programming", false));
   kent.addCourse(new Course ("Appreciating Single Malts", true));

Now I start looking at users of the getter. My first concern is cases in which someone uses the getter to modify the underlying collection, for example:

   kent.getCourses().add(new Course ("Brutal Sarcasm", false));

I need to replace this with a call to the new modifier:

   kent.addCourse(new Course ("Brutal Sarcasm", false));

Once I’ve done this for everyone, I can check that nobody is modifying through the getter by changing the getter body to return an unmodifiable view:

   public Set getCourses() {
       return Collections.unmodifiableSet(_courses);
   }

At this point I’ve encapsulated the collection. No one can change the elements of collection except through methods on the person.

Moving Behavior into the Class

I have the right interface. Now I like to look at the users of the getter to find code that ought to be on person. Code such as

   Iterator iter = person.getCourses().iterator();
   int count = 0;
   while (iter.hasNext()) {
      Course each = (Course) iter.next();
      if (each.isAdvanced()) count ++;
   }

is better moved to person because it uses only person’s data. First I use Extract Method on the code:

  int numberOfAdvancedCourses(Person person) {
       Iterator iter = person.getCourses().iterator();
       int count = 0;
       while (iter.hasNext()) {
           Course each = (Course) iter.next();
           if (each.isAdvanced()) count ++;
       }
       return count;
   }

And then I use Move Method to move it to person:

 class Person...
   int numberOfAdvancedCourses() {
       Iterator iter = getCourses().iterator();
       int count = 0;
       while (iter.hasNext()) {
           Course each = (Course) iter.next();
           if (each.isAdvanced()) count ++;
       }
       return count;
   }

A common case is

 kent.getCourses().size()

which can be changed to the more readable

 kent.numberOfCourses()

 class Person...
   public int numberOfCourses() {
       return _courses.size();
   }

See Also

Personal tools