In the last chapter, we generated a new set of domain classes and continued the approach of connecting them to the Java Swing and SQLite classes. We now want to step back and see what needs to be replaced or modified in our existing system to support a better (decoupled web) architecture.
Analyze the Issues in Your Current System
Before adding anything new, let’s look at what’s currently in place, with an eye towards what will be a problem in the long run. I've said it before, and I'll say it again: we don’t want to continue the situation of just adding on new functionality in an ad-hoc fashion.
This add on approach leads to an architecture called “a big ball of mud” or, in other words, a big mass that can’t be untangled. The longer the code stays muddy, the more difficult it becomes to add or modify functionality, which is a situation we want to avoid.
How do we know if something is muddy?
We can ask ourselves some questions about the current software. Here’s a good starting point. Is it simple? That is, simple to understand, maintain, modify, or test? Specifically:
Do I understand what it is trying to do? Do I understand how it is doing it?
How easy is it to maintain (find and fix bugs when they arise)? How easy is it to make a change? One that doesn’t cause bugs in this area, or some other place?
How easy it to test?
As you can see from our class diagram, we are well on the way to a muddy solution:
Step 1: Ask the Right Questions
We need some measuring sticks that tell us what’s better and what isn’t. For example, let’s look at some typical code from the original airline application:
class AircraftMechanic {
public List<MaintenanceIssue> getAllUnfixedIssues(JavaSwing component) {
Connection conn = DriverManager.getConnection( "jdbc:sqlite:./db/test.db")) {
if (conn != null) {
// get all the records
String sql = "SELECT id, entered, details, fixed FROM maintenance”;
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
// loop through the result set
while (rs.next()) {
// unfixed (i.e. no date set)
if (rs.getString(“fixed”).equals(“”)) {
// show the details for this item
component.addCheckbox(rs.getString(“details”));
}
}
}
}
}
Let's use our questions:
Do I understand what it is trying to do? How it's doing it? This method is doing all the things. Getting records, looping through records, determining which items aren’t fixed, and finally updating the display.
How easy is it to maintain? Make a change? Since it’s trying to do it all, it can’t easily be changed.
How easy is it to test? It also can’t be tested unless it is connected to the GUI and a database.
Why do people write code that's difficult to understand?
It probably wasn’t difficult to understand at the time we (or someone else) wrote it. The idea was fresh in our minds. Let me give you another type of example. I’ve seen code that has a transmitter and receiver that work as a pair. The variables are called tx and rx, respectively:
public void processResponse() {
Receiver rx = new Receiver();
Transmitter tx = new Transmitter();
while (rx.hasData() {
Data data = rx.getData();
tx.sendData(data);
}
}
Now, let's start with our first question: Do I understand what it is trying to do? How it's doing it? As long as the receiver is getting data, it gets that data and transmits it. But now it’s been months (years?) since we (or someone else) wrote that code. Transmit what? To what? Receive what? Oh wait, here it is, something called “data.” Oh great. What kind of data?
Our biggest issue here is we should have named things better. How about this instead?
public void processPrescriptionResponse() {
Receiver patientRxHistory = new Receiver();
Transmitter pharmacySender = new Transmitter();
while (patientRxHistory.hasData() {
Data patientRxRefilled = rx.getData();
pharmacySender.sendData(patientRxRefilled);
}
}
A small change makes this code much easier to understand and work with.
Step 2: Use the SOLID Design Principles
A great set of measuring sticks to use are the SOLID design principles.
One principle that is easy to violate is single responsibility. When new functionality is needed, it is often easy to add the new code to an existing piece of code. As we can see in the getAllClientsWithTripThisWeeky()
method above, the code is violating this principle by doing too much. The client class represents someone who is expected to pay for the trip and be one of the travelers. The class should not be responsible for knowing how to interact with an SQLite database, nor GUI components.
Additionally, data storage should be handled by its own layer of responsibility. Anything that needed to save or retrieve information should do so by accessing that layer of classes. Similarly, the user interface should be handled by its own layer as well.
If we want to factor out behavior based on single responsibility, our architecture will undergo a significant change.
Step 3: Apply Design Patterns
Finally, one other group of measuring sticks are design patterns. Design patterns are already existing solutions to common problems. Why reinvent when we can leverage a proven idea?
Let's look at an example that is associated with object-oriented languages like Java and C++. We want an object to change behavior based on certain circumstances. Now one way to do that is through inheritance. Capture the different functionality in different sub-classes. Then when an object needs to change, create a new object of the correct type and throw away the old.
Ah, but what if other parts of the system are still holding a reference to the old object? We can't add a global "everyone stop what you're doing and replace that old reference with this new one" without creating a big mess. So clearly, the original reference will have to stay in place. But we want that differing behavior.
We can use the object-role design pattern. We can think of the new behavior as being part of a role that the object plays. In this way, the object can easily swap roles, but still be the same object.
How does this apply to our application?
Our last use case concerns clients that have an outstanding balance, and those that owe a lot of money, instead of just a little. We'll want to show these different client types differently. But we don't want to swap out entire objects. Here's a UML diagram of how we can apply this pattern in our application:
We'll call a client's changeStatus()
with a ClientStatusPaidInFull
object to start. Then, if we determine they have an outstanding balance, we'll change it to ClientStatusOwes
, or ClientStatusOwesLots
. When they pay their balance, we'll set it back to PaidInFull. But we'll burn that bridge when we get to it. 😉
The point is, thinking about design patterns can help you solve a multitude of problems in your application.
Let's Recap!
Examine the existing code, looking for places that are not easy to understand, modify, or test.
Use SOLID principles to make decisions about modifying the architecture.
Investigate design patterns and use them where appropriate.
Now that we know which measuring sticks we can use when analyzing our code, let's prioritize our user stories!