`internal` 생성자와 Builder를 이용해 안전하게 인스턴스 생성

생성자를 직접 호출했을 때 오작동 할 여지가 있는 클래스가 있다고 가정해봅시다. 이럴 때 별도의 Builder 클래스를 통해 인스턴스를 생성하면 좋은데요,

public class ActivationViewer
{
   public ActivationViewer(..., ..., ...) // 신중하게 인자를 입력해야 한다고 가정합니다.
}

이럴 때 public생성자 대신 internal로 주고, Builder를 통해 생성하도록 할 수 있습니다.

    public class ActivationViewer : IActivationViewer
    {
        public static readonly string DefaultId = "DEFAULT";

        private readonly Dictionary<Type, Type> _map = new();
        private readonly Dictionary<string, Type> _viewMap = new();
        private readonly Dictionary<string, Type> _viewModelMap = new();
        private readonly IServiceProvider _sp;

        public string StartupId { get; } = DefaultId;

        internal ActivationViewer(IServiceProvider sp, IReadOnlyDictionary<Type, Type> map, string startupId = "")
        {
            _sp = sp;

            foreach (var kv in map)
            {
                _map[kv.Key] = kv.Value;
                _viewModelMap[kv.Key.Name] = kv.Key;
                _viewMap[kv.Value.Name] = kv.Value;
            }

            if (string.IsNullOrWhiteSpace(startupId) == false)
                StartupId = startupId;
        }

        public IView Get(object? activationArgs = null) => Get(StartupId, activationArgs);

        public IView Get(string id, object? activationArgs = null)
        {
            var viewType = _map[_viewModelMap[id]];
            if (_sp.GetService(viewType) is not IView view)
                throw new ArgumentException($"'{id}' is an unregistered ViewModel.");

            return view;
        }

        public sealed class Builder : IBuilder
        {
            private readonly Dictionary<Type, Type> _map = new();

            private Builder()
            {
            }

            public static IBuilder Create() => new Builder();

            public IBuilder AddMap(Type viewModelType, Type view)
            {
                _map[viewModelType] = view;
                return this;
            }
            public IBuilder AddMap<TViewModelType, TView>() => AddMap(typeof(TViewModelType), typeof(TView));

            public ActivationViewer Build(IServiceProvider sp, string defaultId) => new ActivationViewer(sp, _map, defaultId);
        }

        public interface IBuilder
        {
            IBuilder AddMap(Type viewModelType, Type view);
            IBuilder AddMap<TViewModelType, TView>();

            ActivationViewer Build(IServiceProvider sp, string defaultId);

            static abstract IBuilder Create();
        }
}

외부 모듈에서는 ActivationViewer의 생성자가 internal이므로 직접 생성자를 호출할 수 없고, Builder에 의해서만 생성할 수 있게 됩니다.

사용은 다음과 같습니다.

...
            var s = new ServiceCollection()
                .AddSingleton<IActivationViewer>(p => ActivationViewer.Builder.Create()
                    .AddMap<MainViewModel, MainPage>()
                    .Build(p, nameof(MainViewModel))
                );
...
좋아요 2

제네릭 인자의 Where 절을 이용하면 제네릭 인자로 수용할 타입을 지정할 수 있습니다.

가령, View와 ViewModel의 매핑하는 (위의 예제 코드) AddMap의 TViewModelType (TViewModel로 바꿔야겠군요; ) 과 TView는 어떠한 Where 조건도 없습니다. 이를,

where TViewModel: IViewModel, TView: IView

를 추가하여 ViewModel과 View를 IViewModel과 IView 인터페이스를 구현한 대상만 수용하겠다고 변경하면, 두 가지 이점이 생깁니다.

  1. 컴파일 타임 때 올바르지 않는 타입이 주어질 때 "컴파일 오류"로 문제를 감지할 수 있습니다.
  2. 인터페이스의 기능을 제네릭 인자를 사용한 메소드에서 사용할 수 있습니다!
좋아요 1