At the request of a reader, I’ve decided to post some examples on how something like the Employee class described in Allen Holub’s article More on getters and setters could evolve. This may not be the only way, but it certainly is one way.
Let’s start with a simple example: an Employee
domain object, an EmployeeForm
and an EmployeeAction
; the Form
and Action class are from the Struts framework (for the Action
, I’ve got a custom class that splits the service method back into a GET handler and a POST handler). Time for some code:
public class Employee { private String name; private String id; private String salary; public void setName(String name) { this.name = name; } public String getName() { return this.name; } public void setID(String id) { this.id = id; } public String getID() { return this.id; } public void setSalary(String salary) { this.salary = salary; } public String getSalary() { return this.salary; } } public class EmployeeAction extends CustomStrutsAction { protected ActionMapping handleGet(ActionContext context) { EmployeeForm form = (EmployeeForm) context.getForm(); Employee employee = ; form.setID(employee.getID()); form.setName(employee.getName()); form.setSalary(employee.getSalary()); return context.findMapping("show_employee"); } protected ActionMapping handlePost(ActionContext context) { EmployeeForm form = (EmployeeForm) context.getForm(); Employee employee = ; employee.setID(form.getID()); employee.setName(form.getName()); employee.setSalary(form.getSalary()); return context.findMapping("updated_employee"); } } public class EmployeeForm { private String name; private String id; private String salary; public void setName(String name) { this.name = name; } public String getName() { return this.name; } public void setID(String id) { this.id = id; } public String getID() { return this.id; } public void setSalary(String salary) { this.salary = salary; } public String getSalary() { return this.salary; } }
In this simple example, there are a multitude of sins (BTW, this is the most common sort of example I have seen!) The biggest one is that EmployeeAction
understands the inner workings of both Employee
and EmployeeeForm
. If I need to add a new field, I have to change three classes, not two, and that’s bad. So we can improve on this by changing the Action and Form class a bit:
public class EmployeeAction extends CustomStrutsAction { protected ActionMapping handleGet(ActionContext context) { EmployeeForm form = (EmployeeForm) context.getForm(); Employee employee = ; form.loadFromEmployee(employee); return context.findMapping("show_employee"); } protected ActionMapping handlePost(ActionContext context) { EmployeeForm form = (EmployeeForm) context.getForm(); Employee employee = ; form.updateEmployee(employee); return context.findMapping("updated_employee"); } } public class EmployeeForm { private String name; private String id; private String salary; public void setName(String name) { this.name = name; } public String getName() { return this.name; } public void setID(String id) { this.id = id; } public String getID() { return this.id; } public void setSalary(String salary) { this.salary = salary; } public String getSalary() { return this.salary; } public void loadFromEmployee(Employee employee) { setID(employee.getID()); setName(employee.getName()); setSalary(employee.getSalary()); } public void updateEmployee(Employee employee) { employee.setID(getID()); employee.setName(getName()); employee.setSalary(getSalary()); } }
This second example is a lot better; encapsulation is preserved to a much greater extent. As an aside, this is a lot easier to unit test; you can test the behaviour of the Form without needing to drive it from the Action. But the major virtue of this is that the Action class doesn’t care about the details of the Form or Employee class.
This second example has some flaws, still. The first is that I can still forget to update the fields correctly; maybe there’s some business rule that says that Name must be set before ID. The second is that if I add a new field, I have to go and hunt for where it is used. When you start getting multiple views over a domain object, this can become painful. A further problem is that if there is validation logic in the domain object, the only way we can use it is to modify the domain object; afterwards, we have to revert it (or toss it away), if the validation fails.
We could solve these flaws, for this one example, by inverting the dependency. Rather than asking the form object to update itself from the Employee, we can ask the Employee object to update the Form. This makes the code look like this:
public class Employee { private String name; private String id; private String salary; public void updateForm(EmployeeForm form) { form.setName(name); form.setID(id); form.setSalary(salary); } public void loadFromForm(Employee form) { name = form.getName(); id = form.getID(); salary = form.getSalary()); } } public class EmployeeAction extends CustomStrutsAction { protected ActionMapping handleGet(ActionContext context) { EmployeeForm form = (EmployeeForm) context.getForm(); Employee employee = ; employee.updateForm(form); return context.findMapping("show_employee"); } protected ActionMapping handlePost(ActionContext context) { EmployeeForm form = (EmployeeForm) context.getForm(); Employee employee = ; employee.loadFromForm(form); return context.findMapping("updated_employee"); } } public class EmployeeForm { private String name; private String id; private String salary; public void setName(String name) { this.name = name; } public String getName() { return this.name; } public void setID(String id) { this.id = id; } public String getID() { return this.id; } public void setSalary(String salary) { this.salary = salary; } public String getSalary() { return this.salary; } }
This example, however, has a really silly flaw: the domain class is suddenly dependent on the EmployeeForm
class! We solve that by abstracting out an interface instead. For packaging reasons, it’s an inner interface of the Employee
class.
public class Employee { public static interface Builder { void setName(String name); String getName(); void setID(String ID); String getID(); void setSalary(String salary); String getSalary(); } private String name; private String id; private String salary; public void updateForm(Builder builder) { builder.setName(name); builder.setID(id); builder.setSalary(salary); } public void loadFromForm(Builder form) { name = builder.getName(); id = builder.getID(); salary = builder.getSalary()); } } public class EmployeeForm implements Employee.Builder { private String name;form.getName(); private String id; private String salary; public void setName(String name) { this.name = name; } public String getName() { return this.name; } public void setID(String id) { this.id = id; } public String getID() { return this.id; } public void setSalary(String salary) { this.salary = salary; } public String getSalary() { return this.salary; } }
Phew. We’re nearly done. This example is great, except for one thing: classes that only display the Employee
object need to expose “setters” that don’t do anything. Solve this by splitting up the Builder interface into two: one provides the getters, the other the setters. And bam! we’ve ended up with the design put forward by Holub, with one trivial change: the addXYZ and provideXYZ are renamed setXYZ and getXYZ.
What can we do now we’ve got this? The most obvious example is validation: we can inspect the incoming object to see if applying the changes it requests will cause the domain object to become invalid. If it does, we reject it, without changing the domain object at any time. This can reduce the number of useless copies of objects, because we don’t have to create a copy of the domain object just to validate it. There are a bunch of other things we can do, with this design.
But let’s consider some simple benefits. If I add a new field, I update the interface, and rebuild the project. Everywhere the domain object is displayed, I now have a compile error. Thus I can’t forget to add a field to a display (if I don’t want that field, I can consciously choose not to display it, but I can’t forget about it). With even one view, this is powerful. With several, it’s a potential life saver. Similarly, if a new business rule is added to a field, I only have one place to add validation logic.
Anyway, I’m tired and I’m heading back to bed. I hope this fills the gap perceived from my earlier blog. If not, let me know.
excellent, clarifying article. Thanks a lot !