Knowing Software Architects – Part 6, Software Components Architecture and SOLID principles

Hi fellow coders!
From here on we get to dive way deeper into a lot of the building blocks of software architecture. I intend to write about many key points that almost anyone talks of when they talk about software architects and their role in software architecture.

This includes SOLID principles, Design Patterns, System Architecture etc.
So buckle up, for the next few weeks are going to be an intense dive into all these concepts as explained by some of the best minds in tech that I have had a chance to learn from!

In the last blog, we moved ahead in choosing our technology stack as an Architect and meeting the *-ilities that help us achieve our non-functional requirements!

This time we will learn more about the Software Components Architecture and the much-awaited SOLID Principles.

Software Components Architecture


Software components can be thought of as pieces of code that run in a single process.

Modern systems are usually distributed. This means that the systems are:

  • Composed of independent Software Components
  • Deployed on separate processes, containers or servers

Here is an example of a simple distributed architecture

When we talk about software architecture, we talk about two types of architecture:

  • Component Architecture: With Component architecture, we are concerned about the inner components, their interaction with each other and how easy is it to maintain them.
  • System Architecture: With System architecture, we are more concerned about how the bigger picture unfolds and whether it is compliant with the -*ilities.

Layers

Traditionally every software component will have several layers.

The layers in a software component represent a horizontal functionality of code in layers.

There are three basic actions that every software has to perform.

  1. Expose the functionality of the software via some kind of interface.
  2. Execute logic via some business logic code
  3. Save the data into data store and retrieve data from the said store.


This relationship can be further defined by UI, Business, and Data access layers.

So the question arises why do we need layers?

Well, the main reason is that layers help in better organisation of the code. They help us focus our efforts on a well-formed and structured way to write our code that completely segregates responsibilities, makes the code more maintainable and improves code quality.

Layers are modular, a component also makes it easy to substitute any layer if there is a need for that.

Concepts of Layers

In order to make good layers, there are some concepts.

  1. Code Flow – A layer can call only the layer that is directly below it. Also, the layers can’t call layers above the,

This is simply to ensure that each layer is modular and has only the required context.

  1. Loose coupling – Coupling refers to the relationships that layers (here classes) might have with each other.

For a component to be loosely coupled, the layer must have a weak relationship to the other layer in reference such that the changes in one layer do not affect the existence or performance of another layer.

E.g 

In the above example, we create an object of a class and reference the methods through this object. This makes the code tightly coupled as every time we need to call the method we always need to depend on the class.


The correct way to introduce loose coupling here would be to introduce an interface such that whenever we need the methods, we implement an interface and do not depend on the class itself.

This concept is called Dependency injection and we will understand this in later topics.

  1. Exception Handling – This specific concept means that the layers below must not let layers above know the specific inner exceptions but instead show a generic error message to the layers above giving a broader and simpler error message.

E.g. Let us have a system where we use MySQL. Now MySQL always has specific exceptions, but the layers above it have no use for it.

This may also reveal the inner workings of the database and since we want the layers to be as abstracted as possible, this is just not going to do it!

The correct way here would be for the system to analyse the exception, write it to a log and then throw a generic exception.

Difference between layers and tiers

Often times layers and tiers are used interchangeably but should not be done so.

There is a big difference between them.

A layer is a code collection that runs in a single process. There is no networking involved with the various classes in the component to communicate with each other and they all share the same computing resource.

Tier, on the other hand, is a distributed piece of code.

It’s deployed independently and communicates with other tools via network protocols such as HTTP.

So when we talk about a three-tier application for example we mean three independent applications with their own layers communicating with each other via a network.

Interfaces

An interface is a contract that declares the signature of an implementation.

The interface states that given a piece of code that should do a specific task, its methods must look a specific way.

E.g lets us look at an interface of a code that does mathematic calculations.

Interfaces are important as they introduce loose coupling. They make the code less dependent on each other.

Continuing with the previous example, we can have a reference to a calculator class as follows:

Now let us say we need to replace the calculator class with AdvancedCalculator class. In this case we would need to change the code in the main class and thus make the code less modular and extensible as it still depends on something else.

There is a saying in Software architecture that states that – NEW is Glue

This means that whenever we see new keyword, it certainly means that we are introducing strong coupling and hence worsening our code.

