Pull to refresh
62.43
ГК ICL
Цифровые технологии для бизнеса

Пишем maintainable код

Reading time 8 min
Views 47K
У нас сотни программных проектов на поддержке, некоторые из них поддерживаются нами почти десять лет. Нетрудно догадаться, что понятие 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 и подобных методик — это довольно серьезное вложение в проект. И вы должны быть уверены, что эти вложения окупятся. Если проект из серии «написал и забыл», то вложения окуплены не будут. Такое приложение можно написать быстрее и, как итог, дешевле. Умение найти ту самую золотую середину, которая позволит в итоге получить наиболее дешевую цену создания плюс поддержки софта, является одним из главных для серьезного программиста.
Tags:
Hubs:
+46
Comments 202
Comments Comments 202

Articles

Information

Website
icl.ru
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия