Objects, Services, and Dependencies
By: Manning Publications
Tags: Oop, Software Engineering, Programming, Object Oriented, Software Development

ARTICLE

From the Object Design Style Guide by Matthias Noback

In this article we’ll discuss all the relevant aspects of instantiating a service. You’ll learn how to deal with its dependencies, what you can and can’t do inside its constructor, and you should be able to instantiate it once and make it reusable many times.

________________________________________________________________

Take 37% off the Object Design Style Guide by entering fccnoback into the discount code box at checkout at manning.com.

________________________________________________________________

Two types of objects

In an application there are typically two types of objects:

Service objects which either perform a task, or return a piece of information. Objects that hold some data, and optionally expose some behavior for manipulating or retrieving that data.

Objects of the first type are created once, then used any number of times, but nothing can be changed about them. They have a simple lifecycle. Once they’ve been created, they can run forever, like little machines with specific tasks. These objects are called services.

The second type of object is used by the first type to complete tasks. These objects are the materials which the services work with. For instance, a service may retrieve such an object from another service, and it manipulates the object and hands it over to another service for further processing (Figure 1). The lifecycle of a material object may be more complicated than that of a service: after it has been created, it could optionally be manipulated, and it may even keep an internal event log of everything which has happened to it.

Figure 1. This UML-style sequence diagram shows how services call other services, passing along other types of objects as method arguments or return values. Inside a service method, such an object may be manipulated, or a service may retrieve data from it.

Objects that perform a task are often called “services”. These objects are doers, and they often have names which indicate that: controller, renderer, calculator, etc. Service objects can be constructed by using the new keyword to instantiate their class, e.g. new FileLogger() .

Inject dependencies and configuration values as constructor arguments

Services usually need other services to do their job, which are its dependencies, and they should be injected as constructor arguments. An example of a service with its dependency is the FileLogger class in Listing 1.

Listing 1. The FileLogger service.

interface Logger

{

public function log(string message): void;

}



final class FileLogger implements Logger

{

private Formatter formatter;



public function __construct(Formatter formatter) ❶

{

this.formatter = formatter;

}



public function log(string message): void

{

formattedMessage = this.formatter.format(message);



// ...

}

}



logger = new FileLogger(new DefaultFormatter());

logger.log('A message');

❶ Formatter is a dependency of FileLogger .

Making every dependency available as a constructor argument makes the service ready for use immediately after instantiation. No further setup is required, and no mistakes can be made with that.

Sometimes a service needs some configuration values, like a location for storing files, or credentials for connecting to an external service. Inject such configuration values as constructor arguments too, as is done in Listing 2.

Listing 2. Besides a dependency , FileLogger also requires a configuration value.

final class FileLogger implements Logger

{

// ...



private string logFilePath;



public function __construct( ❶

Formatter formatter,

string logFilePath

) {

// ...



this.logFilePath = logFilePath;

}



public function log(string message): void

{

// ...



file_put_contents(

this.logFilePath,

formattedMessage,

FILE_APPEND

);

}

}

❶ logFilePath is a configuration value which tells the FileLogger to which file the messages should be written.

These configuration values may be globally available in your application, in some kind of a parameter bag, settings object, or otherwise large data structure containing all the other configuration values too. Instead of injecting the whole configuration object, make sure you only inject the values which the service should have access to. In fact, only inject the values it needs.

Keeping together configuration values that belong together

A service shouldn’t get the entire global configuration object injected, but only the values that it needs. Some of these values are always used together, and injecting them separately breaks their natural cohesion. Take a look at the following example where an API client gets the credentials for connecting to the API injected as separate constructor arguments (Listing 3).

Listing 3. The ApiClient class with separate constructor arguments for username and password.

final class ApiClient

{

private string username;

private string password;



public function __construct(string username, string password)

{

this.username = username;

this.password = password;

}

}

To keep these values together, you can introduce a dedicated configuration object. Instead of injecting the username and password separately, you can inject a Credentials object which contains both (see Listing 4).

Listing 4. Username and password now reside together in a Credentials object.

final class Credentials

{

private string username;

private string password;



public function __construct(string username, string password)

{

this.username = username;

this.password = password;

}



public function username(): string

{

return this.username;

}



public function password(): string

{

return this.password;

}

}



