Применимость DDD

До сих пор не утихают холивары на тему DDD\rich vs anemic. С одной стороны апологеты DDD (domain driven design, дизайн на основе предметной области) твердят о том как это круто, с другой стороны говоря что оно не везде подходит. На вопрос где же оно подходит обычно затрудняются ответить или отвечают “for compex domain”, причем примеров применения такого встретить непросто.

 

Попробуем разобраться. Если отбросить всю философскую шелуху DDD, то придем к очень простой концепции жирной (rich, насыщенной) модели, описанной Фаулером. С одной стороны Фаулер предлагает поместить логику в классы “сущностей”, соответствующие данным предметной области. С другой стороны он прекрасно понимает что логика будет сложна и надо каким-то образом декомпозировать её. Кроме того есть логика, которая оперирует более чем одной сущностью и поместить её в один из классов сущностей не выгодно. Таким образом создаются классы сервисов, стратегий итп. Их всех объединяет свойство, что они не содержат данные предметной области и для работы обращаются к классам сущностей. По сути вся сложная логика располагается в этих самых сервисах.

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

Тут стоит отступить назад и посмотреть на общую картину. Как выглядит код приложения, использующий жирную модель (на основе модели в статье Фаулера):

class ContractsController
{
    public Result CalculateRecognitionsAction(int contractId)
    {
        var contract = repository.GetById(contractId);
        contract.CalculateRecognitions();
        repository.Save();
    }
}

Contract.CalculateRecognitions свою очередь выглядит как-то так:

public void CalculateRecognitions()
{
    this.Product.CalculateRecognitions(this);
}

А Product.CalculateRecognitions выглядит так:

public void CalculateRecognitions(Contract c)
{
    var recognitions = recognitionService.CalculateRecognitions(this);
    c.SetRecognitions(recognitions);
}

Теперь попробуем выполнить простое преобразование: на верхнем уровне будем вызывать сервис, а не методы сущностей.

class ContractsController
{
    public Result CalculateRecognitionsAction(int contractId)
    {
        var p = repository.GetProductByContractId(contractId);
        var recognitions = recognitionsService.CalculateRecognitions(p);
        repository.SaveRecognitionsForContract(contractId, recognitions);
    }
}

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

Сдвиг предмета моделирования

Если посмотреть какие концептуальные изменения произошли в коде в примере выше, то становится понятно что центральным объектом у нас стал RecognitionsService, а не Contract. То есть вместо модели исходных данных (предметной области) задачи мы начали использовать модель решения этой задачи. Модели исходных данных никуда не делась, просто её роль в решении стала гораздо меньше. Такая модель называется стройной (anemic).

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

Таким образом DDD не подходит для сложных задач.

Лирическое отступление

Если вы думаете что вынося всю логику из класса Product в ProductService\ProductHelper\ProductManager вы получаете модель решения, то вы жестоко ошибаетесь. Фактически это будет DDD, только в худшем его проявлении.

Сложные предметные области

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

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

Но если подойти с точки зрения моделирования решения, то становится понятно что надо проверять правила не при любом изменении свойства, а при попытке записать эти изменения во внешнее хранилище. Для этого можно создавать классы валидаторы, которые будут вызываться в репозитарии (роль которого может играть ORM).

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

Так где же область применимости DDD?

Как не удивительно, но DDD хорошо работает на простых задачах, там где нет необходимости разделять систему на слои, уменьшать связность путем выноса логики в отдельные стратегии итд. В простых задачах модель исходных данных (предметной области) и модель решения почти совпадают.

Например взять блог. Обычный блог как например этот. Моделью предметной области являются: блог, посты, комменты, страницы, виджеты в интерфейсе. Решение включает в себя функции: создать пост, изменить пост, удалить пост, получить список, поменять настройки блога. Все эти функции вполне можно поместить в класс Блога, а умный ORM разберется как потом все изменения положить в БД.

Но почему DDD столь популярен?

Именно потому что DDD хорошо работает на простых примерах. Ведь в книгах и презентациях невозможно привести пример на 20kloc. А на примерах в 100 строк DDD выглядит очевидно и очень привлекательно.

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

