1. What is the Composite Pattern?
The Composite Pattern provides a hierarchical structure and allows us to ignore the differences between individual objects and collections of objects
The caller doesn't know whether what they're holding is a single object or a group of objects, but that's okay. In the Composite Pattern, the caller doesn't need to know these things
2. An Example
Suppose we want to describe a file system. The file system contains files and folders, and folders contain folders and files...
Yes, this is a hierarchical structure, just like a menu. A menu contains menu items and submenus, and submenus contain menu items and sub-submenus... Hierarchical structure is tree structure, and we easily think of defining a Node class containing a set of pointers to children, thereby constructing a complete tree. So our class diagram will look like this:

Note: File only supports the operations listed in the class diagram, while Folder class supports all inherited operations
This is the basic design of the class. Using such a class structure, we can describe the file system. Now let's implement the code:
Define the Directory base class:
package CompositePattern;import java.util.ArrayList;
/**
-
定义目录类
-
@author ayqy */ public abstract class Directory { String name; String description; ArrayList<Directory> files;
/**
- 添加指定文件/文件夹到该目录下
- @param dir 将要添加的文件/文件夹
- @return 添加成功/失败 */ public boolean add(Directory dir){ throw new UnsupportedOperationException();//默认抛出操作异常 }
/**
- 删除该目录下的指定文件/文件夹
- @param dir 将要删除的文件/文件夹
- @return 删除成功/失败 */ public boolean remove(Directory dir){ throw new UnsupportedOperationException();//默认抛出操作异常 }
/**
- 清空该目录下所有文件和文件夹
- @return 清空成功/失败 */ public boolean clear(){ throw new UnsupportedOperationException();//默认抛出操作异常 }
public ArrayList<Directory> getFiles() { throw new UnsupportedOperationException();//默认抛出操作异常 }
/**
- 打印输出 */ public abstract void print();
public String getName() { return name; }
public String getDescription() { return description; }
public String toString(){ return name + description; } }
P.S. Note how we handle Folder-specific methods in the base class (throwing exceptions). Of course, we could use a more harmonious approach. Each has its benefits and drawbacks, which will be detailed later regarding why we use such a rough approach of throwing exceptions
Note that we define an abstract print method in the base class, wanting to output the entire file tree by calling the print method. The Composite Pattern allows us to implement this troublesome process in a very easy and elegant way
Now let's implement the File class:
package CompositePattern;/**
-
实现文件类
-
@author ayqy */ public class File extends Directory{
public File(String name, String desc) { this.name = name; this.description = desc; }
@Override public void print() { System.out.print(this.toString());//输出文件自身信息 } }
The File class is very simple. Since the base class provides default implementations (throwing exceptions) for operations that File doesn't support, File becomes quite slim
Next is the Folder class:
package CompositePattern;import java.util.ArrayList;
/**
-
实现文件夹类
-
@author ayqy */ public class Folder extends Directory{
public Folder(String name, String desc){ this.name = name; this.description = desc; this.files = new ArrayList<Directory>(); }
@Override public void print() { //打印该 Folder 自身信息 System.out.print(this.toString() + "("); //打印该目录下所有文件及子文件 for(Directory dir : files){ dir.print(); System.out.print(", "); } //打印文件夹遍历结束标志 System.out.print(")"); }
@Override public boolean add(Directory dir){ if(files.add(dir)) return true; else return false; }
@Override public boolean remove(Directory dir){ if(files.remove(dir)) return true; else return false; }
@Override public boolean clear(){ files.clear();
return true;}
@Override public ArrayList<Directory> getFiles() { return files; } }
The Folder class provides its own implementations for all supported operations, and does something interesting in the print method. It uses a very simple loop to implement printing all descendant nodes of the current node (this easily reminds one of something? Yes, it's the Decorator Pattern). It seems somewhat incredible, but this is one of the benefits of using the Composite Pattern (providing natural soil for recursion)
3. Effect Example
We've implemented the classes needed to describe the file system. Let's test it and see the results:
The test class code is as follows:
package CompositePattern;/**
-
实现一个测试类
-
@author ayqy / public class Test { public static void main(String[] args){ /构造文件树/ / C a.txt b.txt system
sys.dat windows win32 settings log.txt win32.config */ Directory dir = new Folder("C", ""); dir.add(new File("a.txt", "")); dir.add(new File("b.txt", "")); Directory subDir = new Folder("system", ""); subDir.add(new File("sys.dat", "")); dir.add(subDir); Directory subDir2 = new Folder("windows", ""); Directory subDir3 = new Folder("win32", ""); subDir3.add(new Folder("settings", "")); subDir3.add(new File("log.txt", "")); subDir2.add(subDir3); subDir2.add(new File("win32.config", "")); dir.add(subDir2); dir.print();//打印输出文件树} }
The running result is as follows:
C(a.txt, b.txt, system(sys.dat, ), windows(win32(settings(), log.txt, ), win32.config, ), )
The result is basically the same as expected, but the flaw is: there are redundant comma separators. To eliminate the redundant commas, we need to explicitly check during the last iteration and not output a comma, while outputting a comma in all other cases
We easily think of using an explicit Iterator to implement this (doesn't hasNext perfectly indicate whether it's the last iteration? Don't forget that ArrayList supports iterators). Let's modify the print method:
public void print() {
//打印该 Folder 自身信息
System.out.print(this.toString() + "(");
//打印该目录下所有文件及子文件
Iterator<Directory> iter = getFiles().iterator();
while(iter.hasNext()){
Directory dir = iter.next();
dir.print();
if(iter.hasNext()){
System.out.print(",");
}
}
//打印文件夹遍历结束标志
System.out.print(")");
}
Successfully eliminated the eyesore of redundant commas
4. A Little More Change
How to print file information for all files associated with NotePad.exe
Now we need to add a new attribute linkedExe to the File class, representing the executable program associated with the file, while folders don't support this attribute (here we stipulate that folders don't support the linkedExe attribute, without considering whether the program associated with a folder is Explorer or something else)
To implement this new requirement, we have to make some changes. To achieve type consistency, we must add the linkedExe attribute to the base class Directory (doing so might be criticized, but sometimes we have to sacrifice some benefits to gain others...)

The content in the rectangular box is what we added. These are all supported by File but not by Folder. After making such changes, we can print file information for all files associated with NotePad.exe. Of course, we also need to modify the Folder class's print method:
public void print() {
//打印该目录下所有关联程序为 NotePad.exe 的文件
for(Directory dir : files){
try{
if("NotePad.exe".equalsIgnoreCase(dir.getLinkedExe())){
dir.print();
}
}catch(UnsupportedOperationException e){
//吃掉异常,继续遍历(Folder 不支持 getLinkedExe 操作)
}
}
}
Did you notice something? The disadvantages of the Composite Pattern seem to become increasingly obvious
The Composite Pattern requires ignoring the differences between a single object and a collection of objects, treating them equally
Yes, by doing this we indeed gain transparency (in the print method we don't know whether we're currently processing a File or a Folder). But we even "abuse" the exception handling mechanism to cover up the differences between object collections and single objects, in pursuit of "equal treatment". Whether this is worthwhile depends on the specific situation (we're always sacrificing some things to gain other more useful things. As for whether this sacrifice is worthwhile, of course it needs to be weighed)
5. Iterator and Composite Pattern
What about the promised Iterator? Why haven't I seen it? Where is it?
The Iterator is hidden within the Composite Pattern. Isn't our print method always using an iterator? (Either an implicit iterator or an explicit iterator...)
The iterator used in the example above is called an internal iterator, meaning the iterator is hidden within the constituent classes of the Composite Pattern, so it's not easily discovered. Of course, if you prefer, you can also construct an external iterator, like this:

In DirectoryIterator, we need to manually maintain a stack structure to record the current position (internal iterators are supported by the system stack) to implement the hasNext and next methods
Actually, there's another problem: the File class obviously doesn't support the iterator method, but it has inherited it from the parent class. How should we handle this?
- Return null, then the caller must use an if statement to check
- Throw an exception, then the caller must handle the exception
- (Recommended practice) Return an empty iterator (NullIterator). How to implement an empty iterator? Just have hasNext directly return false... This way has no impact on the caller
6. Summary
The tree-like hierarchical structure provided by the Composite Pattern allows us to treat single objects and collections of objects equally (gaining operational convenience), but this benefit comes at the cost of sacrificing the Single Responsibility Principle of classes. Moreover, the Composite Pattern is implemented using inheritance, lacking flexibility.
Therefore, when using the Composite Pattern, one should carefully consider whether such a sacrifice is worthwhile. If not, consider whether it can be replaced by other design patterns...
7. A Side Note (About Whether to Throw Exceptions)
Sometimes we can choose to return null, return false, return an error code, etc., instead of throwing an exception. These approaches might be more harmonious, but throwing exceptions is sometimes the most accurate expression of the facts
For example, suppose our File class has a hasLinkedExe attribute indicating whether there is an associated application program, while Folder doesn't support the hasLinkedExe attribute, yet this attribute is inherited from the parent class and we cannot delete it.
At this point we can choose to return false or throw an exception:
- Return false: indicates that Folder has no associated application program
- Throw an exception: indicates that Folder doesn't support this operation
Obviously, throwing an exception conveys what we truly want to express
Having said all this, we went to great lengths to use the exception-throwing approach, seemingly just to express things more accurately? No, no, no, absolutely don't think this way. This tiny difference in meaning could lead to serious problems. For example:
Suppose we want to output all files that haven't been associated with an application program (i.e., "unknown files"). If we had initially adopted returning false to indicate that folders don't support this operation, we would get incorrect results (outputting all unknown files and all folders...)
No comments yet. Be the first to share your thoughts.