Using the Composition root architecture in Unity. Part 2. Transition to reference type dependencies
Hello everyone again, it’s time to continue the discussion of using Composition root in Unity. In the last article, I described the main provisions of this architecture, ways of forwarding dependencies and organizing primitive logic within the project. And also, he promised to consider replacing context structures with global class interfaces. This is what we’ll do.

Where did the need come from?
Passing the context in the form of structures is very economical in terms of memory, they lie on the stack and the advantages of this are obvious, but there are also disadvantages, which are just in practice. When I first got acquainted with this architecture, it meant that the context was presented in the form of structures, which is not so bad if there are not so many programmers in the team. Over time, our team grew and it became more difficult for new members to understand the “branches” of dependencies and keep track of all possible changes in reactive variables. At a minimum, the simplest function of the Find Usage editor was missing. So the decision came to move away from constantly created contexts and use a global class, where all the variables and events used will be described.
Implementation step 1.
Within the same project create two scripts Interfaces
and GlobalContext
. In the first, we remove the automatically created class and transfer the context UIEntity
as an interface:
Interface UIEntityCtx
public interface UIEntityCtx
{
ContentProvider contentProvider { get; set; }
RectTransform uiRoot { get; set; }
ReactiveProperty<int> buttonClickCounter { get; set; }
}
In turn, in GlobalContext
you need to implement this interface:
GlobalContext class
public class GlobalContext: UIEntityCtx
{
public ContentProvider contentProvider { get; set; }
public RectTransform uiRoot { get; set; }
public ReactiveProperty<int> buttonClickCounter { get; set; }
}
Implementation step 2.
Now we need to create an object GlobalContext
and fill in the variables. You can create it directly in EntryPoint, but in order not to fill the article with code, I will transfer this moment to GameEntity. Add a variable and set values in the constructor:
GameEntity class
private readonly Ctx _ctx;
private UIEntity _uiEntity;
private CubeEntity _cubeEntity;
private readonly ReactiveProperty<int> _buttonClickCounter = new ReactiveProperty<int>();
private GlobalContext _globalContext;
public GameEntity(Ctx ctx)
{
_ctx = ctx;
_globalContext = new GlobalContext();
_globalContext.contentProvider = _ctx.contentProvider;
_globalContext.uiRoot = _ctx.uiRoot;
_globalContext.buttonClickCounter = _buttonClickCounter;
CreateUIEntity();
CreteCubeEntity();
}
private void CreateUIEntity()
{
_uiEntity = new UIEntity(_globalContext);
AddToDisposables(_uiEntity);
}
Now UIEntity takes as a parameter not the Ctx structure, but the interface UIEntityCtx
whose values were set by the level above.
Implementation step 3.
Let’s create context interfaces in the same way for UIPm
and UIviewWithButton
. And here it is worth paying attention to two points:
Interface
UIEntityCtx
should contain interfacesUIPmCtx
andUIviewWithButtonCtx
, so we get a hierarchy within the contexts. This is done in order to limit the “scope” of each component and achieve better encapsulation. You could give everywhere_globalContext
as dependencies and everything would work the same, but this is already a security issue.In the class
GlobalContext
it is necessary to implement two new interfaces –UIPmCtx
andUIviewWithButtonCtx
, indicating that when they are accessed, they refer to this particular class so as not to get null. You can do this, for example, in this way publicUIPmCtx uIPmCtx {get => this; set { }}
Script Interfaces
public interface UIEntityCtx
{
ContentProvider contentProvider { get; set; }
RectTransform uiRoot { get; set; }
ReactiveProperty<int> buttonClickCounter { get; set; }
UIPmCtx uIPmCtx { get; set; }
UIviewWithButtonCtx uIviewWithButtonCtx { get; set; }
}
public interface UIPmCtx
{
ReactiveProperty<int> buttonClickCounter { get; set; }
}
public interface UIviewWithButtonCtx
{
ReactiveProperty<int> buttonClickCounter { get; set; }
}
GlobalContext class
public class GlobalContext: UIEntityCtx, UIPmCtx, UIviewWithButtonCtx
{
public ContentProvider contentProvider { get; set; }
public RectTransform uiRoot { get; set; }
public ReactiveProperty<int> buttonClickCounter { get; set; }
public UIPmCtx uIPmCtx { get => this; set { }}
public UIviewWithButtonCtx uIviewWithButtonCtx { get => this; set { }}
}
In order for the value of variables in contexts to reach the end point, it is necessary, as in the previous version of the project, to fill them in when creating objects. Values are passed from top to bottom from the parent object.
Updated UIEntity class
public class UIEntity : DisposableObject
{
private readonly UIEntityCtx _ctx;
private UIPm _pm;
private UIviewWithButton _view;
public UIEntity(UIEntityCtx ctx)
{
_ctx = ctx;
CreatePm();
CreateView();
}
private void CreatePm()
{
_ctx.uIPmCtx.buttonClickCounter = _ctx.buttonClickCounter;
_pm = new UIPm(_ctx.uIPmCtx);
AddToDisposables(_pm);
}
private void CreateView()
{
_view = Object.Instantiate(_ctx.contentProvider.uIviewWithButton, _ctx.uiRoot);
_ctx.uIviewWithButtonCtx.buttonClickCounter = _ctx.buttonClickCounter;
_view.Init(_ctx.uIviewWithButtonCtx);
}
protected override void OnDispose()
{
base.OnDispose();
if(_view != null)
Object.Destroy(_view.gameObject);
}
}
I think it makes no sense to describe the transition to the reference type of the context of other parts of the project, everything happens there in the same way as I described above. We create interfaces based on structural contexts and replace them in class constructors.
Most likely, the article will cause controversy and controversy, but I decided to share the experience of just such a refactoring, because. the work done justified the costs and the work became noticeably easier.