Tight Group Of Classes

From: SelfDocumentingCode

When designing a class, sometimes you'll find that it seems to do many disjoint operations to accomplish one goal. In this case, it's best to break the class into a tight group of classes.

Rationale:

In general, one class should do one thing. This keeps things simple and obvious. When a given class that attempts to accomplish a goal is doing several different operations to accomplish that goal, that class is doing several things too many.

In this case, the class should be broken down into its distinct sub-operations, one class for each, and have the original class act as a FacadePattern or controller.

This will result in a simplification of the interface to the original class and the interfaces to the components. Simplified code is easier to read.

It is probably best to keep the grouping tight as possible so that one part of it cannot be used without the rest. Examples of how to accomplish this are shown below.

Arguments:

"Doesn't this cause code bloat?"

In a way, but if make the classes a tight grouping of classes, then the classes come together and are inseparable. Thus, the overall system doesn't really grow all that much.

"More classes will pollute the namespace!"

Not necessarily. In namespaced languages like C++ and Java, this isn't a problem because you can put the names in a lower space. In Smalltalk, you can use name mangling/decoration to accomplish this goal. For instance, if class Foo requires a class Bar, you can rename Bar to be FooBar.

"It's harder to maintain because there are more classes."

It's harder to maintain spaghetti code that has no distinct, unifying purpose. If you're complaining about having too many windows on your desktop or having to jump from file to file, the real problem might be that you aren't working with an ergonomic environment or you don't know how to work more efficiently.

Hard to say, but I don't think this is ever an overriding factor.

Exceptions:

Frequently, classes manipulate their member variables in ways that would seem to translate better into separate objects, but it would take far longer and be more unstable to do so. For instance, if one needed a random access queue, it is common to use a random access sequence and AddTail? and RemoveHead? as needed.

However, perhaps these operations really would be best served in a separate class.

Examples:

A common example is when you need to wrap a set of data into one structured datum.

[Forgive the syntax of the classes below if they are unfamiliar to the hardened Smalltalk programmer. I use SmalltalkExpress, and this example comes from my compilers class submission. -- SunirShah]

Say I had a class called NameTable which represents a symbol table for a compiler. The NameTable keeps track of entries using NameTableEntry. This is an example of the name mangling/decoration mentioned above.

 Object subclass: #NameTable
   instanceVariableNames:
     'scopeList dataSegment '
   classVariableNames: ''
   poolDictionaries: '' !
scopeList is a Dictionary of names to 'NameTableEntry's. dataSegment is a DataSegment object, another member of the group and will be discussed below.

NameTableEntry is defined as:

 Object subclass: #NameTableEntry
   instanceVariableNames:
     'name type offset '
   classVariableNames: ''
   poolDictionaries: ''    !

!NameTableEntry class methods ! !

!NameTableEntry methods ! name ^name!

name: aString name := aString!

offset ^ offset!

offset: anInteger offset := anInteger!

type ^type!

type: aSymbol type := aSymbol! !
Another use is to move behaviour to a separate class. The symbol table also needs to keep track of the data segment offsets. So, in this example, the data segment behaviour was moved into DataSegment.

 Object subclass: #DataSegment
   instanceVariableNames:
      'offset scopeSizeStack '
   classVariableNames: ''
   poolDictionaries: ''  !

!DataSegment class methods !

new ^ super new initialize! !

!DataSegment methods !

allocate: type "Allocate data for a type"

type isLong ifTrue: [ ^ self allocateLong ]. type isFloat ifTrue: [ ^ self allocateFloat ]. type isReference ifTrue: [ ^ self allocatePointer ]. self error: 'Invalid type.'!

allocateFloat "Floats are eight bytes" offset := offset + 8. ^ offset - 8!

allocateLong "Longs are four bytes" offset := offset + 4. ^ offset - 4!

allocatePointer "Pointers are four bytes" offset := offset + 4. ^ offset - 4!

beginScope "Remember where we were when we endScope" scopeSizeStack push: offset.!

endScope scopeSizeStack isEmpty ifTrue: [ ^ self error: 'Unmatched end scope' ]. offset := scopeSizeStack pop.!

initialize offset := 0. scopeSizeStack := IndexedStack? new. "it's a stack, so we'll use it"! !
[I agree that NamedConstants would have been better than hardcoding the size of longs.]

The DataSegment isn't very tightly grouped. Smalltalk doesn't really support tight group of classes.

To accomplish tight group of classes in C++ you have a few choices like TightGroupOfFriendClasses? where the constructors are made protected or private so that only friends can instantiate them. ala:

 class CBar

class CFoo { public: CFoo() {}

protected: CBar m_Bar; };

class CBar { friend CFoo;

public: // Almost nothing in the public interface private: CBar();

// Almost everything in the protected/private interfaces. };
Actually, sometimes it's a good idea to create a public interface for CBar because you may want to pass it to outside objects. Say, if CBar was a Locator (cf. LocatorPattern?).

Or you can use protected or private InnerClasses so that the friend class isn't exposed. This also doesn't pollute the global namespace. This idiom is most common when just structuring data like NameTableEntry above. It is also common to make the inner class public when the structured data gets passed in to and out of the outer class.

 class CFoo
 {
 public:
     CFoo() {}

protected: class CBar { public: CBar(); } m_Bar; private: };
It is typically useful to make CBar a friend of CFoo a la:

 class CFoo
 {
 public:
     CFoo() {}

protected: class CBar { public: CBar(); } m_Bar; friend CBar;

private: };
See also: PrivateInterface


In Java, the common technique is to use give the lower-level classes' constructors package-level protection and make exposed classes' constructors public.

Java also supports inner classes, but they take an implicit this pointer from the outer class. This creates a very tight coupling between the classes.

This is common in C++ as well. It would look something like:

 class CFoo
 {
 public:
     CFoo() : m_Bar(this) {}

protected: class CBar { public: CBar( CFoo *pOwner ) : m_pOwner(pOwner) { assert(m_pOwner); }

protected: CFoo *m_pOwner;

private: } m_Bar; friend CBar;

private: };


Interestingly (to some), the above piece of C++ code doesn't do what its author wanted it to - at least not with a compiler that implements standard C++. It is not possible to grant friendship from CFoo to CBar. A proposed defect fix for the standard, not included in the 2003 revision, would give member classes access to their containing classes private/protected members, just as existing rules do for member functions. Some compilers have implemented such rules in the past. -- JamesDennett


I've recently found that a TightGroupOfClasses is the perfect workaround for the lack of MultipleInheritance in the Java/C# family of languages. You can enforce the 1:1 "marriage" relationship of the classes through their access properties and constructors, and generally view the tight cluster as a single object. This concept is really more of a "tightGroupOfObjects" than of classes... perhaps that is a pattern that should be discussed? I submit the name MarriedObjects?, given that such clusters are extremely intimate.


See also: SynchronizedTightGroupOfClasses, MultipartFormDataParsingExample


EditText of this page (last edited March 17, 2013) or FindPage with title or text search