Samstag, 28. Oktober 2017

PantherDI - Part I: Interfaces

After developing KittDI with a rather naive approach (not thinking much prior, doing refactorings as I went) I decided to start another, more heavyweight library for dependency injection.


The first thing I want to talk about is the Interface of the library. To do this I am going to assume a usual workflow:

  1. Setting up the configuration for the new container
  2. Creating the container using that configuration
  3. Resolving what you need.

Container setup

Basically the setup of a container is a set of registrations. Upon construction, the container will use these to construct resolution strategies for the registered types. 


But what is a registration?
At first a registration is done for a specific type, so the data structure used for registrations needs to provide the type that is registered.
Next the registered type usually fulfils contracts, so for each registration we need to supply which contracts are fulfilled.
Then for each registered type there must be at least one way to create an instance of that type, so the registration needs an enumeration of factories for that type.
Each of these factories can have parameters (as in parameters of a function) which would be treated as dependencies of the registered type, when constructed using that factory.
Each dependency needs an expected type and a list of contracts that need to be fulfilled in order to satisfy the dependency. In the most cases the contract will be the expected type which both are the type of some interface, but any object can serve as contract.
Thus a dependency of a factory is modeled as a separate entity.
Also the factory has an execute-method which takes the resolved dependencies in the order given by the dependency list as an array of objects.

Container creation

To create a container it should be supplied with a catalog that contains the setup for the container. The algorithm that creates the container from a given catalog is the heart of PantherDI and will thus be within its own class for testability. It will create, what I will call the "knowledge base (KB)" of the container in future references. The KB contains an entry for each contract that can be fulfilled within the container. Each of these entries provides all ways to fulfill the given contract. A way to fulfill the contract is called a provider and just like the factories it too can have parameters, but when a provider in the KB does have parameters this means that during container creation this parameter was not resolvable using the catalog. This means that the KB basically contains a version of the factories which already know how to resolve the dependencies that actually can be resolved. Apart from that it it also contains the actual returned type, a full list of contracts fulfilled by this provider and additional metadata used by the container or set via registration.
The KB thus is a dictionary that maps contracts to a enumeration of providers. 

Resolving a registered type

To resolve a registration, the container needs an enumeration which contains at least one contract and a return type. A provider matches the request if it provides all the requested contracts and its actual type can be assigned to the requested return type. Since the typical call will be the return type being the contract, a parameterless call is allowed, causing the return type to be used as contract.

Test first (somewhat)

With that in mind the interfaces can already be created and with the interfaces already there, we can write tests for the behavior. But while I won't work test driven (meaning always implement the minimal solution to satisfy a test), I will write these tests up front and then use them to check if my implementation actually does what it should.
The following behavior should be covered by the tests (for now):

  • When no type is registered, trying to resolve by type or contract fails
  • When a type is registered, it can be resolved by its type and the registered contract type.
    The registered factory is called for each resolution
  • The registered type can also be used as contract
  • When a factory declares a dependency, the factory of that dependency is also called.
  • When multiple factories are registered that fulfil the contract requested, an exception is thrown
  • When there is a circular dependency, an exception is thrown
In order to write the tests without instantiating mocks for each interface, a first implementation of each interface is given, where all properties can be read and written, but without any internal logic.

Glossary

This chapter contains a cumulative list of terms that are used within this project.

Container
The central element of PantherDI.
It offers a way to retrieve instances of objects with all their transitive dependencies resolved.

Registration
Information about a single type, enabling a container to create it, resolve its dependencies and use it as dependency.

Contract
Any object that serves as the promise that a registration can be used as dependency. A registration can be retrieved only by contract, so the consumer does not need to know its actual type.

Factory
Basically the object representation of a function which will create an instance of a registered type.

Dependency (of a Factory or Provider, see below)
A single dependency of a factory is the object representation of one a function parameter. It includes meta-information, for example the contracts the instance passed needs to fulfil.


Provider
Created from a factory by resolving all the dependencies that can be resolved using the given registrations.

Knowledge Base (KB)
The knowledge base contains all providers that can be used by the container.

Code

The state of the code after this blogpost was written can be found under the tag "Blogpost1" in the GitHub-Repository: https://github.com/MarkusPalcer/PantherDI/tree/Blogpost1

Keine Kommentare:

Kommentar posten