Абстрактная фабрика и инверсия управления разрешаются во время выполнения

У меня есть следующая структура класса и интерфейса, и мне трудно заставить код делать то, что мне нужно.

public interface IUserManager
{
    int Add(User user);
}

public class UserManagerA : IUserManager{}
public class UserManagerB : IUserManager{}

В этом примере я использую Ninject в качестве контейнера IoC, но я готов изменить его, если какой-либо другой контейнер решит проблему:

Это внутри моего NinjectWebCommon.cs:

void RegisterServices(IKernel kernel)
{
    string userRole = CurrentUser.Role;//this gets the user logged in
    //This is the part I do not how to do
    //I wish I could just type this in:
    kernel.Bind<IUserManager>().To<UserManagerA>()
        .When(userRole == "RoleA"); // this doesn't work obviously
    kernel.Bind<IUserManager>().To<UserManagerB>()
        .When(userRole == "RoleB"); // same doesn't work
}

Все это для того, чтобы в моем (MVC) контроллере я мог сделать это:

public class UserController
{
    private readonly IUserManager _userManager;
    public UserController(IUserManager userManager)
    {
        _userManager = userManager;
    }
    public ActionResult Add(User user)
    {
        //this would call the correct manager
        //based on the userRole
        _userManager.Add(user);
    }
}

Я читал статьи об абстрактной фабрике, но не нашел ни одной, объясняющей, как интегрировать фабрику с контейнером IoC и передать параметр, полученный во время выполнения, для разрешения реализаций.


person SOfanatic    schedule 12.03.2015    source источник
comment
.When(r => CurrentUser.Role == "RoleA"); ? - предполагая, что When вычисляется для каждого разрешения, которое вы не хотите записывать во время установки...   -  person Alexei Levenkov    schedule 12.03.2015
comment
@AlexeiLevenkov Я не понимаю, что ты имеешь в виду? CurrentUser вычисляется от вошедшего в систему пользователя, так что значение будет меняться при каждом запросе, разве привязка Ninjects не происходит каждый раз при создании контроллера?   -  person SOfanatic    schedule 12.03.2015
comment
Я не знаю, как работает Ninject, и мое предложение основано на том, как будет вести себя Unity. Я предполагаю, что Ninject будет выполнять .When каждый раз, когда необходимо разрешить тип, а частота разрешения по умолчанию (также известная как время жизни в Unity) - каждый раз, когда кто-то запрашивает тип... Таким образом, когда контроллер создается (по крайней мере, один раз для каждого http-запроса), он будет получить IUserMamager соответствие CurrentUser.Role.... (Может быть полностью выключен, так как я не знаю напрямую о Ninject)   -  person Alexei Levenkov    schedule 12.03.2015
comment
@AlexeiLevenkov хорошо, спасибо, я сначала попробую сделать это с помощью ninject, если это не сработает, я попробую единство.   -  person SOfanatic    schedule 12.03.2015


Ответы (2)


Если число реализаций IUserManager не очень велико (вероятно, не достигнет 100 реализаций), вы можете использовать Шаблон стратегии для разрешения всех ваших экземпляров UserManager во время композиции, а затем выберите лучший экземпляр для использования во время выполнения.

Во-первых, нам нужен способ сопоставления IUserManager реализаций с ролями.

public interface IUserManager
{
    int Add(User user);
    bool AppliesTo(string userRole);
}

public class UserManagerA : IUserManager
{
    // Add method omitted

    public bool AppliesTo(string userRole)
    {
        // Note that it is entirely possible to 
        // make this work with multiple roles and/or
        // multiple conditions.
        return (userRole == "RoleA");
    }
}

public class UserManagerB : IUserManager
{
    // Add method omitted

    public bool AppliesTo(string userRole)
    {
        return (userRole == "RoleB");
    }
}

Затем нам нужен класс стратегии, который просто выбирает правильный экземпляр на основе userRole. Экземпляры IUserManager предоставляются контейнером DI при составлении приложения.

public interface IUserManagerStrategy
{
    IUserManager GetManager(string userRole);
}

public class UserManagerStrategy
    : IUserManagerStrategy
{
    private readonly IUserManager[] userManagers;

    public UserManagerStrategy(IUserManager[] userManagers)
    {
        if (userManagers == null)
            throw new ArgumentNullException("userManagers");

        this.userManagers = userManagers;
    }

    public IUserManager GetManager(string userRole)
    {
        var manager = this.userManagers.FirstOrDefault(x => x.AppliesTo(userRole));
        if (manager == null && !string.IsNullOrEmpty(userRole))
        {
            // Note that you could optionally specify a default value
            // here instead of throwing an exception.
            throw new Exception(string.Format("User Manager for {0} not found", userRole));
        }

        return manager;
    }
}

Использование

public class SomeService : ISomeService
{
    private readonly IUserManagerStrategy userManagerStrategy;

