Book Image

Software Architecture with Spring 5.0

By : René Enríquez, Alberto Salazar
Book Image

Software Architecture with Spring 5.0

By: René Enríquez, Alberto Salazar

Overview of this book

Spring 5 and its ecosystem can be used to build robust architectures effectively. Software architecture is the underlying piece that helps us accomplish our business goals whilst supporting the features that a product demands. This book explains in detail how to choose the right architecture and apply best practices during your software development cycle to avoid technical debt and support every business requirement. Choosing the right architecture model to support your business requirements is one of the key decisions you need to take when a new product is being created from scratch or is being refactored to support new business demands. This book gives you insights into the most common architectural models and guides you when and where they can be used. During this journey, you’ll see cutting-edge technologies surrounding the Spring products, and understand how to use agile techniques such as DevOps and continuous delivery to take your software to production effectively. By the end of this book, you’ll not only know the ins and outs of Spring, but also be able to make critical design decisions that surpass your clients’ expectations.
Table of Contents (21 chapters)
Title Page
Copyright and Credits
Packt Upsell
Contributors
Preface
Index

SOLID principles


SOLID is an acronym that represents the five underlying principles that guide a good software design. The design is related to the creation of components that shape your software architecture.

In 2004, Michael Feathers suggested this acronym to Robert C. Martin, the author of these principles. The process for creating them took him around 20 years, and during this period, many of them were added, removed, and merged to achieve a robust set of principles named SOLID. Let's review each one of the principles and provide a brief and clear explanation that will be helpful for getting a precise idea of how we can use them.

We will use the term module in tandem with the idea of modules shaping components, and we will make reference to the object-oriented programming (OOP)world using terms such as classes and interfaces in order to provide a more precise explanation of modules.

The single responsibility principle (SRP)

The SRP is very closely related to the high cohesion that we reviewed earlier. The idea behind this principle is that a module should be changed for one reason only. 

 

 

This definition leads us to conclude that a module should have only one responsibility. One way to verify whether this principle is achieved in your design is to answer the following questions:

  • Does the module's name represent its exposed functionality?

The answer should be yes. For example, if the module's name refers to the domain, then the module should contain domain classes and some functionality around the domain objects related to the module's name itself. You won't want to have code to support audit elements or any other aspect out of the scope of the module you are working with, for example. If the module is supporting additional features, the code supporting those additional features should probably need to be moved to an existing audit module, or a new audit module should be created.

  • When a new change is required, how many parts of the module are affected?

The answer to this question should be many of them; all classes in the module are highly connected, and a new change will change them for this reason. The desired behavior is prevented from being changed through the exposed interface, but the background implementation is often volatile.

The Open–Closed Principle (OCP)

The OCP is simple to write, but difficult to explain. For this reason, I'll write the following definition first and describe it later:

New features can be added to an existing module by extension and not by modification.

It sounds simple, doesn't it? In order to understand this concept from a practical viewpoint, it is necessary to revisit our last example. Let's check that we are accomplishing this principle by answering the following questions:

  • What do we need in order to support a new notification channel?

We need to write a new class (module), and this should implement an existing interface. Note how the open-closed principle makes sense with the provided answer. To support a new notification channel in our application, we need to create a new class, but we don't need to modify the existing code. According to the previous refactoring that we made, if we needed to support this requirement, we had to adjust the existing service to send notifications.

 

A few questions to validate how well this principle is achieved are as follows:

    • Do I add a new IF statement to my code?

No. If you're looking to add a new feature, you will write a new class instead of modifying an existing one. This is because you are adding and not changing features.

    • How much code do I modify in order to support a new feature?

Hopefully, just a little bit. In a perfect world, you won't need to modify anything, but sometimes a few sections should be changed to support new features in the real world. The rule here is that if you are adding a new feature, your original design should be able to support this requirement with minimal changes. If this is not true, refactoring or changing your initial design is recommended.

    • How big should my source code files be?

Big source code files are a bad idea, and there is no reason for them to be large. If your source code file has hundreds and hundreds of lines, revisit your functions and think about moving code to a new file in order to make the source code files smaller and easy to understand.

    • Should I use abstractions within my code?

