What is your relationship to
encapsulation? Do you belong to the group who always start with setting all of the class's internal attributes to private, but who often end up needing to add getters because they couldn’t see a better solution? Or perhaps you belong to the group that has given up on encapsulation and who let their IDEs generate all the getters when the class is created? Maybe you belong to the third group who do everything in their power to ensure they maintain encapsulation; even going so far as to use instrumentation “magic” or code generation (or possibly both) to avoid adding those unwanted getters.
Most programmers agree that encapsulation is a good thing. It allows you to protect internal representation and control which methods are exposed to the outside world. If, for example, you have created the class Car, you have created something that you can relate to in a natural way which will make the code more readable and understandable, easier to work with and become a natural place to put all behavior that is related to the concept
car. All related code will be gathered in one place which is an important principle in programming and helps to prevent unwanted dependencies that otherwise can easily occur when behavior is spread out in various places in the code.
Have you ever wondered in what situations you are forced to violate encapsulation? Let us say that you create the class
Car and add a couple of nice methods that are needed in the business layer, for example
isBig(). You also need to add the getter
getName() but you can't see what harm that could cause. For the first it returns an
immutable type (String) that can't change the inner state of
Car via that reference. And second, you don’t think anyone will do anything else than reading it. What can happen is that code outside
Car operates on
name in other ways than just reading it. If that happens, it is
behaviour, and
moving behaviour out from the object is a bad thing and is not what object orientation is all about. Moving code outside the car can also lead to
code duplication which is not what we want to happen.
You think you have met those two criteria and it makes you feel both happy and proud. All other attributes, such as primary key to the car table in the database, are still set as
private which is a good thing since these are details that the business layer does not care about. But it happens that someone asks you to add an export function for an old banking system. The file that is exported should follow a text format with fixed positions where every position holds an attribute of a car.
This is the moment where it happens - you are forced to violate encapsulation! Why do you need to do that? Because you have
switched context. In the new context, we are suddenly not at all interested in the methods used in the business layer, such as
isBig(), but on the other hand have urgent needs to access the internal class representation. We swallow our pride and add a couple of getters who we then use in our class CarBankExporter. We have now introduced the risk that
behaviour will move outside Car and at the same time managed to
pollute the cars API. Does this feels familiar? The next question is, is there any good solution to this problem?
What if we could create the instance
car of the type
Car and then
context-switch this instance to another class instance with preserved internal representation, but with access to a new set of methods, tailor-made for our new context (export cars to a file)? Or if we could create an instance of the new "handle export" class and then
context-switch to the version of
Car to be used in the business layer. This helps us maintain the object oriented approach where we
move behavior into the object rather than as in the case CarBankExporter where we did the opposite by
moving the object to the behavior.
The latter is an example of procedural programming, and corresponds to
calling a function with a Car struct in C. The solution to our problems is called
context switching and is handled by the pattern
Context Switcher.
The best way to explain something is often by example. Assume that we have our class
Car, which is used in our business layer, and that we both need to export cars to file and store cars in a database. Let's take a look how the code for such a scenario could look like in Java (the source is hosted at
GitHub), class
Main:
package nu.tengstrand.contextswitcher;
import nu.tengstrand.contextswitcher.car.CarFactory;
import nu.tengstrand.contextswitcher.car.banking.CarAsRowInFile;
import nu.tengstrand.contextswitcher.car.business.Car;
import nu.tengstrand.contextswitcher.car.persistence.DbPersister;
import nu.tengstrand.contextswitcher.car.persistence.CarInDb;
public class Main {
/**
* This is an example of the pattern Context Switcher.
*
* Author: Joakim Tengstrand, august 2011.
*/
public static void main(String[] args) {
// 1. Create an instance of Car and run some business logic.
// Only the method isBig(), that think cars >= 400 cm are big,
// is exposed from this context.
Car car = CarFactory.createCar(479, "Volvo");
System.out.println("The car: " + car);
printIsBig(car);
// 2. Switch context to "car in database" and save to database.
// Only the method save() is exposed from this context.
DbPersister dbPersister = new DbPersister();
CarInDb carInDb = car.asCarInDb(dbPersister);
carInDb.save();
// 3. Change context to "as row in file" and append to file.
// Only the method appendToFile() is exposed from this context.
FileWriter fileWriter = new FileWriter("Car.txt");
CarAsRowInFile carToBeStoredInFile = carInDb.asRowInFile();
carToBeStoredInFile.appendToFile(fileWriter);
// 4. Read a car from file and change context to "business layer".
String rowInFile = new FileReader("Car.txt").readNextRowFromFile();
CarAsRowInFile carFromFile = CarFactory.createCarAsRowInFile(rowInFile);
System.out.println("Row in file context: " + carFromFile);
Car businessLayerCarFromFile = carFromFile.asCar();
System.out.println("Business layer context: " + businessLayerCarFromFile);
printIsBig(businessLayerCarFromFile);
}
private static void printIsBig(Car car) {
String not = car.isBig() ? "" : "not ";
System.out.println("This car is " + not + "big!");
}
}
The output looks like this:
The car: Car{lengthInCentimeters=479, name=Volvo}
This car is big!
'CarInDb{lengthInCentimeters=479, name=Volvo, primaryKey=12}' was saved to the database!
'0479,Volvo' was appended to file 'Car.txt'
Row in file context: CarAsRowInFile{"0384,Fiat"}
Business layer context: Car{lengthInCentimeters=384, name=Fiat}
This car is not big!
Our three classes now expose only the methods that are meaningful depending on what context they are in and we haven’t violated encapsulation. VoilĂ ! Problem solved!
How to use the pattern
For this pattern to be meaningful, it should be applied to an object-oriented language. Java is used in this example but any other object oriented language will do. It should also have support for the keywords
private (or similar) to ensure encapsulation.
I have tried to keep the sample code, available for
download here, as simple and readable as possible. It can sometimes miss validations, annotations as @Override and the like. We will concentrate on the class
Car. The other two representations
CarInDb and
CarAsRowInFile is created using the same pattern.
A note about the example. Using a
factory with static methods, works well in certain situations. However, in larger enterprise systems, it is likely you will want to use
dependency injection to provide the code with references to
factories or
repositories which I will write about in a later blog post.
You should find the following packages and classes:
The UML diagram looks like this:
Start by creating the class
CarInternals:
package nu.tengstrand.contextswitcher.car;
/**
* Put the internal representation of a car, that is shared between
* different representations, in this class!
*/
public class CarInternals {
// We don't want to lose track of the PK when switching context
// so we need to put it here together with the other shared attributes.
public Integer primaryKey;
public int lengthInCentimeters;
public String name;
public CarInternals(int lengthInCentimeters, String name) {
if (lengthInCentimeters < 0 || lengthInCentimeters > 999) {
throw new IllegalArgumentException("Illegal length: " + lengthInCentimeters);
}
this.lengthInCentimeters = lengthInCentimeters;
this.name = name;
}
public String toString() {
return "lengthInCentimeters=" + lengthInCentimeters + ", name=" + name;
}
}
This is where you store all the attributes shared by the three different car representations. Remember to set them as
public. It may feel a bit strange, since our goal is to protect our internal representation, but stay cool the internal representation will not be exposed!
Create an ordinary
public constructor in a way that you would, if it was part of the class Car without using this pattern. Add the method
toString where you list the attributes.
Create the class
CarContextSwitcher:
package nu.tengstrand.contextswitcher.car;
import nu.tengstrand.contextswitcher.car.banking.CarAsRowInFile;
import nu.tengstrand.contextswitcher.car.business.Car;
import nu.tengstrand.contextswitcher.car.persistence.DbPersister;
import nu.tengstrand.contextswitcher.FileWriter;
import nu.tengstrand.contextswitcher.car.persistence.CarInDb;
import java.util.HashMap;
import java.util.Map;
/**
* This class is responsible for switching between cars that are tailor-made for a specific context.
*/
public class CarContextSwitcher {
private final CarInternals internals;
// Context swithable car representations
private Car car;
private CarAsRowInFile carAsRowInFile;
private Map<DbPersister,CarInDb> carInDbs = new HashMap<DbPersister,CarInDb>();
public CarContextSwitcher(CarInternals carInternals) {
internals = carInternals;
}
public Car asCar() {
if (car == null) {
car = new Car(internals, this);
}
return car;
}
/**
* The implementation of DbPersister may vary as we have to consider.
*/
public CarInDb asCarInDb(DbPersister dbPersister) {
if (carInDbs.containsKey(dbPersister)) {
return carInDbs.get(dbPersister);
}
CarInDb carInDb = new CarInDb(internals, this, dbPersister);
carInDbs.put(dbPersister, carInDb);
return carInDb;
}
public CarAsRowInFile asRowInFile() {
if (carAsRowInFile == null) {
carAsRowInFile = new CarAsRowInFile(internals, this);
}
return carAsRowInFile;
}
public String toString() {
return "CarContextSwitcher{internals=" + internals + "}";
}
}
Add the attribute
internals and make sure it is set to
private final. To set it as final will ensure that it is assigned by the constructor and will not be changed. Add the attributes
car,
carInDb and
carAsRowInFile. In the case
carInDb we send in
dbPersister which can vary, so we need to handle that case using a Map. Add the constructor and assign
internals. Add method
asCar where you return a
Car.
Continue with the class
Car:
package nu.tengstrand.contextswitcher.car.business;
import nu.tengstrand.contextswitcher.FileWriter;
import nu.tengstrand.contextswitcher.car.CarContextSwitcher;
import nu.tengstrand.contextswitcher.car.CarInternals;
import nu.tengstrand.contextswitcher.car.banking.CarAsRowInFile;
import nu.tengstrand.contextswitcher.car.persistence.DbPersister;
import nu.tengstrand.contextswitcher.car.persistence.CarInDb;
/**
* Represents a car as the business layer wants to see it.
*/
public class Car {
private final CarInternals internals;
private final CarContextSwitcher contextSwitcher;
/**
* DO NOT USE THIS CONSTRUCTOR - use the CarFactory!
*/
public Car(CarInternals carInternals, CarContextSwitcher carContextSwitcher) {
internals = carInternals;
contextSwitcher = carContextSwitcher;
}
public CarInDb asCarInDb(DbPersister dbPersister) {
return contextSwitcher.asCarInDb(dbPersister);
}
public CarAsRowInFile asRowInFile(FileWriter fileWriter) {
return contextSwitcher.asRowInFile();
}
/**
* Here is an example of business logic that operates on the internal representation.
*/
public boolean isBig() {
return internals.lengthInCentimeters >= 400;
}
public String toString() {
return "Car{" + internals + "}";
}
}
Here we have our pure business object.
Add the attributes
internals and
contextSwitcher and make sure they are set to
private final.
Add the
public constructor with the comment DO NOT USE with the signature shown in the example. Now add "as" methods of the other two context-switchable classes, in this case
asCarInDb and
asRowInFile. Then add your business methods such as
isBig(). This is an ordinary class where the only thing you need to think of is to add this constructor!
Create the class
CarFactory:
package nu.tengstrand.contextswitcher.car;
import nu.tengstrand.contextswitcher.car.banking.CarAsRowInFile;
import nu.tengstrand.contextswitcher.car.business.Car;
import nu.tengstrand.contextswitcher.car.persistence.DbPersister;
import nu.tengstrand.contextswitcher.FileWriter;
import nu.tengstrand.contextswitcher.car.persistence.CarInDb;
/**
* Responsible for creating our "context dependent" cars.
*/
public class CarFactory {
public static Car createCar(int lengthInCentimeter, String name) {
CarInternals carInternals = new CarInternals(lengthInCentimeter, name);
return new CarContextSwitcher(carInternals).asCar();
}
public static CarInDb createCarInDb(int lengthInCentimeter, String name, DbPersister dbPersister) {
CarInternals carInternals = new CarInternals(lengthInCentimeter, name);
return new CarContextSwitcher(carInternals).asCarInDb(dbPersister);
}
public static CarAsRowInFile createCarAsRowInFile(String rowInFile) {
return new CarAsRowInFile(rowInFile);
}
}
This is the class responsible for creating instances of our
context-switchable car classes. Make sure all the
create methods are set as
public static. The method signatures must include all parameters
CarInternals needs plus any other parameters that your class needs.
Car is an example that only wants a
CarInternals while eg
CarInDb also wants a
CarDbPersister.
The classes
Car and
CarInDb is created by
context-switch them via
CarContextSwitcher. This works well since we can easily create a
CarInternals. In the case
CarAsRowInFile we don’t want to create a
CarInternals due to this responsibility lies in the class itself, and we therefore use the constructor directly.
Now you can continue with adding more car classes specialized for a
specific context, and if you follow the example, this means you also need to create
CarInDb,
CarAsRowInFile and
Main!
To make the example complete, here are the other two representations of
Car, we begin with
CarInDb:
package nu.tengstrand.contextswitcher.car.persistence;
import nu.tengstrand.contextswitcher.FileWriter;
import nu.tengstrand.contextswitcher.car.CarContextSwitcher;
import nu.tengstrand.contextswitcher.car.CarInternals;
import nu.tengstrand.contextswitcher.car.banking.CarAsRowInFile;
import nu.tengstrand.contextswitcher.car.business.Car;
/**
* Represents a car as a record in a table in the database.
*/
public class CarInDb {
private final CarInternals internals;
private final CarContextSwitcher contextSwitcher;
private final DbPersister dbPersister;
/**
* DO NOT USE THIS CONSTRUCTOR - use the CarFactory!
*/
public CarInDb(CarInternals carInternals, CarContextSwitcher carContextSwitcher, DbPersister dbPersister) {
internals = carInternals;
contextSwitcher = carContextSwitcher;
this.dbPersister = dbPersister;
}
public Car asCar() {
return contextSwitcher.asCar();
}
public CarAsRowInFile asRowInFile() {
return contextSwitcher.asRowInFile();
}
public void save() {
internals.primaryKey = dbPersister.save(internals.primaryKey, contextSwitcher);
System.out.println(" '" + this + "' was saved to the database!");
}
public String toString() {
return "CarInDb{" + internals + ", primaryKey=" + internals.primaryKey + "}";
}
}
And here comes the last class
CarAsRowInFile:
package nu.tengstrand.contextswitcher.car.banking;
import nu.tengstrand.contextswitcher.FileWriter;
import nu.tengstrand.contextswitcher.car.CarContextSwitcher;
import nu.tengstrand.contextswitcher.car.CarInternals;
import nu.tengstrand.contextswitcher.car.business.Car;
import nu.tengstrand.contextswitcher.car.persistence.DbPersister;
import nu.tengstrand.contextswitcher.car.persistence.CarInDb;
/**
* Represents a car in the context of reading and writing it to a file.
* The two constructors needs to keep the two representations,
* internals and rowInFile, in sync.
*
* File format, e.g "0479,Volvo":
* 0-3 = Length in centimeters
* 5- = Name
*/
public class CarAsRowInFile {
private final CarInternals internals;
private final CarContextSwitcher contextSwitcher;
private String rowInFile;
/**
* DO NOT USE THIS CONSTRUCTOR - use the factory!
*
* @param rowInFile row in a text file.
*/
public CarAsRowInFile(String rowInFile) {
this.rowInFile = rowInFile;
internals = createCarInternals(); // We also need to set the internal representation.
contextSwitcher = new CarContextSwitcher(internals);
}
private CarInternals createCarInternals() {
int lengthInCentimeters = Integer.parseInt(rowInFile.substring(0,4));
String name = rowInFile.substring(5);
return new CarInternals(lengthInCentimeters, name);
}
/**
* DO NOT USE THIS CONSTRUCTOR - use the CarFactory!
*/
public CarAsRowInFile(CarInternals carInternals, CarContextSwitcher carContextSwitcher) {
internals = carInternals;
contextSwitcher = carContextSwitcher;
setRowInFile(); // We also need to set the "row in file" representation.
}
private void setRowInFile() {
String lengthCm = "000" + internals.lengthInCentimeters;
rowInFile = lengthCm.substring(8-lengthCm.length()) + "," + internals.name;
}
public Car asCar() {
return contextSwitcher.asCar();
}
public CarInDb asCarInDb(DbPersister dbPersister) {
return contextSwitcher.asCarInDb(dbPersister);
}
public void appendToFile(FileWriter fileWriter) {
fileWriter.appendToFile(rowInFile);
}
public String toString() {
return "CarAsRowInFile{\"" + rowInFile + "\"}";
}
}
It may be worth mentioning a detail of this class. It differs little from the other two as seen in
CarFactory where the other types are sending in the entire internal representation in the parameter list,
int lengthInCentimeter, String name, while CarAsRowInFile is created with the signature
String rowInFile. This means that a
CarAsRowInFile can be created in two ways, either by
CarFactory where you send in a row from the file, which is our alternative representation of a car or via
context-switching an instance of
Car or
CarInDb.
With these words I’m wishing you good luck with this pattern! A special thanks to James Trunk who helped me with the English text and provided constructive feedback. I would also like to thank Magnus Mickelsson, who took the time to look through the solution.