📜 ⬆️ ⬇️

Data Dependency Template

Introduction


In this article I will talk about the Data Dependency pattern of the component implementation in the context of Dependency Injection. In examples I will use language C # and Unity.
We begin by describing a situation in which Dependency Injection is not enough, and there is a need to resort to Data Injection.

Task


It is necessary to develop a component that, based on the list of teams, makes up a rating of K best. Teams must be evaluated through a variety of other ranking components that are not part of the component being developed, and communicate through DI. The average for all rangers should be taken as a score for the team.
Team:
  public interface ITeam { string Name { get; } } 

Ranker:
  public interface IRanker { int Rank(ITeam team); } 

Record in rating:
  public interface ITopRecord { ITeam Team { get; } int Position { get; } double AverageRank { get; } } 

Component Interface:
  public interface ITopBuilder { IEnumerable<ITopRecord> BuildTop(IEnumerable<ITeam> teams, int topCount); } 

Depending on the container used, you may additionally need a primitive supplier of appraisers:
  public interface IRankerProvider { IEnumerable<IRanker> GetRankers(); } 


Implementation


You can begin to implement, and give the interfaces in a separate assembly to the developers of the IRanker ( IRanker ) so that they do their work. The implementation of the IRankerProvider interface lies with the user who knows which ranking ITopRecord should be used, because we only have ITopRecord and ITopBuilder . You can also make a composite of the rangers. Do, hand over and forget.
After a while, an unexpected problem appears. One of the rankings for scoring needs to know how many teams there are. Usually in this place all unkind things are remembered, bad architecture and things like that. Often resorted to the ugly solution - to change the ranking interface of the ranger:
  public interface IRanker { int Rank(ITeam team,int teamCount); } 

thus changing the contract, which will inevitably affect all implementations.
Some in this situation may prefer another solution. It consists in creating two different types of rankings:
  public interface IRanker { int Rank(ITeam team); } public interface IRankerWithCount { int Rank(ITeam team,int teamCount); } 

or a similar scheme that will inevitably require changes in the implementation of ITopBuilder .
Hell begins when a new demand arrives - another ranger needed additional information. He needs to know whether the estimated team in the list is worth an even position. And one more should know how many people will be in the ranking.
At this point it is easier and more correct to surrender, and pass all ITopBuilder parameters to the ITopBuilder :
  public interface IRanker { int Rank(ITeam team, IEnumerable<ITeam> teams, int topCount); } 

You can do this implicitly by entering the notion of context and transmitting the context. In any case, the responsibility for extracting the necessary data rests entirely with those who implement the rangers, and the interfaces are fixed. It seems that it had to be done from the very beginning.

Another way


Although the solution proposed above is universal, it has a flaw. It is redundant. Unnecessary parameters are passed to all rangers, even when they are not needed. Let's try to get rid of unnecessary parameters at all.
And, for a start, we note that the extra parameters are dependencies, for which we have a Dependency Injection pattern and a container. Inserting IEnumerable and Int32 instances into container is bad. Wrap them in the appropriate interfaces:
  public interface ITeamCollection:IEnumerable<ITeam> { } public interface ITopCount { int Count { get; } } 

Suppose that the product of the work of our ranger also needs to be passed to someone as a dependency, and we declare an interface for it:
  public interface ITop : IEnumerable<ITopRecord> { } 

Now the interface of our ITopBuilder component will look like this:
  public interface ITopBuilder { ITop BuildTop(ITeamCollection teams, ITopCount count); } 

We try to transfer parameters through the container. Using Unity produces something like this:
  public ITop BuildTop(ITeamCollection teams, ITopCount count) { using (var container = _container.CreateChildContainer()) { container.RegisterInstance(teams); container.RegisterInstance(count); //do work } } 

Allow the ITeamCollection and ITopCount rankings to be in the constructor. And, therefore, the code will not work. This is because IRankerProvider resolved in the IRankerProvider constructor, and it is called before we register our instances. You can solve this problem by delegating work to another object that will be created each time BuildTop call BuildTop :
  public ITop BuildTop(ITeamCollection teams, ITopCount count) { using (var container = _container.CreateChildContainer()) { container.RegisterInstance(teams); container.RegisterInstance(count); return container.Resolve<BuilerWorker>().DoWork(); } } internal sealed class BuilerWorker { public BuilerWorker( ITeamCollection teams, ITopCount count, IRankerProvider rankerProvider ... ) { } public ITop DoWork() { ... } } 

')
Thus, we have formed a template that is quite versatile and well applicable in practice in order to have a name - Data Dependency.

Source: https://habr.com/ru/post/177075/


All Articles