Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Software Architecture with Spring 5.0

You're reading from   Software Architecture with Spring 5.0 Design and architect highly scalable, robust, and high-performance Java applications

Arrow left icon
Product type Paperback
Published in Aug 2018
Publisher Packt
ISBN-13 9781788992992
Length 372 pages
Edition 1st Edition
Languages
Tools
Concepts
Arrow right icon
Authors (2):
Arrow left icon
Alberto Salazar Alberto Salazar
Author Profile Icon Alberto Salazar
Alberto Salazar
René Enríquez René Enríquez
Author Profile Icon René Enríquez
René Enríquez
Arrow right icon
View More author details
Toc

Table of Contents (16) Chapters Close

Preface 1. Software Architecture Today 2. Software Architecture Dimensions FREE CHAPTER 3. Spring Projects 4. Client-Server Architectures 5. Model-View-Controller Architectures 6. Event-Driven Architectures 7. Pipe-and-Filter Architectures 8. Microservices 9. Serverless Architectures 10. Containerizing Your Applications 11. DevOps and Release Management 12. Monitoring 13. Security 14. High Performance 15. Other Books You May Enjoy

Software architecture principles

Software architecture should improve by following two simple principles that are often difficult to achieve:

  • Low coupling
  • High cohesion

No matter what programming language, paradigm, or tools you are using to architect your applications, these two principles should guide you when building your software architecture components.

In order to build the components that will shape your architecture, it's always worth following the guidelines. These are still relevant, even after many years of existence, and they should always be considered when components are being created. In this section, I'm talking about SOLID principles and Conway's law, which we will discuss in more detail later in this chapter. It is now time to look at what components are in more detail.

Components

A component is a set of functions, data structures, and algorithms that solve one problem. This means that all the code and artifacts that are used to build the component have a high cohesion with each other; the rule here is that the classes or files that create a component should change at the same time and for the same reason.

Software architecture is built using many components, and you should not be worried about having an excessive quantity of these. The more components you write, the more freedom there is to assign them to different developers or even to different teams. Large software architectures can be created using many smaller components that can be developed and deployed independently of each other.

Once we connect these components to each other, they allow us to create the desired software architecture.

As shown in the following diagram, we can see the components as pieces of a puzzle that come together to form an application:

Components forming a larger application

The connected components define application architectures, and their designs describe how each component has been created internally. It's here that pattern designs and SOLID principles must be used to create good designs.

Low coupling

Low coupling refers to the degree to which components depend on each other by their lower structures instead of their interfaces, creating a tight coupling among them. Let's make this easier to understand by using a simple example. Imagine that you need to work on the next user's story:

As a bank customer, I want to receive my bank statement by email or fax in order to avoid having to open the bank application.

As you may discover, the developer should work on two things to solve this problem:

  • Adding the ability to save the user's preferences in the system
  • Making it possible to send the bank statement to the customer by using the requested notification channels

The first requirement seems quite straightforward. To test this implementation, we would use something fairly simple, such as the following code:

@Test 
public void 
theNotificationChannelsAreSavedByTheDataRepository() 
throws Exception 
{ // Test here }

For the second requirement, we will need to read these preferred notification channels and send the bank statement using them. The test that will guide this implementation will look like the following:

@Test 
public void 
theBankStatementIsSendUsingThePreferredNotificationChannels() 
 throws Exception 
{ // Test here }

It is now time to show a tightly coupled code in order to understand this problem. Let's take a look at the following implementation:

public void sendBankStatement(Customer customer) 
{
List<NotificationChannel> preferredChannels = customerRepository
.getPreferredNotificationChannels(customer);
BankStatement bankStatement = bankStatementRepository
.getCustomerBankStatement(customer);
preferredChannels.forEach
(
channel ->
{
if ("email".equals(channel.getChannelName()))
{
notificationService.sendByEmail(bankStatement);
}
else if ("fax".equals(channel.getChannelName()))
{
notificationService.sendByFax(bankStatement);
}
}
);
}

Note how this code is tightly coupled with the implementation of the NotificationService class; it even knows the name of the methods that this service has. Now, imagine that we need to add a new notification channel. To make this code work, we will need to add another if statement and invoke the correspondent method from this class. Even when the example is referring to tightly coupled classes, this design problem often occurs between modules.

We will now refactor this code and show its low-coupled version:

public void sendBankStatement(Customer customer) 
{
List<NotificationType> preferredChannels = customerRepository
.getPreferredNotificationChannels(customer);
BankStatement bankStatement = bankStatementRepository
.getCustomerBankStatement(customer);
preferredChannels.forEach
(
channel ->
notificationChannelFactory
.getNotificationChannel(channel)
.send(bankStatement)
);
}

This time, the responsibility to get a notification channel is passed to the Factory class, no matter what kind of channel is needed. The unique detail that we need to know from the channel class is that it has a send method.

The following diagram shows how the class that sends notifications was refactored to send notifications using different channels and support an interface in front of the implementations per notification channel:

Classes after refactoring

This small but significant change has to lead us to encapsulate the details of the mechanism used to send notifications. This exposes only one well-defined interface that should be used by the other classes.

Although we have shown this example using classes, the same principle is applicable to components, and the same strategies should be used to implement them and avoid coupling among them.

High cohesion

The principle of high cohesion also has a pretty simple definition: one component should perform one and only one well-defined job. Although the description is pretty simple, we often tend to get confused and violate this principle.

In the previous example, we had NotificationService, which was in charge of sending notifications by email and fax. The word  and can be helpful for us when it comes to identifying the violation of this principle. Now that we have two different classes (one per notification channel), it's fair to say that our classes only have one responsibility.

Again, the same is true for components, and another reason to keep the same idea with them is that you will likely have each component accomplishing only one specific requirement. For example, what would happen if all our customers just wanted to receive their bank statements by email; do you think it's okay to depend on a class that has the ability to send faxes too?

Although the previous question may seem unimportant, imagine that you solved an existing issue related to sending notifications using faxes as a notification mechanism, and a new issue was then introduced into the mechanism in order to send email notifications by mistake.

Remember that components shape your software architecture, and architects should design them in a way that maximizes team productivity. Aligning your components to the high-cohesion principle is an excellent way to separate them and allows teams to work independently on different parts of your application. This ability to create various components with clear responsibilities will make it easier when solving other issues and adding new features, and will also make you less prone to introducing bugs.

With regards to the previous example, you are probably wondering why the NotificationChannel class is apparently sending notifications with a BankStatement parameter.

Common sense leads us to believe that we need to replace this class with any other generic type. It can be helpful to allow the application to send different kinds of notifications, and not only bank statements: this may include drawbacks, or when a new deposit is received in the account. Even though the idea of supporting incoming requirements looks like something you might want to include in the program at this stage, the application doesn't currently need this ability. This is why it is not necessary for us to add this feature right now. Instead, this design should evolve when this becomes necessary; in this way, we are sticking to the KISS principle (https://www.techopedia.com/definition/20262/keep-it-simple-stupid-principle-kiss-principle) and following the directions of only building the most basic features to make the application work.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime