Skip to content

4. [TD]: Layered architectures

Keywords: multi-layer architecture, Spring, dependency injection.

4.1. Introduction

Let’s review what we’ve done:

  • In Part 1 of the ELECTIONS exercise, no classes were used. We built a solution as we would have built it in the C language.
  • In Part 2 of the exercise, two classes were introduced:
    • [VoterList], which represents the attributes (id, name, votes, seats, eliminated) of a candidate list
    • [ElectionsException], a class for unhandled exceptions. This type of exception is used whenever a fatal error occurs in the election application. It is unhandled, meaning the developer is not required to handle it with a try-catch block.

Until now, the calculation of election results has been handled by a [main] method of a [MainElections] class

package istia.st.elections;

import java.io.*;

public class MainElections {

  // some data
  private static final double margin = 0.05;

  // ----------------------------------------------------------------------
  // the main procedure
  public static void main(String[] arguments) throws IOException {

    // prepare the keyboard input stream
    BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));

    // Enter the data needed to calculate the seats
...
    // Calculate the seats won by the different lists
....
    // display the results
...
  } // main
} // class

The previous solution includes three standard phases:

  • data acquisition, lines 17-18
  • calculating the solution, lines 19–20
  • displaying and/or saving the results, lines 21–22

Only phase 2 is truly constant. Phase 1 can vary: data can come from the keyboard as in the examples studied, from a text file, from a graphical interface, from a database, from the network, ... Similarly, there are multiple ways to output the results in phase 3: displaying them on the screen as done in the examples studied, saving them to a file, to a database, sending them over the network, etc.

More generally, an application can often be modeled as three layers, each with a well-defined role:

This architecture is also called "three-tier architecture." The term "three-tier" normally refers to an architecture where each tier is on a different machine. When the tiers are on the same machine, the architecture becomes a "three-layer" architecture.

  • The [business] layer contains the application’s business rules. For our election application, these are the rules that calculate the seats won by the different slates once the votes obtained by each are known. This layer requires data to function. For example, in the election application:
  • the lists, each with its name and number of votes
  • the number of seats to be filled
  • the electoral threshold below which a list is eliminated

In the diagram above, the data can come from two sources:

  • the data access layer or [DAO] (DAO = Data Access Object) for data already stored in files or databases. This could be the case here for the names of the lists, the number of seats to be filled, and the electoral threshold. Indeed, this information is known before the election itself.
  • the user interface layer or [ui] (UI = User Interface) for data entered by the user or displayed to the user. This could be the case here for the votes for the lists, which are only known at the last moment, as well as for the display of the election results.
  • Generally speaking, the [DAO] layer handles access to persistent data (files, databases) or non-persistent data (network, sensors, etc.).
  • The [UI] layer, on the other hand, handles interactions with the user, if there is one.
  • The three layers are made independent through the use of Java interfaces.
  • There are various methods for integrating these layers into the application. We will use a tool called "Spring." In the diagram, it cuts across the other layers.

We will revisit the [Elections] application developed previously to give it a three-tier architecture. To do this, we will examine the [UI, Business, DAO] layers one by one, starting with the [DAO] layer, which handles persistent data.

First, we need to define the interfaces for the different layers of the [Elections] application.

4.2. The Interfaces of the [Elections] Application

Remember that an interface defines a set of method signatures. The classes that implement the interface provide the implementation for these methods.

Let’s return to the 3-layer architecture of our application:

