Building Reusable Modules

0
1102
Reusable modules

The requirements on any piece of software change over a period of time. The trick is to absorb the change with no or minimal impact on the existing software. A monolith system does not offer such possibilities. Only modular systems with well-defined interfaces are flexible enough to absorb change. The Gang of Four described a good number of patterns that address the issue of modularity and named them ‘structural patterns’. We look at a few of these patterns in this second part of this series of articles on software design.

In the first part of this column (published in November 2021 issue of Open Source For You), we started developing a rudimentary user management system (UMS) that maintains the names and phone numbers of users. The system has only two requirements — to add user details and to retrieve them. If everything goes well, we might be able to deploy the UMS for multiple customers. In such a case, it is quite possible that some customers may want a few additional features like validation, authentication, authorisation, journaling, etc.

UMS with journaling
Let us take the case of journaling, which is a process of recording every usage of the system. It is also termed auditing. For example, this feature logs an audit record to the journal every time a new user is added to UMS.

Recollect that the original implementation of InMemoryUserRepository consists of only business logic. For example, the add() method was like this:

public void add(Long phone, String name) {
    entries.put(phone, name);
}

In order to add the journaling feature, the implementation of add() method can be refactored as follows:

In order to add the journaling feature, the implementation of add() method can be refactored as follows:

public void add(Long phone, String name) {
    System.out.println(“c=repository, s=add, a=” + phone + “&” + name);
    entries.put(phone, name);
    System.out.println(“c=repository, s=add, r=void”);
}

It can be noticed from the above snippet that the original business logic is now surrounded by a couple of logging statements. Though we are logging only to the console, it is sufficient enough for the illustration. The code that appears before the business logic is referred to as ‘pre-processing’, and that which appears after the business logic is referred to as ‘post-processing’.

The find() method with such pre- and post-processing code looks like this, as you might have already expected:

public String find(Long phone) {
    System.out.println(“c=repository, s=find, a=” + phone);
    String name = entries.get(phone);
    System.out.println(“c=repository, s=find, r=”+name);
    return name;
}

With these changes, the InMemoryUserRepository is equipped with a journaling feature.

Plug-and-play
An interesting observation is that the journaling feature is not a core feature of UMS. Only some customers may show interest in this feature while others do not. In other words, we cannot permanently change the code of InMemoryUserRepository with or without the journaling feature. Instead, journaling must be built only as a pluggable feature to the InMemoryUserRepository.

Usually, a pluggable feature is expected to be added or removed from an existing system just through configuration, without making any code changes. There are many such features like security, validation, transaction management, session management, encoding/decoding, encryption/decryption, etc, which are good candidates for plug-and-play.

In fact, any optional feature that can be implemented by the way of pre-processing and post-processing can be a plug-and-play feature. It is one of the basic essences of a modular system. The proxy pattern explains the design for such a pluggable module.

Proxy
As the name suggests, a proxy acts as a frontend to someone else, which we call a ‘target’. However, not every frontend to a target can be a proxy. There are a couple of expectations with respect to a proxy:

1. A proxy must look the same as a target, for the outsiders. In fact, for them, a proxy is not even noticeable. In other words, the interface of the proxy must be the same as that of the target.

2. A proxy must not replace the target. Instead, it only offloads some optional work from the target and leaves the core functionality with it. In other words, a proxy only takes care of pre-processing and post-processing, leaving the business logic to the target.

Going by these observations, let us redesign the UMS.

We will introduce a class named UserRepositoryJournalProxy to act as a proxy to any implementation of the UserRepository interface, not just only for InMemoryUserRepository. So, the UserRepositoryJournalProxy implements the UserRepository interface.

Apart from that, the UserRepositoryJournalProxy also holds a reference to UserRepository, which acts as the target.

Consumers like the Application in Figure 1 still refer to the UserRepository interface like in the past. Remember, there should not be any impact on the consumer with or without the presence of a proxy.

Proxy pattern
Figure 1: Proxy pattern

Since we plan to offload the journaling work to the UserRepositoryJournalProxy, the InMemoryUserRepository limits itself only to the core business functionality which is in no way different from what we have done in Part 1.

public void add(Long phone, String name) {
    entries.put(phone, name);
}
public String find(Long phone) {
    return entries.get(phone);
}

A proxy always requires a target of the same interface like the InMemoryUserRepository. The target can be injected into the UserRepositoryJournalProxy through the constructor.

public class UserRepositoryJournalProxy implements UserRepository {
    private UserRepository target;

    public UserRepositoryJournalProxy(UserRepository target) {
        this.target = target;
    }
    ...
}