final class ApiClient

{

private Credentials credentials;



public function __construct(Credentials credentials)

{

this.credentials = credentials;

}

}

Exercises

4) Rewrite the constructor of the MySQLTableGateway class in such a way that the connection information can be passed as an object:

final class MySQLTableGateway

{

public function __construct(

string host,

int port,

string username,

string password,

string database,

string table

) {

// ...

}

}

Inject what you need, not where you can get it from

If a framework or library is complicated enough, it offers you a special kind of object which holds every service and configuration value you could ever want to use. Common names for such a thing are: service locator, manager, registry, or container.

What is a service locator?

A service locator is itself a service, from which you can retrieve other services. The following example shows a service locator which has a get() method. When called, the locator returns the service with the given identifier, or throw an exception if the identifier is invalid (Listing 5).

Listing 5. A simplified implementation of a service locator.

final class ServiceLocator

{

private array services;



public function __construct()

{

this.services = [

'logger' => new FileLogger(/* ... */) ❶

];

}



public function get(string identifier): object

{

if (!isset(this.services[identifier])) {

throw new LogicException(

'Unknown service: ' . identifier

);

}



return this.services[identifier];

}

}

❶ We can have any number of services here.

In this sense, a service locator is like a map; you can retrieve services from it, as long as you know the correct key. In practice, this key is often the name of the service class or interface that you want to retrieve.

Most often the implementation of a service locator is more advanced than the one we saw. A service locator often knows how to instantiate all the services of an application, and it takes care of providing the right constructor arguments when doing this. It also reuses already instantiated services, which can improve runtime performance.

Because a service locator gives you access to all of the available services in an application, it may be tempting to inject a service locator as a constructor argument and be done with it, like in Listing 6.

Listing 6. HomepageController uses a ServiceLocator to get its dependencies.

final class HomepageController

{

private ServiceLocator locator;



public function __construct(ServiceLocator locator) ❶

{

this.locator = locator;

}



public function execute(Request request): Response

{

user = this.locator.get(EntityManager.className)

.getRepository(User.className)

.getById(request.get('userId'));



return this.locator.get(ResponseFactory.className)

.create()

.withContent(

this.locator.get(TemplateRenderer.className)

.render(

'homepage.html.twig',

[

'user' => user

]

),

'text/html'

);

}

}

❶ Instead of injecting the dependencies we need, we inject the whole ServiceLocator , from which we can later retrieve any specific dependency, the moment we need it.

This results in a lot of extra function calls in the code, which obscures what the service does. Furthermore, this service needs to have knowledge about how to retrieve dependencies. This is the opposite of the Inversion of control we’re looking for when we use dependency injection: we don’t want our service to bother with fetching its dependencies, we want them to be provided to us. Besides, this service now has access to many more services that can potentially be retrieved from the service locator. Eventually this service ends up fetching all kinds of unrelated things from the service locator ad-hoc, because it doesn’t push the programmer to look for a better design alternative.

Whenever a service needs another service to perform its task, it has to declare the latter explicitly as a dependency and get it injected as a constructor argument. The ServiceLocator in this example isn’t a true dependency of HomepageController ; it’s used to retrieve the dependencies. Instead of declaring the ServiceLocator as a dependency, the controller should declare the dependencies that it needs as constructor arguments, and expect them to be injected, as shown in Listing 7.

Listing 7. HomepageController with its dependencies injected as constructor arguments.

final class HomepageController

{

private EntityManager entityManager;

private ResponseFactory responseFactory;

private TemplateRenderer templateRenderer;



public function __construct(

EntityManager entityManager,

ResponseFactory responseFactory,

TemplateRenderer templateRenderer

) {

this.entityManager = entityManager;

this.responseFactory = responseFactory;

this.templateRenderer = templateRenderer;

}



public function execute(Request request): Response

{

user = this.entityManager.getRepository(User.className)

.getById(request.get('userId'));



return this.responseFactory

.create()

.withContent(

this.templateRenderer.render(

'homepage.html.twig',

[

'user' => user

]

),

'text/html'

);

}

}

The resulting dependency graph is much more honest about the dependencies of the class (See Figure 2).

