:imagesdir: images/ ifdef::env-github[] :imagesdir: images/ endif::[]
== Setup a layered architecture for your project
== Introduction
This tutorial describes how to setup a layered architecture for your project. Before answering the question HOW to do that, we should ask ourselves the question WHY we would do that! An important principle in general, but definitely also in Software Engineering is the KISS-principle: Keep It Stupid and Simple. Things that are simple are less error prone, and can be easily understood by others as well. Making our architecture more complex is therefore only reasonable if it serves a purpose.
== Our goals
What do we want to achieve with a proper project architecture?
Separation of Concerns
Testable code
Re-usable code
Maintainable code
== Our design starting points
A layered architecture that addresses the concerns:
Representing information and interact with the user in a (G)UI-layer Modeling the real world, the business and its business rules in a Business Logic Layer * Storing data in a Persistence Layer*
Program against interfaces, instead of implementations
Use factory classes to create instances
Design principles are mostly focussed on avoiding dependency. But isn't that very logical? In real life, we also want to avoid dependency. Dependency causes complexity, independency gives freedom! When you have a job, you're married, have children, you have dependencies and responsibilities that restrict freedom. Why did UK think that leaving the EU was a good idea? Too much dependency can even cause that rules are set FOR you! Independency is therefore persuable. But is avoiding dependency always a good idea? It comes with a price (its own complexities) as well. Moral of the story? We have to find a balance between making things flexible but still simple.
== Let's set it up
What's key? Our Business Logic of course! The persistence layer is only a service that serves the Business Logic Layer by storing object data at any time, and retrieving these on request. From a business point of view not important. The (G)UI only enables end users to interact with the business logic. From that perspective it's only a passthrough and a messenger; relevant from a software system point of view, not from a business point of view. Assume that we want to write an application that is able to create, store and retrieve customers. We'll explain the setup step-by-step afterwards.
Create a NetBeans project.
-- Choose 'Java with Maven' and 'POM project' as project type. This Netbeans Project will be your overall application, let's call it AirlineInformationSystem (AIS). This AIS will be the parent of all your sub projects that we'll create on the fly. Within this project you can create so-called modules, which are Maven Modules. This comes with some benefits:
All sub projects (Maven modules) will have this project as parent and inherit general settings. Open the POM file of your AIS-project and set the informaticspom as its parent-pom; make sure your pom-file looks like the one below. All libraries that are accessible via the informaticspom will from now be available in all future sub projects (sub modules).
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-- +
Business is key! Create a business logic module within the AIS-project.
-- Right-click the Modules folder and select 'Create new Module' and choose 'Java Application' as project type. A regular project is created. This project acts as business logic layer. What do we need in this layer? Business classes (representing entity types from your domain model!) and classes to manage objects of these classes:
Test classes... Of course your business logic should be tested and you'll use a test-first approach. BusinessLogic tests will be part of this module (to keep this tutorial short, testing has been left out though).
A Customer class to represent a real world Customer (assuming this is part of your domain).
image::AISClassDiagram1.svg[Class diagram after 1st step]
Time to interact! Create a GUI module.
--
+
image::AISClassDiagram2.svg[Class diagram after 2nd step]
Connect the GUI to the Business Logic.
-- Time to wire up things. How could we enable the GUI-module to communicate with the BusinessLogic layer? Or the other way around? Should they know each other? Normally, the GUI will trigger the interaction with the BusinessLogic. Therefore it should at least know how to talk to it, so knowing its interface. The BusinessLogic does not need to know anything about the GUI! It normally answers GUI questions in a Request-Response fashion. There could be multiple front-ends for the BusinessLogic (e.g. a JavaFX Front end, a web front end or even a console front end). Why would the Business Logic worry?!
So, the GUI is a component that uses the BusinessLogic as a service, a Dependent-On-Component. But it shouldn't create this service itself! If it would, the GUI would be tightly coupled. When we would do GUI testing, there is no way to test its interaction with the BusinessLogic without using the real implementation of that BusinessLogic. This real implementation might not be ready or stable (e.g. depending on actual database contents). The GUI should only talk to the BusinessLogic interface (let's call it the BusinessLogicAPI) and get an actual implementation injected.
Final question, 'Who should inject the BusinessLogic implementation?' The businessLogic itself? No, we just learned that the BusinessLogic should be unaware of the presentation layer! We need another module in our AIS-project: an Assembler project that acts as starting point of our application and sets up all layers and connects them properly.
So, what do we need to do? (the steps will be explained in detail below)
-- +
Define the BusinessLogicAPI interface.
-- The BusinessLogic module should define its interface. You can imagine that it, on request, returns a CustomerManager. For example a GUI could request a CustomerManager object to do its interaction with the BusinessLogic. Via the CustomerManager, the GUI gains access to the Customer type as well. This is fine, though layers should be careful to expose their private parts, concrete implementations.
+
Add dependency in GUI to the Business Logic.
Create the Assembler module.
Inject the BusinessLogicAPI object in the presentation layer.
-- This seems to be a fairly easy step. The JavaFX Application class can be constructed from the Assembler directly. There is a trap / pitfall here however. Although the Application class can be instantiated by ourselves, the Controller class behind each window is instantiated automatically by the FXMLLoader (the controller class is identified in the fxml-file); this can only be done automatically when the Controller class has a default constructor. This is, by default, the case. We need a parameterized constructor however, to be able to pass the BusinessLogicAPI implementation to the controller. What we need to do is to provide the FXMLLoader with a separate 'controller factory'. This controller factory can create an instance of a controller class with a non-default constructor. The FXMLLoader has a setControllerFactory(...) method.
-- +
image::AISClassDiagram3.svg[Class diagram after 3rd step]
Setup the persistence layer.
-- We currently have a working application with an in-memory database. What we need is a persistence layer that is able to store and retrieve data on a longer term as well. Different ways to do this could be chosen, like using a relational database, or simply XML- or JSON files. Regardless of the storage type that is chosen, the BusinessLogic uses the persistence layer as a service. A Dependent-On-Component again! (compare to the GUI that depended on the BusinessLogic). But it shouldn't create this service itself! If it would do, the BusinessLogic would be tightly coupled. When we do testing, there is no way to test its interaction with the Persistence layer without using the real implementation of that Persistence layer. The BusinessLogic should only talk to the Persistence interface (let's call it the PersistenceAPI) and get an actual implementation injected. L'histoire se répète. The Persistence layer should act as service for the BusinessLogic exactly like how the BusinessLogic layer acted as service for the GUI-layer. The Assembler can inject the PersistenceAPI implementation in the BusinessLogic. The persistence layer does not need to have any knowledge of the BusinessLogic layer. In the persistence project, we create the PersistenceAPI interface, a PersistenceAPIImpl class providing an implementation of this interface and a PersistenceFactory that can be used externally.
Be careful, two details we should take care of:
The BusinessLogic layer now depends on the persistence layer (the BusinessLogic project has the Persistence project as a dependency). This is fine.
Since we have a persistence layer now, we should avoid having an in-memory database at the same time. This will cause issues, since it's difficult to keep your in-memory database always exactly in sync with your on-disk storage. Therefore remove the cache function from the CustomerManagerImpl class.
-- +
image::AISClassDiagram4.svg[Final class diagram]
== Some remarks...
This architectural setup acts as a starting point, addressing some issues that you definitely will run into when you start setting up an architecture yourself. This example architecture is not completely optimized yet. You'll typically notice that the services offered by both the persistence layer as the business logic layer could be made more generic.
The Factory interfaces in both the BusinessLogic and the Persistence layer could be provided with additional parameters to influence which specific implementation is returned. The demo implementation does not use this feature yet.
Java projects nowadays use the Java Platform Module System (JPMS). This is recognizable when your project contains
a
In the demo project, we've added an example on how to use TestFX, a framework to do GUI testing. We show how Mockito can be used to mock the business logic. Because of properly separated layers, we can test (parts of) layers independent of other layers; we can test the GUI (SUT in this case) without being dependent on the business logic (DOC in this case), and we can test the business logic (SUT in this case) without being dependent on the persistence layer (DOC in this case). Just consider to apply this concept of GUI testing in your project. In order to get it to work, uncomment the contents of the GUIAppTest class. (Warning for MacOS users: the GUI test execution only works properly if the application that initiates the GUI test (e.g. NetBeans or the command line if you start NetBeans from the command line) has authorization to 'control your computer'. Go to Settings / Security & Privacy / Privacy / Accessibility to adapt the authorizations.)
As mentioned already in the remarks above, the persistence layer could be setup in a more generic way. When you don't do that, you'll notice that there will be a lot of duplicated code (at least almost the same) in the different StorageService classes (e.g. CustomerStorageService, FlightStorageService etc.). First step is to move some code to a shared abstract super class, then you might want to make it more generic using Generic Types, and at some point you might consider using reflection to automatically get an objects' fields, their data types and their values (typical things you need to store and retrieve data from a database). Goal is to end up with less and well readable and well testable code. Allow yourself to further optimize your implementation step-by-step. Don't worry, refactoring is often necessary: https://youtube.com/watch?v=vqEg37e4Mkw&feature=share[Martin Fowler on refactoring]
Students with newer MacBooks (M1/M2 ARM Architecture) might encounter issues running JavaFX applications. These issues can be solved by installing a JDK that encorporates JavaFX already: https://www.azul.com/downloads/?version=java-17-lts&os=macos&architecture=arm-64-bit&package=jdk-fx#zulu
Sometimes, developers in a team have to work with different local settings (for example because of using different OS). As described in the previous bullet, also the local JavaFX version might differ. To enable individual settings, without getting annoyed by settings in your shared repo that are not the same to everyone and continuously changed, you can configure personal settings in a maven configuration file:
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<activeProfiles>
<activeProfile>javafxlocal</activeProfile>
</activeProfiles>
The tag name
In the pom where you're using JavaFX as dependency, configure it as below:
.....
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${local-javafx-version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${local-javafx-version}</version>
</dependency>
Above, the version in the JavaFX dependencies refer to ${local-javafx-version}. This variable is defined as property in the