Правильный способ внедрения зависимостей в клиентском приложении Windows (WPF)

Я привык к IoC/DI в веб-приложениях - в основном Ninject с MVC3. Мой контроллер создан для меня, заполнен всеми имеющимися зависимостями, зависимостями и т. Д.

Однако в толстом клиентском приложении все по-другому. Я должен создать свои собственные объекты, или я должен вернуться к подходу стиля локатора службы, где я прошу ядро ​​(возможно, через некоторый интерфейс, чтобы позволить тестируемость) дать мне объект, полный зависимостей.

Тем не менее, я видел несколько мест, где Service Locator был описан как анти-шаблон.

Поэтому мой вопрос - если я хочу использовать Ninject в своем приложении для толстых клиентов, есть ли лучший / более правильный способ получить все это?

  • способность быть свидетелем в суде
  • Правильный DI / IoC
  • Наименьшее количество возможных соединений

Пожалуйста, обратите внимание, что я говорю не только о MVVM, а о представлении моделей представлений. Это особенно вызвано необходимостью предоставить объект типа репозитория из ядра, а затем получить объекты, извлеченные из этого репозитория, с функциональностью (данные, конечно, поступают из базы данных, но им также нужны некоторые объекты в качестве параметров в зависимости от состояния мира, и Ninject знает, как это обеспечить). Могу ли я как-то сделать это, не оставляя и репозитории, и сущности в качестве непроверяемых беспорядков?

Если что-то неясно, дайте мне знать. Спасибо!

РЕДАКТИРОВАТЬ 14 ИЮЛЯ

Я уверен, что оба ответа, вероятно, являются правильными. Тем не менее, каждая клеточка моего тела борется с этим изменением; Отчасти это может быть вызвано недостатком знаний, но есть и одна конкретная причина, по которой мне трудно увидеть элегантность этого способа ведения дел;

Я не достаточно хорошо объяснил это в первоначальном вопросе, но дело в том, что я пишу библиотеку, которая будет использоваться несколькими (сначала 4-5, может быть, позже) клиентскими приложениями WPF. Все эти приложения работают в одной и той же доменной модели и т. Д., Поэтому хранение всего этого в одной библиотеке - единственный способ остаться СУХИМЫМ. Однако есть также шанс, что клиенты этой системы напишут своих собственных клиентов - и я хочу, чтобы у них была простая, чистая библиотека для общения. Я не хочу заставлять их использовать DI в своем корне композиции (используя такой термин, как Марк Симан в своей книге) - потому что это УЖЕ усложняет вещи по сравнению с ними, просто обновляя MyCrazySystemAdapter() и используя его.

Теперь MyCrazySystemAdapter (имя выбрано потому, что я знаю, что люди со мной здесь не согласятся) должен быть составлен подкомпонентами и объединен с использованием DI. Сам MyCrazySystemAdapter не нужно вводить. Это единственный интерфейс, который клиенты должны использовать для общения с системой. Таким образом, клиент, к счастью, должен получить один из них, DI происходит как закулисная магия, и объект состоит из множества различных объектов, используя лучшие практики и принципы.

Я понимаю, что это будет спорный способ хотеть делать вещи. Тем не менее, я также знаю людей, которые собираются стать клиентами этого API. Если они увидят, что им нужно изучить и подключить систему DI, и заранее создать всю структуру своих объектов в точке входа своего приложения (Composition Root), вместо того, чтобы обновлять отдельный объект, они дадут мне средний палец и напрямую связывайтесь с базой данных и разбирайтесь так, как вы вряд ли можете себе представить.

TL; DR. Предоставление правильно структурированного API - слишком сложная задача для клиента. Мой API должен доставлять один объект - созданный за сценой с использованием DI и надлежащей практики - который они могут использовать. Реальный мир иногда превосходит желание построить все задом наперед, чтобы оставаться верным шаблонам и практикам.

2 ответа

Решение

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