Notice that only the proxy is aware of its target, while the target is unaware of the presence of the proxy.

Now is the final step. Like any other proxy, the UserRepositoryJournalProxy should delegate the core functionality to its target and handle only the pre-processing and post-processing, which in this case is to log audit records to the console.

public void add(Long phone, String name) {
    System.out.println(“c=repository, s=add, a=” + phone + “&” + name);
    target.add(phone, name);
    System.out.println(“c=repository, s=add, r=void”);
}

public String find(Long phone) {
    System.out.println(“c=repository, s=find, a=” + phone);
    String name = target.find(phone);
    System.out.println(“c=repository, s=find, r=” + name);
    return name;
}

Separation of concerns
Recollect that we had an ObjectFactory, which was supplying the providers of various interfaces to the consumers. Earlier, the factory was supplying InMemoryUserRepository for the UserRepository interface. However, the new design has two implementations for the UserRepository interface. One is InMemoryUserRepository, which offers the core functionality and the other is UserRepositoryJournalProxy, which offers the pluggable functionality of journaling.

The ObjectFactory provides which of these two to the consumers?

The answer is simple! A proxy cannot exist without a target, whereas a target has no such dependency. In other words, a core implementation of a UserRepository such as InMemoryUserRepository is always required. Along with the core provider, a proxy is required only when the customer chooses to enable a specific feature.

The responsibility of supplying the core provider with or without a proxy still rests with the ObjectFactory. The factory makes the decision based on some sort of configuration. Here is an example for configuration in the form of a ‘properties’ file.

user.repository=com.glarimy.ums.InMemoryUserRepository
user.repository.proxy=com.glarimy.ums.UserRepositoryJournalProxy

In the above sample configuration, the InMemoryUserRepository is chosen as a core module against the key user.repository. In the second line, which is optional, the UserRepositoryJournalProxy is chosen as the proxy.

Our ObjectFactory reads the configuration from the properties file. It first builds the provider based on the key. Then, it checks if a proxy is also indicated in the configuration. If no proxy is indicated, it returns only the core provider. However, in case a proxy is indicated, it builds the proxy, injects the provider into it, and then returns it. We know that a proxy cannot exist without a target. In other words, the consumer always receives a proxy along with the provider, when a proxy is configured.

The factory used the following logic to supply the core providers, in the earlier implementation:

String className = props.getProperty(key);
Class<?> claz = Class.forName(className);
Object o = null;

try {
    o = claz.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
    Singleton singleton = claz.getAnnotation(Singleton.class);
    if (singleton != null) {
        String getter = singleton.getter();
        o = claz.getMethod(getter).invoke(claz);
    } else {
        o = claz.getMethod(“getInstance”).invoke(claz);
    }
}

We now add the following logic to the factory method, to wire an optional proxy to the requested object:

String proxyClassName = props.getProperty(key + “.proxy”);
if (proxyClassName == null)
    return o;
Class<?> proxyClass = Class.forName(proxyClassName);
return proxyClass.getConstructor(claz.getInterfaces()[0]).newInstance(o);

The way you define the configuration properties and a corresponding factory is up to you. You may design the configuration in any other manner. Let’s say, the following can be a configuration file based on XML:

<component>
    <provider>com.glarimy.ums.InMemoryUserRepository</provider>
    <proxy>com.glarimy.ums.UserRepositoryJournalProxy</proxy>
</component>

Or you may use JSON, YML, or any other configuration files. Only make sure that the ObjectFactory knows how to interpret the configuration file.

Before concluding the proxy pattern, let’s make a few observations:

1. A proxy always acts as a frontend to a target, which is another class.

2. The interface of the proxy and the target must be the same.

3. The proxy should only handle pre-processing and post-processing.

4. The proxy should not handle the core functionality.

5. The proxy should delegate the core functionality to the target.

6. The proxy may return before delegating the call to the target, depending on the kind of pre-processing.

7. It is a good idea to build separate proxies for separate pluggable features.

8. Usually, factories wire the proxy to the target, based on a configuration.

The proxies are also known by names like filters, interceptors, middleware, etc, though they may not be exactly the same. The proxy is such a popular pattern that it has shaped a promising approach called aspect-oriented programming (AOP). Also, frameworks like Spring Boot and AspectJ are capable of generating runtime proxies for capabilities like validation, etc.

Handling third party modules
We have seen how modular design is useful in responding to business needs while discussing the proxy pattern. An interesting fact is that we seldom write 100 per cent of the code for any system in-house. It is very common for a developer to use code modules written by someone else. You might have used several off-the-shelf modules like logging libraries, security libraries, JWT token libraries, ORM libraries, etc. Since they offer general-purpose functionality, we love to use them in our system, instead of developing them in-house again.

The only problem with the third party library modules is that they are not in our control. The public interface of such a module may keep changing for various reasons. Or we ourselves may want to move over to another alternate module. We all know that no two third party modules offer the same public interface, even though they offer the functionality that we are looking for. Often, a change in the third party module forces a change in our own code, if these are coupled tightly.

For example, if we want to replace console-logging with file based logging in UserRepositoryJournalProxy, there might be several third party options available in the market already. Let’s assume that the FileJournal is such a third party module that offers file based logging functionality.

package com.glarimy.lib;

import java.io.FileWriter;
import java.util.Date;

public class FileJournal {
    private FileWriter writer;

    public FileJournal() throws Exception {
        writer = new FileWriter(“journal.log”, true);
    }

    public void save(String message) {
        try {
            writer.write(new Date() + “ “ + message);
            writer.write(“\n”);
            writer.flush();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

In order to use it, the UserRepositoryJournalProxy can be refactored to get an instance of the FileJournal.

private FileJournal journal = new FileJournal();

The proxy uses the journal instead of the console, for logging the audit records.

public void add(Long phone, String name) {
    journal.save(“c=repository, s=add, a=” + phone + “&” + name);
    target.add(phone, name);
    journal.save(“c=repository, s=add, r=void”);
}

public String find(Long phone) {
    journal.save(“c=repository, s=find, a=:” + phone);
    String name = target.find(phone);
    journal.save(“c=repository, s=find, r=” + name);
    return name;
}

This arrangement between UserRepositoryJournalProxy and FileJournal can be depicted as shown in Figure 2. However, it is not a good design from the perspective of modularity.

Tight coupling with third party modules
Figure 2: Tight coupling with third party modules

Though FileJournal may be a good library, it is not suggested to couple it with UserRepositoryJournalProxy directly. It leads to tight coupling and vendor-locking. For example, if we want to replace FileJourmal with some other journal library tomorrow, the code in the UserRepositoryJournalProxy also needs to be changed. We already know that a change in provider must not result in a change in consumer.

The Gang of Four suggested keeping a protection layer between our modules and the third party modules that are vulnerable to changes. The pattern to follow is ‘adapter’.

Adapter
The job of an adapter is to sit between the consumer and the third party provider. It offers a known and consistent interface to the consumers irrespective of the changes in the third party provider. The pattern can be depicted as shown in Figure 3, for the UMS.

Adapter pattern
Figure 3: Adapter pattern

The notable observation here is that the JournalAdapter is an interface that offers a consistent function named record().

package com.glarimy.ums;

public interface JournalAdapter {
    public void record(String message);
}

This interface is to be defined by the consumer according to the need. In our case, the consumer is UserRepositoryJournalProxy.

For different third party modules, there will be different implementations of JournalAdapter. Here is an implementation that uses the FileJournal.

package com.glarimy.ums;

import com.glarimy.lib.FileJournal;

public class FileJournalAdapter implements JournalAdapter {
    private FileJournal journal;

    public FileJournalAdapter() throws Exception {
        journal = new FileJournal();
    }

    public void record(String message) {
        journal.save(message);

    }
}

It can be observed that the FileJournalAdapter is hiding FileJournal from its consumers. A consumer, like UserRepositoryJournalProxy, refers only to JournalAdapter.

private JournalAdapter adapter;

The consumer uses a factory to load the adapter:

Factory factory = new ObjectFactory(“config.properties”);
this.adapter = (JournalAdapter) factory.get(“adapter”);

Our existing ObjectFactory works perfectly with the entry in the configuration file, which looks like this:

adapter=com.glarimy.ums.FileJournalAdapter

And the following will be the code in UserRepositoryJournalProxy that uses the adapter. Notice that the consumer is only referring to the interface of the adapter, not the third party module, directly.

public void add(Long phone, String name) {
    adapter.record(“c=repository, s=add, a=” + “&” + name);
    target.add(phone, name);
    adapter.record(“c=repository, s=add, r=void”);
}

public String find(Long phone) {
    adapter.record(“c=repository, s=find, a=” + phone);
    String name = target.find(phone);
    adapter.record(“c=repository, s=find, r=” + name);
    return name;
}

With the help of an adapter interface and an able factory, we can plug in any other third party library into the system, without making changes to the consumer. We only will have different implementations of the adapter interface for different third party modules.

Again, this is a good time to summarise the adapter pattern.

1. An adapter acts as an intermediary between the consumer and a third party library.
2. An adapter offers a consistent interface to the consumers.
3. It translates the calls from the consumers to a specific third party library.

Before getting into the next pattern, let us see what is common between a proxy and an adapter.

Both proxy and adapters are intermediaries between providers and consumers. They both do some sort of optional pre-processing and post-processing. That’s where the similarities end.

The difference between them can be summed up like this. The interface of the proxy and the provider is exactly the same, whereas the interfaces of the adapter and the third party providers are completely different.

Decorator
The last pattern that we look at in this part is the ‘decorator’, which helps in designing modular systems.

A decorator is also an intermediary between consumer and provider, just like the proxy and adapter. However, unlike them, the interface of the decorator is usually a superset of the interface of the provider. That way, a decorator decorates the provider. In other words, a decorator extends the interface of the provider and thereby offers additional functionality.

Decorator pattern
Figure 4: Decorator pattern

In the example depicted in Figure 4, the UserRepositoryDecorator is extending UserRepository:

public class UserRepositoryDecorator implements UserRepository {
    ...
}

However, just like the proxy, it also holds a reference to the target provider through its interface. The original functionality will still be offered by the target provider while the decorator offers the additional functionality.

private UserRepository target;

public UserRepositoryDecorator(UserRepository target) throws Exception {
    this.target = target;
}

The decorator can apply pre-processing and post-processing just like the proxy. At a minimum, the decorator has to implement all the interface methods and delegate the work to the target.

public void add(Long phone, String name) {
    target.add(phone, name);
}

public String find(Long phone) {
    return target.find(phone);

}

And finally, the UserRepositoryDecorator offers additional functionality findOne() that returns Java 8 Optional. This is useful for some consumers who are interested in using UserRepository along with lambdas and other functional programming constructs.

public Optional<String> findOne(Long phone) {
    String name = target.find(phone);
    if (name == null)
        return Optional.empty();
    return Optional.of(name);
}

An interesting point about decorators is that it is not mandatory for them to offer additional interface functions. In such a case, a decorator and a proxy are exactly the same.

Factory factory = new ObjectFactory(“config.properties”);
UserRepository repo = (UserRepository) factory.get(“user.repository”);
repo.add(9731423166L, “Krishna”);
UserRepositoryDecorator fluent = new UserRepositoryDecorator(repo);
fluent.findOne(9731423166L).ifPresent(name -> System.out.println(“Found “ + name));

You may not like the UserRepositoryDecorator in the current shape, since it is a concrete class with additional functionality. Instead, you may want to have an interface with the additional functionality and thereby a concrete class.

For example, you may want the UserRepositoryDecorator as an interface that extends UserRepository and declares the additional function findOne().

public interface UserRepositoryDecorator extends UserRepository {
    public Optional<String> findOne(Long phone);
}

A class like FluentUserRepository can be an implementation of the UserRepositoryDecorator.

public class FluentUserRepository implements UserRepositoryDecorator {
    private UserRepository target;

    public FluentUserRepository(UserRepository target) throws Exception {
        this.target = target;
    }
    ...
}

The consumer of the decorator now refers to the interface, instead of the concrete class directly.

UserRepositoryDecorator fluent = new FluentUserRepository(repo);
fluent.findOne(9731423166L).ifPresent(name -> System.out.println(“Found “ + name));

Interface segregation
Let us try to understand where we use the decorator pattern.

One of the dilemmas for a designer is whether to offer all functionalities in one interface or split them into individual interfaces. The principle of Single Responsibility recommends not to mix multiple features into a single interface. Usually, the core interface is defined only with cohesive mandatory use cases, whereas the optional use cases are handled in individual proxies and decorators. Proxies are useful when the additional use cases can be plugged-in just by adding pre-processing and post-processing. However, sometimes, the additional functionality may require additional interfaces altogether for the consumers to invoke. Such interfaces and their implementations are called decorators.

Finally, our refactored design for UMS is more modular with proxies, decorators, and adapters. This is depicted in Figure 5.

Modular UMS design
Figure 5: Modular UMS design

The best practices learnt in this part can be enumerated like this:

1. Design an adapter between your code and third party modules. It protects your code from changes that happen in the third party modules.
2. Design proxies for different pluggable features.
3. Design decorators for extending the functionality of an object.
4. Design the factory to load the adapters, proxies, etc.

Almost all modern systems are modular. Other patterns like ‘composite’, ‘facade’, etc, are also useful in building modular systems.

In the next part, the focus will be on establishing communication among modules.

LEAVE A REPLY

Please enter your comment!
Please enter your name here