12/7/2008 3:19:59 PM

Вообще-то я давненько уже писал:
- “О пользе IEnumerable
- “Еще о коллекциях и многопоточности.

Сегодня встретил вот постинг Эрика Люпперта (человек имеет прямое отношение к созданию C#) относительно того, когда же стоит пользоваться массивами (которые Array).
По его мнению – никогда. Причины перекликаются с теми, о которых писал и я, но почитать интересно.

Ссылка: http://blogs.msdn.com/ericlippert/archive/2008/09/22/arrays-considered-somewhat-harmful.aspx

12/3/2008 12:04:26 PM
yellow_line

Почти год назад я уже писал о том, для чего нужны DTO(и это не только веб-севисов касается, это вплоть до контрактов между бизнес-логикой и UI, если нужно) и какие проблемы решаются с их помощью. Тогда я говорил об этом в связи с тем, что использовать объекты бизнес-модели в качестве этих самых DTO вредно и вообще плохо.
Однако, в последнее время столкнулся с другой крайностью, которую я бы обозвал DTO Driven Development. Ситуация получается тоже странная. Я объясню.

Представьте себе достаточно крупное приложение: различные там слои, подсистемы, WCF-сервисы, UI и т.д. Теперь в общем виде представьте себе физическую структуру этого приложения: всякие там веб-сайты, WCF-сервисы, куча DLL с разными логиками, все как обычно. А среди всей этой толпы DLL есть одна с названием MyCompany.DTO.dll. В этой библиотеке сложены DTO для всего приложения. Ну, там, UserDTO, CompanyDTO, ProductDTO (суффиксы только для примера). И все “слои” и подсистемы этого приложения пользуются этими DTO для того, чтобы обмениваться данными. Делается это для того, чтобы, дескать, не увеличивать сложность приложения, не “плодить” “лишних” классов, и еще куча-кучей благих побуждений.

Вроде бы все прекрасно, а на самом-то деле просто ужас какой-то. На самом-то деле получается не передача необходимых данных от одной подсистемы (или, там, слоя) в другую, а изобретение “велосипеда” в виде “какими бы имеющимися DTO воспользоваться, чтобы передать то, что я хочу”.

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

Если еще не понятно, то мы получаем навязанный нам “третьей” стороной контракт.
По идее мне нужно вернуть имя/фамилию пользователя и его почтовый адрес, а приходится возвращать пару UserDTO+AddressDTO, при этом и тот и другой объект будут содержать либо кучу совершенно ненужной в рамках данного контекста информации (какие-нибудь поля CompanyID, ManagerID, Age, LoginName для пользователя и какие-нибудь PhoneNumber, FaxNumber для адреса), либо же “ненужные” поля будут незаполнены, что вообще бред полнейший, так как получатель имеет на руках непонятно почему “недозаполненный” объект.
Если же нужно в этих данных изменить и отправить назад – то вообще полный кошмар: как заполнять эти объекты, где брать недостающую информацию, что я обязан заполнить, а что – нет?
По сути нам говорят “вы будете укладывать свои данные вот в эту структуру. Ах не лезет?! Нет уж, упихивайте!”

На стороне же клиента, который обращается к заданной подсистеме, вообще “непонятки”: вместо одного простого объекта, просто содержащего необходимую информацию и укладывающегося в его бизнес-модель, ему приходит два каких-то “левых”. Лично я бы не обрадовался на месте программиста, работающего с таким API.

Кроме того – потенциальный “косяк” на будущее. Если нам вдруг срочно потребуется где-то изменить имеющийся DTO, то это немедленно затронет ВСЕ подсистемы, где он был использован. И ВСЕХ клиентов этих подсистем. Даже если мы всегда точно знаем все места использования этого объекта, всех этих клиентов, то, согласитесь, править (да и хотя бы просто заново тестировать) все это дело как-то не очень-то и хочется.

Это все как-то уже и не очень сочетается с посылом “не увеличивать сложность”, не правда ли? Ведь один из главных столпов ООП – инкапсуляция. А что рекомендуется инкапсулировать? Правильно, инкапсулировать нужно изменения. Ну, чтобы можно было что-то где-то в одном месте поменять, а в других местах это и не аукнулось бы никак. Вот тогда и сложности никакой не будет.

Итак, какая ситуация была бы правильной:

  1. Каждая подсистема, имеющая внешний API, имеет собственный, ни от кого не зависящий набор DTO, который по смыслу подходит этому API.
  2. Изменения, вносимые в API подсистемы, затрагивают только этот API и, возможно, его прямых клиентов и НЕ затрагивают никаких других частей приложения. На самом деле, то, что вы передаете из API-функции своей подсистемы – дело только этой самой API-функции и тех, кто именно ее использует. Это и называется “контрактом”.
  3. Ваши подсистемы будут действительно независимы (к чему мы и стремимся).

Как этого добиться:

  1. Никогда не используйте объекты бизнес-модели в качестве контрактов данных (DTO) и наоборот. DTO – это только DTO. Они передают данные – и только, такая у них работа. Это опять сюда, плюс инкапсуляция потенциальных изменений. Плюс, опять же, в OOP укладывается: каждая сущность делает что-то одно.
  2. Пусть каждая подсистема имеет собственный набор DTO, относящийся только к этой подсистеме.
  3. Наплюйте на количество классов. Поверьте, никто вас в количестве классов не ограничивает, можно создать столько, сколько нужно. Честно. Легче поддеживать два или три маленьких специализированных класса, чем поддерживать и всюду притягивать за уши один большой и унивесальный. Даже и чисто по времени легче.
  4. Попробуйте оценить свои DTO именно как средство транспортировки данных между вашим подсистемой и ее непосредственным клиентом. То есть, ваш DTO не должен служить средством связи дяди Васи с дядей Петей, если вы сами не один из них :) Иначе однажды к вам придет Петя и скажет “хочу, чтобы Вася мне еще и сплясал, а твой убогий транспорт этого не позволяет! Меняй!” И будете менять, и менять везде, ибо переводить Васю с Петей на другой, собственный, транспорт может быть уже слишком накладно.
  5. DTO должен пересекать только одну желтую линию границу: границу той подсистемы, в которой он создан. Других границ (между дядей Васей и дядей Петей) он пересекать не должен.

