We have skimmed over different persistence methods and worked out how to wrap them up in a repository facade pattern. But what are the costs and benefits of using repository patterns?
Maximizing Benefits: Reusability and Reliable Mocking
One of the benefits of the repository facade pattern is the ability to reuse and replace code. Reuse means that since your persistence code is behind a facade, you can package it into a JAR and use it elsewhere in other applications.
Where possible, therefore, you should write persistence implementations using generics so that you can reuse them for other data objects. Even in these examples that you've seen so far, you can reuse the person and PersonPersister for applications other than customer databases.
What about replacing code?
You have already seen that you can swap between different implementations. You might start with a simple JAXB persister, and later - when you need to share much more data efficiently and safely - swap in a Hibernate persister without changing your application code.
This is particularly useful when testing. Automate as many tests as you can so that you can accumulate more tests to run more often and so assure the quality and reliability of your software. Automatic tests require a consistent initial dataset so that they can check the output of the functions they are testing against consistent test criteria.
For example, if you are testing a mailshot application that creates emails for customers in a particular area, an automatic test needs a fixed set of acceptance criteria to check its results against. This set means you need a fixed customer database, with a known number of customers in that particular area.
With a repository facade, you can give a test repository with fixed, known contents, to your code to be used when tested. For example, here is some code for a test repository that, when created, has two starting customers:
Let's recap the code:
public class CustomerMockRepository implements CustomerRepository {
Map<Integer, Person> customers = new HashMap<>();
int maxid = 0;
public CustomerMockRepository() {
reset();
}
public void reset() {
customers.clear();
maxid=0;
Person samjones = new Person();
samjones.name = "Sam Jones";
samjones.address = "12 Letsbe Avenue, Royston Vasey, MK2 3AU";
samjones.email = "sam.jones@openclassrooms.co.uk";
samjones.telephone = "+44 7700 900081";
create(samjones);
Person alexwilliams = new Person();
alexwilliams.name = "Alex Williams";
alexwilliams.address = "2 Cartswell Lane, UpHamsteadton Town, MK2 3AB";
alexwilliams.email = "alex.williams@openclassrooms.co.uk";
alexwilliams.telephone = "+44 7700 900021";
create(alexwilliams);
}
@Override
public void create(Person newPerson) {
maxid++;
newPerson.ID = maxid;
customers.put(newPerson.ID, newPerson);
}
@Override
public Person read(int id) {
return customers.get(id);
}
@Override
public void update(Person editedPerson) {
customers.put(editedPerson.ID, editedPerson);
}
@Override
public void delete(Person deletePerson) {
customers.remove(deletePerson.ID);
}
@Override
public List<Person> findNamed(String name) {
List<Person> foundPeople = new ArrayList<>();
for (Person customer : customers.values()) {
if (customer.name.contains(name)) foundPeople.add(customer);
}
return foundPeople;
}
}
The code above is just an in-memory list of customer people that always starts with the same two people, and provides the CRUD repository methods defined in the API to access and modify them. Tests can use the reset method to ensure that each test starts with the same known dataset that hasn't been modified by previous tests.
Managing Disadvantages
Uncovering Hidden Problems
The same advantage that the repository facade brings - hiding complexity - also brings a disadvantage: it hides problems. When things go wrong, you need to be able to report them to the application so that it can attempt alternatives, fail gracefully, or at least inform the user with a message that can help to resolve the problem.
Think about what explicit exceptions to throw. You may create new ones, or in the case of repositories, you could use an existing suitable one, such as IOException:
public interface CustomerRepository {
public void create(Person customer) throws IOException;
public Person read(String id) throws IOException;
public void update(Person customer) throws IOException;
public void delete(Person customer) throws IOException;
}
The key here is to catch problems in the hidden code and rethrow with suitable details to help deal with them.
For example, if your JdbcPersister cannot connect to its database, it should tell the user what the problem is. Java has a neat way of wrapping exceptions so that you can include the original exception with details of the problem and throw a wrapping exception, like this example similar to the one you saw earlier in this course:
public JdbcCustomerCrudPersister(String url) throws IOException {
try {
connection = DriverManager.getConnection(url);
}
catch (Throwable th) {
throw new IOException(th+" attempting to connect JDBC url:"+url,th);
}
}
This adds detail that can help you investigate and solve the problem.
Dealing with Lost Features
Each persistence mechanism comes with its own set of features, only some of which are available in the other persisters. For instance, JAXB stores object details in a human-readable form, but this takes up quite a lot of space, so you may want to switch XML formatting on or off, or compress it in a ZIP file. These operations don't make sense for Hibernate or JDBC. These, in turn, provide a rich querying language (SQL) that is not available to a JAXB persister.
A naive repository facade interface can only have the common features of the various persistence technologies, and that limits your ability to exploit the rich specialist features that could benefit your application.
One option is to provide methods that work for the persistence mechanisms they apply to and do nothing for others. However, this can lead to cluttered interface methods that appear to provide features but don't and cause confusion for developers who use your repository facade.
Another option is to implement those missing features, but as you might imagine, trying to write a SQL language parser for your non-SQL persister would be quite a lot of work.
Finally, a common approach is to create extensible facades. That is, you provide the basic interface (the CRUD operations in the above CustomerRepository examples) and also an extended interface that defines a particular set of methods that access a feature:
public interface SqlCustomerRespository extends CustomerRepository {
public List<Person> query(String sqlSelect);
}
Then most of your application code can continue to use the CRUD repository methods. Still, code that needs to use SQL could check that the repository is an SQLCustomerRepository
, return an error if not, and use the extra methods if so. Most of your code still uses the common repository and reduces the number of touchpoints of code that cannot cope with non-SQL persisters.
Let's Recap!
The repository facade benefits:
Reuse and replace.
This makes it easier create mock repositories that you can use to thoroughly test your application code, and you can use these in many test environments.
Costs to using the repository facade:
Investigating bugs in hidden code behind interfaces can be harder and take longer. You can mitigate that by catching errors in your persistence code, adding suitable details, and rethrowing them.
Exposing useful specialist features of particular persistence technologies can mean you lose the benefit of having a single facade interface. Restricting your application to use only that facade, however, denies access to specialist features. Strike a balance depending on the situation.
Let's have a look now at what topics you might want to explore next.