Главная Контакты Архив

DDD, MVP, IoC – увеличиваем выгоду и уменьшаем недостатки

Автор Дмитрий Кулагин 21 марта 2011 18:38

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

Для начала совсем коротко опишу свое понимание этих методик:

  1. DDD (Domain-driven design) — основная суть методики в том, что разработка начинается не со структуры данных, как мы все привыкли, а с того, что нужно пользователям, т.е. с User Story, Use Case или интерфейса. Это хорошо помогает в случае, если у нас уже есть понимание того, что нужно, но пока еще непонятно, как это делать. И пока мы разбираемся, как нам это сделать, и меняем туда-сюда требования и логику, разработчики уже занимаются проектом, что ускоряет его выход. Это плюс. Минус в том, что если в середине разработки мы понимаем, что сделать это нереально, то вся проделанная работа насмарку и сделать требуется значительно больше, чем только спроектировать. Второй минус в том, что структура в данном подходе получается многослойная и тяжелая.
  2. MVP (Model-View-Presenter) — достаточно распространенный подход, который неосознанно используют многие. Его основная идея в том, чтобы разделить данные (Model), бизнес-логику (Presenter) и интерфейс (View), а также наладить их взаимодействие от хаотичного до четкого. View работает только с Presenter и ничего не знает о Model, Presenter знает структуру Model и использует ее, а Model лежит и никого не трогает. Есть вариант, где Model является активной сущностью и сама мониторит свое изменение, но это частности реализации и в нашем случае не так важны.
  3. IoC (Inversion of control) — я бы описал данный подход как смену статической связки методов на динамическую. Основное здесь — использование легких интерфейсов и динамический выбор реализаций этих интерфейсов вместо статической связки. Как пример могу привести 2 фразы: «Я знаю, что Петя почтальон и он приносит мне письма, посылки и бандероли» и «Работник почты оказывает мне почтовые услуги». Вторая более абстрактна и гибка, лучше тестируется, легче модифицируется. Например, меняем «почтовые» на «интим» и получаем совсем другой процесс. Из минусов то, что если начальные интерфейсы недостаточно развиты, то их модификация на уже построенной системе существенно сложнее обычного подхода.

Теперь возьмем для примера задачу и попробуем применить все 3 подхода одновременно. Задача следующая: Сделать некое приложение-сервис, которое будет за деньги выполнять некие задачи пользователя, а также будет показывать статистику выполненных работ и хранить историю операций. Задачи могут быть как разовые, так и периодические.

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

В соответствии с DDD мы начнем с разработки P части из MVP, но так как у нас еще есть IoC, то мы начнем с интерфейса этой части без конечной реализации. Итак, пользователю нужно выполнять некоторые задачи за деньги:

public interface IWork {
        IPay DoWork(ITask workToDo);
        string ToString();
    }

Собственно это все, осталось описать ITask и IPay. Давайте сделаем это:

public interface ITask {
        void SetAlgo(IAlgo algo);
        void SetParam(IParam param);
        void Execute();
        string GetResult();
    }

    public interface IPay {
        string ToString();
        decimal ToDecimal();
    }

    public interface IParam {
        void Set(object value);
        object Get();
        string ToString();
    }

    public interface IAlgo {
        string ToString();
    }

Нам понадобилось еще 2 дополнительных интерфейса, чтобы описать работу — это выполняемый алгоритм и параметры. Для всех интерфейсов мы описали ToString() - это необходимо для представления статистики выполненных работ в человеческом виде. Для IWork этот метод может возвращать информацию о том, когда начались и закончились работы по задаче. Для ITask сделан метод GetResult, который по сути то же, но название другое для понятности того, что он возвращает результат выполнения. Для платы у нас есть перевод в человеческий формат и в decimal. Второй будет нужен для биллинга, и вообще не факт, что нам его хватит. Его точно хватит если то, чем надо платить, можно привести в исчислимую форму, а если платой за выполнение задачи будет, например, душа, то последующая реализация должна будет выкинуть нам исключение, но мы про это пока думать не будем.

Осталось написать сервис статистики/истории и сервис, который будет выполнять наши IWork.

public interface ITaskHistory {
        void SaveTask(ITask task, IPay pay, IWork work);
    }

    public interface ITaskExecutor {
        void Execute(ITask work);
    }

