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 UIEntityCtxwhose 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:

  1. Interface UIEntityCtx should contain interfaces UIPmCtx and UIviewWithButtonCtx, 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.

  2. In the class GlobalContext it is necessary to implement two new interfaces – UIPmCtx and UIviewWithButtonCtx, 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 public UIPmCtx 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *