Критерии простоты

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


Первый критерий


Особенности мозга человека таковы, что он плохо хранит и отличает более 7–9 элементов в одном списке при оптимальном их количестве 1–3.
Отсюда рекомендация — в идеале иметь не более трех членов в интерфейсе, трех параметров в методе и так далее и не допускать увеличение их количества свыше девяти.
Этот критерий может быть реализован средствами статического анализа.


Второй критерий


Самое важное — в первых двух строках любого класса.


  1. Имя, параметры типа, реализованные интерфейсы.
  2. Параметры конструктора.

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


  1. Избегайте наследования реализаций
  2. Избегайте внедрения зависимостей иначе, чем через параметры конструктора.

И этот критерий может быть реализован средствами статического анализа.


Третий критерий


Один тип — одна задача. Контракты — интерфейсам, реализации — классам, алгоритмы — методам!
Мультитулы хороши только если ничего другого под рукой вообще нет. Благодаря первым двум критериям писать такие типы несложно. Да, это принцип единственной ответственности (и разделения интерфейсов до кучи)
А вот здесь статический анализ придется доверить человеку.


Четвертый критерий


Ограничения — такая же легальная часть контракта, как и сигнатуры методов.
И следовательно…


  1. Комментарии в контракте — почти всегда полезны, комментарии в коде реализации почти всегда вредны.
  2. DTO — полноценные объекты, чья примитивность поведения вознаграждается автоматической сериализацией.
  3. Неизменяемые объекты — вознаграждают удобством валидации, отсутствием гонок и лишнего копирования.
  4. Статический метод — полноценный класс без состояния, все плюшки от неизменяемых объектов плюс меньший трафик памяти.
  5. Анонимные делегаты и лямбды — заменяют связку интерфейс-класс с одним методом, позволяя выкинуть два лишних типа и продемонстрировать сигнатуру вызова прямо в коде.
  6. Добавьте остальные «ненастоящие объекты» по вашему вкусу.

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


Пятый критерий


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


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

Да, это принцип подстановки Лисков в максимально простой и практичной форме.
Статический анализатор может сообщать об использовании нежелательного наследования реализации.
Также не стоит прятать типы или методы «на всякий случай». Публичным должно быть все, кроме того, что необходимо спрятать как детали реализации.


Шестой критерий


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


Седьмой критерий


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


  1. IService — служба, необходимая для реализации
  2. Lazy — служба, которой может и не быть при старте, для начала использования надо прочитать свойство Value, при первом обращении возможна пауза.
  3. Task — ресурс, который получается асинхронно
  4. Func — параметризованная фабрика ресурсов
  5. Usable — ресурс, который нужен до определенного момента, об окончании использования можно сообщить, вызвав метод Dispose.

Увы, статический анализ мало чем тут поможет.


Восьмой критерий


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


Девятый критерий


Зависимости типов между собой необходимо ограничивать


  1. Циклов в графе типов надо избегать
  2. Также надо избегать прямых ссылок реализаций друг на друга.
  3. Крайне желательно использовать паттерны разбиения типов на слои, добавляя ограничение на зависимости между типами разных слоев друг на друга.

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


Десятый критерий


То же, что и девятый критерий, но для сборок. Сильно упрощает жизнь и ускоряет сборку, особенно если сразу проектировать с его учетом. Для сборок с контрактами проще поддерживать обратную совместимость (опубликованные контракты не меняются). Сборки же с реализациями можно заменять целиком — они все равно друг от друга напрямую не зависят.
Средствами статического анализа можно воспретить одним сборкам ссылаться на другие.


Одиннадцатый критерий


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