Figure 2. In the initial version, HomepageController only seemed to have had one dependency. After we get rid of the ServiceLocator dependency, it’s clear that it has three dependencies.

We should make another iteration here. In the example we only need the EntityManager because we need to fetch the user repository from it. We should make it an explicit dependency instead, as shown in Listing 8.

Listing 8. Instead of the EntityManager , HomepageController needs a UserRepository .

final class HomepageController

{

private UserRepository userRepository;

// ...



public function __construct(

UserRepository userRepository,

/* ... */

) {

this.userRepository = userRepository

// ...

}



public function execute(Request request): Response

{

user = this.userRepository

.getById(request.get('userId'));

// ...

}

}

What if I need the service and the service I retrieve from it?

Consider the following code which needs both the EntityManager and the UserRepository dependency:

user = this.entityManager

.getRepository(User.className)

.getById(request.get('userId'));

user.changePassword(newPassword);

this.entityManager.flush();

If we follow the advice to inject the UserRepository instead of the EntityManager , we end up with an extra dependency, because we’ll still need that EntityManager for flushing (i.e. persisting) the entity.

Situations like this usually require a redistribution of responsibilities. The object which can retrieve a User entity might be able to persist any changes which were made to it. In fact, such an object follows an established pattern, the Repository pattern. Because we already have a UserRepository class, it makes sense to add a flush() , or (now that we’ve the opportunity to choose another name) save() method to it:

user = this.userRepository.getById(request.get('userId'));

user.changePassword(newPassword);

this.userRepository.save(user);

All constructor arguments should be required

Sometimes you may feel like a dependency is optional; the object could function well without it. An example of such an optional dependency could be the Logger we saw. You may consider logging to be a secondary concern for the task at hand. To make it an optional dependency of a service, make it an optional constructor argument, as is done in Listing 9.

Listing 9. Logger is an optional constructor argument of BankStatementImporter .

final class BankStatementImporter

{

private Logger? logger;



public function __construct(Logger? logger = null)

{

this.logger = logger; ❶

}



public function import(string bankStatementFilePath): void

{

// Import the bank statement file



// Every now and then log some information for debugging...

}

}



importer = new BankStatementImporter(); ❷

❶ logger can be null or an instance of Logger .

❷ BankStatementImporter can be instantiated without a Logger instance.

This unnecessarily complicates the code inside the BankStatementImporter class. Whenever you want to log something, you first have to check if a Logger instance has been provided (if you don’t, you’ll get a fatal error):

public function import(string bankStatementFilePath): void

{

// ...



if (this.logger instanceof Logger) {

this.logger.log('A message');

}

}

To prevent this kind of workaround for optional dependencies, every dependency should be a required one.

The same goes for configuration values. You may feel like the user of a FileLogger doesn’t need to provide a path to write the log messages to because a sensible default path exists, and you add a default value for the corresponding constructor argument (Listing 10).

Listing 10. The client doesn’t have to provide a value for logFilePath .

final class FileLogger implements Logger

{

public function __construct(

string logFilePath = '/tmp/app.log'

) {

// ...

}

}



logger = new FileLogger(); ❶

❶ If the user omits the logFilePath argument, ‘/tmp/app.log’ is used.

When someone instantiates this FileLogger class, it isn’t immediately clear to which file the log messages are written. The situation gets worse if the default value is buried deeper in the code, like in Listing 11.

Listing 11. The default value for logFilePath is hidden in the log() method.

final class FileLogger implements Logger

{

private string? logFilePath;



public function __construct(string? logFilePath = null)

{

this.logFilePath = logFilePath;

}



public function log(string message): void

{

// ...



file_put_contents(

this.logFilePath != null this.logFilePath : '/tmp/app.log',

formattedMessage,

FILE_APPEND

);

}

}

To figure out which file path a FileLogger uses, the user is forced to dive into the code of the FileLogger class. Also, the default path is now an implementation detail which could easily change without the user noticing it. Instead, you should always let the user of the class provide any configuration value the object needs. If you do this for all classes, it’s easy to find out how an object has been configured by looking at how it’s being instantiated.

In summary: whether constructor arguments are used to inject dependencies, or to provide configuration values, in both cases constructor arguments should always be required and not have default values.

That’s all for now.

If you want to learn more about the book, check it out on our browser-based liveBook reader here.