четверг, 2 октября 2014 г.

О принципах проектирования

Цикл статей о SOLID принципах

--------------------------------------------------

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

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

Со временем и молодой разработчик, и молодой менеджер придут к пониманию того, что это невозможно. Невозможно найти идеальный абстрактный дизайн, поскольку слова «идеальный» и «абстрактный» противоречат друг другу. Дизайн – это постоянный поиск компромисса между противоречивыми требованиями: производительностью и читабельностью, простотой и расширяемостью, тестируемостью и цельностью решения.

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

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

Помимо формальных критериев, есть универсальные понятия хорошего дизайна – слабая связанность (low coupling) и сильная связность (high cohesion). Данные свойства полезны, но слишком неформальны.

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

Бертран Мейер в своей книге “Agile!: The Good, The Hype, and The Ugly” дает достаточное четкое определение того, что такое принцип проектирования [Meyer2014]:

“Принцип – это методологическое правило, которое выражает общий взгляд на разработку ПО. Хороший принцип является одновременно абстрактным и опровергаемым (falsifiable). Абстрактность отличает принцип от практик, а опровергаемость отличает принцип от банальности (platitude). Абстрактность означает, что принцип должен описывать универсальное правило, а не конкретную практику. Банальность означает, что у разумного человека должна быть возможность не согласиться с принципом. Если никто в здравом уме не будет оспаривать предложенный принцип, то это правило будет полезным, но не интересным. Чтобы правило являлось принципом – не зависимо от вашего мнения – вы должны предполагать наличие людей, придерживающихся противоположной точки зрения”.

Уровни владения принципами проектирования

clip_image002

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

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

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

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

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

На этом этапе принципы не управляют дизайном приложения, а начинают играть скорее коммуникативную роль: «Смотри, у тебя класс делает слишком многое, значит он нарушает SRP! А у этого класса есть два вида клиентов, что нарушает ISP!».

ПРИМЕЧАНИЕ
В боевых искусствах принято выделять три стадии мастерства: сю, ха, и ри (Shu, Ha, Ri). На первой ступени находится ученик, который лишь повторяет движения за мастером. На второй ступени ученик начинает освобождаться от правил и сам начинает решать, когда им следовать, а когда – нет. На третьей стадии правила пропадают, ученик становится мастером и может сам эти правила создавать. Эта же модель, но несколько под иным соусом появилась и в американской культуре, под названием Модель Дрейфуса.

Роль принципов проектирования

Применение любого принципа проектирования имеет свою цену. Дробление класса на более мелкие составляющие, чтобы он отвечал SRP, может привести к размазыванию логики по нескольким классам (low cohesion), а иногда и к падению производительности.

Нарушение принципа открыт-закрыт может быть оправдано вопросами обратной совместимости. Мы можем игнорировать принцип замещения Лисков, поскольку наследование не всегда определяет отношение подтипов. Интерфейс может быть толстым из-за обратной совместимости или удобства использования. А инверсия зависимостей легко может подорвать инкапсуляцию и привести к полнейшему ООП головного мозга.

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

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

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

Главной движущей силой моих изменений в конечном счете является увеличившаяся сложность. Как только мне не удается удержать в голове решение, сложно по нему навигироваться или не понятно, куда вносить изменения, значит пришло время подумать, как его улучшить.

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

При этом я всегда проверяю, не привели ли меня принципы или паттерны в мир перепроектирования (overdesign): стал ли мой дизайн после внесения изменений проще? Не решаю ли я проблему, которой на самом деле не существует? Не попал ли я в сети преждевременного обобщения (premature generalization)? Иногда приходится провести несколько итераций, прежде чем удается найти разумное решение некоторой задачи.

Антипринципы проектирования

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

Anti-SRP – Принцип размытой ответственности. Классы разбиты на множество мелких классов, в результате чего логика размазывается по нескольким классам/модулям.

Anti-OCP – Принцип фабрики-фабрик. Дизайн является слишком обобщенным и расширябельным, выделяется слишком большое число уровней абстракции.

Anti-LCP – Принцип непонятного наследования. Принцип проявляется либо в чрезмерном количестве наследования, либо в его полном отсутствии, в зависимости от опыта и взглядов местного главного архитектора.

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

Anti-DIP – Принцип инверсии сознания или DI-головного мозга. Интерфейсы выделяются для каждого класса и пачками передаются через конструкторы. Понять, где находится логика становится практически невозможно.

7 комментариев:

  1. Спасибо, очень полезная статья!

    ОтветитьУдалить
  2. Анти-паттерны хороши! особенно последний

    ОтветитьУдалить
  3. Антипринципы порадовали. Наверное, каждый в свое время проходит через них :)

    ОтветитьУдалить
  4. Сергей, как вы относитесь к высказываниям Луговского о паттернах (дебильных шаблонов банды четырёх провокаторов)?
    В частности, он утвреждал что синглтон это фиговый листок скрывающий мерзкую сущность глобальной переменной или что под паттерн интерпретатор можно подогнать любой код, и вообще паттерны это кривая попытка лечить ООП, а не решать задачи предметной области.

    ОтветитьУдалить
    Ответы
    1. Да согласен я полностью с этими утверждениями. Но тут такое дело: С++ - просто ужасный язык. Означает ли это, что его не стоит учить и понимать? Есть ли разумные альтернативы в той нише, в которой он активно применяется?

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

      Т.е. вывод такое: есть ли недостатки у паттернов - масса. Означает ли это, что их не стоит знать и понимать? Нет, не означает;)

      Удалить
    2. А что за утверждения? Где с ними можно ознакомитсья? :)

      Удалить
    3. Классная статья. Порой пытался тоже самое до некоторых донести - но у меня с литературным языком слабо, безуспешно) То что SOLID и паттерны не панацея. Всё тут, и описано здорово, на русском! Обязательна к прочтению - лиды, приклейте распечатку этой статьи к каждому рабочему столу.

      Удалить