Resolving God Objects:
Logics Dispatch
Context
This article is part of the God object resolution series. To illustrate it, we use a running example through the whole series. This article is the first step of the resolution.
Problem
In the current state, the God object contains the code of various features. We want instead to have separate features to build on, in other words to extract the code into new features.
Solution
Dispatching the code can take a lot of time, but it can be done iteratively through small steps. The first step is to extract 1 method:
-
Identify a method that relate to a specific feature:
/** Add a new {@link Employee} and returns its ID. */ public int hireEmployee(Employee employee) { int employeeId = ++nextEmployeeId; employees.put(employeeId, employee); return employeeId; }
-
If the feature does not have a class yet, create it.
I would also highly recommend to create an interface:
The interface can help for testing. It also allows to apply the Dependency inversion principle. It will be particularly useful if you want to transform the God object into a façade. We didn't give much thought for the name of the class because we don't know the implementation details yet. We will come back on that later. For now, this new class should only be used inside the God object, so it is not an issue and we will come back on that later.interface Employer {}
class EmployerImpl implements Employer {} -
Copy the method of the God class into the feature interface and class:
Notice that the code might not compile. In our example,interface Employer { public int hireEmployee(Employee employee); } class EmployerImpl implements Employer { /** Add a new {@link Employee} and returns its ID. */ public int hireEmployee(Employee employee) { int employeeId = ++nextEmployeeId; employees.put(employeeId, employee); return employeeId; } }
nextEmployeeId
andemployees
do not exist yet in theEmployer
class. What you should pay attention to at this stage is the state dependencies, like these two fields. You may copy them too, but you break the integrity of the God object state. For instance, hiring an employee throughEmployerImpl
fills theEmployerImpl.employees
map, but not theEmployeeManager.employees
map. Consequently, firing theemployee
fromEmployeeManager
, method which has not been extracted yet, won't work properly because it will remove nothing. To fix that, you may copy all the methods which depend on this state at once, but these methods may rely on other pieces of the state, which relate to other methods, which relate to other fields, and so on. Rather, you may use functional interfaces of thejava.util.function
package, likeSupplier
, to interact with the God object state:class EmployerImpl implements Employer { private final Supplier<Integer> newEmployeeID; private final Supplier<Map<Integer, Employee>> employeeMap; public EmployerImpl(Supplier<Integer> newEmployeeID, Supplier<Map<Integer, Employee>> employeeMap) { this.newEmployeeID = newEmployeeID; this.employeeMap = employeeMap; } /** Add a new {@link Employee} and returns its ID. */ public int hireEmployee(Employee employee) { int employeeId = newEmployeeID.get(); employeeMap.get().put(employeeId, employee); return employeeId; } }
-
Add a field in the God class for this feature, with the required interactions with the God object state:
Notice that the firstclass EmployeeManager { private int nextEmployeeId = 0; private final Map<Integer, Employee> employees = new HashMap<>(); private final Employer employer = new EmployerImpl(() -> ++nextEmployeeId, () -> employees); ... }
Supplier
is not a mere accessor: it also take in charge the increment required to update the state. -
Replace the code of the God class method by a direct call to the feature object:
class EmployeeManager { ... /** Add a new {@link Employee} and returns its ID. */ public int hireEmployee(Employee employee) { return employer.hireEmployee(employee); } ... }
At this point, you can run the tests of the God class: they should all be green and the coverage should cover both the God class and the feature class.
In our case, both EmployeeManager
and EmployerImpl
have a 100% line coverage.
We only extracted a single method, but in a retrocompatible way.
Consequently, you can stop there and come back later to continue your refactoring.
Understanding that you can do such a small step one at a time is important to not be overwhelmed when dealing with a massive God object.
You should always keep in mind that you can go one method at a time without breaking anything.
This process should of course be iterated until all the methods have been extracted.
In particular, you should not pass to the next phase before this one is complete.
As you saw, the new feature class may build on Supplier
instances and other tricks to interact with the state of the God object.
In other words, we extracted the logics, but not the state, and the God object won't be completely refactored before the state is also extracted.
But before to extract it, we should extract all the methods.
By repeating the previous steps on the firing method, we extract it to the same Employer
class.
We reuse the employeeMap
already available for the state:
class EmployerImpl implements Employer {
...
/** Fire an {@link Employee} and returns its ID. */
public Employee fireEmployee(int employeeID) {
return employeeMap.get().remove(employeeID);
}
}
class EmployeeManager {
...
/** Fire an {@link Employee} and returns its ID. */
public Employee fireEmployee(int employeeID) {
return employer.fireEmployee(employeeID);
}
...
}
For the other methods, we will create new features. For instance, the personal details should contain the address and phone number methods:
interface EmployeeDetails { // New feature interface
public Address getAddress(int employeeID);
public void setAddress(int employeeID, Address address);
public String getPhoneNumber(int employeeID);
public void setPhoneNumber(int employeeID, String phoneNumber);
}
class EmployeeDetailsImpl implements EmployeeDetails { // New feature class
private final Supplier<Map<Integer, Address>> addressesMap;
private final Supplier<Map<Integer, String>> phonesMap;
public EmployeeDetailsImpl(Supplier<Map<Integer, Address>> addressesMap, Supplier<Map<Integer, String>> phonesMap) {
this.addressesMap = addressesMap;
this.phonesMap = phonesMap;
}
/** Return the {@link Address} of the given {@link Employee}. */
public Address getAddress(int employeeID) {
return addressesMap.get().get(employeeID);
}
/** Change the {@link Address} of the given {@link Employee}. */
public void setAddress(int employeeID, Address address) {
addressesMap.get().put(employeeID, address);
}
/** Return the phone number of the given {@link Employee}. */
public String getPhoneNumber(int employeeID) {
return phonesMap.get().get(employeeID);
}
/** Change the phone number of the given {@link Employee}. */
public void setPhoneNumber(int employeeID, String phoneNumber) {
phonesMap.get().put(employeeID, phoneNumber);
}
}
class EmployeeManager { // Updated God object
...
private final Map<Integer, Address> addresses = new HashMap<>();
private final Map<Integer, String> phoneNumbers = new HashMap<>();
private final EmployeeDetails details = new EmployeeDetailsImpl(() -> addresses, () -> phoneNumbers);
...
/** Return the {@link Address} of the given {@link Employee}. */
public Address getAddress(int employeeID) {
return details.getAddress(employeeID);
}
/** Change the {@link Address} of the given {@link Employee}. */
public void setAddress(int employeeID, Address address) {
details.setAddress(employeeID, address);
}
/** Return the phone number of the given {@link Employee}. */
public String getPhoneNumber(int employeeID) {
return details.getPhoneNumber(employeeID);
}
/** Change the phone number of the given {@link Employee}. */
public void setPhoneNumber(int employeeID, String phoneNumber) {
details.setPhoneNumber(employeeID, phoneNumber);
}
}
At some point, we consider that the setEmployee(employeeID, Employee)
is also about the update of the details of the employee.
The difference with the address and the phone number is that we update a more complete structure.
We can see, with the current implementation, that addresses and phones are not part of the employee
, since we store them separately.
In other words, the employee
probably provides complementary information.
Let's say that it contains contractual details, like the content of the contract, the date of signature, etc.
With that in mind, we can think that a mere "setEmployee" is not clear enough, and we choose to rename it "setContract".
Extracting the setEmployee
method thus leads us to this result:
interface EmployeeDetails { // Add the renamed method
...
public Employee setContract(int employeeID, Employee employee);
}
class EmployeeDetailsImpl implements EmployeeDetails { // Add the renamed method and state supplier
...
private final Supplier<Map<Integer, Employee>> contractsMap;
public EmployeeDetailsImpl(..., Supplier<Map<Integer, Employee>> contractsMap) {
...
this.contractsMap = contractsMap;
}
...
/**
* Replace the {@link Employee}'s contractual details and returns the previous
* version.
*/
public Employee setContract(int employeeID, Employee employee) {
return contractsMap.get().put(employeeID, employee);
}
}
class EmployeeManager { // Add the state accessor and rewrite the method.
...
private final EmployeeDetails details = new EmployeeDetailsImpl(..., () -> employees);
...
/** Replace the {@link Employee}'s details and returns the previous version. */
public Employee setEmployee(int employeeID, Employee employee) {
return details.setContract(employeeID, employee);
}
...
}
Notice that the God object still have the old method name, but the new feature uses the new one.
This way, when the God object will be replaced by the feature, we will have a clearer method to use.
We may go further with the Employee
parameter of this method, where using a Contract
class seems more relevant.
You may create for example a new Contract
interface, and create an adapter from Employee
to Contract
.
You should then adapt employee
into a contract
before to call setContract(contract)
.
The state accessor should also be adapted, which requires to adapt the contract
into an employee
.
This is out of our scope, so we don't do it here and we don't go further in details, but it is possible.
getEmployee
can also be extracted in the same way:
interface EmployeeDetails {
...
public Employee getContract(int employeeID);
}
class EmployeeDetailsImpl implements EmployeeDetails {
...
/** Return the {@link Employee}'s contractual details. */
public Employee getContract(int employeeID) {
return contractsMap.get().get(employeeID);
}
}
class EmployeeManager {
...
/** Return the {@link Employee}'s details. */
public Employee getEmployee(int employeeID) {
return details.getContract(employeeID);
}
...
}
Now, the only remaining methods are the ones related to the salary, which we extract in a new feature:
interface SalarySettings {
public void setSalary(int employeeID, double salary);
public double getSalary(int employeeID);
public void raiseSalary(int employeeID, double amount);
}
class SalarySettingsImpl implements SalarySettings {
private final Supplier<Map<Integer, Double>> salariesMap;
public SalarySettingsImpl(Supplier<Map<Integer, Double>> salariesMap) {
this.salariesMap = salariesMap;
}
/** Change the salary of the given {@link Employee}. */
public void setSalary(int employeeID, double salary) {
salariesMap.get().put(employeeID, salary);
}
/** Return the salary of the given {@link Employee}. */
public double getSalary(int employeeID) {
return salariesMap.get().get(employeeID);
}
/** Raise the salary of the given {@link Employee}. */
public void raiseSalary(int employeeID, double amount) {
Map<Integer, Double> salaries = salariesMap.get();
salaries.put(employeeID, salaries.get(employeeID) + amount);
}
}
class EmployeeManager {
...
private final Map<Integer, Double> salaries = new HashMap<>();
private final SalarySettings salarySettings = new SalarySettingsImpl(() -> salaries);
...
/** Change the salary of the given {@link Employee}. */
public void setSalary(int employeeID, double salary) {
salarySettings.setSalary(employeeID, salary);
}
/** Return the salary of the given {@link Employee}. */
public double getSalary(int employeeID) {
return salarySettings.getSalary(employeeID);
}
/** Raise the salary of the given {@link Employee}. */
public void raiseSalary(int employeeID, double amount) {
salarySettings.raiseSalary(employeeID, amount);
}
}
At that point, all the methods have been extracted and the initial state of the God object is not used anymore but to feed them. We can now start to dispatch this state to the relevant features.