In this type of architecture, it is often the user who takes the initiative. They make a request at [1] and receive a response at [8]. This is called the request-response cycle. Let’s take the example of calculating the seats won on election night. This will require several steps:

  1. The [ui] layer will need to ask the user for the number of votes received by each of the lists. To do this, it will need to display the names of the competing lists to the user. The user will then simply enter the number of votes next to each list and request a seat calculation.
  2. The [ui] layer does not have the names of the lists. These are stored in the data source to the right of the diagram. It will use the path [2, 3, 4, 5, 6, 7] to retrieve them. Operation [2] is the request for the lists, and operation [7] is the response to that request. Once this is done, it can present them to the user via [8].
  3. The user will transmit to the [ui] layer the number of votes obtained by each list. This is operation [1] above. During this step, the user interacts only with the [ui] layer. It is this layer that will verify the validity of the entered data. Once this is done, the user will request the list of seats obtained by each list.
  4. The [ui] layer will ask the business layer to calculate the seats. To do this, it will send the data it received from the user to the business layer. This is operation [2].
  5. The [business] layer needs certain information to perform its task. It already has the lists from operation (b). It also needs the number of seats to be filled and the electoral threshold value. It will request this information from the [DAO] layer via the path [3, 4, 5, 6]. [3] is the initial request and [6] is the response to that request.
  6. With all the data it needed, the [business] layer calculates the seats won by each list.
  7. The [business] layer can now respond to the request from the [ui] layer made in (d). This is path [7].
  8. The [UI] layer will format these results to present them to the user in an appropriate form and then display them. This is path [8].
  9. One can imagine that these results need to be stored in a file or a database. This can be done automatically. In this case, after operation (f), the [business] layer will ask the [DAO] layer to save the results. This will be path [3, 4, 5, 6]. This can also be done only upon user request. Path [1-8] will be used by the request-response cycle.

We can see from this description that a layer uses the resources of the layer to its right, never those of the layer to its left. Consider two contiguous layers:

Layer [A] makes requests to layer [B]. In the simplest cases, a layer is implemented by a single class. An application evolves over time. Thus, layer [B] may have different implementation classes [B1, B2, ...]. If layer [B] is the [DAO] layer, it may have an initial implementation [B1] that retrieves data from a file. A few years later, we may want to store the data in a database. We will then build a second implementation class [B2]. If, in the initial application, layer [A] worked directly with class [B1], we are forced to partially rewrite the code for layer [A]. Suppose, for example, that we wrote something like the following in layer [A]:

1
2
3
B1 b1 = new B1(...);
..
b1.getData(...);
  • line 1: an instance of class [B1] is created
  • line 3: data is requested from this instance

If we assume that the new implementation class [B2] uses methods with the same signature as those of class [B1], we will have to change all [B1] references to [B2]. This is a very favorable scenario and quite unlikely if we haven’t paid attention to these method signatures. In practice, it is common for classes [B1] and [B2] to have different method signatures, meaning that a significant portion of layer [A] must be completely rewritten.

We can improve the situation by introducing an interface between layers [A] and [B]. This means that we define the method signatures presented by layer [B] to layer [A] in an interface. The previous diagram then becomes the following:

Layer [A] no longer communicates directly with layer [B] but with its interface [IB]. Thus, in the code for layer [A], the implementation class [Bi] of layer [B] appears only once, at the time of implementing the interface [IB]. Once this is done, it is the interface [IB] and not its implementation class that is used in the code. The previous code becomes the following:

1
2
3
IB ib = new B1(...);
..
ib.getData(...);
  • line 1: an instance [ib] implementing the interface [IB] is created by instantiating the class [B1]
  • line 3: data is requested from the instance [ib]

Now, if we replace the implementation [B1] of layer [B] with an implementation [B2], and both implementations adhere to the same interface [IB], then only line 1 of layer [A] needs to be modified, and no other lines. This is a major advantage that alone justifies the systematic use of interfaces between two layers.

We can go even further and make layer [A] completely independent of layer [B]. In the code above, line 1 poses a problem because it hard-codes a reference to class [B1]. Ideally, layer [A] should be able to use an implementation of interface [IB] without having to specify a class name. This would be consistent with our diagram above. We can see that layer [A] interacts with interface [IB], and there is no reason why it would need to know the name of the class that implements this interface. This detail is not useful to layer [A].