Все гораздо проще, чем вам бы того хотелось :)

3/14/2007 9:27:00 AM

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

Писать этот постинг меня сподвигло то, что сразу с двумя человеками из разных компаний и разных городов недавно разговаривал о том, как (и почему именно так) нужно строить архитектуру при работе с веб-сервисами.
Поскольку сейчас я как раз занят тем, что пишу одновременно и слой веб-сервисов и клиента  к нему (с использованием того самого WPF), то об этом я и расскажу. Заодно прерву "затяжное молчание".

Итак, начнем есть трехслойный пирог (серверная логика, веб-сервисы, клиентская логика) с середины, то есть, со слоя веб-сервисов.

Создавая слой веб-сервисов мы создаем контракт между двумя остальными частями (серверной и клиентской). Сам же слой представляет собой набор доступных операций, этакий API, для слоя клиентской логики и является не более, чем клиентом для слоя логики серверной.
Подчеркну важный момент: в этой схеме "клиент" ничего не обязан знать о том, как там устроена серверная часть, какими данными она оперирует, в каком виде все это хранит и т.д. Клиентский слой логики "мыслит" совсем другими категориями, у него совершенно другие задачи и, быть может, даже другая бизнес-модель.

Отсюда вытекает простая рекомендация: не нужно передавать клиентскому приложению объекты бизнес-модели серверной логики. С точки зрения развития и поддержки приложения, я бы даже сказал "никогда не нужно".

Создавайте отдельные объекты для передачи посредством веб-сервисов. Такие объекты называются "контрактами данных" (Data Contracts).
Ваш слой веб-сервисов будет оперировать объектами бизнес-логики серверной части, но для передачи клиенту будет "транслировать" их и их данные в объекты-контракты данных и лишь затем передавать.

Зачем это нужно? Очень просто. Как я уже сказал, клиентское приложение, пользуясь веб-сервисом Reports, совершенно не заинтересовано "знать" бизнес-модель серверной части. И для него бизнес-сущность "User" вовсе не является набором "запись в таблицу Users + ссылка в таблицу Occupations + ссылка в таблицу Departments + 2 ссылки в таблицу Addresses по их IDшникам". Ему и надо-то воспользоваться сервисом Reports всего лишь для того, чтобы этот самый репорт отобразить, и для него User - это "монолитный" объект со всеми необходимыми адресами, должностями и т.д. А вот информация о паролях, логинах, ролях и т.д. ему совершенно не нужна, поэтому и передавать ее в сервисе Reports не за чем.
Поэтому слой веб-сервисов "берет" все эти серверные бизнес-объекты (адреса, должности), трансформирует эти данные в отдельный объект, определенный контрактом, и уже его передает клиенту.
Кроме этого, контракты данных получаются достаточно простыми и четкими, что позволяет легко использовать сервисы даже из не-.net приложений, если придется. Да и в любом случае удобнее, чем передавать сложные объекты бизнес-модели, часто имеющие свою иерархию, избыточную с т.з. веб-метода информацию и т.д.
Все счастливы.

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

