Search Unity

How to couple pure C# classes loosely (Service Locator, Factory pattern, ...)

Discussion in 'Scripting' started by mahdiii, Mar 19, 2019.

  1. mahdiii

    mahdiii

    Joined:
    Oct 30, 2014
    Posts:
    856
    Hi. I have written some client/server services (For example Http request/response to get leaderboard data)
    My problem is about tight coupling.
    You can see an example below. First, which one do you prefer (parameterless constructor or with paramter)?
    Code (CSharp):
    1.  
    2. public interface ILeaderboardService
    3. {
    4.     LeaderboardData GetLeaderboard(string name, int topIndex = 0, int count = 100);
    5. }
    6.  
    7.  
    8. public class LeaderboardService1 : ILeaderboardService
    9. {
    10.     public LeaderboardData GetLeaderboard(string name, int topIndex = 0, int count = 100)
    11.     {
    12.         return null;
    13.         //...
    14.     }
    15. }
    16.  
    17. public class LeaderboardService2
    18. {
    19.     private readonly string _name;
    20.     private readonly int _topIndex;
    21.     private readonly int _count;
    22.  
    23.     public LeaderboardService2(string name, int topIndex = 0, int count = 100)
    24.     {
    25.         _topIndex = topIndex;
    26.         _count = count;
    27.         _name = name;
    28.     }
    29.  
    30.     public LeaderboardData GetLeaderboard()
    31.     {
    32.         //...
    33.     }
    34. }
    35.  
    36. public class ServiceTest1 : MonoBehaviour {
    37.     private void OnEnable() {
    38.         var result1 = new LeaderboardService1().GetLeaderboard("MainLeaderboard", count: 200);
    39.  
    40.         var result2 = new LeaderboardService2("MainLeaderboard", count: 200).GetLeaderboard();
    41.     }
    42. }
    43.  
    You should notice that LeaderboardService1 and LeaderboardService2 cause tight coupling (new())
    Therefore, I changed it and employed Factory pattern.
    I would like to know when I should utilize Factory pattern.

    I need to send a type, enum or string to it. Why is it more suitable than previous approach(directly instantiate using new operator)? Here we have a concrete type again! (CreateService<LeaderboardService1>)

    Code (CSharp):
    1.  
    2. public class ServiceTest2 : MonoBehaviour {
    3.     [SerializeField]
    4.     private int _count = 200;
    5.     //[SerializeField] private LeaderboardServiceType _leaderboardServiceType;
    6.     // or string
    7.  
    8.     private void OnEnable() {
    9.  
    10.  
    11.         //Factory pattern
    12.         //type
    13.         var result3 = LeaderboardServiceFactory.CreateService<LeaderboardService1>()
    14.             .GetLeaderboard("MainLeaderboard", count: _count);
    15.         //string
    16.         var result4 = LeaderboardServiceFactory.CreateService("LeaderboardService1")
    17.             .GetLeaderboard("MainLeaderboard", count: _count);
    18.         //enum
    19.         var result5 = LeaderboardServiceFactory.CreateService(LeaderboardServiceType.LeaderboardService1)
    20.             .GetLeaderboard("MainLeaderboard", count: _count);
    21.     }
    22. }
    23. public class LeaderboardServiceFactory
    24. {
    25.     public static ILeaderboardService CreateService<T>() where T : ILeaderboardService
    26.     {
    27.         if (!(typeof(T) is ILeaderboardService))
    28.         {
    29.             throw new ArgumentException();
    30.         }
    31.  
    32.         //or use switch case
    33.         return Activator.CreateInstance<T>();
    34.     }
    35. }

    After that, I changed again and utilized Service Locator.
    I know service locator is anti pattern because codes have to know about it and its mechanism.
    Also, we require to register them properly.
    In addition, we have access to big Service Locator and send requests to resolve inside classes and finally, the major problem is that Service Locator hides dependencies. We have to go inside codes to understand them.

    Code (CSharp):
    1.  
    2. public class ServiceTest3 : MonoBehaviour {
    3.     [SerializeField]
    4.     private int _count = 200;
    5.     private void OnEnable() {
    6.         //Service Locator
    7.  
    8.         ServiceLocator.Resolve<ILeaderboardService>().GetLeaderboard("MainLeaderboard", count: _count);
    9.     }
    10. }
    11. public class App3:MonoBehaviour
    12. {
    13.     private void OnEnable()
    14.     {
    15.         ServiceLocator.Register<ILeaderboardService>(new LeaderboardService1());
    16.     }
    17. }
    18. public static class ServiceLocator
    19. {
    20.     private static readonly Dictionary<Type, object> _dic = new Dictionary<Type, object>();
    21.  
    22.     public static void Register<TInterface>(TInterface obj)
    23.         where TInterface : class
    24.     {
    25.         if (_dic.ContainsKey(typeof(TInterface)))
    26.         {
    27.         }
    28.         else
    29.             _dic.Add(typeof(TInterface), obj);
    30.     }
    31.  
    32.     public static TInterface Resolve<TInterface>() where TInterface : class
    33.     {
    34.         var obj = _dic[typeof(TInterface)];
    35.         return obj as TInterface;
    36.     }
    37. }

    Code (CSharp):
    1.  
    2. public class ServiceTest4 : MonoBehaviour
    3. {
    4.     [SerializeField]
    5.     private int _count = 200;
    6.  
    7.     private ILeaderboardService _leaderboardService;
    8.     public void Initialize(ILeaderboardService leaderboardService)
    9.     {
    10.         _leaderboardService = leaderboardService;
    11.     }
    12.     private void OnEnable() {
    13.         _leaderboardService.GetLeaderboard("MainLeaderboard", count: _count);
    14.     }
    15. }
    16. public class App4 : MonoBehaviour
    17. {
    18.     [SerializeField] private ServiceTest4 _test;
    19.     private void OnEnable() {
    20.         _test.Initialize(new LeaderboardService1());
    21.     }
    22. }

    Also, I can use scriptableObjects with abstract classes.
    The advantage is that they can be serialized and I can simply change it and assign a test class with fake data but I can only utilize abstract classes and not interfaces to be able to serialize them.

    Code (CSharp):
    1.  
    2. public class ServiceTest5 : MonoBehaviour {
    3.     [SerializeField] //ScriptableObject
    4.     private BaseLeaderboardService _leaderboardService;// It can be a test class with fake data or real http client service
    5.  
    6.     [SerializeField] private int _count=200;
    7.     private void OnEnable() {
    8.         _leaderboardService.GetLeaderboard("MainLeaderboard", count:_count);
    9.     }
    10. }
    11. public abstract class BaseLeaderboardService
    12. {
    13.     public abstract LeaderboardData GetLeaderboard(string name, int topIndex = 0, int count = 100);
    14. }
    15.  
    Eventually, which one do you prefer to construct and instantiate pure C# objects when you want to test it without any DI frameworks like Zenject?
    1- new operator directly and then change it. (new Class1()--> new TestClass1())
    2- Factory pattern with enums,types or strings
    3- Service Locators
    4- Initialize method with interfaces and outer Initializer class to set them
    5- ScriptableObjects with abstract class (not pure C# classes)
    6- DI frameworks
     
    Last edited: Mar 19, 2019
    ModLunar likes this.