Компания
58,05
рейтинг
30 июля 2015 в 14:02

Разработка → Пишем maintainable код

У нас сотни программных проектов на поддержке, некоторые из них поддерживаются нами почти десять лет. Нетрудно догадаться, что понятие maintainable кода (переведу это понятие как код, легкий в поддержке) является у нас одним из основных. По счастливому стечению обстоятельств легкий в поддержке код также является и легким для (unit-)тестирования, легким для освоения новыми членами команды и т.д. Скорее всего, это связано с тем, что для создания maintainable кода приходится озаботиться хорошей архитектурой проекта и завести несколько хороших привычек.
В этой статье и поговорим о таких привычках, благодаря которым часто хорошая архитектура получается сама собой. Постараюсь также иллюстрировать все хорошими примерами.


Но начну я с того, что расскажу чем мне не нравятся некоторые аббревиатуры на букву M: MVC, MVP, MVVM и другие. У неофита, читающего свои первые книги и статьи о том, как проектировать приложения, эти аббревиатуры всплывают одними из первых. У него создаётся ложное впечатление, что программа — это некая триада состоящая, например, из модели, контроллера и представления, и, что самое опасное, члены этой триады равны по важности! Многие из этих статей и видео-уроков подкрепляют эту опасную ложь примерами приложений из серии: «ну пусть за представление у нас отвечает такой-то шаблонизатор, контроллеры — это контроллеры нашего фреймворка, а модель — это какой-нибудь ActiveRecord ORM». После такого могут понадобиться годы Толстых Тупых Уродливых Контроллеров, чтобы неофит осознал, что что-то он делает не так. Что Модель в этих триадах занимает главное место и чем сложнее приложение, тем более это выражено.

Главный принцип деления программы на части высокого уровня не меняется уже несколько десятилетий: Data access layer, Business (logic) layer и Presentation layer. Причем, очевидно, что слой отражающий суть и всю ценность нашего приложения это Business layer, а DAL и PL являются некого рода обслуживающими слоями. А все эти аббревиатуры на букву M представляют собой архитектурные паттерны, описывающие как организовать Presentation layer в программах, не более того.

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

И сразу же, без предисловий, вторая хорошая привычка: SOLID. Не знаю как остальное в SOLID, но важность Single responsibility принципа трудно переоценить. Я бы назвал его необходимым и достаточным условием хорошего кода. В любом плохом коде всегда найдется класс, который делает больше одного дела (Form1.cs или index.php, размером в тысячи строк наверно каждый видел, а то и делал). Остальные принципы из SOLID не так важны для меня и, кстати, недавно на хабре была хорошая статья на эту тему, куда вас и отсылаю. Я во многом солидарен с написанным там и благодарен автору этой статьи, что мне не придется объяснять это самому.
Принцип Single responsibility (дальше просто принцип S) буквально заставляет писать качественный код и многие, очень многие методики, являются просто инструментами для написания кода, удовлетворяющего данному принципу.
И примером является то, что я выделю в третью хорошую привычку: Dependency Injection. Я слабо представляю себе более-менее большой проект, исповедующий принцип S, без DI. Я обещал приводить примеры и здесь хорошее место, чтобы начать это делать. Обычный класс, представляющий собой логику работы с заказами какого-нибудь интернет-магазина.

public class OrderService
{
    public void Create(...)
    {
        // Создаем заказ
        
        // Хотим отправить email клиенту с деталями заказа
        var smtp = new SMTP();
        // Настраиваем smtp.Host, UserName, Password и другие параметры
        smtp.Send();
    }
}

Всем ценителям хорошего кода, которые воротят носы, увидев такое, могу сказать, что обычно все хуже — OrderController который делает всё это и многое другое. Но и этот код, мягко говоря, не идеален. Помимо сложной работы по обработке заказов OrderService вынужден разбираться в тонкостях работы SMTP! Да и к копипасту кода это легко приводит. Поэтому проведем небольшой рефакторинг:

public class OrderService
{
    private SmtpMailer mailer;
    public OrderService()
    {
        this.mailer = new SmtpMailer();
    }

    // тут где-то в коде мы будем отправлять письма, уведомляющие пользователей о деталях сделанных ими заказов
}

public class SmtpMailer
{
    public void Send(string to, string subject, string body)
    {
        // Именно тут будет работа с SMTP
    }
}

Вообще, благодаря тому же принципу S, мы понимаем, что OrderService ну никак не должен заниматься отправкой писем, но давайте немного потерпим и «поймем» это позднее. В данный момент мы видим другое: помимо очень сложной и ответственной работы с заказами, OrderService точно знает, что письма, которые он отправит, по крайней мере, будут отправлены неким SmtpMailer. Это знание совершенно ему ненужно. Вместо этого хорошим тоном является создание интерфейса IMailer

public interface IMailer
{
    void Send(string to, string subject, string body);
}

SmtpMailer будет этот интерфейс реализовывать. А также в нашем приложении будет использоваться какой-нибудь IoC-контейнер, где мы пропишем, что IMailer у нас реализовывается классом SmtpMailer. А OrderService изменится так:

public class OrderService
{
    private IMailer mailer;
    public OrderService(IMailer mailer)
    {
        this.mailer = mailer;
    }

    // тут где-то в коде мы будем отправлять письма, уведомляющие пользователей о деталях сделанных ими заказов
}

Что изменилось? Жесткая связь между OrderService и SmtpMailer превратилась в мягкую. Связь между ними идет через интерфейс IMailer и OrderService теперь просто отправляет письма через свой mailer, абсолютно не интересуясь, кто он, собственно, такой. Главное, что он умеет отправлять письма, а как конкретно он это делает OrderService не важно.
Самое сложное здесь — объяснить, зачем это нужно. Многие не видят ничего страшного в жестких связях, особенно если в приложении нет юнит-тестов. Можно высказать много аргументов: такой код легко тестировать (а у нас нет юнит-тестов!), легко поддерживать — все зависимости регулируются в конфигурации IoC-контейнера и поменять SmtpMailer на NewTechnologyMailer во всем приложении — дело пары строк (да у нас ничего не поменяется!). Но все эти аргументы слабо действуют пока человек не прочувствует это сам. Плохая архитектура ничем себя не выдает когда проект существует в тепличных условиях. Но как только идет поток постоянно меняющихся требований, или например, высокая нагрузка и необходимо какие-то части проекта физически отделить друг от друга, тогда хорошая архитектура позволит гораздо дешевле пережить все эти изменения. Но обьяснить это человеку, который видел только проекты в тепличных условиях — сложно. Посему, я оставлю это бесполезное занятие. Тем более, впереди у нас важное дело — Великое Осознание того, что OrderService не должен посылать письма!

Я только отмечу четвертую хорошую привычку: все зависимости в коде должны быть мягкими (неявными, слабыми).
Посмотрим еще раз на OrderService. Заметим, что он обзавелся IOrderRepository, который теперь занимается хранением заказов. СУБД он для этого использует или файл на дискете, случайно оставленной в 1999 году на сервере, OrderService не интересно. Сам же сервис занимается строго бизнес-логикой заказов. Заметим также, что сам OrderService теперь реализует интерфейс IOrderService (с методами Create и другими) и все остальное приложение использует именно этот интерфейс, чтобы связь с данным классом была мягкой.

public sealed class OrderService: IOrderService
{
    private IOrderRepository repository;
    private IMailer mailer;
    public OrderService(IOrderRepository repository, IMailer mailer)
    {
        this.repository = repository;
        this.mailer = mailer;
    }

    public void Create(...)
    {
        var order = new Order();
        // заполняем объект заказа, применяя всю мощь нашей бизнес-логики. Скидки, акции и т.д.
        this.repository.Save(order);

        this.mailer.Send(<orders user email>, <subject>, <body with order details>);
    }
}

Обычная логика подсказывает нам, что без строчки с mailer код метода Create выглядит полным и законченным действием. И даже глянув с точки зрения бизнес-логики: в workflow создания заказа в интернет-магазине отправка письма покупателю является второстепенным действием. В довесок, любой знакомый с хайлоадом человек, скажет, что поток, обрабатывающий веб-запрос не должен отправлять письма. Ну и простой пример из maintenance. Новое бизнес-требование: в настройках пользователя появляется галочка — слать ему письма с деталями заказа или нет. Чтобы узнать значение этой настройки для данного юзера, нашему сервису заказов нужна еще одна зависимость от, например, IUserParametersService. А кроме того, ведь приложение у нас многоязыковое, от ITranslator (чтобы корректно сформировать заголовок письма на нужном языке). Несколько таких лишних действий и у нас огромное количество зависимостей и конструктор, не умещающийся на экране. А это является запахом неправильной архитектуры. В этой хорошей статье это красиво названо Injection happy. Когда IoC-контейнеры позволяют легко добавлять зависимости, мы можем легко собирать кучу их в одном классе. А это является верным признаком того, что сделали что-то не так. Скорее всего, нарушили принцип S. Однако, как в данном случае красиво отделить отправку письма от OrderService класса не всегда понятно. Мне нравится решение с помощью Event Driven архитектуры. Пример:

namespace <base namespace>.Events
{
[Serializable]
public class OrderCreated
{
    private readonly Order order;

    public OrderCreated(Order order)
    {
        this.order = order;
    }

    // В C# более естественно свойство с {get; private set}, но ради понятности для остальных сделаем более стандартно
    public Order GetOrder()
    {
        return this.order;
    }
}
}

Мы создаем класс события «Заказ создан». И вместо отправки письма, OrderService будет просто генерировать данное событие. А в самом приложении будут настроены обработчики.

namespace <base namespace>.EventHandlers
{
public class OrderCreatedEmailSender : IEventHandler<OrderCreated>
{
    public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator)
    {
        // в конструкторе зависимости непосредственно касающиеся одного конкретного действия
    }

    public void Handle(OrderCreated event)
    {
        this.mailer.Send(...);
    }
}
}

Атрибут Serializable у класса события неспроста. Мы можем данное событие обрабатывать не сразу же, а помещать его, сериализованное, в очередь (Redis, ActiveMQ или другой вариант). И обрабатывать уже не в веб-потоке, а отдельными воркерами, чем мы непременно обрадуем наш веб-сервер. В этой статье автор более подробно говорит о событийной архитектуре, правда примеры кода мне не совсем понравились (ТТУК).

Как итог, мы получили OrderService, занимающийся только логикой заказов (генерацию нужных событий все-таки, с натяжкой, можно в нее включить), а вытащенное из него действие отправки письма выделили в отдельный класс и связали эти классы еще более слабой связью, чем DI. Кто-то может возразить, что теперь сложно понять что вообще происходит при создании заказа. Но это не так. Find usages в IDE для класса OrderCreated и мы увидим все действия связанные с этим событием.

Не всегда простым является вопрос, где какой метод ослабления связи применять. Я обычно использую DI и Events, и первое применяю когда зависимое необходимо для основного действия, как в данном примере IOrderRepository. Все-таки сохранить заказ — это единственное основное действие, которое должно быть выполнено в любом случае. А все второстепенное пусть обрабатывает событие OrderCreated. Это и выделим в последнюю для этой статьи, пятую хорошую привычку: используйте событийную архитектуру для всех второстепенных действий.