The Spring framework (http://www.springframework.org) allows us to achieve this result. The previous architecture evolves as follows:

The cross-cutting layer [Spring] will allow a layer to obtain, via configuration, a reference to the layer to its right without needing to know the name of the class implementing that layer. This name will be in the configuration files and not in the Java code. The Java code for layer [A] then takes the following form:

1
2
3
IB ib; // initialized by Spring
..
ib.getData(...);
  • line 1: an instance [ib] implementing the [IB] interface of layer [B]. This instance is created by Spring based on information found in a configuration file. Spring will handle creating:
    • the instance [b] implementing layer [B]
    • the [a] instance implementing layer [A]. This instance will be initialized. The [ib] field above will be assigned the [b] reference of the object implementing layer [B]
  • line 3: data is requested from the [ib] instance

We can now see that the implementation class [B1] of layer B does not appear anywhere in the code of layer [A]. When the implementation [B1] is replaced by a new implementation [B2], nothing will change in the code of class [A]. We will simply change the Spring configuration files to instantiate [B2] instead of [B1].

The combination of Spring and Java interfaces brings a decisive improvement to application maintenance by making the layers of the application tightly coupled with one another. This is the solution we will use for the [Elections] application.

Let’s return to the three-tier architecture of our application:

In simple cases, we can start from the [business] layer to discover the application’s interfaces. To function, it needs data:

  • already available in files, databases, or via the network. This data is provided by the [DAO] layer.
  • not yet available. It is then provided by the [UI] layer, which obtains it from the application user.

What interface should the [DAO] layer provide to the [business] layer? What interactions are possible between these two layers? The [DAO] layer must provide the following data to the [business] layer:

  • the number of seats to be filled
  • the electoral threshold below which a list is eliminated
  • the names of the lists

This information is known before the election and can therefore be stored. In the [business] -> [DAO] direction, the [business] layer can ask the [DAO] layer to record the election results, including the number of seats won by the various lists.

With this information, we could attempt an initial definition of the [DAO] layer interface:


public interface IElectionsDao {

  public double getVotingThreshold();

  public int getNumberOfSeatsToBeFilled();

  public VoterList[] getVoterLists();

  public void setVoterLists(VoterList[] voterLists);
}
  • Line 1: The interface is called [IElectionsDao]. It defines four methods:
    • three methods for reading data from the data source: [getVotingThreshold, getNumberOfSeatsToBeFilled, getVoterLists]. These three methods will allow the [business] layer to obtain the data characterizing the current election.
    • one method for writing data to the data source: [setVoterLists]. This method will allow the [business] layer to request the recording of the results it has calculated.

Let’s return to the three-layer architecture of our application:

What interface should the [business] layer present to the [ui] layer? Let’s examine the possible interactions between these two layers.

  1. The [ui] layer will be responsible for asking the user for votes for the various competing lists. To do this, it must know the number of lists. It can request this information from the [business] layer, which in turn can request the table of competing lists from the [dao] layer. If the [business] layer has this table, it might as well transfer it to the [UI] layer. The [UI] layer will then have the names of the lists and can refine its messages to the user by asking, for example, "Number of votes for List A."
  2. Once the [UI] layer has obtained the votes for all lists, it will request the seat calculation from the [business] layer. The [business] layer can perform this calculation and return the result to the [UI] layer.
  3. The [ui] layer can then present these results to the user. The user may also request that they be saved.
  4. The [ui] layer may also wish to present additional information to the user, such as the electoral threshold or the number of seats to be filled.

With this information, we could attempt an initial definition of the interface for the [ metier] layer:


public interface IElectionsMetier {

    public VoterList[] getVoterLists();

    public int getNbSeatsToBeFilled();

    public double getVoterThreshold();

    public void recordResults(VoterList[] voterLists);

    public VoterList[] calculateSeats(VoterList[] voterLists);

}
  • Line 1: The interface is called [IElectionsMetier]. It defines the following methods:
    • line 3: a method [getVoterLists] that will allow the [ui] layer to obtain the array of competing lists;
    • line 5: the [getNbSiegesAPourvoir] method retrieves the number of seats to be filled;
    • line 7: the method [getElectoralThreshold] retrieves the electoral threshold;
    • line 11: a method [calculateSeats] (line 36) that will allow the [ui] layer to request the calculation of seats once the vote counts for the various lists are known. The parameter is the array of competing lists, without their seats and without the "eliminated" boolean. The returned result is this same array, but this time with the [seats, eliminated] fields initialized;
    • Line 9: a method [recordResults] that will allow the [ui] layer to request the recording of results.

Note: Due to its position, the [business] layer reuses some of the methods from the [DAO] layer to make them available to the [UI] layer. Because of this redundancy, one might be tempted to group everything into a single layer that would combine both the business logic and data access. This single layer is sometimes called the model, the M in the MVC acronym (Model-View-Controller). MVC is a design pattern widely used in web applications.

Let’s examine the signature of the [calculateSeats] method:


public VoterList[] calculateSeats(VoterList[] voterLists);

It was stated earlier: “The parameter is the array of competing lists, without their seats and without the ‘eliminated’ boolean. The result is the same array, but this time with the [seats, eliminated] fields.” The method signature could also be as follows:


public void calculateSeats(ElectoralList[] electoralLists);

The parameter [voterLists] is an object reference, in this case an array. Each element is in turn an object reference, in this case of type [VoterList]. The method [calculateSeats] will modify the fields [seats, eliminated] of each of these objects. The calling method holds a pointer [voterLists] that:

  • before the call, is a reference to an array of [VoterList] objects with their [seats, eliminated] fields uninitialized;
  • after the call, is the reference (the same one) to an array of [VoterList] objects with their [seats, eliminated] fields initialized;

So why use the signature:


public VoterList[] calculateSeats(VoterList[] voterLists);

When writing an interface, it is important to remember that it can be used in two different contexts: local and remote . In the local context, the calling method and the called method are executed in the same JVM (Java Virtual Machine):

If the [ui] layer calls the calculateSeats method of the [DAO] layer, it does indeed have a reference to the [VoterList[] voterLists] parameter that it passes to the method.

In the remote context, the calling method and the called method are executed in different JVMs:

In the example above, the [ui] layer runs in JVM 1 and the [business] layer in JVM 2 on two different machines. The two layers do not communicate directly. Between them lies a layer we will call the communication layer [1]. This consists of a transmission layer [2] and a reception layer [3]. The developer generally does not have to write these communication layers. They are automatically generated by software tools. The [business] layer is written as if it were running in the same JVM as the [DAO] layer. Therefore, no code changes are required.

The communication mechanism between the [ui] layer and the [business] layer is as follows:

  • the [ui] layer calls the calculateSeats method of the [business] layer, passing it the parameter [VoterList[] voterLists1];
  • this parameter is actually passed to the transmission layer [2]. This layer will transmit the value of the `listesElectorales1` parameter over the network, not its reference. The exact form of this value depends on the communication protocol used;
  • the receiving layer [3] will retrieve this value and use it to reconstruct an object [VoterList[] voterLists2] that mirrors the initial parameter sent by the [business] layer. We now have two identical objects (in terms of content) in two different JVMs: voterLists1 and voterLists2.
  • The receiving layer will pass the listesElectorales2 object to the calculerSieges method of the [business] layer, which will persist it in the database. After this operation, the listesElectorales2 reference points to an array of [VoterList] objects with their [seats, eliminated] fields initialized. This is not the case for the listesElectorales1 object, to which the [ui] layer has a reference. If we want the [ui] layer to have a reference to the listesElectorales2 object, we must pass it to the [ui] layer. Therefore, we use the following signature for the [calculerSieges] method:

public VoterList[] calculateSeats(VoterList[] voterLists);
  • With this signature, the `calculateSeats` method will return the `electoralLists2` reference as its result. This result is returned to the receiving layer [3], which had called the [business] layer. The [business] layer will return the value (not the reference) of `electoralLists2` to the sending layer [2];
  • The sending layer [2] will retrieve this value and use it to reconstruct an object [VoterList[] voterLists3] that mirrors the result returned by the calculateSeats method of the [business] layer.
  • The object [VoterList[] voterLists3] is returned to the method in the [UI] layer whose call to the calculateSeats method of the [DAO] layer had initiated this entire mechanism;

In this process, objects of type [VoterList] will pass between layers [2] and [3]:

  • When layer [2] transmits the value of a [VoterList] object to layer [3], the object is said to be serialized. The exact form of this serialization depends on the communication protocol used;
  • When layer [3] retrieves the value of a [VoterList] object in order to create a new [VoterList] object, the object is said to be deserialized;

For an object to undergo this serialization/deserialization, certain protocols require that the object implement the [Serializable] interface. This interface is merely a marker; there are no methods to implement. Therefore, the [VoterList] class will now be declared as follows:


public abstract class VoterList implements Serializable {
    private static final long serialVersionUID = 1L;
  • The field on line 2 is mandatory. It can be kept as is and used for any class of type [Serializable].

4.3. The exception class

Let’s return to the [DAO] layer interface:


public interface IElectionsDao {

  public double getVotingThreshold();

  public int getNumberOfSeatsToBeFilled();

  public VoterList[] getVoterLists();

  public void setVoterLists(VoterList[] voterLists);
}

These methods work with a database and may encounter various errors, such as a database being unavailable. When writing a method, you must always handle error cases. These are typically signaled by an exception. We have already encountered the [ElectionsException] class in Section 3.3. We will continue to use it but enhance it as follows:


package ...;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

// exception class for the Elections application
// the exception is unchecked

public class ElectionsException extends RuntimeException implements Serializable {

    // serial ID
    private static final long serialVersionUID = 1L;

    // local fields
    private int code;
    private List<String> errors;

    // constructors
    public ElectionsException() {
        super();
    }

    public ElectionsException(int code, Throwable e) {
        // parent
        super(e);
        // local
        this.code = code;
        this.errors = getErrorsForException(e);
    }

    public ElectionsException(int code, String message, Throwable e) {
        // parent
        super(message, e);
        // local
        this.code = code;
        this.errors = getErrorsForException(e);
    }

    public ElectionsException(int code, String message) {
        // parent
        super(message);
        // local
        this.code = code;
        List<String> errors = new ArrayList<>();
        errors.add(message);
        this.errors = errors;
    }

    public ElectionsException(int code, List<String> errors) {
        // super
        super();
        // local
        this.code = code;
        this.errors = errors;
    }

    // list of error messages for an exception
    private List<String> getErrorsForException(Throwable th) {
        // retrieve the list of error messages for the exception
        Throwable cause = th;
        List<String> errors = new ArrayList<>();
        while (cause != null) {
            // retrieve the message only if it is not null and not empty
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    errors.add(message);
                }
            }
            // next cause
            cause = cause.getCause();
        }
        return errors;
    }

    // getters and setters
...
}
  • lines 16-17: the [ElectionsException] type encapsulates:
    • an error code, line 16;
    • a list of error messages, line 17;