Заключение

Не стоит слепо следовать DDD. У догматичного DDD очень узкая область применения. Старайтесь моделировать решение задачи, не зацикливайтесь на моделировании исходных данных. Всегда оценивайте как то или иное решение повлияет на качество кода безотносительно DDD, ООП или других баззвордов.



В поисках неподвижной точки

Наверное все знают что в C# 3.0 лямбда выражения, которые позволяют записывать анонимные функции (то есть функции без имени).
Например:

seq.Select(x => x * x);

Выражение x => x*x является функцией одного аргумента и возвращает значение квадрата числа.

А теперь попробуем  записать функцию факториала:

Func<int, int> f = x => x > 1 ? x * f(x - 1) : 1;

Компилятор C# такое выражение не компилирует. Ругается на неинициализированную переменную f в правой части. Кстати roslyn такое прожевывает нормально. Тем не менее код выше не является выражением, его нельзя передать параметром в функцию.

Попробуем превратить его в выражение.

fact = f => x => x > 1 ? x * f(x - 1) : 1;

Тип выражения справа получится Func<Func<int,int>, Func<int,int>>, параметром передается рекурсивный вызов, чтобы его сформировать надо снова подставить рекурсивный вызов в функцию.  Получится что-то вроде бесконечного вызова
fact(fact(fact(fact(…. но реально число вызовов конечно.

Немного теории

Для функций f которая принимает аргумент и возвращают значения из одного и того же множества (на C# это записывается как Func<T,T>, а в математике T –> T) может существовать “неподвижная точка” x, для которой f(x) = x. Чтобы находить неподвижные точки можно построить комбинатор g:(T->T)->T, такой что g(f) = x и f(x) = x. Функция g называется комбинатором неподвижной точки.

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

Краткая формула для рекурсивного комбинатора неподвижной точки Y выглядит как Y(g) = g(Y(g)). Если выполнить подстановку то получится g(g(Y(g))), выполняя подстановку бесконечное число раз получим как раз то что нам нужно для факториала.

Вернемся к практике

Попробуем написать на C#

static T Y<T>(Func<T, T> f)
{
    return f(Y(f));
}

Но язык C# использует энергичные вычисления и Y-комбинатор сразу попытается посчитать бесконечную рекурсию. Что приведет к StackOverflowException.

Ленивость вычислений как всегда вводится через лямбды.

static Func<T, T> Y<T>(Func<Func<T, T>, Func<T, T>> f)
{
    return x => f(Y(f))(x);
}

После этого вполне можно написать следующий код:

var fact = Y<int>(f => x => x > 1 ? x * f(x - 1) : 1);

Или например

seq.Select(Y<int>(f => x => x > 1 ? x * f(x - 1) : 1));

Таким образом получили возможность записывать анонимную рекурсию в виде выражения.

Заключение

Знание фундаментальной теории очень помогает писать программы и зачастую дает возможность улучшить их крайне неожиданными способами. Изучение таких тем никогда не будет лишним багажом.



Октябрьская встреча Russian SharePoint User Group

Ссылка на анонс на сайте RUSUG: http://rusug.net/News/Lists/Posts/Post.aspx?ID=64

Один из докладов на встрече, про использование поиска в приложениях SharePoint, буду читать я. Чтобы больше удовлетворить интерес и потребности аудитории предлагаю выбрать интересующие темы из списка ниже:

  1. Настройка поиска SharePoint Server
  2. Out-of-box возможности поиска
  3. Кастомизация out-of-box функциональности
  4. Использование серверного API поиска
  5. Использование клиентского API поиска в sandboxed решениях
  6. Архитектура поиска SharePoint Server

Все темы в пределах одного доклада не смогу рассказать, но 3-4 из них вполне могу пройти достаточно подробно.

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



Почему вам нужен SharePoint

Последнее время на различных конференциях я слышу один и тот же вопрос:

А зачем мне нужен SharePoint?

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

Ниже “краткий” ответ, может быть вы найдете что-нибудь для себя.

  • Если вы руководитель или ИТ-директор компании более 5 человек и
    • Используете  MS Office
    • Отправляете документы по электронной почте
    • Использует расшаренные папки для хранения документов в электронном виде
    • У вас есть процессы согласования и утверждения документов
    • Храните данные в Excel или Access
    • Хотите создать базу знаний
    • Хотите развернуть helpdesk
    • Пользуетесь средствами средствами Microsoft BI
    • Хотите отображать данные из разных источников в одном месте
    • Хотите развернуть корпоративный портал для сотрудников

    если одно из вышеперечисленного верно, то вам однозначно нужен SharePoint. Он поможет вам создать единое хранилище документов и табличных данных с богатыми возможностями отображения и поиска. SharePoint позволяет создавать решения без среды разработки и написания кода. Продвинутые пользователи самостоятельно смогут создавать и улучшать решения в SharePoint.

  • Если вы менеджер проектов, то вам должны быть знакомы продукты MS Project и Project Server. Последний является надстройкой над SharePoint.
    Но даже без Projet Server вы можете:
    • Создавать отдельные сайты для проектов несколькими кликами мыши, где можно будет размещать и согласовывать документы
    • Хранить и отображать на портале списки задач из Microsoft Project
    • Отслеживать риски и проблемы
    • При необходимости вывести создать на портале SharePoint интерфейс к другим системам управления проектами
    • Получать сводку по вашим проектами
    • Получать отчеты и KPI на портале

  • Если вы архитектор или ведущий разработчик и разрабатываете корпоративный софт, то вам нужен SharePoint потому что:
    • он включает в себя возможности управления документами
    • он имеет надежную систему разграничения доступа
    • он интегрируется с MS Office
    • он позволяет искать по всему содержимому
    • он поддерживает длительные рабочие процессы, которые могут продолжаться больше чем время непрерывной работы серверов
    • он имеет модульный пользовательский интерфейс
    • он позволяет интегрироваться с другими системами
    • он поддерживает масштабируемость всех своих компонент
    • он почти весь функционал SharePoint поддается расширению и кастомизации
    • содержит систему установки и удаления приложений, гораздо проще, чем написание инсталляторов вручную

  • Если вы IT-специалист, то вам обязательно нужен SharePoint. Он вам позволит:
    • Собирать в одном месте данные из различных систем
    • Отображать таблицы, графики, отчеты, KPI на портале
    • Автоматизировать процессы процессы организации с помощью простых инструментов
    • Размещать веб-контент не имея навыков веб-разработки
    • Управлять множеством сервисов со сложной топологией с помощью простого графического интерфейса
    • Заскриптовать любые действия с помощью PowerShell

  • Если вы рядовой .NET разработчик, то вы сможете в SharePoint:
    • Применить уже имеющиеся навыки
      • для разработки интерфейса
      • для создания решений по интеграции с другими системами
      • для создания рабочих процессов
    • Изучив платформу более детально вы сможете создавать любые решения и превратитесь из рядового разработчика в высокооплачиваемого специалиста :)

  • Если вы веб-разработчик, то ваши навыки будут очень востребованы в среде SharePoint
    • для брендинга портала, это сейчас очень востребованная тема
    • для разработки макетов веб-страниц для размещения контента
      (html + css + js)
    • для создания представлений данных и результатов поиска
      (xslt + html + css + js)
    • для приложений на javascript или silverlight, большая часть функциональности SharePoint доступна на клиентской стороне

 

Несмотря на богатые возможности примеров успешных внедрений не так много, как хотелось бы. Это связано с тем что платформа SharePoint сложна, а специалистов не хватает. По большей части не хватает именно разработчиков, которые хорошо владеют функционалом и могут собрать из него решение.

Если у вас будут возникать вопросы по SharePoint , то присоединяйтесь с сообществу http://area51.stackexchange.com/proposals/35899/sharepoint-in-russian, поддержите его развитие и вы сможете получать много полезной информации.



Решение задач. Задача таймера, совсем конец.

Посты в этой серии:

  1. Список задач для проверки навыков.
  2. Создание задачи таймера.
  3. Использование подходящих классов.
  4. Передача команд задаче таймера.
  5. Взаимодействие веб-фронтэнда с задачами таймера (этот пост).

В прошлом посте был показан способ создать worker, который будет получать сообщения от frontend и обрабатывать их.

Теперь необходимо придумать как оправлять эти сообщения. В 2010 есть почти универсальный способ для размещения функционала – Ribbon. Даже если у вас возникает непреодолимое желание сделать ссылку\кнопку\пункт меню в другом месте, то скорее всего для целей usability надо будет повторить его в ribbon.

Для того чтобы добавить кнопку в ribbon надо создать custom action, я для этого использую cks:dev. Руководство по расширению риббона можно найти как на MSDN, так и в блогах глубоко уважаемых людей (надеюсь меня не забанят за такое количество ссылок).

Мой код получился такой:

<CustomAction Id="CleanupLibraryButton"
              Title="Cleanup"
              RegistrationType="List"
              RegistrationId="101"
              Location="CommandUI.Ribbon">
  <CommandUIExtension>
    <CommandUIDefinitions>
      <CommandUIDefinition Location="Ribbon.Library.Settings.Controls._children">
        <Button Id="Ribbon.Library.Settings.Cleanup"
                Command="CleanupLibraryCommand"
                TemplateAlias="o2"
                LabelText="Cleanup"
                Sequence="100"
                Image16by16="/_layouts/images/warning16by16.gif"
                Image32by32="/_layouts/images/CRIT_32.GIF"                   
                />
      </CommandUIDefinition>
    </CommandUIDefinitions>
    <CommandUIHandlers>
      <CommandUIHandler Command="CleanupLibraryCommand" 
                        CommandAction="???"/>
    </CommandUIHandlers>
  </CommandUIExtension>
</CustomAction>

Немного громоздко, но если разобраться в схеме, то довольно очевидно. Кнопка добавляется в последнюю группу кнопок на закладке “Билиотека”.

Теперь самый интересный вопрос, что написать в CommandAction, где я в схеме поставил вопросы.

Самый прямолинейный способ – указать ссылку на application page, который вызовет site.AddWorkItem, но это кардинально противоречит самой идее ribbon.

 

SharePoint 2007 way

В SharePoint 2007 часто применялся delegate control, и действия в интерфейсе выполняли postback, а этот самый контрол обрабатывал форму и производил действия.

Для начала добавлю еще один cutom action с location равным ScriptLink. В таком custom action можно размещать ссылку на javascript или блок кода, который будет выведен на каждой странице.

<CustomAction 
    Location="ScriptLink"
    ScriptSrc="~site/_layouts/CleanupTimerJob/script.js" />

Команда для ribbon будет выглядеть так:

<CommandUIHandler 
    Command="CleanupLibraryCommand" 
    CommandAction="javascript:submitLibraryToCleanup()"/>

В самом же js файле будет простой код:

function submitLibraryToCleanup() {
    var listId = SP.ListOperation.Selection.getSelectedList();
    if (listId) {
        __doPostBack('CleanupPostBackEvent', listId);
    }
}

SP.ListOperation.Selection – класс, позволяющий получать данные о текущем представлении списка. Представлений списка на странице может быть больше одного, а ribbon – один.

Функция __doPostBack создается веб-формами, в первом параметре обычно указывается id элемента управления, которому адресуется postback, а во втором передаются параметры. Но узнать id нашего delegate control (который будет ниже) не представляется возможным, поэтому требуется заранее определенная константа и ручной анализ в теле контрола.

Сам контрол:

[Guid("0a3b8df1-0a43-485d-be83-983c1df8b30d")]
public class PostbackHandler : WebControl
{
    protected override void OnLoad(EventArgs e)
    {
        if (this.Page.Request["__EVENTTARGET"] == Constants.CleanupPostBackEvent)
        {
            var listId = new Guid(this.Page.Request["__EVENTARGUMENT"]);
            var web = SPContext.Current.Web;
                            
            SPSecurity.RunWithElevatedPrivileges(() =>
                {
                    using (var site = new SPSite(SPContext.Current.Site.ID))
                    {
                        site.AddWorkItem(
                            new Guid(), DateTime.UtcNow, Constants.WorkItemType,
                            web.ID, listId, -1,
                            true, new Guid(), web.ID,
                            web.CurrentUser.ID, null, null, new Guid());
                    }
                });
        }
    }
}

Основное внимание стоит уделить функции AddWorkItem. Во-первых её необходимо вызывать с правами администратора семейства сайтов, иначе будет ошибка. Во-вторых при указании времени доставки WorkItem необходимо указывать время в формате UTC. В-третьих, даже если не используются параметры listItemId и  userId, то все равно надо указывать ненулевые значения иначе SharePoint попытается записать в базу null, что приведет к ошибке так как эти поля not null. И в-четвертых лучше всего указывать batchId, такой же как у webId, если нет других соображений. Судя по документации msdn и реализации TimerJobUtility это может сэкономить ресурсы.

Чтобы ваш delegate control заработал необходимо добавить элемент в фичу. Здесь также поможет cks:dev, в нем уже есть шаблон.

<Control Id="AdditionalPageHead" 
         Sequence="1000" 
         ControlAssembly="$SharePoint.Project.AssemblyFullName$" 
         ControlClass="$SharePoint.Type.0a3b8df1-0a43-485d-be83-983c1df8b30d.FullName$" />

Но сразу после развертывания этого элемента контрол не заработает, нужно еще добавить класс в safecontrols в web.config. Ни в коем случае не надо писать модификацию конфига самостоятельно, для этого уже есть инструменты в студии.

image

Ну вот и все…

SharePoint 2010 way

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

Для этого нужно сделать: веб-сервис в sharepoint, который обработает запрос и поменять script.js, чтобы он вызывал этот веб-сервис.

Чтобы сделать веб-сервис снова нужно воспользоваться cks:dev с готовым шаблоном. В готов шаблоне надо поменять в svc файле тип фабрики с MultipleBaseAddressBasicHttpBindingServiceHostFactory на MultipleBaseAddressWebServiceHostFactory чтобы можно было вызывать методы сервиса из js.

Выкинув все лишнее можно получим до безобразия простой сервис:

[Guid("36471285-d168-49ea-b191-6c83cfe1fe3e")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
[ServiceContract]
public class CleanupService
{
    [OperationContract]
    [WebInvoke(
        UriTemplate = "/SubmitLibraryToCleanup({listId})",
        BodyStyle = WebMessageBodyStyle.Bare,
        RequestFormat = WebMessageFormat.Json,
        ResponseFormat = WebMessageFormat.Json)]
    public void SubmitLibraryToCleanup(string listId)
    {
        CleanupUtility.AddCleanupWorkitem(SPContext.Current.Web, new Guid(listId));
    }
}

Как видите, большую часть сервиса занимают атрибуты. Метод вызывается с помощью POST, параметры передаются прямо в строке запроса.

Далее необходимо в script.js файле поправить метод submitLibraryToCleanup чтобы он вызывал веб-сервис.

ВНИМАНИЕ. Приведенный далее код работает, но крайне не рекомендую использовать его в своих решениях. Используйте более человеческие библиотеки, вроде jQuery.

function submitLibraryToCleanup(isOldSchool) {
    var listId = SP.ListOperation.Selection.getSelectedList();
    if (listId) {
        var notification = null;
        var request = new Sys.Net.WebRequest();
        request.set_url(GetWebUrl() + "_vti_bin/CleanupTimerJob/CleanupService.svc/SubmitLibraryToCleanup("+listId+")");
        request.set_httpVerb("POST");
        request.add_completed(function(executor, eventArgs) {
            SP.UI.Notify.removeNotification(notification);
            SP.UI.Notify.addNotification("Done",false);
        });             
        notification = SP.UI.Notify.addNotification("Submitting library to cleanup",true);
        request.invoke();
    }
}

Этот код использует так называемую Microsoft Ajax Library для выполнения запроса на сервер, и методы Client OM SharePoint для отображения оповещений.

Функция GetWebUrl скопирована отсюда.

Заключение

Надеюсь вы все таки дочитали до сюда и узнали что-то новое. Весь код можно найти на spsamples.codeplex.com