Счастливы, в конце концов, те, кому все это потом поддерживать. Ибо в данном случае слои получаются слабо связанными, легко понимаемыми и легко модифицируемыми (в том числе и независимо друг от друга).

Кстати, еще о клиентской части.
Здесь я тоже предложил бы не использовать в приложении объекты (контракты данных), полученные от веб-сервиса.
Вместо этого часто лучше трансформировать данные из этих контрактов в бизнес-объекты клиентской модели.
Плюса здесь два, и оба существенные.

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

Второй: Вы сами контролируете объекты, с которыми вы работаете.
После получения данных от веб-сервиса и трансформации их в собственные объекты клиентской бизнес-модели, программисты оной могут творить с/из этих объектов свободно что угодно.
Нужно сделать Undo? Пожалуйста, добавили необходимые методы. Нужно, чтобы объект оповещал об изменении его свойств? Пожалуйста, реализовали INotifyPropertyChanged и получили беспроблемный двусторонний байндинг в UI. Нужна собственная иерархия? Опять пожалуйста, хозяин-барин.

Итак, коротко обобщим основные рекомендации:

  1. Создавайте на стороне серверной логики полноценные объекты бизнес-модели и работайте с ними (а не с именованными наборами данных, за поведение которых отвечают сторонние классы, которые хрен найдешь, если не знаешь).
  2. Создавайте контракты сервисов (API, наборы операций, веб-методы), которые передают и принимают контракты данных (простые типы-носители необходимых для операции данных). Никогда не используйте бизнес-объекты серверной логики в качестве контрактов данных.
  3. Не работайте напрямую с объектами-контрактами данных в клиентском приложении. Создайте полноценные объекты бизнес-модели клиентской части, и заполняйте их данными, полученными от веб-сервисов. Так вы одновременно получите возможность развивать и управлять этой моделью и избавитесь от необходимости реализовывать странные сторонние классы, отвечающие за манипуляцию данными (те самые, которые потом хрен найдешь и проконтроллируешь).

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

Пример (упрощенный) веб-метода, возвращающего данные о производителе товара:

public DataTypes.Brand GetByID(int id)
{
    BusinessLogic.ManufacturerGetById action = new BusinessLogic.ManufacturerGetById();
    BusinessEntities.Manufacturer m = action.Execute(id);
 
    return Translators.BrandToManufacturerTranslator.Translate(m);
}

Здесь ManufacturerGetById - это класс-action, умеющий вернуть сложный объект бизнес-модели серверной части по его идентификатору. Далее он трансформируется в простой объект контракта данных Brand, который и будет предоставлен клиенту.

Клиент же, получив эти данные, таким же способом заполняет ими свой объект
Brand : Company, INotifyPropertyChanged, ISearcheable
с которым и продолжает работать дальше.

2/17/2007 10:30:00 PM

M-V-P (Model-View-Presenter) - это разновидность шаблона M-V-C (Model-View-Controller) с некоторыми существенными отличиями.
Я не буду описывать отличия, если кто-то интересуется, то суть этих отличий кратко описана у Дэррона Шэлла здесь и детально у Мартина Фаулера здесь.

Так же я не буду останавливаться на той части триады, которая называется Model. Полагаю, что с ней и так все понятно, так как эта часть в принципе не осведомлена о двух других, может существовать и без них. К тому же отличий от M-V-C в данном случае нет.

Я остановлюсь на реализации View и Presenter.

Справедливости ради нужно отметить, что Composite UI Application Block предлагает именно M-V-C, но при работе со Smart Client Software Factory вы будете общаться именно с M-V-P. Класс презентера (как и некоторые другие) не являются частью CAB, но будут созданы фабрикой в общем модуле вашего приложения.
В Composite Web Application Block презентер уже имеется изначально, поэтому я думаю, что соответствующая реализация будет включена и в следующий релиз CAB.

