October 12, 2011
Fluent interfaces while trying to make sense of Prototype Pattern
Sometimes I don’t have great opportunities to apply some nice concepts, but I have some time to learn them.
While trying to make some sense of the Prototype Pattern, I was doing some experiments and I needed, of course, the tests to prove my experiments.
It was when coding the tests, trying to define the client interface (not user interface :)), that to my mind came this article from Martin Fowler: Fluent Interface.
Well, what it matters are these two points:
- Making your tests first helps you, a lot, to define not only the behavior, but also importantly, the client API.
- Using Fluent Interface with a Builder, it’s a great way to send our message to the objects implementing the API.
Example of using a fluent interface:
package eu.jpereira.trainings.designpatterns.creational.prototype; import static org.junit.Assert.*; import java.util.List; import java.util.Properties; import org.junit.Test; import eu.jpereira.trainings.designpatterns.creational.prototype.exception.CannotHaveZeroPartsException; import eu.jpereira.trainings.designpatterns.creational.prototype.exception.CouldNotCloneLastObjectException; import eu.jpereira.trainings.designpatterns.creational.prototype.exception.NeedToPackLastVehicleException; import eu.jpereira.trainings.designpatterns.creational.prototype.exception.VehicleDoesNotHavePartsException; import eu.jpereira.trainings.designpatterns.creational.prototype.model.Shell; import eu.jpereira.trainings.designpatterns.creational.prototype.model.Tire; import eu.jpereira.trainings.designpatterns.creational.prototype.model.Vehicle; import eu.jpereira.trainings.designpatterns.creational.prototype.model.VehiclePartEnumeration; import eu.jpereira.trainings.designpatterns.creational.prototype.model.VehiclePartPropertiesEnumeration; import eu.jpereira.trainings.designpatterns.creational.prototype.model.Window; /** * @author jpereira * */ public class ClientTest { /** * Integration Test * @throws CouldNotCloneLastObjectException * @throws CannotHaveZeroPartsException * @throws NeedToPackLastVehicleException * @throws VehicleDoesNotHavePartsException */ @Test public void testCreateBUS() throws CouldNotCloneLastObjectException, CannotHaveZeroPartsException, NeedToPackLastVehicleException, VehicleDoesNotHavePartsException { Client client = new Client(); //create a bus car //Create props for tire Properties tiresProps = new Properties(); tiresProps.put(VehiclePartPropertiesEnumeration.SIZE,10); //Create props for shell Properties shellProps = new Properties(); shellProps.put(VehiclePartPropertiesEnumeration.COLOR,"blue"); Properties windowProps = new Properties(); windowProps.put(VehiclePartPropertiesEnumeration.WIDTH,20); windowProps.put(VehiclePartPropertiesEnumeration.WIDTH,20); //client.createVehicle().with(new Tires()).times(4). Vehicle vehicle = client.vehicleBuilder().createVehicle().with(new Tire(tiresProps)).times(3).with(new Window(windowProps)).times(8).with(new Shell(shellProps)).times(1).packIt(); //Get all windows List<VehiclePart> parts = vehicle.getParts(VehiclePartEnumeration.WINDOW); assertEquals(8, parts.size()); } }
A hypothetical Builder implementation with a fluent interface:
package eu.jpereira.trainings.designpatterns.creational.prototype; import java.util.ArrayList; import java.util.List; import eu.jpereira.trainings.designpatterns.creational.prototype.exception.CouldNotCloneLastObjectException; import eu.jpereira.trainings.designpatterns.creational.prototype.exception.CannotHaveZeroPartsException; import eu.jpereira.trainings.designpatterns.creational.prototype.exception.NeedToPackLastVehicleException; import eu.jpereira.trainings.designpatterns.creational.prototype.model.Vehicle; /** * @author jpereira * */ public class SimpleVehicleBuilder implements VehicleBuilder { private List<VehiclePart> vehicleParts; public SimpleVehicleBuilder() { this.vehicleParts = createNewPartsBag(); } @Override public VehicleBuilder createVehicle() throws NeedToPackLastVehicleException{ //Just check this is allways the first call on the builder if (vehicleParts.size()!= 0) { throw new NeedToPackLastVehicleException(); } return this; } @Override public VehicleBuilder with(VehiclePart part) { this.vehicleParts.add(part); return this; } @Override public VehicleBuilder times(int times) throws CouldNotCloneLastObjectException, CannotHaveZeroPartsException { if (times==0) { throw new CannotHaveZeroPartsException(); } //get the last one and clone it xtimes if ( this.vehicleParts.size()>0) { VehiclePart lastObject = this.vehicleParts.get(this.vehicleParts.size()-1); //add it xtimes for (int i=0; i< times-1; i++) { //new object try { this.vehicleParts.add((VehiclePart)lastObject.clone()); } catch (CloneNotSupportedException e) { //Could not clone it. Wrap exception throw new CouldNotCloneLastObjectException(e); } } } return this; } @Override public Vehicle packIt() { Vehicle vehicle = new Vehicle(); vehicle.setParts(this.vehicleParts); //clear this reference for the chicle parts. this.vehicleParts = createNewPartsBag(); return vehicle; } //Can be overriden by subclasses protected List<VehiclePart> createNewPartsBag() { return new ArrayList<VehiclePart>(); } }
That’s it