И вот наши интерфейсы для последней части задачи.
Коротко резюмируем, что получилось:

  • IWork — работа или даже работник. Работу можно сделать за вознаграждение, и логика расчета вознаграждения хранится именно здесь, потому что более подходящего места нет. Можно было бы ввести классы «Работодатель» или «Бухгалтерия», но они тут будут мешать.
  • ITask — это вполне конкретная задача, которую надо выполнить. Например сходить в магазин за молоком.
  • IPay — плата за выполнение работы.
  • IParam — параметр задачи. Из примера выше это молоко (на самом деле там 2 параметра: магазин и молоко, но эту модификацию рассмотрим позднее).
  • IAlgo — некое описание алгоритма работы.
  • ITaskHistory — хранилище выполненных работ.
  • ITaskExecutor — исполнитель задач.


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

public class ListTaskHistory : ITaskHistory {
        private struct HistoryElem {
            internal ITask task;
            internal IPay pay;
            internal IWork work;
        } 

        List history = new List();

        public void SaveTask(ITask task, IPay pay, IWork work) {
            HistoryElem historyElem = new HistoryElem { task = task, pay = pay, work = work };
            history.Add(historyElem);
        }
    }

    public class SimpleTaskExecutor : ITaskExecutor {
        public void Execute(ITask task) {
            IWork work = GetWorkForTask(task);
            IPay pay = work.DoWork(task);
        }

        private IWork GetWorkForTask(ITask work) {
            return new SimpleWork();
        }
    }

    public class SimpleWork:IWork {
        public IPay DoWork(ITask workToDo) {
            workToDo.Execute();
            return new SimplePay();
        }
    }

    public class SimplePay:IPay {
        public decimal ToDecimal() {
            return 1;
        }

        public new string ToString() {
            return "Стоимость 1 у.е.";
        }
    }

    public class SQRTask:ITask {
        private IAlgo algorithm;
        private IParam param;
        private double result;

        public void SetAlgo(IAlgo algo){
            algorithm = algo;
        }

        public void SetParam(IParam setParam) {
            param = setParam;
        }

        public void Execute() {
            double paramExec;
            if ((algorithm is SQRAlgo) && (double.TryParse(param.Get() as string, out paramExec))) {
                result = Math.Pow(paramExec, 2);
                return;
            }
            throw new AlgoException();
        }

        public string GetResult() {
            return result.ToString();
        }
    }

    public class AlgoException : Exception{}

Собственно задача сделана, осталось дореализовать методы ToString() у части классов и прикрутить к ним интерфейс. При использовании MVC от Microsoft достаточно наследовать контроллеры, создаваемые VS, от нужных нам интерфейсов и будет счастье. Можно добавить ко всему ServiceLocator из все того же IoC, вынести его настройки в конфиг и, добавив нужную реализацию, мы можем распределить приложение по серверам. Причем можно делать это не сразу, и дальнейших сложностей это создать не должно.

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

Реализация новых алгоритмов также не составляет больших проблем — пара новых классов и в путь.
Некоторые читатели могут подумать, что в наши интерфейсы закрался баг, заключающийся в том, что у нас принимается только 1 параметр, может возникнуть желание расширить интерфейс, впихнув туда void SetParam(IParam[] param). Делать это нежелательно, потому что это нагрузит интерфейс без надобности. Нам ведь никто не запрещает вызвать метод void SetParam(IParam param) столько раз, сколько нам хочется, и только от реализации наследника зависит количество параметров и принцип их обработки. К тому же это упрощает обработку алгоритмов, которые могут принимать на вход разное число параметров или могут использовать значения параметра по умолчанию, если они не заданы. Как правило, такие алгоритмы при обычной реализации обрастают 20-30 конструкторами с разными параметрами, и чтобы разобраться, какой из конструкторов мне нужен, уходит прилично времени. Например, фраза «Сходи в магазин» с одним параметром должна выбросить ошибку. Возникает резонный вопрос «А зачем?». С другой стороны фраза «Сходи за молоком» вполне логична, потому что по умолчанию можно использовать магазин. Это внутренняя логика алгоритма и незачем всем рассказывать о том, что этот параметр можно не говорить.

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

Из плюсов. Хорошая устойчивость к смене требований/алгоритмов на этапе разработки. Хорошая интерпретация объектов кода с объектами реального мира, что улучшает взаимодействие с другими членами команды (заказчиками, аналитиками и т.д.). Возможность работы в процессе разработки требований (почти то же, что и первое, но я все же выделю отдельно). Плюсы от MVP смотри в книгах по MVP.

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

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

Комментарии (2) -

Oleg
Oleg
23 марта 2011 12:48 #

Хотелось бы увидеть примеры библиотек для реализации тех или иных методик

Ответить

Сергей Шебанин
Сергей Шебанин
24 марта 2011 13:10 #

Здесь же дело не в библиотеках. Это определенные принципы объектно-ориентированного программирования. Ты имеешь в виду библиотеки, которые применяют эти принципы?

Ответить

Добавить комментарий



biuquote
  • Комментарий
  • Предпросмотр
Loading