Итоги

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

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

  • 18 октября 2016 в 04:29

    +2

    Про 1–3 метода, свойства и т. п., мне кажется, это перебор. Минимальная сложность решения задачи эквивалентна сложности самой задачи и не всегда можно 1 цельный логический объект разбить на кучу маленьких идеальных объектов по 1–3 метода. Если у тебя 5 связанных свойств в объекте, то методов для работы с ними скорее всего будет еще больше, а при попытке разбить этот объект на более мелкие прийдется как минимум это состояние в 5 полей пробрасывать между мелкими объектами, что возможно еще и к нарушению инкапсуляции приведет.
    • 18 октября 2016 в 08:32

      0

      Идеал не всегда достижим, но совершенно нормально знать о нем и стремиться к нему.

      • 18 октября 2016 в 10:04

        0

        Лучшее — враг хорошего.

        Вы написали советы, описали плюсы, указали что детектируется до\во время компиляции. В итоге в статье не хватает двух важных моментов: недостатков от ваших советов, а также границ применимости (или хотя бы контрпримеров). На мой взгляд, статья «Когда не нужно следовать этим советам» будет не менее полезна, чем ваша. Хотя и ваша содержит немало здравых мыслей.

        • 18 октября 2016 в 11:12

          0

          недостатков от ваших советов

          Главная особенность указана — будет много маленьких типов.


          а также границ применимости

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

  • 18 октября 2016 в 05:47

    +6

    Согласно первому критерию, их оптимальное количество должно быть 3, максимум 9, а у вас их аж 11. Противоречие.
    • 18 октября 2016 в 08:28

      –1

      А кто вам сказал, что я держу все этот в голове в виде линейного списка? Это результат обратной разработки — попытки анализа, почему мне одно представляется более простым и удобным, чем другое. Критерии относятся к разным уровням предпочтений и связаны между собой не только соседством в списке.

  • 18 октября 2016 в 07:50

    0

    Какая шикарная статья, но жаль что нет визуализации. Хотелось бы картинок в стиле Хорошо/плохо.
    • 18 октября 2016 в 08:09

      0

      К «хорошо» / «плохо» нужно еще добавить «модно».
      А то я помню сперва всех заставляли венгерскую нотацию и комментирование кода, а теперь все это запрещают.
      • 18 октября 2016 в 08:30

        0

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

  • 18 октября 2016 в 08:15

    0

    В промышленном программировании приоритеты стоят несколько по-другому:
    1. Надёжность и отказоустойчивость;
    2. Время доступа к данным;
    3. Время на изменение/корректировку алгоритмов.

    Кроме того, сложность системы определяет на количество объектов, а количество связей между ними. Гирлянда из 10 000 лампочек ничуть не сложнее гирлянды из 10 лампочек.

    • 18 октября 2016 в 08:23

      0

      Вы приводите критерии другого уровня — внешнее качество. А я о том, что позволяет добиться такого качества максимально дешево.
      Гирлянда из 10 тысяч лампочек соответствует коллекции из 10 тысяч элементов, а не классу с 10 тысячами свойств.

      • 18 октября 2016 в 08:44

        0

        Вы говорите об этом прямым текстом
        Особенности мозга человека таковы, что он плохо хранит и отличает более 7–9 элементов в одном списке при оптимальном их количестве 1–3.
        и качество и количество никак не упоминаете и не разделяете.
        • 18 октября 2016 в 08:51

          0

          Неужели вы помните все 10 тысяч лампочек из гирлянды?
          Или это все таки больше похоже на Гирлянда<Лампочка> { Количество: 10000 } ?

          • 18 октября 2016 в 09:19

            0

            1. Да (точнее, не я лично, а электрик).
            2. Похоже это может быть на что угодно. Сама система и описание этой системы — несколько разные вещи.
            • 18 октября 2016 в 09:40

              0

              1. Это противоречит вашим словам про одинаковую сложность гирлянд разной длины. По крайней мере для электрика.
              2. Так программисты имеют дело как раз с описанием системы на языке программирования.
              • 18 октября 2016 в 10:04

                0

                1. Не вижу противоречия. Исходя из вашего же определения, которое я уже процитировал, согласно которому сложность вы определяете от количества элементов, способных к хранению и различению в мозгу человека.
                2. Ну да. Но всё же нужно разделять сложность системы и сложность описания системы.
                • 18 октября 2016 в 11:08 (комментарий был изменён)

                  0

                  способных к хранению и различению в мозгу человека.

                  … в виде линейного списка. Даже телефонные номера из 10 цифр люди запоминают, разбивая их на группы (обычно 3–3–2–2).
                  Вы у гирлянды тоже помните размер, а не каждую лампочку. И «электрик» у программистов тоже есть — это компьютер.


                  Ну да. Но всё же нужно разделять сложность системы и сложность описания системы.

                  И моя статья как раз про сложность описания.
                  PS: Человек может помнить на много порядков больше информации, но вся эта память — ассоциативная. Поэтому гирлянда на 10 тысяч лампочек будет экземпляров типа с двумя свойствами, а не с 10 тысячами. А тип с 10 тысячами свойств будет мусором, с которым нереально работать как с целым.

© Habrahabr.ru