Хотя нет, не последнюю. Я очень часто замечаю, что программисты стесняются заводить новые классы. Слышу мнения, что много классов делают программу более сложной для понимания. Возражу лишь тем, что код, красиво разбитый на десятки классов, имеющих четко обозначенную единственную цель, лучше кода, где все это сложено в один класс. И нет лучшего способа доказать это, чем долговременная поддержка проекта с более-менее сложной бизнес-логикой. Главное не мельчить уж совсем. Создание, изменение и другие действия с заказом вполне умещаются в одном классе. До той поры, пока вы не почувствуете, что этот класс перестал удовлетворять принципу S ;-)

P.S. В комментариях часто отмечали, что все эти паттерны нужно применять с опаской. Они могут усложнить проект, не принеся особой выгоды. Решая как именно декомпозировать свои проекты, вы должны понимать, что использование IoC, Event-Driven и подобных методик — это довольно серьезное вложение в проект. И вы должны быть уверены, что эти вложения окупятся. Если проект из серии «написал и забыл», то вложения окуплены не будут. Такое приложение можно написать быстрее и, как итог, дешевле. Умение найти ту самую золотую середину, которая позволит в итоге получить наиболее дешевую цену создания плюс поддержки софта, является одним из главных для серьезного программиста.
Автор: @Adelf
ICL Services
рейтинг 58,05