This is a tricky one. If you only have one concrete implementation for something, you won't need to have an abstract class or interface. Writing code and inventing new possible scenarios is not desirable at all, but if you have at least two concrete implementations that are related to each other, you have to think about writing an abstraction for them. For example, if we only need to send email notifications, there would be no reason to write an interface for this. However, since we are sending notifications via two different channels, we certainly need an abstraction to deal with them.

The Liskov substitution principle 

The Liskov substitution principle (LSP) has a fancy definition:

Module A can be replaced by module B as long as B is a subtype of A.

Well-defined contracts heavily support this definition and help us reduce the coupling between modules. The following questions can help you figure out how well this principle is achieved:

  • Are the modules interacting using abstractions or concrete implementations?

Here, the answer should be that the modules should not be interacting with either option. There is no reason to establish interactions among modules by using their concrete implementations instead of their interfaces.

  • Should I be casting objects in order to use them?

I hope not. If so, it's because the interface is not well-designed, and a new one should be created to avoid this behavior. The use of the instanceOffunction is also not desirable at all.

  • Is the interaction between modules guided by IF statements?

There is no reason for this to be the case. Your modules should be connected in a way that can be taken care of by the use of an interface and the correct dependency injection to solve their concrete implementations.

The interface segregation principle (ISP)

The principal motivation of the interface segregation principle is aligned with the lean movement where creating values with fewer resources is essential. Here's a short definition for it:

Avoid things that you don't use.

You may have already seen classes (modules) implementing interfaces with some method implementations, such as the following:

public  class Abc implements Xyz 
{ 
  @Override 
  public void doSomething(Param a) 
  { 
    throw new UnsupportedOperationException 
    ("A good explanation here"); 
  } 
  // Other method implementations 
} 
 

Alternatively, another option called comment asimplementation tends to be used, as shown in the following code: 

public  class Abc implements Xyz 
{ 
  @Override 
  public void doSomething(Param a) 
  { 
    // This method is not necessary here because of ... 
  } 
  // Other method implementations 
} 

The preceding examples successfully describe the problem that this principle was created to address. The best way to deal with this issue is by creating more consistent interfaces that conform to the other explained principles. The main problem with this issue is not related to having empty method implementations, but having additional functionality that is not used at all.

Suppose that an application depends on an XYZ library and the system is only using 10% of the available functionality. If a new change is applied to solve an issue that was present in the other 90%, that modified code represents a risk to the part that the application is using, even when it's not directly related to it.

The following questions will help you identify how well you are doing:

  • Do I have empty or silly implementations like the ones mentioned earlier?

Please don't answer YES.

  • Does my interface have a lot of methods?

Hopefully not, as this will make it more difficult to implement all the abstract methods in concrete implementations. If you have many methods, please refer to the next question.

  • Are all the method names consistent with the interface name?

The method names should be consistent with the interface name. If one or more methods don't make sense at all, then a new interface should be created to place them.

  • Can I split this interface into two instead of only one?

If yes, go ahead and do it.

 

  • How many functions am I using from the whole set of exposed functions?

If the modules interacting with an interface are only using a few of the exposed functions, then the other ones should probably be moved to another interface, or even to new modules.

The dependency inversion (DI) principle

It is now time to define the dependency inversion principle:

Modules should depend on abstractions rather than on concrete implementations.

Abstractions represent the high-level details of a module, and the interaction among modules should be done at this level. Low-level details are volatile and ever-evolving. We previously stated that there are no problems with evolved modules, but of course, we don't want to break module interactions because of low-level details, and an excellent way to do this is to use abstractions rather than concrete implementations. The following questions will help you identify how well you are doing:

  • Do I have abstractions as part of my modules?

As discussed earlier in this chapter, many concrete implementations should have an abstraction in front of them. However, when it comes to one specific implementation, this is probably not the case.

  • Am I creating new instances by myself every time?

The answer here should be no. Your framework or mechanism that is in charge of the dependency injection inside your application is responsible for doing this.