Now let us say we use the interface ICalculator.

The interface defines what a calculator can do, but gives no hint on what the concrete implementation of the calculator is.
The calculator interface can be implemented by any class and the Main class will have no idea at all that how this class functions, making it very loosely coupled.

Now, where does this implementation come from?

Here we are actually injecting the implementation through the GetInstance() method. 

Dependency Injection –  DI

Dependency injection complements the interface pattern we discussed in the previous topic.


Interfaces are the better mechanism for communicating between classes in order to make

the code more modular and flexible.

Dependency injection can be defined as a technique whereby one object supplies the dependencies of another object.

Let us again take the previous example:

Here the calculator is a dependency of the main class. This means that the main class depends on calc and is able to function correctly because of it.

Using the dependency injection technique we are able to inject a concrete implementation or a class to the interface representing the dependency without the middle class knowing which implementation is being used.

Here the GetInstance() method returns the class that implements the calculator interface.

The main class has no idea which class this is. We simply introduced a middleman that removes the strong coupling between two classes – Main and Calculator with each functioning independently through this middleman.

Now what is happening with this GetInstance method?

Well this becomes a factory method and executes some logic to decide which class should be injected to the main class.

An object is then instantiated from the class and returned as an object.

The Interface simply returns the instance of the calculator class here as shown above thus injecting the dependency.

A more advanced implementation can be as above, where we specify to the interface method what kind of instance we need.

Another method would be to pass no parameter at all and simply use a configuration file to supply the instance type making it a factory implementation.

One of the most common ways of dependency injection is actually a constructor injection.

Here the constructor itself receives the instantiated instances of the classes to use.

The main advantage of the constructor injection pattern over the traditional pattern is testability.

A class that gets injected in its constructor is much easier to test.

Naming Conventions

Naming conventions are a set of rules that define how we name various code elements, such as classes, methods, variables, constants and more.

The role of naming conventions is to make our code more readable and easy to understand.


When we agree on a convention, every code segment becomes clear with the various elements easily distinguished from the others.


Note that naming conventions are not enforced by compilers and the code will work perfectly without using any convention.


But a code without conventions will be messy and hard to work with.


Naming conventions usually deal with two types of rules.

  • One structure of the name( casing underscores, etc.)
  • Two content of the name.


Some popular conventions are:

  • CamelCase
  • lowercase_separated_by_underscore
  • Capitalised_By_Underscore
  • Hungarian Notation – Type information is part of the name, e.g string strFirstName
  • Kebab-case
  • ClassNames referenced by Nouns, e.g. DataRetriever, Car, Network
  • MethodNames referenced by Imperative verbs, e.g. RetrieveData, Drive, SendPacket

Exception Handling

One of the most important aspects of a well-written component is its exception handling

We should catch exception only if we have something to do with it.

This does not count logging which is a separate topic altogether.

Instead of try catching the exceptions, we can have a central filter to catch the exceptions and throw them effectively.

Some Best practices for exception Handling are:

  1. If there is a specific action to be taken, if an exception is thrown, such as rolling back a transaction or implementing some kind of algorithm or wrapping’s exception in a different one, then it’s a good idea to catch it. But other than that, it’s better to avoid it.
  2. Always catch a specific exception. When catching an exception, you should be aware of what kind of an exception to look for. For example, when working with databases, try to catch an SQL exception and handle it correctly.
  3. Use Try catch on the smallest code fragment possible.

Logging

Logging is important very important.

Wikipedia defines a log file as a file that records either events that occur in an operating system or other software runs or messages between different users of communication software. Logging is the act of keeping a log.

A good system uses logging for two purposes.

  1. To track errors: If there are any exceptions during the system’s activity, the system will write those exceptions to the log, complete with all the relevant details.
  2. To Gather Data: Logs should not be used only for exceptions, but to collect and store operational data on the system.
    For example, using logging, you can find out which model is the most visited by users and which one is less popular.


Some popular tools to store logging include kibana, grafana etc.


SOLID Principles

SOLID is perhaps one of the most important acronyms in the whole of software architecture.

SOLID was coined by Bob Martin in 2000 and represents five code design principles that when implemented make the code easy to understand and more maintainable in the long run.


SOLID stands for –

S: Single Responsibility Principle
O: Open/Close Principle
L: Liskov Substitution Principle