The class supports five constructors:

  • line 20: ElectionsException()
  • Line 24: ElectionsException(int code, Throwable e): The second parameter is of type [Throwable], which is the superclass of the [Exception] class. This constructor allows the exception e to be encapsulated along with an error code. The [Throwable] type (and therefore the Exception type) allows you to encapsulate one or more exceptions. The idea is:
    • to catch an exception that occurs;
    • enrich it with a message by encapsulating it in a new exception;
    • to throw the new exception;
try{
...
} catch (Exception1 e1) {
   throw new Exception2("a message", e1);
}

Encapsulation occurs on line 34 via the [super(message, e)] statement. This encapsulation process can be repeated, and the initial exception can be enriched with different messages. This is referred to as an exception stack. The method [private List<String> getErrorsForException(Throwable th)] allows you to retrieve the various messages associated with the encapsulated exceptions:

  • (continued)
    • (continued)
      • The encapsulated exception is obtained using the Throwable method [Throwable].getCause();
      • the message associated with an exception is obtained via the method String [Throwable].getMessage();
  • lines 28–29: the [code, errors] fields are constructed;
  • line 32: public ElectionsException(int code, String message, Throwable e): this constructor is similar to the previous one, except that it enriches the exception it will encapsulate with a code and a message;
  • line 40: public ElectionsException(int code, String message): constructor without exception encapsulation;
  • line 50: public ElectionsException(int code, List<String> errors): constructor without exception encapsulation or message;

The [ElectionsException] class can be used as follows:

try{
...
} catch (Exception1 e1) {
   throw new ElectionsException(a_code, a_message, e1);
}

where the message may or may not be present. Once created, the [ElectionsException] exception is not intended to encapsulate new exceptions. In the example above, it encapsulates the exception e1 and the exceptions that e1 encapsulates. There are no further encapsulations beyond that.

The [ElectionsException] class can also be used as follows:

// code likely to encounter an error (but not in the form of an exception)
...
if(error){
    throw new ElectionsException(some_code, some_message);
}