Getty Images


How to use abstracted repositories in dependency injection

Dependencies in software design can complicate matters. Dependency injection helps simplify it -- but it's not without its faults. Here's what you need to know.

One common software design strategy is to define a class hierarchy explicitly, with major classes calling on the specific services they use. This makes the major class dependent on those subordinates because the services must reuse the entire structure to repurpose the class and recode any changes. This significantly reduces the utility of the classes involved.

Many applications use a web front-end process to invoke deeper transaction processing -- a common model for hybrid cloud applications. This requires admins to create and bind a web- and cloud-side context to a subordinate process context that runs with the database, often in the data center.

The development of these components involves object classes. Traditional techniques make the structure of the database visible to the superior class, which supports the user. Changes in the database structure could create a reverse dependency. This makes the superior class dependent on the subordinate's implementation.

Dependency injections

The dependency injection process and repository pattern resolve this problem by passing the subordinate classes as a service to the major classes when invoked. Subordinate services are injected to the client class. The client class cannot have explicit dependencies on subordinate elements; admins must write the client class to accept the services as injected based only on its visible interface. The client, or superior, class must understand the interfaces of the injected services, but not the implementations, which are opaque.

The traditional direct coupling approach to design enables the class representing the user context to access the database services directly and create a reverse dependency. The injection of the service into the user class and context eliminates the dependency reversal, as the following diagram shows.

Diagram that shows direct coupling versus dependency injection
Direct coupling versus a dependency injection.

Dependency injection isn't without its issues, though. It's essential to build both dependent services and superior classes to a common interface, but the two can get out of synchronization. Some developers also find that dependency injection hampers automatic testing and debugging.

Working with abstracted repositories

Use the repository design pattern to simplify managing injected dependencies and organizing the interfaces. Create an abstraction between the data access and structure and the business logic of the primary classes and application. This abstraction, stored in a repository, assures that the client class and services use the same interface specifications. It enables -- but does not ensure -- the decoupling of the dependencies.

In a repository abstraction, the repository sits between the client class and the service to define the interface. The injection process injects the repository-abstracted interface, which also implements the service interface. This process prevents multiple interface definitions from affecting the exchange of information. However, each injected service requires a repository abstraction. This demand multiplies the number of repositories where many superior and subordinate classes already exist and requires a dependency reversal.

To resolve this, make the abstract repository generic. This step ensures that a repository service with specific arguments for each of the dependent services it supports can inject the proper service, rather than create a repository for every dependency injection. The abstraction layer created will select the proper service and bind it and any of its dependencies to the superior class through the dependency injection process.

The unit of work model

A more difficult problem occurs when there's a difference between a transaction from the perspective of the application user and a transaction from the perception of the database. The superior class and application tend to see the user interaction, but it must somehow decompose into database transactions.

A design pattern called unit of work creates a basket service that interfaces through repositories to the database. The goal is to represent the entire user transaction as a single unit so that the superior class doesn't inherit a dependency on the sequence of steps taken on the database and create another reverse dependency problem.

The dependency injection introduces the subordinate element as an injected service, which decouples it via the interface the service presents. An abstract repository creates an intermediary abstraction layer between the user process class and the database service. The common abstraction couples the parent and service subordinates.

The unit of work concept means the abstraction layer produces a single representation class for the sum of the database transactions. This provides the context that's coupled to the user through the user's superior class and context. The single representation class manages the individual database transactions, so the context of this sequence is also opaque to the user class.

Both the abstract repository and unit of work techniques are valuable in software testing because the services needed by the user-side application integrate through the dependency injection and become a part of the unit testing process rather than requiring separate integration testing. It's also easier to use an intermediary abstraction layer to couple test or production data.

The specific tools and procedures used in this progression depend on the programming language and the middleware framework, such as Microsoft's .NET. Examine the application workflow from online inception context to database context to decide which model offers the greatest gain in code reuse, functionality, ease of development and testing.

Dig Deeper on Systems automation and orchestration