Pull to refresh

Layers + Unity Container

Programming *
Всем привет! Хочу привести пример 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. Но это на любителя:)

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

Tags: .netunitylayering
Hubs: Programming
Total votes 32: ↑24 and ↓8 +16
Comments 17
Comments Comments 17

Popular right now