    public SomeService(IUserManagerStrategy userManagerStrategy)
    {
        if (userManagerStrategy == null)
            throw new ArgumentNullException("userManagerStrategy");
        this.userManagerStrategy = userManagerStrategy;
    }

    public void DoSomething()
    {
        string userRole = CurrentUser.Role;//this gets the user logged in

        // Get the correct UserManger according to the role
        IUserManager userManager = this.userManagerStrategy.GetManger(userRole);

        // Do something with userManger
    }
}

void RegisterServices(IKernel kernel)
{
    kernel.Bind<IUserManager>().To<UserManagerA>();
    kernel.Bind<IUserManager>().To<UserManagerB>();

    // Ninject will automatically supply both IUserManager instances here
    kernel.Bind<IUserManagerStrategy>().To<UserManagerStrategy>();

    kernel.Bind<ISomeService>().To<SomeService>();
}

Этот метод не требует внедрения контейнера в приложение. Место службы не используется.

Также обратите внимание, что нет оператора switch case, который нужно было бы модифицировать каждый раз, когда вы добавляете новый UserManager в приложение. Логика использования UserManager является частью реализации UserManager, а порядок выполнения логики определяется конфигурацией цифрового входа.

Кроме того, это будет работать независимо от того, какой контейнер внедрения зависимостей вы используете.

Вы можете объединить это с CurrentUserProvider из ответа RagtimeWilly, чтобы получить чистый способ получить роль пользователя в службе, где она используется.

Ссылка: Лучший способ использовать StructureMap для реализации шаблона стратегии< /а>

person NightOwl888    schedule 14.03.2015

Создайте класс, отвечающий за предоставление правильного UserManager, и внедрите его в свой контроллер:

public class UserManagerProvider : IUserManagerProvider
{
    private readonly IContext _context;

    public UserManagerProvider(IContext context)
    {
        _context = context;
    }

    public IUserManager Create(User currentUser)
    {
        if (currentUser.Role == "User A")
            return _context.Kernel.Get<UserManagerA>();

        if (currentUser.Role == "User B")
            return _context.Kernel.Get<UserManagerB>();

        // Or bind and resolve by name
        // _context.Kernel.Get<IUserManager>(currentUser.Role);
    }
}

И в контроллере:

private readonly IUserManager _userManager;

public UserController(IUserManagerProvider userManagerProvider)
{
    _userManager = userManagerProvider.Create(CurrentUser);
}

Кроме того, в качестве примечания у вас, вероятно, должен быть CurrentUserProvider, отвечающий за получение текущего пользователя. Использование статического метода усложнит модульное тестирование, и вы, по сути, скроете зависимость во всех классах, которые на нее ссылаются:

private readonly IUserManager _userManager;
private readonly User _currentUser;

public UserController(IUserManagerProvider userManagerProvider, ICurrentUserProvider currentUserProvider)
{
    _currentUser = currentUserProvider.GetUser();
    _userManager = userManagerProvider.Create(_currentUser);
}
person RagtimeWilly    schedule 12.03.2015
comment
+1. Вот как я это делаю. Если вы не хотите, чтобы ссылка на ядро ​​находилась за пределами корня композиции, ваша реализация UserManagerProvider может быть частью этого проекта (с более доступным интерфейсом). Кроме того, рассмотрите возможность регистрации и разрешения менеджеров пользователей по имени вместо использования if операторы в реализации поставщика. - person Phil Sandler; 13.03.2015
comment
@PhilSandler Я согласен, утверждение if неприятно. Просто хотел, чтобы реализация соответствовала заданному вопросу - person RagtimeWilly; 13.03.2015
comment
@PhilSandler, что вы подразумеваете под регистрацией и разрешением по имени? - person SOfanatic; 13.03.2015
comment
@RagtimeWilly На самом деле я не использую статический метод для CurrentUser, который был предназначен только для демонстрационных целей. Я пытался избежать передачи роли каждому вызову Manager, поскольку после входа пользователя в систему я уже знаю, какие менеджеры следует использовать. Я собираюсь немного подождать, чтобы увидеть, появится ли кто-то еще с реализацией, которая не требует включения роли в каждый вызов менеджеру, если нет, мне придется пойти по этому маршруту, который вы предлагаете . Спасибо. - person SOfanatic; 13.03.2015
comment
Привязка и разрешение по имени: github.com/ нинжект/нинжект/вики/ - person Phil Sandler; 13.03.2015
comment
Как отмечено в вики, вы должны делать это разумно, так как это немного попахивает расположением службы. Тем не менее, это наименее неправильный способ решения конкретной реализации на основе контекста времени выполнения (IMO). - person Phil Sandler; 13.03.2015
comment
@PhilSandler Я обновил ответ, включив примечание о разрешении по имени. Спасибо. - person RagtimeWilly; 13.03.2015