По сути, вы должны создать полное приложение в вашем app.xaml. Если некоторые детали необходимо создать позже, потому что вы еще не знаете всего, чтобы создать их при запуске, то введите фабрику в виде интерфейса (см. Ниже) или Func (см. Поддерживает ли Ninject Func (автоматически генерируемая фабрика)?) В классе, который необходимо создать этот экземпляр. Оба будут изначально поддерживаться в следующем выпуске Ninject.

например

public interface IFooFactory { IFoo CreateFoo(); }
public class FooFactory : IFooFactory
{
    private IKernel kernel;
    FooFactory(IKernel kernel)
    {
        this.kernel = kernel;
    }

    public IFoo CreateFoo()
    {
        this.kernel.Get<IFoo>();
    }
}

Обратите внимание, что реализация фабрики логически относится к конфигурации контейнера, а не к реализации ваших бизнес-классов.

Я ничего не знаю о WPF или MVVM, но ваш вопрос в основном о том, как вытащить вещи из контейнера, не используя Service Locator (или контейнер напрямую) повсюду, верно?
Если да, я могу показать вам пример.

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

Примечание: я буду использовать пример с WinForms и не привязанный к конкретному контейнеру (потому что, как я уже сказал, я не знаю WPF...и я использую Castle Windsor вместо NInject), но поскольку ваш основной вопрос не является конкретным привязанный к WPF/NInject, вам будет легко "перенести" мой ответ на WFP/NInject.

Фабрика выглядит так:

public class Factory : IFactory
{
    private readonly IContainer container;

    public Factory(IContainer container)
    {
        this.container = container;
    }

    public T GetStuff<T>()
    {
        return (T)container.Resolve<T>();
    }
}

Основная форма вашего приложения получает эту фабрику с помощью инжектора конструктора:

public partial class MainForm : Form
{
    private readonly IFactory factory;

    public MainForm(IFactory factory)
    {
        this.factory = factory;
        InitializeComponent();  // or whatever needs to be done in a WPF form
    }
}

Контейнер инициализируется при запуске приложения, и основная форма разрешается (поэтому он получает фабрику с помощью инжектора конструктора).

static class Program
{
    static void Main()
    {
        var container = new Container();
        container.Register<MainForm>();
        container.Register<IFactory, Factory>();
        container.Register<IYourRepository, YourRepository>();

        Application.Run(container.Resolve<MainForm>());
    }
}

Теперь основная форма может использовать фабрику для извлечения таких вещей, как ваш репозиторий, из контейнера:

var repo = this.factory.GetStuff<IYourRepository>();
repo.DoStuff();

Если у вас есть больше форм и вы хотите использовать фабрику оттуда, вам просто нужно вставить фабрику в эти формы, как в основную форму, зарегистрировать дополнительные формы при запуске и открыть их из основной формы с фабрикой,

Это то, что вы хотели знать?


РЕДАКТИРОВАТЬ:
Рубен, конечно ты прав. Виноват.
Все, что содержалось в моем ответе, было старым примером, который я где-то лежал, но я спешил, когда опубликовал свой ответ, и недостаточно внимательно прочитал контекст моего старого примера.

Мой старый пример включал наличие основной формы, из которой вы можете открыть любую другую форму приложения. Это то, для чего была создана фабрика, поэтому вам не нужно вводить любую другую форму через конструктор в основную форму.
Вместо этого вы можете использовать фабрику, чтобы открыть любую новую форму:

var form = this.factory.GetStuff<IAnotherForm>();
form.Show();

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

public partial class MainForm : Form
{
    private readonly IAnotherForm form;

    // pass AnotherForm via constructor injection
    public MainForm(IAnotherForm form)
    {
        this.form = form;
        InitializeComponent();  // or whatever needs to be done in a WPF form
    }

    // open AnotherForm
    private void Button1_Click(object sender, EventArgs e)
    {
        this.form.Show();
    }
}

public partial class AnotherForm : Form
{
    private readonly IRepository repo;

    // pass the repository via constructor injection
    public AnotherForm(IRepository repo)
    {
        this.repo= repo;
        InitializeComponent();  // or whatever needs to be done in a WPF form

        // use the repository
        this.repo.DoStuff();
    }
}
Другие вопросы по тегам