M-V-P в данном случае предполагает наличие View - пользовательского интерфейса и наличие Presenter'а, который содержит логику работы пользователя над моделью.
При этом View "знает" о наличии Presenter'а "напрямую" в то время, как Presenter "знает" о наличии View опосредованно - через интерфейс.

То есть, мы, к примеру, имеем:

  1. Интерфейс ICustomerView, в котором определяются свойства и методы, отвечающие за поведение View;
  2. Класс CustomerView, который является реализацией ICustomerView и занимается непосредственно представлением данных. Кроме этого экземпляр CustomerView содержит ссылку на соответствующий Presenter, с которым и общается в рамках своей работы;
  3. Класс CustomerViewPresenter, который ничего не знает о CustomerView. Вместо этого он знает об интерфейсе ICustomerView, ссылку на который содержит.

В коде это выглядит так:

 

//Interface
public interface ICustomerView
{
    void ShowCustomerInfo(Customer customer);
}
 
//Presenter
public class CustomerViewPresenter : Presenter<ICustomerView>
{
    public override void OnViewReady()
    {
        base.OnViewReady();
        //Get the customer somewhere
        .....
        View.ShowCustomerInfo(customer);
    }
}
 
//View
public partial class CustomerView : UserControl, ICustomerView
{
    public CustomerView()
    {
        InitializeComponent();
    }
 
    [CreateNew]
    public CustomerViewPresenter Presenter
    {
        set
        {
            _presenter = value;
            _presenter.View = this;
        }
    }
 
    protected override void OnLoad(EventArgs e)
    {
        _presenter.OnViewReady();
    }
 
    public void ShowCustomerInfo(Customer customer)
    {
        if (this.InvokeRequired)
        {
            this.BeginInvoke(new MethodInvoker(delegate() { ShowCustomerInfo(customer); }));
            return;
        }
 
        .....
    }
}

 

Отсюда видно, что при создании экземпляря CustomerView ObjectBuilder создает экземплярCustomerViewPresenter, ссылка на который теперь доступна внутри CustomerView. Сам же презентер получает ссылку на экземпляр, реализующий известный ему интерфейс, поэтому ему все равно, какой тип пользовательского интерфейса скрывается за ICustomerView.

Важно отметить то, что в случае M-V-P слой View сам обрабатывает пользовательский ввод и "уведомляет" презентер о необходимости получить/обработать данные модели, а так же о своем состоянии. В указанном примере View уведомляет Presenter о том, что он загружен и готов к работе (событие OnLoad в CustomerView).

Презентер же, выполнив какие-то действия (например, получив от ICustomerService экземпляр класса Customer), сообщает View о необходимости отобразить информацию об этом кастомере.

Здесь очень важным является тот момент, что View никакой информацией о логике Presenter'а  не располагает, поэтому реализация метода ShowCustomerInfo делается потокобезопасной. На самом деле, Presenter может начать обрабатывать данные асинхронно для того, чтобы пользовательский интерфейс не блокировался на время работы. Пока ведется обработка данных, Presenter может "просить" View информировать пользователя о прогрессе и лишь после окончания обработки вызвать метод ShowCustomerInfo. Поэтому View должен быть готов к тому, что практически любой вызов от презентера придет из контекста другого потока, реализуя методы интерфейса ICustomerView потокобезопасными.

На самом деле, это очень простая схема и именно так и нужно делать всегда: View лишь уведомляет Presenter о том, что пользователь захотел сделать что-то (например, вызывая _presenter.ApproveCustomer). View не в праве расчитывать на то, что Presenter ответит немедленно, вернув какой-то результат. Строго говоря, результаты вообще не в компетенции View, View не в праве решать, что делать с результатом. Вместо этого обработкой данных и получением результата занимается Presenter и после того, как операция выполнена, просто заставляет View сделать что-то другое.

Такой подход позволяет нам получить следующие плюсы:

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

Ко всему этому писать (и поддерживать!) код легко и приятно (а то иной раз просто хочется запустить мышкой в монитор, разбирая очередные 350 спагетти-строк в _button1_Click).

Пользуйтесь шаблонами проектирования, в них мудрость :) Возьмите "полуфабрикаты" в виде Composite Application Blocks, возьмите Software Factories - и они помогут вам придерживаться "правильного" пути.
И будет вам счастье.

Tags: , ,

Powered by BlogEngine.NET 1.6.0.0

About the author

Alexey Raga Alexey Raga
.NET software developer.

E-mail me Send mail

Twitter


Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2010

Sign in