Большинство разработчиков даже с маломальским опытом сталкиваются с проблемами дизайна своего решения, когда поставленную задачу можно решить десятком разных способов и нужно выбрать определенный, более или менее адекватный вариант.
Отношение к этапу проектирования (дизайна) может быть самым разным, начиная от подхода, принятого на ранних этапах развития методологии XP, когда считалось, что дизайн и архитектура – это динозавры, которым нет места в динамично развивающемся мире agile разработки. Многие и сейчас не задумываются о дизайне решения, считая, что итеративный процесс разработки + рефакторинг сделают все за нас и хороший дизайн появится сам собой.
Есть и другая крайность, когда команда можем потратить недели в поисках идеального решения (Святого Грааля архитектора), когда дизайн будет способен «расширяться» во всех возможных направлениях, и быть настолько «гибким», что реализовать его не будет никакой возможности.
За каждым из этих крайностей любопытно наблюдать, но только если они происходят не у тебя в команде. В большинстве же случаев разумное отношение к дизайну находится где-то посередине, когда этапы дизайна и разработки тесно связаны между собой и итеративно следуют один за другим практически непрерывно. При этом каждый раз, когда разработчик сталкивается с принятием какого-либо решения, то он старается найти компромисс среди бесконечного множества требований, которые на него давят: использовать более эффективное решение, или более расширяемое; что важнее, согласованность или наличие ломающих изменений; как быть, нарушить SRP (Single Responsibility Principle) или сделать модуль более удобным в использовании; стоит ли пожертвовать сопровождаемостью кода ради эффективности и т.п.
В результате к большинству разработчиков приходят понимание, что…
Дизайн – это искусство поиска компромисса среди множества противоречивых целей и требований, важность которых может изменяться с течением времени.
Меня несколько напрягают категоричность многих авторов книг/статей или просто коллег, которые выражают свое мнение в абсолютной форме: никогда не пользуйтесь синглтонами, покрытие тестами должно быть 100%, открытые поля – всемирное зло. Проблема таких высказываний в том, что они вполне корректны в большинстве случаев, но это не значит, что этим советам следует слепо верить не задумываясь.
Да, в большинстве случаев открытые поля это и правда опасная практика, но кто мешает нам их использовать в структурах (значимых типах .NET-а) для взаимодействия с существующими системами? Да, юнит-тесты – это отличная штука, но это не значит, что без 100%-го покрытия тестами ваш проект провалится. Именно по этой причине, когда опытному программисту задается вопрос «Что лучше?», то «мудрый перец» не станет отвечать на него «в лоб», а задаст уточняющий вопрос, чтобы понять контекст решаемой задачи? Ведь то, что разумно применять в одном случае (писать юнит тесты для продакш кода), может быть совершенно не нужным в другом (а вдруг речь идет об однодневном прототипе?).
По этой же причине мы очень часто осуждаем решения других и качество этого решение зачастую измеряется количеством WTF в секунду; но дело в том, что зачастую у нас просто нет достаточно информации о том, в каких условиях оно было принято. Вот пример: в языке C# существует сверх сомнительная возможность, под названием ковариантность массивов. Это означает, что следующий фрагмент кода является корректным и приведет к ошибке времени выполнения, а не компиляции:
Причина появления этой возможности связана с тем, что она была с первой версии в языке Java, а при разработке языка C# в конце 90-х важность «подсадить» на новый язык существующих программистов была решающей. Именно поэтому используется привычный С-подобный синтаксис, который уже был знаком программистам С/С++ и Java, пусть у него и есть свои недостатки. Подобные решения могут казаться сомнительными сейчас, но понимание причин, побудивших к их принятию (согласованность с другими языками vs возможность ошибок времени выполнения) дают понять, почему языки или библиотеки реализованы так, а не иначе, и что влияет на их развитие.
Нельзя судить о чужом решении в абсолютных категориях, чтобы правильно оценить принятое решение, нужно понять, под давлением каких ограничений оно было принято.
Сколько раз вам говорили: «Эй, ну ты же сам говорил, что так не нужно делать, а сам делаешь!» или «Ну, блин, ты даешь, мы же с тобой все уже обсудили, а теперь говоришь, что нужно по-другому!». Дело все в том, что наши решения меняются с течением времени. Со временем важность одних критериев (эффективность кода) может уменьшаться, а важность других критериев (согласованность архитектуры и простота сопровождения) может возрастать.
Я никогда не буду отстаивать свое решение до конца, только чтобы кому-то что-то доказать, мое отношение меняется при появлении новых фактов или изменении веса существующих критериев. Это происходит постоянно при уточнении требований или когда становится очевидным, что в данном конкретном случае производительность важнее сопровождаемости или удобством использования можно пожертвовать, поскольку количество клиентов будет ограничено. Да и само решение может влиять на задачу настолько, что исходное решение изменится до неузнаваемости.
Определить качественные характеристики хорошего дизайна сложно, как и сложен тот путь, который должен пройти разработчик, чтобы его добиться. Большинство из нас прекрасно видит проблемы в дизайне, особенно если автором этого дизайна является кто-то другой. Определить проблемы в собственном дизайна сложнее, прежде всего потому, что все решение лежит у тебя в голове, и каждый аспект дизайна кажется очевидным.
Отличный способ найти проблемы дизайна – это посмотреть на него со стороны самому, или попытаться объяснить его кому-то. Хороший дизайн – это дизайн, который вы сможете объяснить своему коллеге за 10 минут, не жертвуя при этом полнотой или точностью. Если же при объяснении «как это работает» приходится учитывать множество факторов, закапываться во множество деталей, и 15 раз возвращаться к одному и тому же, то с дизайном явно что-то не так. Хороший дизайн зачастую оказывается достаточно простым, с минимальным количеством хитросплетений, и минимумом лишних или неочевидных связей. Хороший дизайн, как и хорошая архитектура, борется с неотъемлемой сложностью, а не привносит дополнительную сложность, которой и так с избытком хватает в самой природе решаемой задачи.
Сама степень формализма и качество дизайна – это тоже компромисс между стоимостью ошибки и затратами на ее исправления. Если интерфейс между двумя модулями более или менее определен, то вполне возможно стоит уже переходить к их реализации, а не доводить этот интерфейс до идеала, опасаясь, что при его изменении придется поправить аж 5 строк существующего кода двум разработчикам, которые сидят за соседними столами.
Хороший дизайн – это не самоцель, так что не нужно стремиться к идеальному решению. Как и во многих других вещах, здравый смысл и прагматизм, являются вашими лучшими советчиками.
—
Определение дизайна, данное в середине заметки, является вольным переводом (с некоторым дополнением) мысли Эрика Липперта, которую он выразил в одном из своих постов: Design is the art of compromising amongst various incompatible design goals.
Отношение к этапу проектирования (дизайна) может быть самым разным, начиная от подхода, принятого на ранних этапах развития методологии XP, когда считалось, что дизайн и архитектура – это динозавры, которым нет места в динамично развивающемся мире agile разработки. Многие и сейчас не задумываются о дизайне решения, считая, что итеративный процесс разработки + рефакторинг сделают все за нас и хороший дизайн появится сам собой.
Есть и другая крайность, когда команда можем потратить недели в поисках идеального решения (Святого Грааля архитектора), когда дизайн будет способен «расширяться» во всех возможных направлениях, и быть настолько «гибким», что реализовать его не будет никакой возможности.
За каждым из этих крайностей любопытно наблюдать, но только если они происходят не у тебя в команде. В большинстве же случаев разумное отношение к дизайну находится где-то посередине, когда этапы дизайна и разработки тесно связаны между собой и итеративно следуют один за другим практически непрерывно. При этом каждый раз, когда разработчик сталкивается с принятием какого-либо решения, то он старается найти компромисс среди бесконечного множества требований, которые на него давят: использовать более эффективное решение, или более расширяемое; что важнее, согласованность или наличие ломающих изменений; как быть, нарушить SRP (Single Responsibility Principle) или сделать модуль более удобным в использовании; стоит ли пожертвовать сопровождаемостью кода ради эффективности и т.п.
В результате к большинству разработчиков приходят понимание, что…
Дизайн – это искусство поиска компромисса среди множества противоречивых целей и требований, важность которых может изменяться с течением времени.
Контекст важен
Меня несколько напрягают категоричность многих авторов книг/статей или просто коллег, которые выражают свое мнение в абсолютной форме: никогда не пользуйтесь синглтонами, покрытие тестами должно быть 100%, открытые поля – всемирное зло. Проблема таких высказываний в том, что они вполне корректны в большинстве случаев, но это не значит, что этим советам следует слепо верить не задумываясь.
Да, в большинстве случаев открытые поля это и правда опасная практика, но кто мешает нам их использовать в структурах (значимых типах .NET-а) для взаимодействия с существующими системами? Да, юнит-тесты – это отличная штука, но это не значит, что без 100%-го покрытия тестами ваш проект провалится. Именно по этой причине, когда опытному программисту задается вопрос «Что лучше?», то «мудрый перец» не станет отвечать на него «в лоб», а задаст уточняющий вопрос, чтобы понять контекст решаемой задачи? Ведь то, что разумно применять в одном случае (писать юнит тесты для продакш кода), может быть совершенно не нужным в другом (а вдруг речь идет об однодневном прототипе?).
По этой же причине мы очень часто осуждаем решения других и качество этого решение зачастую измеряется количеством WTF в секунду; но дело в том, что зачастую у нас просто нет достаточно информации о том, в каких условиях оно было принято. Вот пример: в языке C# существует сверх сомнительная возможность, под названием ковариантность массивов. Это означает, что следующий фрагмент кода является корректным и приведет к ошибке времени выполнения, а не компиляции:
object[] o = new string[] {“1”,”2”, “3”};
o[0] = 42;
Причина появления этой возможности связана с тем, что она была с первой версии в языке Java, а при разработке языка C# в конце 90-х важность «подсадить» на новый язык существующих программистов была решающей. Именно поэтому используется привычный С-подобный синтаксис, который уже был знаком программистам С/С++ и Java, пусть у него и есть свои недостатки. Подобные решения могут казаться сомнительными сейчас, но понимание причин, побудивших к их принятию (согласованность с другими языками vs возможность ошибок времени выполнения) дают понять, почему языки или библиотеки реализованы так, а не иначе, и что влияет на их развитие.
Нельзя судить о чужом решении в абсолютных категориях, чтобы правильно оценить принятое решение, нужно понять, под давлением каких ограничений оно было принято.
Ну, ты же говорил?!!
Сколько раз вам говорили: «Эй, ну ты же сам говорил, что так не нужно делать, а сам делаешь!» или «Ну, блин, ты даешь, мы же с тобой все уже обсудили, а теперь говоришь, что нужно по-другому!». Дело все в том, что наши решения меняются с течением времени. Со временем важность одних критериев (эффективность кода) может уменьшаться, а важность других критериев (согласованность архитектуры и простота сопровождения) может возрастать.
Я никогда не буду отстаивать свое решение до конца, только чтобы кому-то что-то доказать, мое отношение меняется при появлении новых фактов или изменении веса существующих критериев. Это происходит постоянно при уточнении требований или когда становится очевидным, что в данном конкретном случае производительность важнее сопровождаемости или удобством использования можно пожертвовать, поскольку количество клиентов будет ограничено. Да и само решение может влиять на задачу настолько, что исходное решение изменится до неузнаваемости.
Хороший дизайн
Определить качественные характеристики хорошего дизайна сложно, как и сложен тот путь, который должен пройти разработчик, чтобы его добиться. Большинство из нас прекрасно видит проблемы в дизайне, особенно если автором этого дизайна является кто-то другой. Определить проблемы в собственном дизайна сложнее, прежде всего потому, что все решение лежит у тебя в голове, и каждый аспект дизайна кажется очевидным.
Отличный способ найти проблемы дизайна – это посмотреть на него со стороны самому, или попытаться объяснить его кому-то. Хороший дизайн – это дизайн, который вы сможете объяснить своему коллеге за 10 минут, не жертвуя при этом полнотой или точностью. Если же при объяснении «как это работает» приходится учитывать множество факторов, закапываться во множество деталей, и 15 раз возвращаться к одному и тому же, то с дизайном явно что-то не так. Хороший дизайн зачастую оказывается достаточно простым, с минимальным количеством хитросплетений, и минимумом лишних или неочевидных связей. Хороший дизайн, как и хорошая архитектура, борется с неотъемлемой сложностью, а не привносит дополнительную сложность, которой и так с избытком хватает в самой природе решаемой задачи.
Сама степень формализма и качество дизайна – это тоже компромисс между стоимостью ошибки и затратами на ее исправления. Если интерфейс между двумя модулями более или менее определен, то вполне возможно стоит уже переходить к их реализации, а не доводить этот интерфейс до идеала, опасаясь, что при его изменении придется поправить аж 5 строк существующего кода двум разработчикам, которые сидят за соседними столами.
Хороший дизайн – это не самоцель, так что не нужно стремиться к идеальному решению. Как и во многих других вещах, здравый смысл и прагматизм, являются вашими лучшими советчиками.
—
Определение дизайна, данное в середине заметки, является вольным переводом (с некоторым дополнением) мысли Эрика Липперта, которую он выразил в одном из своих постов: Design is the art of compromising amongst various incompatible design goals.