Yes, the composite pattern is indeed the right choice if you want to map tree structures. With reference to your example, the composite design pattern implies that your class Employee acts as a node, the class ProjectManager acts as a branch and the class Developer acts as a leaf. Within this context, the main advantage of the composite pattern is that it treats objects of your compositions uniformly. As a result, you can represent entire hierarchies of instances with this particular GoF design pattern.
You need the following participants:
- The
abstract class Employee must declare the interface of the composition and implements a common behaviour to a certain degree.
- The
ProjectManager class extends the abstract class Employee and implements a behaviour to treat Employee children, i.d. in your case ProjectManager or Developer instances.
- The
Developer also extends the abstract class Employee and represents a leaf which does not have any children.
I used your example code to demonstrate the composite pattern. Please note that it may vary from your desired outcome, but you can take it as a reference.
Employee.java (node):
package me.eckhart;
import java.util.List;
public abstract class Employee {
private String name = null;
public static final String OPERATION_NOT_SUPPORTED = "Operation not supported.";
public String getName() {
return name;
}
public Employee setName(String name) {
if (name == null) throw new IllegalArgumentException("Argument 'name' is null.");
this.name = name;
return this;
}
public Employee addEmployee(Employee employee) {
throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED);
}
public List<Employee> getEmployees() {
throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED);
}
public Employee setEmployees(List<Employee> employees) {
throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED);
}
public Employee setLanguagesList(List<ProgrammingLanguages> languagesList) {
throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED);
}
public Employee addProgrammingLanguage(ProgrammingLanguages language) {
throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED);
}
public List<ProgrammingLanguages> getLanguagesList() {
throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED);
}
/* Composite operations. */
public abstract int getWorkload(ProgrammingLanguages language);
public abstract Employee setWorkload(int workload);
}
ProjectManager.java (branch):
package me.eckhart;
import java.util.ArrayList;
import java.util.List;
public class ProjectManager extends Employee {
private List<Employee> employeeList = null;
public ProjectManager() {
this.employeeList = new ArrayList<>();
}
@Override
public Employee addEmployee(Employee employee) {
if (employee == null) throw new IllegalArgumentException("Argument 'employee' is null.");
this.employeeList.add(employee);
return this;
}
@Override
public List<Employee> getEmployees() {
return this.employeeList;
}
@Override
public Employee setEmployees(List<Employee> employeeList) {
if (employeeList == null) throw new IllegalArgumentException("Argument 'employeeList' is null.");
this.employeeList = employeeList;
return this;
}
/* Composite operations. */
public int getWorkload(ProgrammingLanguages language) {
int workload = 0;
for (Employee employee : employeeList) {
workload += employee.getWorkload(language);
}
return workload;
}
public Employee setWorkload(int workload) {
throw new UnsupportedOperationException(Employee.OPERATION_NOT_SUPPORTED);
}
}
Developer.java (leaf):
package me.eckhart;
import java.util.ArrayList;
import java.util.List;
public class Developer extends Employee {
private List<ProgrammingLanguages> languagesList = null;
private int workload = 0;
public Developer() {
this.languagesList = new ArrayList<>();
}
@Override
public Employee setLanguagesList(List<ProgrammingLanguages> languagesList) {
this.languagesList = languagesList;
return this;
}
@Override
public Employee addProgrammingLanguage(ProgrammingLanguages language) {
this.languagesList.add(language);
return this;
}
@Override
public List<ProgrammingLanguages> getLanguagesList() {
return this.languagesList;
}
/* Composite operations. */
public Employee setWorkload(int workload) {
if (workload < -1) throw new IllegalArgumentException("Workload cannot be negative.");
this.workload = workload;
return this;
}
public int getWorkload(ProgrammingLanguages language) {
if (this.languagesList.contains(language)) return workload;
return 0;
}
}
ProgrammingLanguages.java (enumeration):
package me.eckhart;
public enum ProgrammingLanguages {
JAVA,
JAVASCRIPT,
C,
PHP,
SWIFT,
PYTHON
}
I created a unit test to demonstrate how you can access the workload for one particular programming language.
EmployeeTest.java (JUnit 4.11):
package me.eckhart;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class EmployeeTest {
protected Employee projectManagerIt;
@Before
public void setUp() throws Exception {
Employee webDevSr = new Developer();
webDevSr.setName("Jane").addProgrammingLanguage(ProgrammingLanguages.JAVASCRIPT).addProgrammingLanguage(ProgrammingLanguages.PYTHON).setWorkload(10);
Employee webDevJr = new Developer();
webDevJr.setName("Alex").addProgrammingLanguage(ProgrammingLanguages.PHP).setWorkload(15);
Employee projectManagerWebDev = new ProjectManager();
projectManagerWebDev.setName("James").addEmployee(webDevSr).addEmployee(webDevJr);
Employee softwareDevSr = new Developer();
softwareDevSr.setName("Martin").addProgrammingLanguage(ProgrammingLanguages.C).addProgrammingLanguage(ProgrammingLanguages.JAVA).setWorkload(35);
Employee softwareDevJr = new Developer();
softwareDevJr.setName("John").addProgrammingLanguage(ProgrammingLanguages.JAVA).setWorkload(30);
Employee projectManagerBackend = new ProjectManager();
projectManagerBackend.setName("Tom").addEmployee(softwareDevSr).addEmployee(softwareDevJr);
Employee freelanceSoftwareDev = new Developer();
freelanceSoftwareDev.setName("Marco").addProgrammingLanguage(ProgrammingLanguages.JAVA).addProgrammingLanguage(ProgrammingLanguages.PYTHON).addProgrammingLanguage(ProgrammingLanguages.C).setWorkload(25);
Employee freelanceWebDev = new Developer();
freelanceWebDev.setName("Claudio").addProgrammingLanguage(ProgrammingLanguages.SWIFT).addProgrammingLanguage(ProgrammingLanguages.JAVASCRIPT).addProgrammingLanguage(ProgrammingLanguages.PHP).setWorkload(10);
Employee freelanceProjectManager = new ProjectManager();
freelanceProjectManager.setName("Angie").addEmployee(freelanceSoftwareDev).addEmployee(freelanceWebDev);
projectManagerIt = new ProjectManager();
projectManagerIt.setName("Peter").addEmployee(projectManagerWebDev).addEmployee(projectManagerBackend).addEmployee(freelanceProjectManager);
}
@Test
public void testComposite() throws Exception {
Assert.assertEquals(90, projectManagerIt.getWorkload(ProgrammingLanguages.JAVA));
Assert.assertEquals(20, projectManagerIt.getWorkload(ProgrammingLanguages.JAVASCRIPT));
Assert.assertEquals(60, projectManagerIt.getWorkload(ProgrammingLanguages.C));
Assert.assertEquals(25, projectManagerIt.getWorkload(ProgrammingLanguages.PHP));
Assert.assertEquals(10, projectManagerIt.getWorkload(ProgrammingLanguages.SWIFT));
Assert.assertEquals(35, projectManagerIt.getWorkload(ProgrammingLanguages.PYTHON));
}
}
The UML class diagram looks like this:

The code in the setUp() method of EmployeeTest.java implements the following tree structure:

The main disadvantage of the composite design pattern is that you need to restrict certain operations with runtime-checks, since clients usually do not know whether they are dealing with a ProjectManager (branch) or a Developer (leaf) instance.