Комментарии (202)

  • +8
    Лёгкая, понятная статья с простыми примерами. Может и лишнее, но в конце можно по пунктам перечислить «привычки» как итог.
    • +3
      Прекрасная статья.
      Мои 5 с:
      1. во всем должна мера и во всем должен быть резон.
      Как здесь многие отметили, если отправка почты — это главное действие в этой программе, но можно оставить и первый, самый короткий и тупой вариант.
      Если данный код — только прототип, тоже самое.
      И масса других случаев, когда тот первый, неправильный код будет правильным.
      Самый нехороший, но жизненный пример: у вас, как разработчика нет времени на чистый код. Так тоже бывает. Вы лепите прототип, один из многих. И если этот код пойдет дальше, тогда его можно рефакторить, улучшать.
      Понимаю, что статья — не об этом, но и сказат не могу, т.к.постоянно вижу программистов, не умеющих остановиться на «пути к совершенству».
      В статье идет речь об "очень важном" коде и его надо делать именно так. Такого кода у вас может быть 10% или меньше.
      Опять же, опытный программист автоматически сделает правильно и неважный код. И это нормально, как и то, что новичок сделает плохой код, тоже нормально.

      2. не следовать тупо «лучшим паттернам», типа SOLID всегда и везде. Иначе результат будет удивлять. Всегда требуйте резон для любой доп.работы.
      • +3
        По моим наблюдениям, если писать изначально корявый код потому что нужно побыстрее, то с большой вероятностью этот код так и останется висеть таким вот корявым на очень долгое время. То есть, его-то и поправят когда-нибудь, возможно, но вряд ли это будет скоро, потому что как правильно еще кучу всего другого нужно делать. Наблюдал такое уже на нескольких проектах. Поэтому, если это не какой-то суперкритичный хотфикс, я лично всегда стараюсь написать настолько хорошо, насколько могу.
      • +1
        Опять же, опытный программист автоматически сделает правильно и неважный код.


        Это происходит потому, что большинство кода пишется по привычке. Если постоянно себя одёргивать, то привычки запоминаются правильные. Если говорить, что время жмёт, мне нужен только прототип и требования изменились, то потом даже если захотеть писать правильно и дать себе достаточно времени (у кого когда так было последний раз?) красивый код не получается.
      • 0
        Всегда требуйте резон для любой доп.работы.


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

        Поэтому советую отталкиваться от цели. Если цель Junior'а вначале — научиться писать качественный код, и ему действительно необходимо следовать этим принципам, чтобы привыкнуть, то для Senior'а не первое место выходят оптимальность затрат и легкость поддержки. И на этом уровне уже не важно, циклом обходить коллекцию или завернуть в filter-map — легкость поддежки будет одинаковая. Поэтому естественным образом выбирается второй вариант — писать меньше и читать проще.

        И пока midlle-разработчики определяются с тем, какой подход лучше, senior'ы пишут framework'и и plugin'ы для IDE, которые позволят еще ускорить воплощение разработческой мысли (не только в насписании кода, а вообще во всем процессе разработки).
        • 0
          Попробуйте для примера написать какую-нибудь простую задачку а-ля «Сапер», потом прокачаться в использовании возможностей IDE, а потом написать еще раз — разница во временных затратах будет весьма значительная.
  • –8
    Отличный пример, как из простого куска кода, который пишется за несколько минут, сделать огромную структуру, увеличить количество кода в разы, и тем самым снизить его надежность, поддерживаемость и простоту, а также повысить косты на разработку ПО.

    Наглухо завязались на SMTP? Ужас то какой. А что, сейчас используется еще что-то? Что, в будущем появится еще какой-то протокол? У него будет точно такой же интерфейс? Уверены? Точно, точно не придется переделывать все приложение, потому что интерфейс мейлера просто не был предусмотрен для чего-то совсем нового?
    Или может быть источник данных из БД вдруг надо будет заменить на лоулевел драйвер флоппика? Серьезно?

    Все, что тут описано, это борьба очень средненького программиста с самим собой и получение им удовольствия от того, что он использовал паттерн, хоть и не к месту, сделал интерфейс, а интерфейсы повышают ЧСВ, это факт, ну и бахнул ИОК по приколу.

    А может быть просто стоит написать простейший код без усложнений, а если он устареет, выкинуть его целиком и написать новый опять за несколько минут?

    • +12
      Наглухо завязались на SMTP? Ужас то какой. А что, сейчас используется еще что-то?

      А тестировать отправку почты вы как собираетесь?
      • –12
        Запустить и проверить? Или Вы искренне считаете, что если сервис будет работать на моке, то он 100% заработает в реальном боевом окружении? Попробуйте мыслить чуть дальше, чем своим кодом. Не только программой, но еще и всем стеком, а также прилегающей инфраструктурой. И окажется, что написать такой юнит-тест, который все учтет, может оказаться очень проблематичным. А также это может вселить ложную уверенность в том, что раз код протестирован, то он работает. А на самом деле юнит тест будет проверять ту логику, которая предполагалсь. А то что программист упустил в своем коде, он упустит и в тесте, так как просто не думал об этом
        • +17
          Запустить и проверить?

          Клевая идея, совершенно честно. У нее есть только одна проблема: время, необходимое на то, чтобы проверить, что обработка заказа рассылает все 18 нужных писем в разных сценариях. Даже если это автоматизировать (включая работу с почтовым ящиком), все равно будет небыстро. А юнит-тест проверяет это за секунды, и каждый раз, при каждом билде.

          Не только программой, но еще и всем стеком, а также прилегающей инфраструктурой.

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

          И окажется, что написать такой юнит-тест, который все учтет, может оказаться очень проблематичным.

          А не надо учитывать все. Надо покрывать сценарии в порядке их важности — сначала то, что описано в требованиях, затем регрессию на найденные баги.
          • –2
            Клевая идея, совершенно честно. У нее есть только одна проблема: время, необходимое на то, чтобы проверить, что обработка заказа рассылает все 18 нужных писем в разных сценариях. Даже если это автоматизировать (включая работу с почтовым ящиком), все равно будет небыстро. А юнит-тест проверяет это за секунды, и каждый раз, при каждом билде.


            А тут зависит от архитектуры. Если есть сервер приложений, то логично организовать заполнение им очереди на отправку и отдельного демона на саму задачу отправки. Протестировать заполнение очереди можно автоматизированно и комплексно, а то, что демон может отправить сообщение — это и так видно, в 3 строках кода сложно ошибиться.

            Так мы получим нужное решение, которое кроме всего прочего декомпозировано (сервер приложений и демон), транзакционно (если приложение упадет, то почта не будет отмечена как отправленная и не потеряется) и простое (несколько модулей, которые может написать и поддерживать даже школьник).
            • +6
              Ну во-первых, вот вам живой ответ на вопрос «что, используется что-то еще?». Да, используется. Был SMTP, стала очередь команд.

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

              Понимаете, если письма нет в очереди — то что это значит? Что его туда не положили, или что очередь не работает? А если письмо в очереди есть — это его туда положили, или очередь с прошлого теста грязная? В случае чистого мока у вас просто нет этих вопросов.
              • –3
                А юнит тест сможет ответить лишь на вопрос, сможет ли полностью изолированный класс дернуть команду мока почтового сервиса. На вопросы почему было дернуто слишком много и мало раз в боевом окружении он вряд ли ответит.
                • +12
                  Задача юнит-теста — гарантировать, что при таком, таком и таком состоянии заказа отданы команды на отправку такого, такого и такого письма. И это реально самый простой, самый быстрый и самый надеждый способ это протестировать.
                  • –1
                    > гарантировать, что при таком, таком и таком состоянии заказа отданы команды на отправку такого, такого и такого письма. И это реально самый простой, самый быстрый и самый надеждый способ это протестировать.

                    Простите, что вклиниваюсь. Но в данном случае делать интерфейс и его реализацию для тестов — это, по-моему, не самый простой способ протестировать, что отданы нужные команды на отправку.
                    • +2
                      Предложите вариант проще.

                      Сделать интерфейс — одна команда (Extract Interface).
                      Реализация для теста — new Mock<ISmtpService>().
                      • –1
                        Иметь метод, который выдает команды в зависимости от состоянии заказа. Интерфейс не нужен, мок мейлера не нужен. Взяли коллекцию команд и проверили.
                        • 0
                          Ээээ… Куда выдает?

                          Вот у вас есть метод, изменяющий состояние заказа (например, по приходу оплаты). Он void, потому что вызывающая сторона от него ничего не ожидает (и это логично; ну ок, он может отдавать ошибки, если с оплатой что-то не так, но письмо — это не ошибка, это побочный эффект).
                          • 0
                            Тут напрашивается событийная модель. То есть в конце выполнения метода изменяющего состояние заказа он инициирует событие — «Заказ изменен». И все кто подписался на это событие его получают. Это, собственно, другой вариант получить слабую связанность в данном месте (и, как мне кажется, архитектурно тут более правильный). А уже обработчик события имеет зависимость от SMTP. При тестировании мы подписываем только мок обработчик, который нам сообщает, что событие произошло.
                            • 0
                              Ну собственно в посте-то там потом событийную модель и прикрутили. Только вот это уже добротная такая смена архитектуры приложения, что, конечно, мило и правильно, но не всегда работает. И для понимания традиционно сложнее, чем модель с прямыми вызовами.
                            • 0
                              А вы статью читали?
                              • 0
                                Я, собственно, отвечал на комментарий а не на статью. То есть мой комментарий нужно рассматривать в контексте как протестировать не сложнее чем через экстракцию интерфейса и мок.
                          • 0
                            > Куда выдает?

                            Той самой void-функции с побочным эффектом, который будет заключаться в том, чтобы сунуть эти команды в Send() мейлеру. Заодно еще будет выдавать юнит-тесту.
                            • 0
                              Покажите пример кода, пожалуйста. Входная точка, она же code under test — метод public void PaymentReceived(Money amount). Проверяемое требование тоже простое — если сумма платежа совпадает с суммой заказа, надо выслать письмо «ваш заказ полностью оплачен» на емейл, указанный для оповещений в том же заказе.
                              • 0
                                Тогда давайте начнем с начала — покажите код юнит-теста. Как и что именно он будет проверять? Если я правильно понял изначально процитированные слова о том, что нужно проверить — я покажу как его упростить.
                                • +1
                                  var emailService = new Mock<IEmailService>();
                                  
                                  var order = _fixture.CreateValidOrder(emailService.Object);
                                  order.PaymentReceived(order.Amount);
                                  
                                  emailService.Verify(
                                    s => s.Send(
                                        order.NotificationEmail,
                                        "Payment received for order #" + order.Number,
                                        "Your order #" + order.Number + " was paid in full."),
                                    Times.Once);
                                  
                                  • 0
                                    Итак, в представленом юнит-тесте, насколько я понял, проверяется 2 вещи:
                                    1. Метод Send() был вызван 1 раз
                                    2. Сообщение было сгененировано в соответствии с ордером.

                                    Теперь берем изначальный код автора (слегка модифицированный по моему вкусу):

                                    // заполняем объект заказа, cкидки, акции и т.д.
                                    var order = GenerateOrder(....);
                                    SaveOrder(order);
                                    var orderNotification = GeneratePaymentReceivedMessage(order);
                                    SendEmail(orderNotification);

                                    Нужно ли проверять, что SendEmail() вызывается ровно один раз (если ранее не было исключения)? Нет, это очевидно.

                                    Как проверить корректность сообщения:

                                    var order = _CreateValidOrder();
                                    var expectedNotification = _Msg(order.NotificationEmail, «Payment..» + order.Number, «Your order #» + order.Number + "....")
                                    Assert.AreEqual(expectedNotification, GeneratePaymentReceivedMessage(validOrder));

                                    Да, в этом варианте появляется новая сущность — сообщение, но не факт, что это минус.

                                    При этом в этом коде нет IMailer, нет необходимости его мокать, и не используется фреймворк для мокинга.

                                    PS. Прошу прощение за отсутствие возможности отформатировать код.
                                    PPS. Не судите строго, если накосячил с синтаксисом. Последний раз писал на C# в 2008 году.
                                    • +1
                                      Нужно ли проверять, что SendEmail() вызывается ровно один раз (если ранее не было исключения)? Нет, это очевидно.
                                      Помоему, вы не совсем понимаете смысл юнит-тестирования. Проверять это нужно обязательно. Юнит-тесты полезны не столько тем, что позволяют проверить что все работает нормально на момент написания теста. А тем, что смогут указать что последние изменения что-то сломали. Если код поменяется и отправка email при каких-то условиях может не произойти юнит-тест покажет это почти сразу как только неверный код будет написан.
                                      • 0
                                        Да уж куда мне понять смысл юнит-тестирования… Я-то всегда думал, что нет смысла тестировать очевидную последовательность действий. Вот если она станет менее очевидной, например, кто-то добавит циклы или условные переходы — это будет его ответственность исправить тесты соответствующим образом.

                                        Буду благодарен, если опровергните мое скромное и несовершенное понимание ссылкой на какого-нибудь признанного авторитета вроде Бека или Фаулера.
                                        • НЛО прилетело и опубликовало эту надпись здесь
                                          • 0
                                            Ладно бы это. Хуже то, что можно изменить код так, что письмо не отправится вообще, а тесты все равно пройдут.
                                            • –1
                                              > кто-нибудь сможет изменить код так, что письмо отправится несколько раз, а тесты все равно пройдут.

                                              > можно изменить код так, что письмо не отправится вообще, а тесты все равно пройдут.

                                              Думаю, я смогу изменить код так, чтобы он создавал background thread, который будет потихоньку удалять папки с диска, забивать память мусором и ддосить сайт виндовс10. И, о ужас, все тесты пройдут.

                                              Повторю другими словами. Тест пытается сломать существующий код, а не гипотетическое его изменение в будущем. Ответственность за тестирование изменения ложится на автора этого изменения.
                                              • +1
                                                Вы действительно не понимаете смысла юнит-тестов. Они в первую очередь фиксируют ожидаемое поведение, а не пытаются что-то сломать. Просто представьте, что код внутри PaymentReceived выглядит вот так:

                                                AmountPaid += amount;
                                                if (AmountPaid < Total)
                                                {
                                                  _email.Send(NotificationEmail, "Payment for order #" + Number " received", ...);
                                                  return;
                                                }
                                                if (AmountPaid > Total)
                                                  _paymentService.Refund(Customer, AmountPaid-Total);
                                                
                                                Status = OrderStatus.Paid;
                                                _email.Send(NotificationEmail, "Payment for order #" + Number " received", ...);
                                                
                                                • 0
                                                  > Вы действительно не понимаете смысла юнит-тестов

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

                                                  > Просто представьте, что код внутри PaymentReceived выглядит вот так

                                                  В моем примере код, который отвечает за генерацию сообщений был бы помещен в GeneratePaymentReceivedMessage(). Возвращаемый тип изменился бы на Option[Message]. Тестировался бы по-прежнему GeneratePaymentReceivedMessage(), без IMailer, без моков, без фреймворка для них.
                                                  • +2
                                                    Но если поделитесь авторитетными ссылками по поводу проведения черты между очевидными с однйо стороны и требующими тестирования с другой вещами — буду благодарен.

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

                                                    В моем примере код, который отвечает за генерацию сообщений был бы помещен в GeneratePaymentReceivedMessage(). Возвращаемый тип изменился бы на Option. Тестировался бы по-прежнему GeneratePaymentReceivedMessage(), без IMailer, без моков, без фреймворка для них.

                                                    Но как вы гарантируете вызов этого метода в заданных условиях?
                                                    • 0
                                                      > В моей личной системе ценностей любое бизнес-требование, реализованное на уровне доменного уровня, должно быть протестировано.

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

                                                      > Но как вы гарантируете вызов этого метода в заданных условиях?

                                                      Не совсем уверен, что понял вопрос. Но на всякий случай: GeneratePaymentReceivedMessage — «чистая» функция, зависит только от своих неизменяемых аргументов, никаких побочных эффектов внутри.
                                                      • +2
                                                        Понимаете ли, тестируемое бизнес-требование звучит как «если получена сумма, достаточная для покрытия заказа, отправляется соответствующее уведомление» (я опускаю другие действия в тех же условиях). Соответственно, тесты должны гарантировать, что уведомление отправляется. Вы же проверяете только то, что некая функция правильно генерирует уведомление.
                                                        • 0
                                                          А поясните, пожалуйста, человеку, бесконечно далёкому что от C#, что от юнит-тестов, как проверить, что событие генерации ордера (или вызов почтовика, неважно) случатся в итоге когда-нибудь, если они асинхронные? Если OrderService::Create() запускает какой-нибудь бекграунд-тред, в котором считается что-нибудь тяжёлое, например?

                                                          Я вот на C++ пишу, периодически с Qt. Если кутешный QtConcurrent::run() дёрнуть из треда, который не был создан как QThread, то там всё плохо будет, если вкратце, и для внешнего наблюдателя всё будет выглядеть так, будто run никогда не завершится… Как убедиться, что тут с асинхронностью не всё плохо?

                                                          Хотя, похоже, этот вопрос эквивалентен тому, что как проверяется, что пусть даже синхронная функция генерации заказа не зависнет.
                                                          • +1
                                                            Асинхронная задача бьется на синхронные, и те тестируются отдельно. Асинхронию per se тестировать тяжело и, в среднем, не надо.

                                                            К счастью, .net уже достаточно давно позволяет писать асинхронный код не используя тредов, а такой код тестируется как синхронный. Пример:

                                                            async Task ProcessPaymentAsync(Money amount)
                                                            {
                                                              await SomeKindOfHardWorkAsync().ConfigureAwait(false);
                                                              AmountPaid += amount;
                                                              if (AmountPaid < Total)
                                                              {
                                                                await _email.SendAsync(NotificationEmail, "Payment for order #" + Number " received", ...).ConfigureAwait(false);
                                                                return;
                                                              }
                                                              if (AmountPaid > Total)
                                                                await _paymentService.RefundAsync(Customer, AmountPaid-Total).ConfigureAwait(false);
                                                            
                                                              Status = OrderStatus.Paid;
                                                              await _email.SendAsync(NotificationEmail, "Payment for order #" + Number " received", ...).ConfigureAwait(false);
                                                            }
                                                            


                                                            Все, теперь этот код готов к асинхронной работе — т.е., какой-нибудь RefundAsync внутри себя может породить запрос к веб-сервису, повесить событие на результат, отпустить поток, дождаться события, получить другой поток, вернуть результат, и все это будет прозрачно для обсуждаемого метода. Более того, когда я в тестировании буду вбрасывать мок для RefundAsync, я вброшу синхронную реализацию, и у меня не будет проблем с тестированием.
                                                            • 0
                                                              Разумно, и я что-то такое и ожидал, спасибо.

                                                              Впрочем, похоже, это уже противоречит
                                                              Соответственно, тесты должны гарантировать, что уведомление отправляется. Вы же проверяете только то, что некая функция правильно генерирует уведомление.


                                                              Кстати, я ещё где-то отрывочно слышал, что async/await в C# при определённых условиях может приводить к дедлокам. Как проверить, что это не тот случай? Банально ждать по таймауту, что ли, завершилась ли задача?
                                                              • 0
                                                                Впрочем, похоже, это уже противоречит
                                                                Соответственно, тесты должны гарантировать, что уведомление отправляется. Вы же проверяете только то, что некая функция правильно генерирует уведомление.



                                                                Не противоречит. «Уведомление отправляется» равно «вызван метод сервиса, отвечающего за отправку». Этот сервис, в свою очередь, протестирован отдельно.

                                                                Кстати, я ещё где-то отрывочно слышал, что async/await в C# при определённых условиях может приводить к дедлокам.

                                                                Если вкратце, то не может. Там нужна комбинация async/await с .Wait()/.Result().

                                                                Как проверить, что это не тот случай?

                                                                В юнит-тесте — никак (асинхрония, повторюсь, практически не тестируются юнит-тестами). В интеграционном — ловить ошибки о таймаутах.
                                                  • 0
                                                    Но на самом деле, и так видно, что вы используете функциональную композицию, а не объектную; а в ФП многие вещи выражаются (и тестируются) несколько иначе, нежели в ООП.
                                                    • 0
                                                      > видно, что вы используете функциональную композицию

                                                      Совершенно верно. Я же из упомянутой автором «монструозной» Scala сюда свалился.

                                                      > вы используете функциональную композицию, а не объектную

                                                      Тут подразумевается некое противопоставление. На деле их можно разумно сочитать.
                                                      • +1
                                                        Можно, и C# прекрасно это делает. Но подходы к проектированию меняются.
                                                  • +1
                                                    Но если поделитесь авторитетными ссылками по поводу проведения черты между очевидными с одной стороны и требующими тестирования с другой вещами — буду благодарен.

                                                    Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code (Addison-Wesley Professional, 2007), Chapter 3,
                                                    Goals of Test Automation.

                                        • +1
                                          Зачем же вы тогда тестируете текст сообщения, там что, есть циклы и условные переходы?
                                          • 0
                                            > Зачем же вы тогда тестируете текст сообщения, там что, есть циклы и условные переходы?

                                            Вспомним, что все началось с фразы lair «гарантировать, что при таком, таком и таком состоянии заказа отданы команды на отправку такого, такого и такого письма. И это реально самый простой, самый быстрый и самый надеждый способ это протестировать». Т.е. нетривиальная логика генерации предполагалась по условию. Я показал более простой, быстрый и надежный способ ее протестировать.
                                            • 0
                                              Вот только во фразе lair был пункт «гарантировать… что отданы команды», который вы решили не тестировать.
                                              • 0
                                                > Вот только во фразе lair был пункт «гарантировать… что отданы команды», который вы решили не тестировать.

                                                Ну мы вроде это уже обсудили… Идея не в том, что я по своему призволу решаю, что тестировать, а что нет. А в том, чтобы используя ФП сделать отправку команд тривиальной (гарантированной) и не требующей тестирования. Чтобы достаточно было протестировать только генерацию, которая не требует искусственных интерфейсов и моков.
                                                • +1
                                                  Во-первых, вынос генерации в отдельную функцию — такой же искусственный интерфейс.
                                                  А во-вторых, задача была проверить конкретное требование. Не объявить, что оно тривиально, а проверить его.

                                                  И если вы посмотрите на пример моего кода, то вы поймете, почему ничего гарантированного в нем нет.
                                                  • 0
                                                    > вынос генерации в отдельную функцию — такой же искусственный интерфейс

                                                    Нет. Исходя из наличия сложной логики генерации, выделение этой логики как отдельной функции — это вполне естественный шаг рефакторинга. А вот специальный интерфейс, который нужен только для тестирования — это искусственная конструкция.

                                                    > Не объявить, что оно тривиально, а проверить его.

                                                    Зачем проверять тривиальные вещи, если можно этого не делать и потратить высвободившееся время с реальной, измеряемой пользой?

                                                    > посмотрите на пример моего кода, то вы поймете, почему ничего гарантированного в нем нет

                                                    Я уже прокомментировал этот пример. Если прорефакторить его в соответствии с предложенным мною (в комментарии выше) подходом, то ничего не меняется. Усложнившаяся генерация сообщений по-прежнему тестируется юнит-тестом без моков, отправка сообщений по-прежнему тривиальна и не требует тестирования. (Понимаю, что вместо слов хорошо бы привести код, но без форматирования будет полный отстой.)
                                                    • +1
                                                      Исходя из наличия сложной логики генерации, выделение этой логики как отдельной функции — это вполне естественный шаг рефакторинга.

                                                      Да нет там сложной логики генерации. Есть сложные условия генерации.

                                                      Зачем проверять тривиальные вещи, если можно этого не делать и потратить высвободившееся время с реальной, измеряемой пользой?

                                                      Потому что (а) они не тривиальные (б) завтра они точно не будут тривиальными и (ц) лично я ошибаюсь в коде произвольной степени тривиальности.

                                                      Если прорефакторить его в соответствии с предложенным мною (в комментарии выше) подходом, то ничего не меняется.

                                                      Покажите результат рефакторинга.
                                            • 0
                                              А, ну так вы феншуя не достигли. Смотрите, выносим одну из веток нетривиального года наружу

                                              Assert.AreEqual(expectedNotification, GeneratePaymentReceivedMessage(new MessageFormatterNumber3(), validOrder));

                                              И код и внутри GeneratePaymentReceivedMessage, и внутри MessageFormatterNumber3 остаётся примитивным, этот тест тоже можно удалять.

                                              Если вы захотите проверять, что при нужной комбинации условий будет вызываться всё же с MessageFormatterNumber3 — не забудьте проверить, что и письмо тем нетривиальным кодом в SendEmail отправляется.
                                        • 0
                                          Буквально на днях выкатили на тестирование билд, тесты которого обнаружили, что клиентом отправляется по две смс, вместо одной. Тестирование, разумеется, зафейлиолсь, начали разбираться.
                                          Архитектурно смс были сделаны в виде событий модели, реагирущюих на определенные изменения. Так случилось, что в одном месте не было проверки на то, не дублируется ли изменение, а в другом месте написали кривой if, из-за чего заказ при завершался, затем возвращался на предыдущий статус, и завершался опять.

                                          Да, проблема скорее архитектурная, но не было бы теста и проверки на дубли — и ничем хорошим релиз бы не закончился.

                                          Последовательность действий пишут люди, а автоматические тесты способны пресекать человеческий фактор.
                                    • +1
                                      Итак, в представленом юнит-тесте, насколько я понял, проверяется 2 вещи:
                                      1. Метод Send() был вызван 1 раз
                                      2. Сообщение было сгененировано в соответствии с ордером.

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

                                      Нужно ли проверять, что SendEmail() вызывается ровно один раз (если ранее не было исключения)? Нет, это очевидно.

                                      Нет ничего «очевидного». Если внутри PaymentReceived много логики (а ее там много), то условия вызова будут разными, и именно эту логику и надо проверять.
                                      • 0
                                        > это происходит в момент получения платежа. Ваш тест этого не проверяет

                                        В моей версии отправка происходит после сохранения платежа. Это очевидно из кода и не требует тестирования.
                                        • 0
                                          Я специально изначально написал, что логика отправки нетривиальная (пример кода — выше). Ну и да, если она «не требует тестирования» сейчас, то она не будет покрыта тестами никогда.
        • 0
          Запустить и проверить юнит-тест в IDE дело нескольких секунд, а всякие удобные штуки типа nCrunch вообще запускают их автоматически. А вот выполнить из UI какого-нибудь запрос, который в итоге создаст заказ и отправит это письмо, это точно не пара секунд. А потом вы меняете пару строк в этом сервисе и приходится опять лезть в этот UI и опять создавать заказ. Я уж не говорю о том, что UI может и не быть на данном этапе разработки, либо он разрабатывается параллельно и вообще не стабилен.
        • 0
          А на самом деле юнит тест будет проверять ту логику, которая предполагалсь. А то что программист упустил в своем коде, он упустит и в тесте, так как просто не думал об этом


          Я бы даже согласился, если бы по опыту не знал, что при новом коде желательно проверять то, что было уже написано и работало ранее.
          То есть, да, тесты учитывают только то, что программисты учли. Но даже такие тесты всё же полезны! Потому что, бывает так, что код работает, но при добавлении нового кода или каких-то исправлений перестает работать старый код.
          Да надо писать части кода независимо друг от друга, но всё же не всегда это возможно.
      • 0
        А каким образом тестирование почты связано с её абстракцией? Для тестирования существуют UnitTest-ы
        • 0
          А как вы будете тестировать взаимодействие с чем-то, что не абстрагировано?
          • 0
            Абстрактные объекты нет смысла тестировать, их можно только моделировать (через те же заглушки). Если мы говорим про модульное (Unit) тестирование, то достаточно проверить, что сообщение размещено в очереди в нужном нам виде. Полный процесс уже проверяем на этапе интеграционного тестирования.
            • 0
              Так тестируется-то не абстрактный объект, а взаимодействие с ним. Собственно, ровно то, что вы описываете дальше.
        • 0
          Тестируется не работа почты, а факт обращения к почте. Для этого вместо почты подставляется мок, который вместо работы регистрирует факт вызова и возвращает заранее заданное значение. Чтобы можно было подставить мок, рабочий класс «объинтерфейсивается» и применяется инъекция зависимостей для того, чтоб подставить «моковую» реализацию экстрагированного интерфейса.

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

          Фишка в том, что, по крайней мере в Java, создание «на лету» реализации интерфейса — стандартная операция, а создание «на лету» потомка класса — хитрая манипуляция с байт-кодом, да ещё и не всегда возможная.
    • +11
      Знал, что такой комментарий появится.
      Я работал и в проектах, где все сделано «по-простому». И могу сравнивать. Но, как я написал в тексте, это сложно обьяснить. И уж тем более — доказать. Могу лишь привести пример: один из мейнстримовых на данный момент PHP фреймворков — Laravel имеет из коробки IoC и содержит тот самый код, который по вашему мнению, «низко надежный, плохо подерживаемый» и т.д. Я понимаю, что популярность фреймворка не значит автоматически, что его код высококачественный. Но уверяю вас — Laravel написан хорошо.
      Да, людям, которые привыкли писать «по-простому» сложно привыкнуть к логике, разделенной на кучу классов. Но в этом деле хорошо помогают IDE, позволяющие без проблем читать такой код. И я считаю, что правильный код с интерфейсами, IoC и прочими SOLID-ами гораздо проще кода, написанного «по-простому».
      • –4
        Я не говорю, что код надо писать одной функцией на миллион строк. Или что паттерны это зло, их вообще использовать не стоит. Но придумывать синтетические ситуации для того, чтобы использовать паттерн, потому что это круто — вещь очень сомнительная. Стоит понимать, для чего пишется проект, какие у него, нужен ли он заказчику, или это «русский бизнес».

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

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

        Насчет мира PHP, достаточно сложно говорить на его примере вообще о чем либо. Это достаточно неплохой язык, на котором можно при желании делать красивые, качественные и отличные решения. Но изначально этот язык проектировался как перл для домохозяек, и обладает крайне низким порогом для вхождения, что делает с одной стороны его простым и легким в изучении, а с другой стороны притягивает к себе начинающих программистов, не обладающих достаточным опытом и практикой для того, чтобы оценить адекватность и применимость подхода и решения. Поэтому они начинают бездумно использовать то, что сейчас модно и наиболее афишируется, то есть всякие иоки, юнит тесты и контейнеры, и в итоге не понимают, что делают не совсем то. Но опыта для sanity check своего решения у них пока еще нет, он будет позже
        • 0
          > Это как говорить что раз стул был сделан молотком, значит он хороший, и если все остальное делать молотком, то это тоже хорошая практика.

          Мне прям Фрейд вспоминается. Если я хочу делать стулья, то не кустарным, молотком и пилой, а промышленным — каким-нибудь станком с ЧПУ(ох уж эти гуру абстракции), который 24*7 пеньки нарезает в одинаковые стулья.

          Это не значит, что ваши стулья хуже — это значит ваши стулья геморнее. А, что характерно, заказчик платит за стулья, а не за то, что вы _скажете_, что делали с умом. Вы сами то часто ведётесь, когда вам говорят «мы отбираем лучшее сырьё и технологии специально для Вас»?

          А что школота на PHP иоки и юнит-тесты изучает — это вы фантазируете. В иоках нет «крайне низкого порога вхождения», а в PHP — «модного».
        • 0
          Это хорошо если вы написали проект, прошли приёмку и благополучно о нём забыли. Все работает по ТЗ, все счастливы. Но, что если вы продолжаете дальше поддерживать проект, клиент будет вносить новые требования (часто не совмещающиеся с изначальными), ваша команда разрабов меняется и новые члены привносят свое «видение»? Проблемы будут расти как снежный ком и каждое новое требование будет отнимать все больше времени. И ваш безупречный код будет все неуправляемее и неуправляемее.
          Это не фантазия, а суровая правда жизни, новые седые волосы и пятничные удары по печени :)
          • 0
            Тут, как объяснили, сфера деятельности — разработка и поддержка банковского ПО. Клиенты «небезбашенные», про нужность непротиворечивости собственных требований обычно понимают. Разработчики меняются достаточно редко, чтоб с этим риском можно было справляться «дублированием»-«триплированием», т.е. когда в группе из двух-трёх каждый знает код другого/других может его поддерживать и продолжать — в результате при том, что какой-никакой уровень коллективного владения кодом обеспечен, приспосабливать код под быстрое «въезжание» в него, в общем, не требуется. Таким образом повышенная регламентированность сферы деятельности позволяет сохранять эффективность и при работе и «по старинке»…
            • 0
              ну если заказчик один это хорошо. А вот в сфере b2b все куда веселее. иногда требования к проекту от двух пользователей могут быть диаметрально противоположны.
    • +10
      А может быть просто стоит написать простейший код без усложнений, а если он устареет, выкинуть его целиком и написать новый опять за несколько минут?

      А вот это, кстати, верный признак работы в тепличных проектах. Так серьезные проекты не пишут.
      • 0
        А серьезный проект это какой? Когда пишется сложный продукт в сжатые сроки с реализацией всех бизнес потребностей и который в итоге работает как этого хочет клиент, или долгий проект у аутсорсера на несколько лет, где можно с умным видом ковырять в носу, переносить сроки и ленивенько писать юниттесты?
        • +1
          Меня всегда живо интересовал вопрос: с какого перепугу сложный продукт с реализацией всех бизнес потребностей и который в итоге работает как этого хочет клиент обязательно пишется в сжатые сроки? Это такая best practice?
          • +5
            Это такой плохой менеджмент. Недавно была годная хабрастатья о тимлиде, который выбирал между «сжатыми сроками» и о том, чтобы просто сказать менеджеру, что мы не успеем. Не смог быстренько найти ссылку…
            • –2
              К сожалению многие программисты забывают, зачем они делают свою работу. Стремятся написать «идеальный код», но не думают о том, какие задачи они решают и для чего.

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

              И что, в каком нибудь из этих случаев ответ тимлида о том, что не успеем, мол еще не все юнит тесты написаны и не все иоки бахнуты будет адекватен?

              Так что скорее те проекты, где есть много времени — как раз они тепличные
              • +4
                1) Забить на поддерживаемость кода. Надо просто быстро наваять MVP и запускать. А потом, если все взлетит — будут деньги и на переписывание кода.
                2) В хорошо написанной системе эти изменения реализуются в разы легче и, соответсвенно, дешевле. Статья, кстати, как раз в об этом.
                3) Пишите хороший код и задержки будут не по вине кода.
                • –3
                  1) это же очевидно, что нет более постоянно решения, чем временное. Даже если начать переписывать, опять же очень не факт что новый проект вообще завершится, а не утонет в утехах тру программистов
                  2) в хорошей системе все делается лучше, только вот не факт, что паттерны и модные фишки облегчат жизнь, а не загонять разрабов в полный рефакторинг
                  3) я имел в виду задержки в разработке, не в коде. От кода обычно проблем меньше, чем от человеческого фактора
                • 0
                  1) Это как раз вариант «написать простейший код без усложнений, а если он устареет, выкинуть его целиком и написать новый опять за несколько минут». Вплоть до того, что вообще писать прототип на Python вместо Java.
                  • +1
                    Да. Но тут ситуация такая. Писать чуть дольше, но лучший код в разы дешевле в долгосрочной перспективе.
              • 0
                > 2) команда разработки и поддержки некой системы документооборота внезапно получает новые правила для набора документов, регламентируемый государством.

                Чисто из любопытства, какие именно правила государство ввело за 2 недели до начала применения?
              • +1
                3) в банковской сфере задержка даже в день может нести колоссальные убытки
                Если «задержка даже в день» критична, то тут два варианта (исключая случаи, что нужно просто быть быстрее конкурентов или когда «колоссальные убытки» наличествуют у подрядчика и представляют штрафы на за просрочку сдачи продукта):
                1) На разработку имеется время, меньшее недели. Значит, объем работ не может быть сильно большим. Это либо небольшая функциональность либо прикрытие старых «дыр» — тогда вопрос, откуда они взялись.
                2) Критично, чтоб функциональность была выпущена к какой-то определённой дате, время до которой может быть довольно большим. Тогда на самом деле есть запас времени, и нужно просто его хорошо использовать. Тут можно и о качестве кода подумать и об облегчении себе будущей работы.
              • 0
                К (3). Про «колоссальные убытки» от задержки «даже на день» в разработке вы говорите, а про возможность убытков из-за ошибки в программах — не упоминаете. Интересный момент…
          • –3
            Потому что если продукт пишется под девизом «у нас достаточно времени, чтобы сделать хорошо», то обычно он уходит в пучину треша и угара
            • +1
              У меня сложилось ощущение, что вы работаете в «веб-студия-сайт-за-три-дня» или в чем-то таком, клепающим сайты для заказчиковю
              Но вы, вероятно, забываете, что в мире разработки и интернет-бизнеса существуют проекты, которые разрабатываются годами, имеют у себя огромные (или не очень) команды разработки, фулл тайм работающие над одним и тем же кодом. Именно такие проекты составляют основу интернет-коммерции.

              Я последние лет 5 работаю только в таких проектах. Работу в студиях не рассматриваю в принципе, так как в студиях происходит то, о чем вы пишете выше, а именно «нам нужно срочно написать говносайт за 100к, желательно завтра, тз нет, давайте постараемся, парни!».

              В работе над долгосрочными проектами таких проблем нет. У тебя всегда есть задачи, на которые ты ставишь эстимейты. Если у тебя адекватный менеджер сверху и сильная команда, то в эти эстимейты, разумеется, входит время на то, чтобы писать качественный и гибкий код, туда же закладываются код ревью тимлидом при необходимости, написание тестов для нового фнукционала, и прочее прочее. И никто не говорит «нет, у нас заказчик, ему это не надо, надо завтра!». Нам нужен качественный код, потому что проект ориентирован на годы и годы вперед.
              • 0
                В работе над долгосрочными проектами таких проблем нет. У тебя всегда есть задачи, на которые ты ставишь эстимейты. Если у тебя адекватный менеджер сверху и сильная команда, то в эти эстимейты, разумеется, входит время на то, чтобы писать качественный и гибкий код, туда же закладываются код ревью тимлидом при необходимости, написание тестов для нового фнукционала, и прочее прочее. И никто не говорит «нет, у нас заказчик, ему это не надо, надо завтра!». Нам нужен качественный код, потому что проект ориентирован на годы и годы вперед.

                Где дают пропуска в идеальный мир?

                Я вот только что с долгосрочного (>5 лет) проекта, ориентированного на годы и годы вперед, и там есть заказчик, который говорит «мне надо вчера, мне плевать, какой там говнокод».
                • 0
                  В наукоёмком софте.

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

                Из того что я вижу, я и делаю выводы. Команды, которые делают продукт, который работает, обычно достаточно спокойно относятся к качеству кода, и предпочитают функционал качеству кода. Другие же команды, которые могут сидеть год, а потом оказалось, что они за все время написали только юнит-тесты, обычно даже до первого релиза не доходят, так как просто не успевают в сроки, и иногда проекты сворачивают, как неуспешные.
          • 0
            Это суровая реальность жизни в конкурентной среде
            • 0
              Если у вас из конкурентных преимуществ только продажник, прогибающийся на сокращение сроков — это беда.
        • 0
          Фразы «в сжатые сроки» и «в итоге» смотрятся в одном предложении несколько взаимоисключающе. Думаю, в реале это противоречие разрешается тем, что «в сжатые сроки» пишется «нечто», которое потом, уже после этих «сжатых сроков», путём взаимодействия с клиентом, включающего и корректирование его «хотелок», итеративно, интерактивно и постепенно «в итоге» приводится к виду «как этого хочет (уже 'скорректированно') клиент».
    • +3
      А может быть просто стоит написать простейший код без усложнений, а если он устареет, выкинуть его целиком и написать новый опять за несколько минут?

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

      Все, что тут описано, это борьба очень средненького программиста с самим собой и получение им удовольствия от того, что он использовал паттерн, хоть и не к месту, сделал интерфейс, а интерфейсы повышают ЧСВ, это факт, ну и бахнул ИОК по приколу.

      Допустим так. Но лучше уж пускай пихает интерфейсы, бахает ИОК, чем всю дорогу будет решать задачи в лоб, заниматься копипастом и при этом называть себя программистом.
      • –3
        А чем неуместное использование паттернов это не решение в лоб? Единственное что мне приходит на ум почему оно не в лоб, это потому что оно через ***у :)
        • –4
          И такой подход с пиханием паттернов куда не попадя приводит к тому что приложение разрабатывается слишком долго, работает так-же. Но конечно у таких приложений бывают и преимущества, ведь хелло ворлд почти не тормозит, и ему хватит всего 86гб памяти.
          • +2
            Хорошо. Какой паттерн я использовал неуместно в примерах в статье?
            • 0
              По моему мнению код в самом начале статьи наиболее адекватен для решения поставленной задачи.
              Введение интерфейсов, IoC и депенденси контейнера избыточны для конкретного случая
              • +3
                Ну это стандартная проблема статей и книг. Реальные примеры, где станет понятно для чего нужно вводить все эти паттерны, просто не уместятся в тексте. Да и не будет читатель их читать. Разумеется, пример упрощен.
                • 0
                  А потом люди начитаются что так стоит делать, и как раз так и лепят. Я видел очень много кода, где паттерны применены в простых местах и вообще нужны, а там где правда надо было подумать, разработчика уже не хватило, и там совсем странные дела происходят. Вообще кода, в котором паттерны применены адекватно задаче весьма мало, обычно их везде лепят, потому что это «круто» и «технологично».
                  • 0
                    Есть такая проблема. Согласен. Все эти IoC не нужны Hello world проектам. Они там только делают дороже и написание и поддержку. Постараюсь написать P.S. к статье хороший на эту тему.
            • +2
              И поясню свое мнение, почему я так считаю.
              Создание интерфейса избыточно, так код изначально проектируется под SMTP. То есть по факту добавляя интерфейс, мы не даем сильно больше возможностей для расширения (мы вообще тупо копируем интерфейс SMTP), но идеологически уже привязываемся к интерфейсу. Далее этот интерфейс пойдет дальше в код, и может возникнуть ситуация, когда появится какой то принципиально новый мессенджер, ну вот допустим телеграм, и мы захотим писать в него, а не в почту. Получится, что придется делать рефакторинг (а это бесполезная с точки зрения бизнеса работа) или отказаться от нового решения. С куском «говнокода» проще, его просто удалил и поставил что-то новое. Далее IoC и контейнеры это уже следствие неуместного применения интерфейсов.

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

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

              Предлагаемый Вами подход может иметь смысл, если рядом сидит человек, знающий будущее, который скажет что сразу предусмотреть в интерфейсе. Но я вот таких не видел…
              • +3
                Так и я об этом же! Первый код был «спроектирован под SMTP». В последнем варианте OrderService вообще ничего не знает ни об SMTP, ни вообще о Mailer. Какие бы перемены не сотрясали проект в будущем — код OrderService будет меняться только в том случае, если поменяется бизнес-логика заказов. Single Responsibility принцип как раз об этом.
                • 0
                  Ну хорошо, у нас было 10 строчек кода, стало 100. Из новых 100 строк около 20 менять точно не надо будет.
                  В первом решении не надо будет менять 0 строчек, а во втором 20.
                  По моему достойный результат :)
                  • 0
                    Мир 100 строчек и мир сотен тысяч строк очень разные, поймите.
                    • –4
                      Задача грамотного программиста — удержать свой проект в «масштабах 100 строчек». Вы будете удивлены, какие сложные приложения могут быть написаны малым количеством кода. Также как и простейшее приложение из пары табличек может занимать сотни тысяч строк сомнительного кода.
                      Понятное дело, что каждая задача требует решения, которое порождает лишние строчки, но зачем целенаправленно раздувать свой код лишними конструкциями, для меня до сих пор остается большой загадкой.

                      Даже если проект правда огромен, наиболее разумным решением будет разбить его на много «конкрит» кирпичиков из по вашему мнению «неподдерживаемого» кода, и красивое и в то же время простое ядро, в которое изменения вносятся крайне редко и при большой необходимости. Весь остальной мусор живет в одноразовых кирпичиках. Именно такие проекты имеют шанс разрабатываться быстро, могут пережить большую команду и хреновых специалистов.
                      • –1
                        Задача грамотного программиста — удержать свой проект в «масштабах 100 строчек».
                        Хорошее мнение, хотя не то чтоб я с ним был согласен в плане категоричности, скорее оно мне нравится как благое пожелание… Ещё мне кажется, что мир строго типизированных языков (за исключением Scala) и мир «проектов в 100 строчек» практически не пересекаются (eсли «проект в 100 строчек» — это не интерпретатор какого-нибудь гибкого самописного языка с какой-нибудь гибкой самописной системой конфигурации, и так древовидно слоями, а в каждом «узле» — те самые «100 строчек»). Хочется проектов в 100 строчек? Python, Javascript, Ruby к вашим услугам…
                        • 0
                          Haskell к моим услугам, спасибо. На питонах как-то многословнее получается.
                      • 0
                        > Задача грамотного программиста — удержать свой проект в «масштабах 100 строчек»

                        Я то думал, какая цель у грамотного программиста? А оказывается вот оно как. У вас и заказчики это первым пунктом в договоре прописывают, и премии выдают, если в 90 уложились?

                        И зачем вы «класс» называете «кирпичиком»?
                  • +1
                    Допустим, у вас есть автопарк и в нем машины разных типов (седаны, джипы, трактора, грузовики и т.д.). Разные машины имеют разные качества и — иногда — даже разное поведение.

                    Почему бы нам не создать класс Vehicle со строковым атрибутом Type и во всех методах класса не сделать switch по всем значениям Type, для того чтобы реализовать это разное поведение?

                    А потом, когда в автопарке появятся еще и разные модели, не добавить в Vehicle атрибут Model и не ввернуть еще один вложенный switch по всем значениям Model?

                    А потом, когда еще появится машины разных поставщиков, не добавить третий атрибут Vendor и вместе с ним — еще один вложенный switch по всем значениям Vendor?

                    Зачем думать о наследовании, громоздить иерархии классов, и тем более применять какие-нибудь пафосные штуки типа фасадов и декораторов, когда можно взять и вот так вот в лоб, прямолинейно решить задачу?
                    • +1
                      В этом случае иерархия классов как раз уместна.
                      Но например стоит ли, проектируя автопарк, делать не IVehicle, а например IAsset, который в свою очередь расширяет ISomething, который расширяет IUniverse? А также сделать так, что машины могут быть как с обычными двигателями, так и потенциально с ядерными (разумеется с интерфейсом обычного)?
                      • +3
                        А жизнь, между прочим, гораздо сложнее. В автопарке могут быть и прицепы какие-нибудь. И чтобы знать может ли объект двигаться сам(или чтобы его переместить нужен какой-нибудь эвакуатор) нужен какой-нибудь ICanMoveMyself(не нашел хорошего названия). И это нормально. Этот способ понять умеет ли объект переместить сам себя, имхо, лучший. Интерфейсы не такие уж и страшные, если ими правильно пользоваться. Не надо расширять IUniverse. Она и сама по себе неплохо расширяется.
                        • 0
                          Жизнь вообще сложная штука. Но хорошей практикой является делать то, что от тебя требуется, а не то, что считаешь «правильным». Если заказчик хочет получить учет автомобилей, но стоит делать программу с учетом тяжелой строительной техники и детских колясок. Это просто усложнит код, повысит затраты на разработку и увеличит штат сотрудников. И Вы же не ожидаете, заказав в макдоналдсе гамбургер подождать лишний час, что вам сделали такой бургер, который при желании может стать и блинчиком?

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

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

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

                          • 0
                            И Вы же не ожидаете, заказав в макдоналдсе гамбургер подождать лишний час, что вам сделали такой бургер, который при желании может стать и блинчиком?

                            Какой-то не совсем удачный пример. Это вы представьте, что вы покупаете машину, а вам продают нечто, где нельзя заменить ни одну деталь, а можно только выкинуть всю машину и купить новую. Я сомневаюсь, что кто-то бы такое купил.
                            • 0
                              Вы не поверите, но в современных машинах обычно можно поменять только крупные узлы. Например поменять шестеренку в коробке передач или флап во впускном коллекторе невозможно, нужно покупать только новый узел или колхозить. А вот на старых тазиках можно поменять и купить ЛЮБОЙ болтик, вообще все что угодно, но что-то тазики не очень популярны. Зато машина становится легче, проще, дешевле. И да, такие машины покупают.
                              • 0
                                И вообще скажите честно, Вам машина нужна чтобы на ней ездить или детали менять?
                                • +1
                                  Могу сказать одно: я точно знаю, что детали приходят в негодность (так же, как и меняются бизнес-требования и технические требования в приложениях). И я не хочу из-за неисправного масляного фильтра менять весь двигатель, или покупать новую машину только из-за того, что хочу литые диски, а не железные.
                                  Ваша проблема в том, что вы утрируете. Придумываете какие-то небылицы с превращением обычного двигателя в ядерный или бургеров в блинчики. В реальной жизни и в программировании великое благо, когда можно при необходимости легко заменять одни части другими (в разумных пределах), а не выкидывать все и писать/покупать с нуля.
                                  • 0
                                    А в чем проблема исправить кусок написанного кода а не реализацию интерфейса, ну допустим там не использовать другого наследника, а просто изменить реализацию? В этом абсолютно нет никакой разницы, однако тут интерфейсы преподносятся как великое благо. Интерфейсы не дают большей гибкости, чем конкретная реализация, это я и хочу сказать. Это инструмент для проектирование иерархии классов в подходящих для этого задачах.
                                    • 0
                                      Удобство интерфейсов тут в том, что реализация вынесена в отдельный файл, и случайно испортить соседний код при этом невозможно.
                                    • 0
                                      Первое что приходит в голову: у вас есть какой-нибудь класс FileSystemStorage, который используется для сохранения картинок в локальной файловой системе. Этот класс используется в 100500 местах в проекте. Потом поступает требование, чтобы для хранения использовался Amazon S3. Без интерфейса мы создаем класс AmazonS3Storage, и нам приходится менять код во всех 100500 местах, где использовался FileSystemStorage. Если бы мы делали зависимости от интерфейса, а не от класса, то нам бы понадобилось исправить всего 1 строку в конфигурации DI контейнера.
                                      • 0
                                        Теоретически, если есть IDE с хорошими встроенными механизмами рефакторинга, и все 100500 мест подгружены, то замена одной реализации на другую займёт даже чуть меньше времени, чем выделение интерфейса и «натягивание» его на одну и другую реализации. Но практически лучше потратить чуть больше времени и выделить интерфейс, чтоб потом иметь возможность легко переключать и добавлять реализации.

                                        Да, и, нередко, изначально, либо код вообще тупо дублируется в 100500 местах, либо имеется класс типа FileSystemStorageUtils со статическими методами, в которых сосредоточена уже «вырефакторенная» в одно место необходимая функциональность. Тогда более значительное время займёт сам процесс внедрения инъектируемой зависимости — возможно, в каждом классе, который использует Storage, нужно будет делать поле под Storage.
                              • 0
                                В некоторых моделях ещё и возможность капремонта двигателя не предусмотрена производителем…
                            • 0
                              Современные макбуки, говорят, именно такие.
                            • 0
                              Смотря по какой цене.
                          • 0
                            Другим же альтернативным подходом, к которому я склоняюсь на данный момент, является организация работы микрокомандами по несколько человек (от 2 до 4), которая каждая делает свой проект, а с другими командами общается только на уровне интеграции.


                            Как же вы интеграцию без автоматизированного модульного тестирования делаете?
                            • 0
                              Например можно использовать интеграционные тесты :)
                              Кстати расскажите, а как можно использовать юнит-тесты для проверки корректности интеграции двух систем?
                              • 0
                                Вот у вас во время ночного прогона тестов упал интеграционный тест. Это ошибка в вашем коде, в коде смежной системы или реально ошибка интеграции?
                                • 0
                                  Если команда хорошо знает код, который пишет, то обычно быстро все становится понятно. А если команда большая, и никто уже ничего не знает, то да, юнит-тесты — единственный выход.
                                  • 0
                                    Если команда большая, что качественно писать код с моками и внедрением зависимостей, постоянным рефакторингом, непрерывным автотестированием и даже едиными стандартами форматирования — единственный выход. А рост размера команды часто становится неизбежным императивом…
                                    • 0
                                      "…, то качественно писать код ..."
                      • 0
                        Двигатели уже сейчас могут быть как дизельными и бензиновыми; разница в некоторых аспектах существенная; поэтому типизация двигателей однозначно нужна. Почему бы реализуя два типа сразу не заложить в реализацию возможность расширения под появление третьего типа?

                        Здесь по хорошему, нужно обратиться к постановщику задачи: классификатор будет допускать расширение в будущем или нет, появится ли электрокар? Насколько существенно что мы не сможем завести ему электродвигатель, если он все же появится?

                        Если нет четкого ответа «да, появится» или «да, вполне может появиться через пару лет», я бы лично выбрал вариант реализации с наименьшими текущими трудозатратами, а не тот, который было бы легко поддерживать в будущем. Кто знает, настанет ли это будущее вообще?
                    • 0
                      > Допустим, у вас есть автопарк и в нем машины разных типов (седаны, джипы, трактора, грузовики и т.д.). Разные машины имеют разные качества и — иногда — даже разное поведение.
                      > Зачем думать о наследовании, громоздить иерархии классов

                      Почему нет, давайте подумаем. Вы седан от трактора наследуете, или наоборот?
                      Про декоратор седана в трактор и думать страшно…
    • 0
      Создатели этого кода тоже так думали. Как итог — там была дырка на дырке, ломали их сервис постоянно. А кончилось вот чем. Причём рефакторить подобный код уже невозможно, об этом надо было думать раньше.
    • 0
      Или может быть источник данных из БД вдруг надо будет заменить на лоулевел драйвер флоппика? Серьезно?


      Из БД на флоппик врядли. А вот из БД на вебсервис или на EJB — легко. Лично такое приходилось делать, когда сверху спустили изменение в архитектуре. Пришлось всего лишь подменить класс DAO.

      Тот же SMTP могут потребовать заменить на какой-нибудь Enterprise Mailer Service и придется бегать по сотне тысяч строк кода выискивая обращения к SMTP
  • 0
    (не тот уровень)
  • +3
    Статья опоздала лет на 10, мир меняется и не стоит на месте, где современные вопросы и ответы?

    Зачем выделять Data Acesss из инфраструктуры?
    Где Application слой, который является клеем для инфраструктуры и бизнес логики?
    Где упрощение количества связей путем агрегатов?
    Где разделение на комманды и запросы (CQ, CQRS)?
    Где Microservices?
    Где реализация cross cutting-concerns, аспектов?
    Где средства loosely coupled messaging, благодаря которому отдельные слои могут общаться между собой?
    Как бороться с утечками функциональности в слои, которым они не принадлежат?
    Как сделать приложение масштабируемым изначально?
    Где наша любимая транзакционность в распределенной системе?

    Статья неструктурна, сначала идет логический дизайн, потом объектно-ориентированный дизайн в виде SOLID, потом инфраструктура в виде DI, потом loose coupling, c которого было бы неплохо начать. В итоге надергано с миру по нитке. Пригодится студенту написать CRUD в виде customers/orders.
    • +4
      У вас тут тем на несколько книг :) Да. Весьма поверхностная статья о просто хороших практиках. В основном для новичков. Вам очень везет, если весь код, который вы встречаете написан хотя бы так, как у меня. Мне не так везет…
    • НЛО прилетело и опубликовало эту надпись здесь
      • +3
        Microsoft .NET — Architecting Applications for the Enterprise (2nd Edition) — хороша как обзор для начала, дальше на интересующие темы уже можно копать глубже.

        (это еще, заметим, реактивные приложения не помянули)
      • 0
        У меня еще «в закладках» хабрастатья-перевод про Микросервисы
      • 0
        Найти литературу по всем вообще будет трудно, но я бы рекомендовал начать с книг, которые развивают в плане объектно-ориентированного дизайна:
        Dependency Injection In .Net by Mark Seeman.
        The Art Of Unit-Testing by Roy Osherove.

        после можно читануть:
        Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans (там много букв и напсано в духе научного эссе)
        Design Patterns by GoF

        По микросервисам Фаулер, также хороши его паттерны рефакторинга. По CQRS Udi Dahan.

        Издания Microsoft по архитектуре рекомендую читать людям с уже устоявшимися взглядами.
  • +2
    Господа, а кто может провести аналогию подходов из статьи с Python? Язык другой, намного более динамичный и гибкий, и в силу этого стиль написания программ на нем, определенно, отличается. Иногда, по старой памяти, так и тянет наворотить в Python такую же монструозную (но «правильную») архитектуру, как в Java или C#, но кажется, что она будет выглядеть неуместно.
    • +1
      Хорошо замечено и я об этом думал недавно. Стиль совсем другой. Мне кажется языки Python, Ruby(в них нет всяких этих интерфейсов и все очень сахарно) хорошо подходят для быстрой разработки прототипа, MVP. Но когда проект переходит в стадию монструозности(а это часто происходит при успехе), то более монструозные языки уже более уместны. Потому что, возвращаясь к началу статьи, все эти «монструозные» архитектуры призваны облегчить поддержку проекта в будущем. Она просто становится дешевле. Поэтому в энтерпрайз-проектах зачастую они стандарт де-факто(их 100% придется долго поддерживать).

      Ну и простой пример:
      В апреле 2009 года Twitter объявил, что перевёл значительную часть своего серверного кода с Ruby на Scala и собирается перевести оставшийся.
      Разумеется, это просто мои мысли и причины перехода твиттера на Scala мне неизвестны.
      • 0
        Разумеется, это просто мои мысли и причины перехода твиттера на Scala мне неизвестны.

        www.infoq.com/articles/twitter-java-use
      • 0
        > Разумеется, это просто мои мысли и причины перехода твиттера на Scala мне неизвестны.

        Так почитайте (например, здесь: www.redfin.com/devblog/2010/05/how_and_why_twitter_uses_scala.html) прежде чем выдумывать сказки про «монструозные языки». Скалу любят в том числе за то, что она может дать скорость разработки и гибкость динамических языков, не жертвуя типобезопасностью и скоростью выполнения.
        • 0
          Ну по ссылке выше, там выбирали между java и scala. И да, пример неудачный. Там perfomance был главной причиной. Но мысли мои про языки не поменялись. Скорость разработки и стоимость поддержки коррелируются слабо.
          • 0
            > Скорость разработки и стоимость поддержки коррелируются слабо.

            Ну, в моей практике случалось, что благодаря высокой скорости разработки было проще и дешевле выкинуть и переписать с нуля, чем поддерживать. Кстати, особенно тяжело как выкидывать, так и поддерживать код с избыточным и неадекватным применением паттернов. Но это так, музыка навеяла.
            • 0
              DeleteAllAndWriteAgain Development — хорошая методология. Но очень уж дорогая :) Может у кода, который вы выкидывали был тот самый фатальный недостаток?
              • +2
                Инфраструктура кода, которая обспечивает «красивую» поддержку кода обычно может занимать места больше чем сам эффективный код, а ведь ее тоже надо поддерживать. Применение современных модных паттернов порой приводит к несоразмерному раздуванию кода, а значит к повышению вероятности ошибок. Также больший объем кода требует больше тестирования, а если это юнит-тестирование, то дополнительного времени разработчиков. При том что юнит тесты ничего не гарантируют.

                И возникает вопрос, что лучше: 100 строк простого кода без тестов или 1000 строк кода покрытого тестами. У меня нет однозначного ответа на этот вопрос, но лично мне приятнее первый вариант.

                Также не стоит забывать о вопросах производительности, конечно это не всегда самое важное, но современные тенденции запуска хелоуворлда на кластерах несколько удивляют.
                • 0
                  что лучше: 100 строк простого кода без тестов или 1000 строк кода покрытого тестами
                  Если код будет расти, то лучше 1000 строк покрытые тестами. Хотя есть компромиссное решение: сначала 100 строк простого кода без тестов, а если проект взлетит и будут деньги, то первым делом переписать их в 1000 строк качественного кода, покрытого тестами. Если же проект не взлетит, то хоронить его можно прямо вместе с техническим долгом.
                  • 0
                    Получается, что вместо одного программиста, Вам будет нужно десять. Все еще хотите код покрытый тестами?

                    А вы не думали, что такая гадостная вещь, как регрессия, в основном происходит не потому что в команде одни осьминоги с кривыми щупальцами, а потому что в проекте там много кода, что нужна большая команда, а в большой команде никто уже целиком картину не видит и может запросто написать код, который сломает старый? Получается, что юнит-тесты это отличный механизм борьбы с регрессией, но применение этого подхода косвенно регрессию провоцирует…
                    • 0
                      Получается, что юнит-тесты это отличный механизм борьбы с регрессией, но применение этого подхода косвенно регрессию провоцирует
                      Скажем так, позволяет не бояться.
                    • 0
                      Получается, что вместо одного программиста, Вам будет нужно десять.
                      С ростом размеров команды нечто подобное неизбежно. Непрерывное автотестирование и прочие гибкие и прогрессивные методики позволяют при росте команды сохранить управляемость процесса и затормозить снижение производительности.
                    • 0
                      в большой команде никто уже целиком картину не видит и может запросто написать код, который сломает старый
                      Вот поэтому надо 1) научиться ориентироваться в коде не «глазами» по «целой картине», а «на ощупь» 2) получить адекватную обобщённую «карту» без деталей всей картины для чего, по крайней мере, 2.1) обеспечить, чтобы система была такова, что её можно было бы «распилить» на кубики, соответствующие объектам карты, чтоб между кубиками в реале не было тех связей, которых нет на карте, и чтоб карта была читабельна (т.е. обеспечить нормальную декомпозицию, отсутствие ненужных связанностей и т.д.). 3) переключить парадигму работы с кодом с «работать со своим кодом и знать его как свои пять пальцев» на «уметь работать с чужим кодом, в котором изначально разбираешься приблизительно», для чего 3.1) научиться читать чужой код, 3.1.1) освоить средства IDE, которые помогают читать чужой код 3.2) научиться писать легко читаемый код, 3.2.1) освоить средства IDE, которые помогают писать более легкочитаемый код (рефакторинг там всякий и прочее «причёсывание» кода) 3.2.1.1) научиться писать код в стиле «быстро написал (говно)код — тут же 'причесал' до большей читаемости и компактности», и вследствие этого 4) научиться «в одно лицо» «менеджить» в десят раз больше кода, чем при подходе «работаю только со своим кодом, который знаю как свои пять пальцев».
                    • +1
                      Получается, что вместо одного программиста, Вам будет нужно десять.

                      Иногда лучше иметь 10 программистов и возможность подключить ещё 20 если будет нужно, чем одного и проект который кроме него никто никогда не поймёт.
              • 0
                В самую точку! Конечно я ненавижу чужой код да и свой собственный за компанию. И если нахожу даже единственную несчастную опечатку, то разумеется удаляю сразу все, вместе с репозиторием.

                Теперь серьезно. Если я вижу подсистему, которая в силу проблем архитектуры/дизайна не может справиться с возложенными задачами, бывает, что проще переписать с нуля, чем расшибаться в лепешку и лепить костыли вокруг очевидных косяков. Считаете, это всегда будет дороже? Почему?
                • 0
                  Нет конечно. Что дешевле — переписать или поддерживать старое — часто возникающий вопрос. Но если стараешься писать с учетом будущей поддержки, то вероятность, что код отправится на помойку, существенно ниже.
                  • 0
                    Стараешься писать с учетом будущей поддержки, накручиваешь все паттерны которые только знаешь для решения существующих и потенциальных проблем… А потом бац, требования поменялись, и пилить-то совсем другое надо, как оказалось. И код отправляется в помойку, независимо от всех стараний.

                    Я не утверждаю, что так бывает всегда, на всех проектах. Но бывают проекты, где решить текущую задачу быстро и просто важнее, чем думать о поддержке, которой может никогда и не случиться, если нет новых фич, нет привлеченных ими пользователей, и фирма обанкротилась.
                    • 0
                      > накручиваешь все паттерны которые только знаешь для решения существующих

                      А вы что делаете с существующими проблемами?
                  • 0
                    Тут есть еще такой аспект. Чаще всего, как я уже и говорил, интерфейсы делаются не первично, а как прокси для уже некоторой существующей реализации, поэтому говорить о полноценной абстрактности достаточно сложно. Единственное, что чего они отлично подходят, так это для юнит тестов, но это уже совсем другая история.

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

                    А если вдруг потребовалось нечто, что требует изменения интерфейса, какая разница, тащить через весь код обновленный интерфейс, или конктрит реализацию?
                    • 0
                      У меня интерфейс почти всегда первичен. Привык так проектировать.
                      • 0
                        Не уверен, что такое возможно. Вот например в вашем примере, интерфейс — это копия конкретной реализации. И как можно первично все предусмотреть, ведь делая интерфейс все обычно опираются на то, что уже будет под ним. Это как сказать, что можно например спроектировать отличный интерфейс для _любой_ базы данных, изначально разрабатывая его под какую-то конкретную одну…
                        • 0
                          Ну вот прямо конкретно сейчас я для средненького проекта создаю интерфейс IConfiguration и я пока даже не представляю как и где будут храниться настройки. Реализую я этот интерфйес может через недельку.
                    • 0
                      Интерфейсы такие интерфейсы… У них обычно бывают не потомки (которые тоже могут быть только интерфейсами и реализовывать множественное наследование), а реализации. Классу ничто не мешает реализовывать несколько интерфейсов. Новый метод можно вынести в отдельный интерфейс или в интерфейс-потомок вместо того, чтоб исправлять имеющийся интерфейс.
    • 0
      Насколько я поверхностно знаком, в Python нет нормальных интерфейсов как в Java, зато есть множественное наследование. К тому же в Python типизация динамическая вместо статической как в Java — вплоть до того, что поля класса не имеют типа, а только имя. В Java инъектор, чтобы понять, объект какого класса создавать или искать, смотрит на тип поля, в Python же этой зацепки нет.

      Вся монструозность правильных творений на Java проистекает из потребности создать гибкую архитектуру из «жёстких», максимум «поворотных» с одной степенью свободы, компонентов — получается длинная конструкция из шарниров. В Scala бОльшую часть этой «шарнирности» предусмотрительно убрали «под капот» — в результате получили смесь краткости скриптовых функциональных языков с возможностью опираться на строгую типизацию.
      • 0
        Да, звучит здраво, сам приходил к такому выводу. Но не отвечает на вопрос, так что же там с Python? Как поступают на нем, чтобы поддержать большой проект на плаву.
  • –3
    Single responsibility, Dependency Injection — не хватает раскрытия смысла простыми словами. Что-то мне подсказывает, что даже простое знание английского не всегда тут поможет.
    • 0
      Бывает ещё привычка гуглить. Тоже иногда помогает…
      • 0
        В контексте данного обсуждения — это список первого (основного) уровня вложенности. Мне показалось, что тут автору стоило бы раскрыть эти термины. Иначе теряется смысл всего пункта.
        • 0
          Я эти термины всю статью фактически раскрываю. Но если вам нужно именно определение, то я действительно рассчитывал, что кому надо — загуглят. Зачем копипастить? :)
  • +2
    А еще очень хорошо и полезно бывает второстепенные задачи решать с помощью AOP. Например, то же логирование упрощается, и не приходится в каждый обработчик инъектить еще и логгер.
  • 0
    В довесок к хорошей статье скажу что и события не обязательны — решать обработку сообщений можно и на уровне данных, имею ввиду отдельную джобу, которая выгребает заказы, которым еще не было отправлено в сообщение, помещает из в очередь, а оттуда они уже рассылаются. В таком подходе есть место для маневров масштабируемости и полной разгрузки основного сервиса с которым взаимодействуют пользователи
    • 0
      А тестировать эту «отдельную джобу» как?
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      В любых нормальных IDE есть функция Find Usages. Если мы применим ее для класса события OrderCreated, то мы найдем все участки кода, которые относятся к созданию заказа.
      • +2
        Особенно хорошо это работает, когда обработчик находится в другом solution.
    • 0
      Правильно понимаете. Это плата за увеличение гибкости.
    • +2
      Отлаживать или поддерживать приложения с использованием событий гораздо сложнее, имел опыт
      • 0
        Почему?
        • +1
          Например, без отладчика, просто читая код, очень сложно разобраться, где и в какой момент возникают события, кто их получает. В этом смысле события напоминают goto.
    • 0
      Еще надо понимать, что автор использовал самодельные события вместо встроенных в framework, где всё событие выглядело бы одной строчкой:

      public event EventHandler<Order> OrderCreated = delegate { };
      
      • 0
        И сделал это не просто так. События, которые я предложил(не придумал конечно, а предложил) — гораздо более мощный механизм.
        • 0
          Я был уверен, что это было написано в образовательных целях, т.к. не вижу ни одного аргумента, зачем заниматься велосипедостроением когда не то что библиотека, а языковые средства позволяют этого добиться. Всегда считал события через интерфейсы детской болезнью и головной болью джава-разработчиков. По крайней мере монструозные примеры из последних глав «чистого кода» на шарповых событиях ужимались в разы.
          • –1
            Аргументы я прямо в статье предлагал. Отправка email не прямо сейчас, когда пользователь ждет моментального ответа от нашего веб-сервера, а чуток позже, уже из очереди обработать это событие. Мыслите шире, не привязывайтесь к инструментам языка. «Программируйте с использованием языка, а не на языке.» (с) МакКоннелл.
          • +4
            Просто события в .net и события в терминах бизнеса радикально друг от друга отличаются; в первую очередь, тем, что первые — синхронные, а вторые — нет.
            • –3
              Ах, асинхронность. Сколь много в этом слове для сердца русского слилось… Как-то я и забыл про этот «нюанс»…
  • +1
    Надо добавить ещё одно правило: научиться определять, когда пришло время остановиться в плане разделения на кирпичики. Представьте на минутку проект из ~10 тысяч файлов с кодом (не считая шаблонов...), где только главный Solution состоит из пары-тройки десятков проектов (вспомогательный сервисы так же выделены в отдельные Soltuion'ы). И что бы от какого-нибудь контроллера сквозь интерфейсы-обёртки-классы-делегирования-сервисы-новые-интерфейсы дойти до кода, который делает хоть что-нибудь конкретное — нужно десяток-другой «нырков» вглубь на каждую строчку. И в какой-то момент становится очень, очень сложно собрать в голове картину из кирпичиков и понять, «а что же черт побери конкретно делает вот этот вот кусок кода»? Плата за гибкость, другая крайность относительно «спагетти-кода».
    • 0
      научиться определять, когда пришло время остановиться в плане разделения на кирпичики

      Я сам-то не уверен, что умею :) Поэтому постеснялся писать это.
  • 0
    Очень интересно услышать от автора практические примеры применения SOLID по части open / closed principle или liskov substitution principle
    • 0
      Я сосредатачиваюсь на S. Остальное как-то само собой складывается. Я приводил в статье ссылку на другую, как раз об этом.
      habrahabr.ru/company/skbkontur/blog/260781
      Особенно принцип Лисков. Я не наследую реальные обьекты. Только абстрактные.
  • 0
    На мой взгляд, использование большого количества абстракций снижает обслуживаемость ( — maintainable) кода, но повышает расширяемость (+ extendability). Пример: Angular.js. Очень мощный и трудно поддерживаемый. К тому же, абстракции очень сложный инструмент и легко может выйти неудачное архитектурное решение. Пример: ZF2. С другой стороны, при грамотном подходе получаются отличные архитектуры типа Laravel или Yii.
  • 0
    вы должны понимать, что использование IoC, Event-Driven и подобных методик — это довольно серьезное вложение в проект

    По-моему, отсутствие IOC = очень странный проект. В OWIN уже встроили готовый, да и IOC-контейнеров навалом, ставятся из nuget.
  • 0
    Отличная light-статья.

    Но такое понимание возникает только после 5-10 летних проектов на саппорте. Все «быстрые на коленке» решения вылезают потом в очень неприятные попоболи. Новичку в разработке это просто не понять не почуяв, что такое разрабатывать проект больше 5 лет с изменяющимися требованиями, участниками и прочими прелестями. Такие методики это уже вопрос выживания и нервного равновесия :)

    С нетерпением жду следующей статьи :)

    Единственный вопрос по event-driven — как обрабатываете исключительную ситуацию в обработчике события? Или если код вынесен в обработчик то его успешной выполняемостью можно пренебречь? Пример из жизни: для одного из пользователей b2b системы необходим плагин который по OrdersCreated делает выгрузку в API третьей стороны. Самое понятное — сделать обработчик события дабы не засорять Orders контроллер. но обработка этого события критична. Т.е. такой подход нормальный?:

    var order = new Order(); this.repository.Save(order); try{ handlers.OrderCreated(order); } catch { this.repository.Rollback(); }

    • 0
      В «чистом» event-driven ошибка в обработчике порождает новое событие, обработчик которого откатывает транзакцию.
      • 0
        а не слишком оверхедно? т.е. помимо OrdersCreatedHandler еще OrdersCreatedHaldlerFailedHandler? :)
        • 0
          А события — это вообще оверхед почти всегда. Этот оверхед — плата за практически минимальную связность и, как следствие, высокую подвижность. Еще, кстати, этот подход позволяет делать систему произвольно распределенной.
      • 0
        Транзакция — вроде практически «по определению» вещь «синхронная» ввиду своей атомарности. Асинхронный блок внутри синхронного — это жесть. Нет, конечно, сделать возможно, но будет очень тормозно.
        • 0
          Я уже после написания коммента понял, что надо было уточнить: бизнес-транзакцию (платеж, покупку, создание заказа). Они как раз, чаще всего, длинные — и, как следствие, неатомарные.
          • 0
            Важное уточнение, спасибо. Обращает внимание на необязательность совпадения бизнес-транзакции и БД-транзакции. Есть ещё такие «БД-транзакции» как отмена платежа, отмена заказа… и ещё возможность такой веселухи как «ошибка отмены заказа»…
          • 0
            И как с ними работать? Если, например, платеж прошел, а покупка/создание заказа потом зафейлилась. Если это атомарная транзакция, то ничего дополнительно делать и не нужно. В случае же неатомарной транзакции получается что деньги списаны, а услуги нет.
            • 0
              А у вас своя платежная система? Не думаю. Значит, списание денег все равно происходит за границей контролируемой вами транзакции, а, следовательно, у вас должны быть способы его (списания) отката: если это карточка, то вы просто не проводите списание после блокировки, а в других случаях — делаете возврат.
              • 0
                Своя. В том смысле, что деньги хранятся у меня в бд рядом со всем остальным. Но по факту, действительно, нет разницы это внешняя система или отдельная бд-транзакция.
                • 0
                  И БД у вас одна на все на одном сервере?

                  Рано или поздно любая крупная система утыкается в то, что границы ACID-транзакции ее не устраивают, и приходится делать компенсации. Дальше вопрос техники.
                  • 0
                    Ценное замечание на тему архитектуры…
  • 0
    Спасибо. Меня как-то поставили в тупик на одном интервью вопросом «зачем нужна инъекция зависимостей». Когда я ответил что-то про крутую универсальную настраиваемую фабрику, мне сказали что-то про ответственность. Теперь я узнал детально, что там про ответственность.

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

Самое читаемое Разработка