I: Interface Segregation Principle
D: Dependency Inversion Principle

Single Responsibility Principle

The single responsibility principle states that each class module or method should have one and only one responsibility.

This simply means that a function must be fully encapsulated within the class or module.

Let us take an example of the following System:

Here we have a logging engine which has two important responsibilities as stated above.
According to the Single Responsibility principle, these two functionalities should be represented by two separate classes, modules or methods.

Open/Close Principle

The Open/Close principle states that a software entity should be open for extension but closed for modification.

This simply means that for a software entity such as a class, we won’t have to modify its code and then recompile and redeploy it in order to change its behaviour. But we can achieve this by making the code extensible without touching the code.

This principle strongly relates to the extensibility attribute of non-functional requirements that we had talked about in previous topics.

There are multiple ways of achieving that.

  • The first is through Class inheritance. When we inherit an abstract or a concrete class we are basically extending the functionality without touching the code of the inherited class.
  • Plug-in where we simply use a third-party resource to extend the functionality of an existing code

There are many other ways of achieving this principle too.

The reason behind this principle is quite clear.

We want our code to be as flexible as possible and enable us to make changes quickly without modifying

Liskov Substitution Principle


Liskov Substitution principle states that if S is a subtype of T, then objects of type T may be replaced with objects of type S, without altering any of the desired properties of the program.

At first look, this might look similar to the polymorphism definition within object-oriented languages.

The polymorphism basically states that type can be replaced by its subtype without breaking the code.

But they are actually different. Liskov Substitution Principle does not talk about coding or compiling but about something called behavioural subtyping.

Let us explain with an example:

Say we have a code that needs to SendMail the code called a class named Sender and calls its send method, which, as you probably guessed, sent the mail.

A few months later, the developers come up with a better method in a class called Advanced Sender, which

inherits from the original sender.

The developers want to substitute messenger with Advanced Sender in their code.

The Liskov Substitution principle says that when doing this substitution, the behaviour of the send method should not be changed.

If the original method only sent the mail, the new method should do the same.
There should be no new functionality that is unexpected by the calling code, such as throwing new exceptions as a result of the substitutions.

For example, if the new method not only sent the mail, but also automatically sends a copy of the mail to a central archive inbox that will break the principle and should be avoided since it breaks the behavior of the original code.

The reason behind this principle is to avoid hidden behaviours that were not intended by the calling code, thus making the code much more difficult to debug and maintain.

Interface Segregation Principle

This principle states that many client-specific interfaces are better than one general-purpose interface.

Again let us take an example to understand this.

Let’s say we have a class that handles data processing, the class begins small with two methods ReadData and ValidatedData.

An interface named DataProcessor was created to define those two methods while working with the class.

Now let’s change requirements changed and additional methods were added to it that handle more tasks, such as the Decodedata EncodedData and SendDataToExternalSystem.

Now we have a bloated interface with five methods that must be implemented by a single class

If we remember the single responsibility principle, this clearly breaks that as we have now five different methods doing three different things all of which will be implemented by a class.

We are handling the data, decoding and encoding it and finally sending the data all through the same class

The interface aggregation principle says we better create multiple thin and well-defined interfaces than a single general one.

So in our case, instead of a single interface with five methods, we would be better off with three

interfaces with one or two methods.

This way our code can be modular and flexible and keep the single responsibility principle.

Dependency Inversion Principle

The last principle of SOLID is the Dependency Inversion Principle.
It states that high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.
This is clearly a reference to creating injections so that different layers do not depend on each other for execution but a separate middle layer that injects the required dependencies without the need to supply information or dependent attribute.

We already discussed this in the previous topic of dependency injection.


So that is it for part 6! In part 7 we jump to Design patterns. Now this would be really interesting and I am waiting to publish this soon!

Discover more in the next blog!!

I keep on coding something cool, visit ankush.tech to see what I am doing!

If you wish to read about my work, here is a book that I published recently – “CSS Bullets, a comprehensive guide to all the CSS you need!

Interested in React? Learn react from scratch with my book, “REACT Bullets“.

Not subscribed to the newsletter? Subscribe now!!!

Thanks for sticking around!

Leave a Reply

Discover more from The CodeWolf

Subscribe now to keep reading and get access to the full archive.

Continue reading