Unity: loosely coupled architecture for specific tasks

Hello!
 
 
Everyone knows about the charms of loosely coupled application architecture. Everyone knows, but not everyone does. This happens for various reasons: laziness, timing, ignorance of how to properly apply, etc. I offer for review a small example of a loosely coupled architecture used to solve a specific problem.
 
 
Let's start with the task itself.
 
It is given: a player who moves around the level and collects coins.
 
Purpose: to display the current amount of coins in the player in the game interface.
 
 

Theory


 
We omit the implementation of the selection of coins, the storage of their quantity and other things, since he himself can do it the way he likes, and we will immediately turn to the theory of solving this problem. Since the interface element that informs us about the number of coins is one, it would be logical to make it a singleton, however, this forces us either to check this object for null everywhere, or to instantiate a new object if it is not there. Permanent checks on null can create a bunch of duplicate code, and this should be avoided. If we forget such a check, we get the most common error in statistics. Instantiating a new object can also be an extremely inconvenient solution, for example, in a test scene, where we check some mechanics and we don’t need interface elements, or we don’t need everything.
 
 
As you know, this problem can be solved by the “proxy” pattern. If there are those who do not know, then it is easily googled, but briefly recall about him. “Proxy” is a layer that replaces an object and provides access to it. Appeal to the proxy will guarantee us that the appeal will be made to an existing instance of the object. Next, the proxy itself will determine whether the target object exists or not. If it does, the proxy will call the requested method on the target object.
 
 
Since we want to get rid of duplication of code and constant checks for null in this way, it would be logical to think that singletons and proxies can be many, but they are all united in that they are singletons and proxies. Universal c # classes will help us solve the problem of duplicating code when describing them.
 
 

Solution


 
Let's start with the universal singleton.
 
 
3r3197. 3r3198. public class Singleton
where T: new ()
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
instance = new T ();
return instance;
}
}
} 3r3202.
 
Inheritance from this class will allow us to ensure the creation of a singleton. But it is not suitable for those classes that we want to add to the GameObject, since they must inherit from MonoBehaviour. And MonoBehaviour, in turn, is created through Instantiate, and not through the constructor. For him, we can make your universal singleton.
 
 
3r3197. 3r3198. public class MonoBehaviourSingleton
: MonoBehavior where T: MonoBehavior
{
public static T Instance;
protected virtual void Awake ()
{
Instance = GetInstance ();
}
protected T GetInstance () {
return this as T;
}
} 3r3202.
 
Now inheritance from this class will give us a singleton for MonoBehaviour objects.
 
 
In order for us to use a proxy instead of the required object, we need to create a common interface and implement it in the CoinUI class and its proxy.
 
 
3r3197. 3r3198.
public interface ICoinUI
{
void SetCount (int count);
}
3r3202.
 
The code of the interface element for displaying the number of coins may look like 3r33232.  
 
3r3197. 3r3198. public class CoinUI: MonoBehaviourSingleton
, ICoinUI
{
public Text coinCount;
private void Start ()
{
}
public void SetCount (int count)
{
coinCount.text = count.ToString ();
}
} 3r3202.
 
We will immediately create a proxy class for CoinUI, which will check for the presence of the CoinUI instance and, if it exists, call the required method in it.
 
 
3r3197. 3r3198.
public class CoinUIProxy: ICoinUI
{
public void SetCount (int count)
{
CoinUI.Instance? .SetCount (count);
}
} 3r3202.
 
If there is no CoinUI instance, then nothing will happen.
 
 
Let us turn to the creation of a universal proxy constructor.
 
 
3r3197. 3r3198. public class UIProxy
: Singleton
where proxyType: new ()
{} 3r3202.
 
And that is all the code. Above, we have already described the class parent Singleton, so we simply inherit from it, indicating a universal type, as the type of singleton.
 
 
The final class for our design will be a kind of provider - a class that will provide us with an object that implements the ICoinUI interface.
 
 
3r3197. 3r3198.
public class UIProvider
{
public static ICoinUI CoinUI => UIProxy
.Instance;
}
3r3202.
 
This provider will return to us an instance of the CoinUIProxy class, which, when calling SetCount, will check if CoinUI exists, if it does, will call the SetCount method on the CoinUI instance and transfer the value.
 
 
In order to change the value of the number of coins in the game interface, you need only one line of code 3r-3248.  
 
3r3197. 3r3198.
UIProvider.CoinUI.SetCount (100500);
3r3202.
 
You can call it at any place where you wish, for the test you can make a button with a method that executes this code to make sure that everything works correctly.
 
 

Conclusions


 
Cons of this approach:
 
 
3r33232.  
It is necessary to create a harness in the form of a provider and a universal proxy.
 
Instead of one class, we have 2 (main and proxy) + interface, but ideally you should have loosely coupled code that implies working mainly with interfaces and their implementations. so this is a minus only for super lazy people who don't like to write code correctly.
 
 
Pros:
 
 
3r33232.  
There are no checks for null in all places where the change in the value of the number of coins is called.
 
You can call a method from any class and any place in the code, without worrying about the fact that we forgot to add an object that represents the number of coins on the stage.
 
You can safely change the internal structure of CoinUI to achieve the desired display of changes in the number of coins - this will not affect the operation of the main code.
 
 
The use of such an architecture is possible not only for interface elements, but also for system sounds, as well as for any other situations that require working with singletones.
 
 
Hope the article was helpful. Thanks for attention!
+ 0 -

Add comment