Layers + Unity Container

    Всем привет! Хочу привести пример layers-архитектуры и роль контейнера Unity в ней. А то народ про сам контейнер пишет, а как его c с пользой использовать толком написать не могут. Давайте я попробую.


    Начну очень издалека, извините сразу. Просто, для того, чтобы объяснить что-то полезное, необходимо придумать что-то действительно полезное.

    (Не очень) жизненный пример


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

    Теперь — вдруг откуда ни возьмись вам звонит заказчик из банка IBBC и звонко верещит:
    -Володья! Нам срочно нужно получить приложение, которое будет показывать предметы на экране! Срок неделя. Денег — миллион!
    И кладет трубку.

    Поскольку любое конструирование начинается с проектирования, будем проектировать.

    Слои и черные ящики


    Систему будем строить на слоях.

    Определение программного слоя из Microsoft Architecture Application Guide таково (в несколько вольном переводе меня, пардон):

    Слоем называется логическая группировка программных компонент, составляющих приложение или службу.

    Иными словами, в «слоенной архитектуре» та самая куча программных компонент, из которых состоит приложение, разделяется на несколько логически связанных подмножеств. Все знают классику жанра — слой UI, слой бизнес-логики, слой данных.

    Судя по словам заказчика, нам нужно показывать что-то в пользовательском интерфейсе и забирать данные из какого-то источника. Поэтому система будет состоять из двух слоев — UI-слой, Data-слой, а так же драйвера (который соединяет слои + main()).

    Договорились, что UI будет писать Колян, а слой данных будет писать Толян. Вы же будете осуществлять координирующую деятельность и писать программу драйвер плюс cross-cutting код.

    Черные ящики


    Слой — это программный модуль (сборка или сборки с одной главной в .net). Этот модуль можно реализовать в виде черного или белого ящика.
    • Черный ящик подразумевает, что вы знаете интерфейсную составляющую слоя и можете оперировать функциональностью ящика только через нее.
    • Белый ящик означает, что все управляющие сущности программного компонента видимы другими компонентами, которые знают о существовании ящика.

    Другими словами, вы не знаете, что в черном ящике, но у вас есть пульт управления (интерфейсы) к нему.
    В белом ящике вы хозяин — что хотите, то с его наполнением и делаете. Он прозрачный и у него видны все внутренности.

    В нашем случае дизайнить слои методом черного ящика кошернее, вот почему:
    1. Вы добиваетесь концептуальной целостности и инкапсуляции. Разработав единый механизм доступа к слою через его интерфейс вы обретаете семантическую полноту (то бишь конечность), позволяющую вам оперировать только сущностями ящика.
    2. Инкапсуляция и (что очень главное) неведение о том, каким образом сконструированы конкретные сущности слоя, позволяют вам сосредоточиться на более общей проблеме, убрав из головы заботу о ненужных тонкостях.
    3. В конце концов, об этих постулатах нам твердят лучшие умы от программирования, поэтому давайте послушаем умных дядечек:)


    Контейнер и его роль


    Из всего вышеперечисленного получается, что организовывать программный модуль нужно таким образом, чтобы он:
    1. Реализовывал назначенные ему контракты
    2. Скрывал всю реализацию конкретных сущностей

    Поскольку мы инкапсулируем все реализации в ящике, то мы не можем обращаться к ним напрямую и работать с ними обычным образом. Нам нужно каким-то образом связать контракт ящика и его реализацию, где-то ее сохранить, чтобы потом другие участники процесса смогли ими воспользоваться. Для этого нужен контейнер. В самом вырожденном случае он является обычным словарем «интерфейс-конкретный тип». В более сложном случае, как, например, с Unity, это еще и инъектор зависимостей и прочие шлюхи с блэкджеками.

    В момент запуска приложения нужно окунуть контейнер в слой и позволить слою зарегистрировать свои реализации для обязательных ему контрактов. После того, как все реализации собраны, можно уже писать управляющий код, не волнуясь, кто же у нас ху.

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

    Код



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

    public interface IDataService
    {
      object GetData();
    }


    * This source code was highlighted with Source Code Highlighter.


    А Коляну, соответственно, достался UI:

    public interface IDataView
    {
      void ShowData();
    }

    public interface IViewFactory
    {
      IDataView Create(Type viewType);
    }


    * This source code was highlighted with Source Code Highlighter.


    Программный модуль должен содержать в себе единственный публичный класс, чтобы мы могли засунуть в него руку с контейнером и зачерпнуть пригорошню реализаций:

    public interface IModule
    {
      void Configure(IUnityContainer container);
    }


    * This source code was highlighted with Source Code Highlighter.


    Все, контракты определены. Теперь нам нужно собрать все воедино в управляющем приложении:
    class ShellApplication
    {
      public ShellApplication()
      {
        _container = new UnityContainer();
      }

      public void Run()
      {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        _container.RegisterInstance<IUnityContainer>(_container);
        _container.Resolve<Layers.Data.Module>().Configure(_container);
        _container.Resolve<Layers.UI.Module>().Configure(_container);

        MainForm form = (MainForm)_container.Resolve(typeof(MainForm));
        
        Application.Run(form);
      }

      private IUnityContainer _container;
    }


    * This source code was highlighted with Source Code Highlighter.


    partial class MainForm : Form
    {
      public MainForm(IViewFactory factory)
      {
        InitializeComponent();
        _factory = factory;
      }

      protected override void OnLoad(EventArgs e)
      {
         base.OnLoad(e);

        IDataView view = _factory.Create(typeof(IDataView));
        Controls.Add((Control)view);
        view.ShowData();
      }

      private IViewFactory _factory;
    }


    * This source code was highlighted with Source Code Highlighter.


    С вашей частью все.

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

    Итеративность тоже поддерживается — можно писать нисходящим образом. Сначала нужно написать управляющий код, навесив на него «фальшивые слои» и изменяя контракты под требования в итерациях. После того, как управляющий код отлажен и зафиксирован, останется подмести по углам — реализовать ящики.

    У Коляна, Толяна и меня получилось вот что:

    Смотрите, кругом одни конвертики. Слои высунули наружу только свой хвостик — класс модуля.

    А мужику из IBBC мы сделали его приложение и огребли кучу денег. Толян и Колян купили себе десятки, а я — приору.

    Стойте. Чуть не забыл. Вы можете мне сказать «Ha-ha-ha-ha!!! Sucker! Sucker!», примерно так же, как это сделал Henry Rollins в конце четвертой минуты своей очень хорошей песенки. Как же это я скрыл все реализации, а юнит-тестирование?!?!

    Ну, я делаю так:
    [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Services.Tests, PublicKey=00240000048000009[skipped]1fbcb51f56dfcc8db5320774f354553ad")]

    * This source code was highlighted with Source Code Highlighter.


    Пояснять не нужно?:)

    Особенности, выводы, примечания, бонусы и недостатки



    Сори, что я делаю пример на Windows Forms. Я бы очень хотел рассказать, как таким макаром устроить архитектуру приложения ASP.NET MVC, но меня за это побъет товарищ XaocCPS. Я бы с радостью подрался, но он меня шустрее и больше. Оставим эту тему ему.

    1) Управляющая сборка должна иметь ссылки на все слои и сборку контрактов. Иначе, нельзя будет собрать реализации.

    2) Коляну пришлось использовать сервис данных Толяна (согласно парадигме, слой UI зависит от слоя данных):

    partial class DataView : UserControl, IDataView
    {
      public DataView(IDataService data)
      {
        InitializeComponent();
        _dataService = data;
        listBox1.DataSource = _source;
      }

      #region IDataView Members

      public void ShowData()
      {
        _source.DataSource = _dataService.GetData();
      }

      #endregion

      private IDataService _dataService;
      private BindingSource _source = new BindingSource();
    }


    * This source code was highlighted with Source Code Highlighter.


    Но из-за того, что мы резолвим зависимости слоя данных раньше, чем слоя UI плюс необычной, уличной магии контейнера Unity, у нас не возникает с простановкой зависимостей никаких проблем.

    А вот так Толян реализовал IModule у себя в слое:
    public class Module : IModule
    {
      #region IModule Members

      public void Configure(Microsoft.Practices.Unity.IUnityContainer container)
      {
        container.RegisterType(typeof(IDataService), typeof(DataService));
      }

      #endregion
    }


    * This source code was highlighted with Source Code Highlighter.


    3) Пожалуй, основным преимуществом описанного подхода является инкапсуляция. Мы оперируем интерфейсами. Нам не нужно знать, что за ними скрывается. При малейшей неадекватности Толяна или Коляна мы просто выбрасываем все то, что он написал, и пишем свое, пребывая (законно) в полной уверенности за сохранность другого кода. Нужно просто реализовать контракты заменяемого ящика :D

    4) Некто sse спросил совершенно замечательный вопрос в статье про юнити:

    Потому что для таких простых примеров

    var svc = new MyService(10);

    выглядит куда лучше, чем

    var uc = new UnityContainer();
    uc.RegisterType<IService, MyService>();
    uc.RegisterType(new InjectionConstructor(10));
    var svc = uc.Resolve();


    Я думаю, я пояснил его недоразумение.

    5) Вообще говоря, контейнер Unity предназначается для инъектора зависимостей и фабрики объектов нового фреймворка от МС — Unity Application Block. И вроде как бы он нам говорит, что его лучше не использовать вне стен фреймворка, но возможности его применения действительно очень широки.

    6) Указанные в коде формочки и пользовательские контролы нужно зарефакторить через шаблон MVP. Но это на любителя:)

    Спасибо за внимание:)

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 17

      0
      а чего под замком?
        0
        Устал, сори. Открыл всем. Но теперь уже в ленту не попадет, да?
          0
          попадет, если наберет +7
        +1
        А давайте устроим конкурс «Кто доходчевее сможет объяснить, что такое Unity, DI, IoC итп...» :o)
          +1
          Да вы знаете, здесь дело не в тех терминах, которые вы написали. Здесь дело в практической применимости. Все вышесказанное можно написать без Unity, на ServiceContainer например. Просто, кода будет немножко больше.
            0
            Некто sse на самом деле давно пользуется Spring.NET и даже (гусары, молчать) его xml-config'ами, так что спрашивал он исключительно для того, чтобы уточнить границы применимости, бо волнует его этот вопрос с практической точки зрения. Layering он тоже успешно применяет и вам рекомендует, хоть и испытывает серьезные трудности с вбиванием это в головы коллегам по цеху.
              0
              Ну и как, понял границы?:)
                0
                В том посте мне не ответили, да еще и минуснули комментарий, хз почему так :) Из практики могу сказать, что для приложения до 5-10 тыс строк (c#) контейнер стоит применять, только если а) рафинированы слои и/или б) разработка предполагает наличие мощного test harness.
                Для более крупного приложения _не использовать_ слои есть чуть менее, чем ритуальное самоубийство, и потому вопрос про контейнер там не ставится вовсе )
                  0
                  Вот бляха, и правда из-за этой галки пост в ленту не попал.

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

                  Вот мы на своем проекте все в жопу запороли, хотя да, «слои» есть:)
                    0
                    Ответ в начале 3й части про Unity вообще-то.
                      0
                      Послал так послал, ага. Попробую повторить. Вопрос был вот какой:

                      [начало вопроса] Если код

                      var svc = new MyService(10);

                      выливается в

                      var uc = new UnityContainer();
                      uc.RegisterType<IService, MyService>();
                      uc.RegisterType(new InjectionConstructor(10));
                      var svc = uc.Resolve();


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

                      var svc10 = new MyService(10);
                      var svc15 = new MyService(15);
                      ? [конец вопроса]

                      Таки нашел статью, несмотря на всё противодействие поиска на Хабре; ответа там нет. Вы можете прямо здесь ответить?
                        +1
                        Цитирую:

                        Ситуация, в которой регистрируется несколько мэппингов одного типа поддается контролю когда нужно передать все зарегистрированные типы сервисов (то есть IService[]). Но что если нужно получить один конкретный сервис из контейнера? Для этого в Unity предусмотрена возможность давать объектам имена. Например, чтобы реализовать аналог вот этого кода
                        var svc10 = new MyService(10);
                        var svc15 = new MyService(15);
                        нужно зарегистрировать как раз “именные” мэппинги, а точнее:
                        var uc = new UnityContainer();
                        // регистрируем с именем
                        uc.RegisterType<IService, MyService>(«ten»,
                        new InjectionConstructor(new InjectionParameter(10)))
                        .RegisterType<IService, MyService>(«fifteen»,
                        new InjectionConstructor(new InjectionParameter(15)));
                        // получаем
                        var _10 = uc.Resolve(«ten»);
                        var _15 = uc.Resolve(«fifteen»);
                        Также, имеется возможность получить все зарегистрированные мэппинги. Делется это с помощью функции ResolveAll():
                        foreach (IService svc in uc.ResolveAll())
                        svc.DoSomething();
                          0
                          Млять, извините, не ту статью смотрел ) Все нашел.

                          Тем не менее:

                          var uc = new UnityContainer();// регистрируем с именем
                          uc.RegisterType<IService, MyService>(«ten», new InjectionConstructor(new InjectionParameter(10)))
                             .RegisterType<IService, MyService>(«fifteen», new InjectionConstructor(new InjectionParameter(15)));

                          // получаем
                          var _10 = uc.Resolve<IService>(«ten»);
                          var _15 = uc.Resolve<IService>(«fifteen»);


                          куда более громоздко, чем:

                          var svc10 = new MyService(10);
                          var svc15 = new MyService(15);
                            +1
                            Зато более гибко.
                  0
                  А вам не кажется что например просачивание того же IUnityContainer как инъецируемого параметра в класс Module это не есть хорошо? У меня например возникают сомнения даже когда я пишу [Dependency] перед свойствами в объектах доменной логики (а писать приходится).
                    0
                    Че-то не ушел мой комент. Отвечу еще раз.

                    1) Строить объекты доменной логики через фабрику-контейнер — практически моветон, месье. Практически — это потому, что оно моветон за исключением случая, когда доменные объекты строятся и наполняются данными через фабрику (как DTO объекты). В этом случае доменные объекты должны иметь публичные интерфейсы и, по всей видимости, должны быть read-only, чтобы можно было защититься от нарушения целостности данных. Еще минус в том, что вы должны явно следить за жизненным циклом создаваемых доменных объектов — удалять их из контейнера и диспоузить по требованию.

                    2) У меня в примере класс-модуль не инъецируется. Вы путаете с composite application library, там бутстраппер действительно делает инъекцию в модуль, а я всего лишь создаю это класс и вызываю его метод.

                    Понимаю ваши опасения в плане явного задания свойств-зависимостей. Для этого их нужно делать пабликами, что не всегда верно с точки зрения инкапсуляции. Делайте микросервисную архитектуру, где сервисы или нужные вам объекты инъектятся через конструктор.

                    Ну и вообще еще совет — используйте контейнер и инъектор как можно реже и только там, где он действительно нужен. Контейнер сам по себе представляет расшаренный ресурс между всеми участками кода, которые с ним работают. Через это нарушается концептуальная целостность и опять же, инкапсуляция, потому что работающие участки кода должны знать, что лежит в контейнере и в каком состоянии он находится, дабы не нарушить свою работоспособность.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое