Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Decoupling - How to

Discussion in 'Scripting' started by Dextozz, Apr 12, 2021.

  1. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    488
    Let's imagine we have 3+ classes. One parent class and two child classes. The child classes need to communicate with each other. My question is, how can this be done cleanly? Let me give you an example:


    Code (CSharp):
    1. // Parent class is a singleton
    2. public class ParentClass : MonoBehaviour
    3. {
    4.     public static ParentClass Instance => instance; // singleton implementation
    5.  
    6.     private static ParentClass instance;
    7.  
    8.     public FirstChild firstChild;
    9.     public SecondChild secondChild;
    10. }
    11.  
    12. public class FirstChild
    13. {
    14.     public var boo()
    15.     {
    16.         return ...
    17.     }
    18. }
    19.  
    20. public class SecondChild
    21. {
    22.     public void foo()
    23.     {
    24.         // How can I make this line more clean?
    25.         var firstChildInfo = ParentClass.Instance.firstChild.boo();
    26.  
    27.         // Execute some code based on data in firstChildInfo...
    28.     }
    29. }
    The biggest issue here is that the child must be aware of the parent, which is totally unsafe if you ask me. My initial guess was to use events or try something with the observer pattern, but I can't see how you could cleanly get the return value of the boo() method with events. Is there a way to establish communication between children without them knowing about the parent? Could this be a code smell perhaps? Maybe I'm making a mistake in segregating responsibilities?

    Edit: This also ties to the saying "Prefer composition over inheritance", sure, but how can this possibly be accomplished cleanly? All I see is just a mess of references everywhere
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,756
    In my Datasacks package I use a pattern like this:

    Anything interoperating with Text needs a Text Abstraction.

    ALL things on a given GameObject can use this same Text Abstraction.

    This is a thing that asks for a Text Abstraction:

    https://github.com/kurtdekker/datas...Assets/Datasack/Output/DSTextDisplayString.cs

    And this is the actual text abstraction object:

    https://github.com/kurtdekker/datas...s/Assets/Datasack/Output/DSTextAbstraction.cs

    It works pretty well. I do the same thing for "colorable" abstraction:

    https://github.com/kurtdekker/datas...ts/Datasack/Control/DSColorableAbstraction.cs

    which returns "things that are colorable" in pretty much the same way.
     
  3. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    488
    @Kurt-Dekker so, what's the idea behind this? You decide you want to have an object edit some UI text, you attach "DSTextDisplayString" on it. That then attaches DSTextAbstraction to the same object and through some events, you change the text?

    Could you elaborate more, please? I can't really understand much from those three scripts.
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,756
    it's sort of a local service locator pattern.

    I intend to interact with text on an object in some way. I have a lot of ways to interact with text, basically generic "text output" or "text retrieval" mechanisms.

    When TextMeshPRo came along I didn't want to make copies of all those ways of outputting text.

    Therefore I developed a TextAbstraction service and an easy-cheesy way to demand one.

    That way, all the text interaction was refactored to only work against the Text Abstraction.

    If in the future I wanted it to operate against the old TextMesh object (an ancient form of in-scene text that still works fine today), all I need to do is modify TextAbstraction and everything else behaves the same.

    For your child-to-child interoperation, you could have a central registration with the parent.

    You could also implement a custom interface for inter-child communication, then your children would implement that interface, and be able to find each other on the same GameObject.
     
  5. Hikiko66

    Hikiko66

    Joined:
    May 5, 2013
    Posts:
    1,302
    A func is the delegate that can return a value
    Code (CSharp):
    1. public class Parent : MonoBehaviour
    2. {
    3.     public ChildA a;
    4.     public ChildB b;
    5.  
    6.     void Awake()
    7.     {
    8.         a = gameObject.AddComponent<ChildA>();
    9.         b = gameObject.AddComponent<ChildB>();
    10.  
    11.         b.ReturnsABool = a.TrueOrFalse;
    12.     }
    13.  
    14. }
    15.  
    16. public class ChildA : MonoBehaviour
    17. {
    18.  
    19.     public bool TrueOrFalse()
    20.     {
    21.         return false;
    22.     }
    23. }
    24.  
    25. public class ChildB : MonoBehaviour
    26. {
    27.     public Func<bool> ReturnsABool;
    28.  
    29.     public void Start()
    30.     {
    31.         Debug.Log(ReturnsABool?.Invoke());
    32.     }
    33.  
    34. }
     
    Gravesend likes this.
  6. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    488
    @Hikiko66 This requires all logic to be handled inside of the parent class? What if I wanted the parent to be free from all logic whatsoever? It should only hold references to children and the children will be those that do the work
     
  7. Hikiko66

    Hikiko66

    Joined:
    May 5, 2013
    Posts:
    1,302
    The parent class doesn't handle the logic.

    Parent just wires things up.

    Parent doesn't know what the function does. ClassA does.
    Parent doesn't invoke the function. ClassB does.

    And the Children don't know about each other, or the parent.
     
  8. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Well, composition as a pattern and design, as it is present in the Unity engine, breaks a lot with what one might have learned about creating black boxes, facades and hiding stuff from the "evil" outside. In Unity, components are exposed to the public, there is no way around that. In some cases, you might be able to mitigate this by having a facade controller component with instances of custom classes as modells "inside", but overall you are just laying yourself stones there.

    When I faced the same problem as you, I ended up loosely following MVC and strictly separating controllers and models. I can recommend taking a dive into DOTS and ECS even if only to take a quick look on how they do things differently, as there they do it somewhat similar. They define components with data and then write systems that work with these components. As a result, you don't have achieved any kind fo security, but you have greatly decoupled your code and objects from one another.

    A quick example, let's say you have a
    TimeSinceDamaged
    component on your player character that keeps track of the time that has passed since the character last got damaged. In a first instinct, you might end up writing something like this, in -pseudo-code:
    Code (CSharp):
    1. public class TimeSinceDamaged : MonoBehaviour
    2. {
    3.     private float secondsSinceDamage;
    4.     public float SecondsSinceDamage => secondsSinceDamage;
    5.  
    6.     private SomeEvent damageEvent;
    7.  
    8.     void Awake()
    9.     {
    10.         damageEvent += Reset();
    11.     }
    12.  
    13.     void Reset()
    14.     {
    15.         secondsSinceDamage = 0f;
    16.     }
    17.  
    18.     void Update()
    19.     {
    20.         secondsSinceDamage += Time.deltaTime;
    21.     }
    22. }
    However, now you have black-boxed it nicely and secure, but you are also tightly coupling the property itself and the logic on how this property changes into the same object, which violates the single responsibility principle. Instead, consider something like this, which decouples the property value itself and allows you to easily scale it by adding more controller components with new logic that requires this property without stuffing it all into the same object:
    Code (CSharp):
    1. public class TimeSinceDamagedProperty : MonoBehaviour
    2. {
    3.     public float SecondsSinceDamage;  
    4. }
    5.  
    6. public class TimeSinceDamagedController : MonoBehaviour
    7. {
    8.     [SerializeField]
    9.     private TimeSinceDamagedProperty property;
    10.  
    11.     private SomeEvent damageEvent;
    12.  
    13.     void Awake()
    14.     {
    15.         damageEvent += Reset();
    16.     }
    17.  
    18.     void Reset()
    19.     {
    20.         property.SecondsSinceDamage = 0f;
    21.     }
    22.  
    23.     void Update()
    24.     {
    25.         property.SecondsSinceDamage += Time.deltaTime;
    26.     }
    27. }
     
    Dextozz likes this.
  9. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    First determine who's dependent on whom.
    So it seems "SecondChild" is dependent on "FirstChild".

    Next, find out to what kind of dependency is required, in other words, how much / to which extent is one dependent on the other.

    Here are the most common cases:

    Case #1:
    An instance of "SecondChild" always needs an instance of "FirstChild", and that's the only type of object that you need to know about for your implementation
    - use a dependency injection pattern with that exact type, e.g. pass it via constructor or property

    Case #2:
    An instance of "SecondChild" always needs something that is currently implemented by "FirstChild", but there are potentially other implementation out there:

    a) other implementations always share the same base type => use a base class, proceed with #1
    b) other implementations are not necessarily sharing a common base type, or are even fundamentally different in nature yet share a part of their public API that you want to address => use an interface, proceed with #1

    Case #3:
    An instance of "SecondChild" needs to react to an event that's raised by "FirstChild"
    => determine whether criteria from #1 or #2 meet your requirements, let "FirstChild" expose an event and subscribe to that event in "SecondChild", or invert the dependency and let "FirstChild" know about "SecondChild" (concrete type, base type, interface ...) and "notify" it ... in both cases, the data you'd return in #1 or #2 via a method would be passed via (event) arguments

    Case #4: none of the above => your so called "parent" needs to orchestrate the communication
     
    Last edited: Apr 12, 2021
    Dextozz likes this.
  10. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    488
    @Ardenian @Suddoha these are both amazing answers!

    @Suddoha your answer is really detailed, I'll definitely do a double check in my code to see which criteria I meet. I definitely have something to go by now. Thanks a lot!

    @Ardenian that's a really interesting approach. I'm glad I got an answer from someone that has faced this issue in the past. I'll definitely take a look at some basics of DOTS. This has given me a few ideas, thanks!