Львиная доля программистов с чистой совестью заявит, что предпочитает решать задачи просто, руководствуясь прежде всего здравым смыслом. Вот только это "просто" у каждого свое и как правило отличное от других. После одного долгого и неконструтивного спора с коллегой я решил изложить, что именно считаю простым сам и почему. Это не привело к немедленному согласию, но позволило понять логику друг друга и свести к минимуму лишние дискуссии.
Первый критерий
Особенности мозга человека таковы, что он плохо хранит и отличает более 7-9 элементов в одном списке при оптимальном их количестве 1-3.
Отсюда рекомендация — в идеале иметь не более трех членов в интерфейсе, трех параметров в методе и так далее и не допускать увеличение их количества свыше девяти.
Этот критерий может быть реализован средствами статического анализа.
Второй критерий
Самое важное — в первых двух строках любого класса.
- Имя, параметры типа, реализованные интерфейсы.
- Параметры конструктора.
В результате мы знаем о классе все, что необходимо для его использования (имя, параметры, входы, выходы), даже не заглядывая в оставшийся код.
Разумеется, это работает при условии ограничений:
- Избегайте наследования реализаций
- Избегайте внедрения зависимостей иначе, чем через параметры конструктора.
И этот критерий может быть реализован средствами статического анализа.
Третий критерий
Один тип — одна задача. Контракты — интерфейсам, реализации — классам, алгоритмы — методам!
Мультитулы хороши только если ничего другого под рукой вообще нет. Благодаря первым двум критериям писать такие типы несложно. Да, это принцип единственной ответственности (и разделения интерфейсов до кучи)
А вот здесь статический анализ придется доверить человеку.
Четвертый критерий
Ограничения — такая же легальная часть контракта, как и сигнатуры методов.
И следовательно...
- Комментарии в контракте — почти всегда полезны, комментарии в коде реализации почти всегда вредны.
- DTO — полноценные объекты, чья примитивность поведения вознаграждается автоматической сериализацией.
- Неизменяемые объекты — вознаграждают удобством валидации, отсутствием гонок и лишнего копирования.
- Статический метод — полноценный класс без состояния, все плюшки от неизменяемых объектов плюс меньший трафик памяти.
- Анонимные делегаты и лямбды — заменяют связку интерфейс-класс с одним методом, позволяя выкинуть два лишних типа и продемонстрировать сигнатуру вызова прямо в коде.
- Добавьте остальные "ненастоящие объекты" по вашему вкусу.
Статический анализ тут может помочь лишь рекомендациями по использованию более простых вариантов типов, если их увидит.
Пятый критерий
Максимальное удобство для повторного использования.
Применяйте наследование интерфейсов, избегайте наследования реализаций и получите сразу три источника повторного использования кода
- Повторное использование контрактов — очень полезно, даже если ни одна готовая реализация не подошла.
- Повторное использование реализаций — везде где принимается контракт можно задействовать уже написанную реализацию.
- Повторное использование клиентского кода — весь код, работающий с интерфейсом (включая тесты), можно переиспользовать с каждой новой реализацией.
Да, это принцип подстановки Лисков в максимально простой и практичной форме.
Статический анализатор может сообщать об использовании нежелательного наследования реализации.
Также не стоит прятать типы или методы "на всякий случай". Публичным должно быть все, кроме того, что необходимо спрятать как детали реализации.
Шестой критерий
Ваши типы должны быть более простыми для композиции, декорирования, адаптации, организации фасада и т.п., чем для изменения. При соблюдении предыдущих критериев этого добиться несложно.
Это позволит как можно больше изменений свести к добавлению нового кода вместо переписывания текущего, что уменьшит регрессию и необходимость править еще и старые тесты.
Идеальный пример — методы расширения интерфейсов, добавляющие функциональность с одной стороны и никак не меняющие исходные контракт и реализацию с другой.
Да, это принцип открытости-закрытости.
Нет, статический анализ тут мало чем поможет
Седьмой критерий
Типы параметров конструктора должны как можно больше говорить о том, как надо использовать их значение.
Например:
- IService — служба, необходимая для реализации
- Lazy<IService> — служба, которой может и не быть при старте, для начала использования надо прочитать свойство Value, при первом обращении возможна пауза.
- Task<IResource> — ресурс, который получается асинхронно
- Func<IArgument, IResource> — параметризованная фабрика ресурсов
- Usable<IResource> — ресурс, который нужен до определенного момента, об окончании использования можно сообщить, вызвав метод Dispose.
Увы, статический анализ мало чем тут поможет.
Восьмой критерий
Маленькие типы лучше больших, так как даже плохо спроектированные или реализованные маленькие типы намного проще исправить, переписать или удалить по сравнению с большими.
Большое количество маленьких типов не является само по себе фатальной проблемой, так как типы — это не список, а граф. В голове одновременно достаточно держать связи текущего типа, а их количество ограничивается первым критерием.
Девятый критерий
Зависимости типов между собой необходимо ограничивать
- Циклов в графе типов надо избегать
- Также надо избегать прямых ссылок реализаций друг на друга.
- Крайне желательно использовать паттерны разбиения типов на слои, добавляя ограничение на зависимости между типами разных слоев друг на друга.
Целью является максимальное упрощение графа зависимостей типов. Это помогает как при навигации по коду, так и при определении возможного аффекта от тех или иных изменений.
Да, в этот критерий входит принцип инверсии зависимостей.
Да, тут может помочь статический анализ.
Десятый критерий
То же, что и девятый критерий, но для сборок. Сильно упрощает жизнь и ускоряет сборку, особенно если сразу проектировать с его учетом. Для сборок с контрактами проще поддерживать обратную совместимость (опубликованные контракты не меняются). Сборки же с реализациями можно заменять целиком — они все равно друг от друга напрямую не зависят.
Средствами статического анализа можно воспретить одним сборкам ссылаться на другие.
Одиннадцатый критерий
Все предыдущие критерии могут быть нарушены при необходимости оптимизации. Но одно, на мой взгляд работает всегда:
Проще оптимизировать корректный код, чем корректировать оптимизированный.
Итоги
Письменное изложение собственных представлений многое прояснило для меня самого.
Хотелось бы увидеть ваши критерии простоты в комментариях.
Упоминания статического анализа означают возможность реализации, а не ее наличие на текущий момент.
Дополнения и критика традиционно приветствуются.
asdf87
Про 1-3 метода, свойства и т. п., мне кажется, это перебор. Минимальная сложность решения задачи эквивалентна сложности самой задачи и не всегда можно 1 цельный логический объект разбить на кучу маленьких идеальных объектов по 1-3 метода. Если у тебя 5 связанных свойств в объекте, то методов для работы с ними скорее всего будет еще больше, а при попытке разбить этот объект на более мелкие прийдется как минимум это состояние в 5 полей пробрасывать между мелкими объектами, что возможно еще и к нарушению инкапсуляции приведет.
Bonart
Идеал не всегда достижим, но совершенно нормально знать о нем и стремиться к нему.
Oxoron
Лучшее — враг хорошего.
Вы написали советы, описали плюсы, указали что детектируется до\во время компиляции. В итоге в статье не хватает двух важных моментов: недостатков от ваших советов, а также границ применимости (или хотя бы контрпримеров). На мой взгляд, статья «Когда не нужно следовать этим советам» будет не менее полезна, чем ваша. Хотя и ваша содержит немало здравых мыслей.
Bonart
Главная особенность указана — будет много маленьких типов.
Единственная известная мне на практике граница — оптимизация (про нее в статье есть). Причем совсем необязательно по скорости или памяти. Например, используя некоторые фреймворки, вы вынуждены использовать наследование реализаций и некоторые другие вредные вещи, но свой велосипед может обойтись заведомо дороже и потребовать специфических навыков, которых у вашей команды нет.
Oxoron
Почему вы не указали лишь главного преимущества, а указали плюсы от применения каждого из критериев?
Теперь собственно по недостаткам и областям применения.
1. Всякого рода конвертеры часто содержат в себе десятки методов. Есть еще перегрузки типа AddParameter(long parameter), AddParameter(int parameter),… Есть DTO и прочие классы для обмена данными, регулярно (де)сериализуемые и содержащие десятки полей.
2.
Надеюсь, вы не предлагаете избегать использования абстрактных классов?
И наслаждайтесь инициализацией тестовых классов. В каждом тесте.
4.
Статический метод частенько зависит от состояния своего класса.
5.
Особенно при поддержке какой-либо числодробилки, с алгоритмами Кнута-Матиясевича-Хаммурапи-и-еще-1000-людей. Особенно если вы реализуете нечто оптимальное на указателях. Особенно если вы вставляете какой-нибудь грязный хак перед дедлайном.
6. А как же YAGNI? Развернутый ответ — тут.
И это еще lair не высказался.
Еще раз: ваши критерии хороши, добавления инфы про статический анализ в такого рода статьях я ранее вообще не встречал, но нет простой мысли: ничто не дается бесплатно. И часто хочется четко понимать, чем же я заплачу за выполнения каждого критерия.
Bonart
Не могу сказать что это хорошо.
Лучший абстрактный класс — это интерфейс. У абстрактного класса есть скорее вредная, чем бесполезная частичная реализация.
Как будто что-то плохое.
Для таких вещей предпочитаю обобщения и методы расширения.
Такое предпочитаю разбивать.
Все это входит в "почти".
Сам по себе не нужен. Он защищает от перепроектирования, но SRP с этой задачей справляется лучше.
Большим количеством маленьких типов и, возможно, недостатком производительности там, где это критично. Специально отбирал средства без серьезных побочных эффектов.
С ним традиционно заруба, иногда на трехзначное число комментариев.
Oxoron
Ok, есть класс для формирования запросов к AzureStorage, в нем
Bonart
Переделаю на что-то вроде
Oxoron
Это один метод. А у нас 10 групп по 6 методов. Что делать с оставшимися 59 методами?
Bonart
То же самое. На методы расширения правило о числе методов в классе не действует.
Плюс, судя по сигнатурам, и там поможет кодогенерация.
Oxoron
Отлично, мы выяснили, что в методах расширения может быть сколько угодно методов. Точнее, сколько нужно.
Вопрос: чем методы расширения лучше обычных методов? У них есть один недостаток: они не имеют доступа к приватным полям обрабатываемых объектов (если они не nested, что в нашем случае бессмысленно).
Cryvage
Если задача — сократить количество методов, то можно сделать так:
Впрочем, применимость такого подхода зависит от того, насколько разнится реализация всех этих перегрузок.
Oxoron
У нас T ограничено (как правило) int, double, long, Guid, DateTime. Generic не сработает.
Bonart
Методы расширения против методов контракта, плюсы:
Так что если для реализации метода не требуется доступ к внутренней информации, то метод расширения лучше обычного метода буквально всем.
Oxoron
1. При переносе метода из класса в «расширение» просто меняется зависимость: была зависимость от this, стала от (this ArgumentType ArgumentValue).
2. Контракты обычных классов зависят друг от друга?
3. Вместо изменения контракта основного класса меняется контракт класса с расширениями.
4. Метод основного класса не может быть переиспользован?
Методы расширения позволяют расширять функциональность класса без изменения самого класса, об этом даже название говорит. Иногда они позволяют писать более выразительный код, иногда более короткий.
Платить за них приходится размазыванием логики по нескольким классам (что терпимо если модель это позволяет) и некоторыми проблемами с доступом.
И да, изначальный вопрос был «Почему мы можем иметь много методов расширения в классе, но ограничены 9 обычными методами на класс».
Bonart
Меняется две зависимости — контракт исходного типа перестает зависеть от метода, метод перестает зависеть от реализации контракта.
Двойной профит.
Методы одного контракта друг от друга в общем случае зависят. И точно не зависят от методов расширения.
Нет никакого "общего контракта класса с расширениями"!
Есть отдельные контракты — контракт класса и по одному контракту на метод расширения.
Добавление метода класса — изменение контракта класса. С аффектом в виде возможной регрессии, нарушениями принципов единственной ответственности и разделения интерфейсов. И это ДО любого переиспользования. Добавление метода интерфейса — все то же самое, еще и помноженное на число реализаций.
Как будто что-то плохое.
Это не проблема, а ограничение: если методу нужны закрытые данные — он не может быть методом расширения.
Ответ все тот же — обычные методы меняют контракт, методы расширения добавляют каждый (!) по новому контракту, которые можно использовать независимо друг от друга.
Oxoron
Контракт библиотеки остался прежним, при переносе один контракт разбился на два. Далеко не факт, что это к лучшему. Если метод использовал какую-то реализацию до переноса — он её использует и после переноса, и значит зависит от нее. Вот только теперь разработчик основного класса должен поддерживать не только основной класс, но и расширения. Сомнительный профит.
Если вы перенесли метод из основного класса в расширение — все зависящие от него методы все равно от него зависят.
В итоге у нас вместо одного большого контракта куча атомарных. И я бы не сказал, что это хорошо. Малина в банке практичнее малины на полу.
Добавление метода расширения — создание нового контракта (и реализации). Вы еще не забыли, что мы не можем зависеть больше чем от 3 контрактов\реализаций? И да, добавление методов к контракту совершенно необязательно влияет на переиспользование уже существующих.
Вполне возможно, что плохое. Не очень удобно метаться по десятку классов при анализе кода. Человек, знаете ли, не может держать в голове больше 7-9 уровней вложения кода.
То есть, раньше я зависел от одного большого контракта на 60 методов, теперь завишу от 20 малых (по методу в каждом). А больше 9 зависимостей для класса — зло. Finita.
Bonart
Да неужели? Основной контракт (требующий реализации в виде объекта) стал меньше и проще.
Профит несомненный — метод расширения имеет простейшие контракт и реализацию, по сравнению с парой интерфейс-класс. Никакого внедрения зависимостей, никакой подмены реализации — просто переиспользуемая процедура. Вся "поддержка" метода расширения сводится к "работает — не трогай". Зато поддержка "похудевшего" основного контракта становится заметно легче.
А зачем метаться по десятку классов? Методы расширения не требуют знать реализацию расширямого контракта. Реализация вообще никак не связана с методами расширения. У самого метода расширения зависимость ровно 1 (одна) — расширяемый контракт.
С учетом того, что метод расширения не имеет состояния и зависит только от расширяемого контракта, он вообще не является зависимостью в терминах DI. А пытаться делать вид, что зависимость от контракта с 60 методами проще — есть и более дешевые способы себе лгать.
Вы еще скажите, что IEnumerable создает зависимости от всех методов расширения LINQ.
Oxoron
Если метод поддерживается как «работает — не трогай», какая разница где его не трогать?
Опять-таки, это сейчас метод не зависит не зависит от внутренней реализации. А если зависимость появится? Рефакторить, перенося кучу кода из класса в класс?
Но в целом, вы во многом правы. Спасибо.
Bonart
Большая разница. Метод расширения опирается только на публичный контракт, метод контракта — на детали каждой конкретной реализации. Во втором случае "не трогать" не получится даже если не надо менять ничего старого, а достаточно добавить новую реализацию для контракта.
Зависимость может появиться только при изменении публичного контракта. А это в любом случае аффектит и реализации, и клиентский код, т.е. даже в худшем случае работы максимум столько же.
Oxoron
Разумно, о таком сценарии я не думал. Хотя вы опираетесь на сценарий с необходимостью дополнительной реализации, вероятность которой не всегда высока.
Вот тут ошибка. Есть сценарий: основной контракт, и потенциальное расширение. Один контракт — одна реализация (тесты не считаю). Предположим, мы добавили кеш в основную реализацию. Соответственно, юзание кеша ускоряет методы. Кеш приватный, содержит специфические Func<...> экземпляры.
Тут у нас 2 варианта.
1. Мы вынесли «потенциальное расширение» в методы расширения. После появления кеша у нас выбор: либо переносить все методы обратно (и менять контракты), либо оставить методы как есть, но тормознутыми.
2. Мы не выносили «потенциальное расширение». После появления кеша мы на него спокойно переключаемся, получаем профит в скорости без изменения интерфейсов.
3. Вариант 3: сделать кеш публичным (или внутренним) — нарушение инкапсуляции.
Если же откинуть данный пример — получается, что чем вероятнее изменения в контракте, тем больше смысла не выносить методы в расширения. А если у нас одна «боевая» реализация на контракт — смысл выделения методов расширения еще меньше.
Bonart
Я вижу три сценария, при которых затрагивается метод контракта:
На метод расширения действует только третий сценарий.
Откуда берется "тормознутость" для методов расширения при условии неизменности контракта?
Oxoron
А в чем проблема? Nested static class — и все закрытые члены к вашим услугам.
Oxoron
Окей, есть класс
Bonart
Такой класс предпочту порождать кодогенерацией.
Oxoron
Само собой. Но половину классов Magento после кодогенерации надо тюнить руками. После тюнинга остается 75 полей. И тут выбор — либо тюнинг выносить в кодогенератор (40 полей изменять), либо после первичной генерации изменять его только руками.
В любом случае в итоге либо здоровенный DTO, либо здоровенный кодогенератор. И перед нами снова вопрос: как это разделять? И вообще, стоит ли?
Bonart
Partial классы?
Вам изнутри оно конечно виднее, но на первый взгляд при вашей вводной встроенный шаблонизатор + partilal должны помочь.
Oxoron
Окей, будем считать у нас есть шаблонизатор.
Вопрос: что мы выигрываем за счет partial? Был один файл с DTO классом, стало 10. Навигация усложнилась.
Oxoron
Все это входит в разделы «Оптимизация», «Грязные хаки», «Костыли», и, самое главное, «Cложная предметная область». Немного многовато для «почти», не находите?
Bonart
Нет, не нахожу. Ибо с кодом, состоящим из «Оптимизация», «Грязные хаки», «Костыли» со ссылкой на «Cложная предметная область» поработал изрядно и всякий раз оказывалось, что большая часть рефакторилась в простое и понятное.
Oxoron
Шикарно.
Мы таки подошли к главному: какая часть не рефакторилась в простое и понятное? Было ли у этих кодовых кусков что-то общее?
Bonart
Они явно не были простыми и как следствие, несколько выбивались из темы статьи.
Oxoron
А было ли у этих кусков какое-то более формализуемое общее? В каких именно реализациях нужны таки комментарии?
Refridgerator
Согласно первому критерию, их оптимальное количество должно быть 3, максимум 9, а у вас их аж 11. Противоречие.
Bonart
А кто вам сказал, что я держу все этот в голове в виде линейного списка? Это результат обратной разработки — попытки анализа, почему мне одно представляется более простым и удобным, чем другое. Критерии относятся к разным уровням предпочтений и связаны между собой не только соседством в списке.
Tenqz
Какая шикарная статья, но жаль что нет визуализации. Хотелось бы картинок в стиле Хорошо/плохо.
nckma
К «хорошо» / «плохо» нужно еще добавить «модно».
А то я помню сперва всех заставляли венгерскую нотацию и комментирование кода, а теперь все это запрещают.
Bonart
"Модно" — ерунда, тех же венгерских нотаций две разновидности и та из них, что для приложений, вполне хороша во все времена.
Refridgerator
В промышленном программировании приоритеты стоят несколько по-другому:
1. Надёжность и отказоустойчивость;
2. Время доступа к данным;
3. Время на изменение/корректировку алгоритмов.
Кроме того, сложность системы определяет на количество объектов, а количество связей между ними. Гирлянда из 10 000 лампочек ничуть не сложнее гирлянды из 10 лампочек.
Bonart
Вы приводите критерии другого уровня — внешнее качество. А я о том, что позволяет добиться такого качества максимально дешево.
Гирлянда из 10 тысяч лампочек соответствует коллекции из 10 тысяч элементов, а не классу с 10 тысячами свойств.
Refridgerator
Вы говорите об этом прямым текстом
и качество и количество никак не упоминаете и не разделяете.Bonart
Неужели вы помните все 10 тысяч лампочек из гирлянды?
Или это все таки больше похоже на Гирлянда<Лампочка> { Количество: 10000 } ?
Refridgerator
1. Да (точнее, не я лично, а электрик).
2. Похоже это может быть на что угодно. Сама система и описание этой системы — несколько разные вещи.
Bonart
Refridgerator
1. Не вижу противоречия. Исходя из вашего же определения, которое я уже процитировал, согласно которому сложность вы определяете от количества элементов, способных к хранению и различению в мозгу человека.
2. Ну да. Но всё же нужно разделять сложность системы и сложность описания системы.
Bonart
… в виде линейного списка. Даже телефонные номера из 10 цифр люди запоминают, разбивая их на группы (обычно 3-3-2-2).
Вы у гирлянды тоже помните размер, а не каждую лампочку. И "электрик" у программистов тоже есть — это компьютер.
И моя статья как раз про сложность описания.
PS: Человек может помнить на много порядков больше информации, но вся эта память — ассоциативная. Поэтому гирлянда на 10 тысяч лампочек будет экземпляров типа с двумя свойствами, а не с 10 тысячами. А тип с 10 тысячами свойств будет мусором, с которым нереально работать как с целым.
Refridgerator
Вот об этом я говорю. Вы упорно рассматриваете задачу исключительно с точки зрения описания, вне контекста самой системы и управления.
qadmium
Раскройте пожалуйста это утверждение, мысль где-то далеко и я за ней не поспел
Bonart
Есть такая точка зрения, что DTO, объекты-значения, неизменяемые объекты и т.п. не являются полноценными и не должны использоваться true-ООП программистами.
Я полагаю ее в корне неверной и считаю процедурное программирование частным случаем объектно-ориентированного. Если вам не нужны данные экземпляра, то это не делает ваш статическим метод неполноценным.
Да, у него нет класса-владельца, он сам себе является таковым. Просто у этой разновидности классов есть ограничение — отсутствие состояния.
qadmium
видели, знаем :D
т.е с этой точки зрения static это такой синтаксический сахар чтобы не писать new и не протягивать везде обьект?
s-kozlov
Комментарии в хорошем коде практически всегда вредны, особенно в контракте (если речь не идет о публичном API).
Bonart
Выбрасываемые исключения надо определять телепатией?
Возможность использования в многопоточке — ей же?
Влияние одних вызовов на результаты других — ясновидением?
Любые ограничения, кроме тех, что ловит компилятор — вне закона?
Расскажите, как понять без комментария, что IEnumerable не гарантирует повторную итерацию?
s-kozlov
Список throws уже отменили?
Возможность использования чего в многопоточке? Кастомной структуры данных? Думаю, если она thread-safe, т.е. тормозная, это полезно указать.
С примерами, пожалуйста, а то мне сейчас что-то кроме влияния сеттеров на результаты геттеров ничего в голову не приходит.
Во-первых, что это за исключения, которые ловит компилятор? Это вы так называете checked exceptions? Если да, то они-то тут при чем?
Никак
Bonart
В дотнете и не применяли. Да и в яве у списка слишком неприятные побочки.
Thread-safe необязательно тормозная, но обязательно thread-safe. Факт о thread-(un)safe желательно знать у же при первом взгляде на интерфейс.
Методы помещения значения в очередь и извлечения их оттуда.
Не исключения, а ограничения. Например, в метод передается два значения, имеющие смысл верхней и нижней границ. Нижняя должна быть меньше верхней.
Вот вы и сами дали ответ о полезности комментариев в контракте.
s-kozlov
Интересно даже. Какие?
Жесть какая-то. Неужели из названия интерфейса «Queue» и методов «void push(E elem)», «E pull()» не очевидно, как это работает?
По остальному и всему сразу еще раз:
Выражение «практически всегда» означает, что могут быть исключения, но их мало. «Публичный API» значит, что IEnumerable — не очень подходящий пример. Про вред подробнее читать «Чистый код» Р.С.Мартина
Bonart
Недостатки у явовских контролируемых исключений те же, что и у прочей истерической типизации — надо соблюдать очередной многословный ритуал, профит от которого есть только во внешнем по отношению к классу или пакету коде. В результате эта особенность явы нигде не прижилась, при создании дотнета тот же Хейлсберг отверг ее сознательно и аргументированно.
Вы просили пример влияния одних вызовов на результаты других, кроме сеттеров-геттеров, потому что не могли придумать сами, а теперь вас смущает его очевидность? В том или ином вашем собственном контракте влияние вполне может быть совсем не очевидным для других разработчиков.
То есть тех, кто использует наш код снаружи, вы считаете нужным предупредить о том, что контракт не исчерпывается сигнатурой, а коллеги внутри компании должны проявлять телепатические способности и ясновидение?
s-kozlov
Вы считаете, что в списке throws в Java можно объявлять только checked exceptions?
В этих редких случаях комментарии не помешают. Вот только этого надо по возможности избегать.
Коллеги внутри компании должны создавать нормальные интерфейсы, которые не нуждаются в комментариях. А еще внутренний код часто меняется, а про изменение комментариев — давайте честно — забывают. Вот и получается, что там оказывается ложь. Зачастую проще посмотреть в код.
Вы уже таки прочитали главу Мартина про комментарии? Мне уже начинает надоедать ее пересказывать.
third112
В такой осторожной формулировке с этим правилом мало кто не согласится. Тем более, что через несколько страниц (С.100) автор приводит исключение из этого правила: тело функции determineIterationLimit содержит всего две строки кода и целых три строки необходимых, по мнению автора, комментариев. Хорошо, интересно и убедительно написано. ИМХО нужно писать полезные комментарии и не писать вредные. Ok.
s-kozlov
Ну вот, собственно, об этом я и говорил.
Bonart
Понятия не имею — как нетрудно догадаться из моего профиля и списка хабов в статье я на яве не программирую. Вот только ценность такого списка у неконтролируемых исключений все та же: если компилятор не контролирует — тогда это де-факто комментарий.
Не такие уж и редкие случаи, если под них даже специальный язык был запилен (Eiffel). А для дотнета есть целая библиотека CodeContracts. Она не удалась, но ее проблемы были не в редксти случаев, а в производительности сборки.
IEnumerable — нормальный интерфейс?
Часто меняющиеся внутренние интерфейсы без комментариев? Это как же надо ненавидеть собственных коллег.
s-kozlov
Вот вы опять приводите частные примеры, которые в нормальном коде должны быть исключением, а не правилом.
Изменения требований? Рефакторинг? Не, не слышал.
third112
Уважаемый автор,
но ранее Вы писали:
Пересмотрели свое отношения к комментариям? или я чего-то не понял?
Bonart
Это комментарий контракта
А это — то самое "почти" для комментариев в коде.
Просто в данной статье про комментарии ровно одна строка. И это неудивительно — в коде комментарии разъясняют сложные места, а статья о том, что я считаю простым.
third112
ИМХО не только сложные, но и неочевидные места. Тривиальный пример. Переменную Х делим на переменную Y при этом нет проверки, что Y не равно нулю. Не очевидно: м.б. кодер забыл поставить проверку, а м.б. Y не равно нулю по условию подзадачи, проверялось ранее и т.д. Комментарий-пояснение ИМХО и в таком тривиальном случае необходим.
Bonart
Очевидно, что в случае равенства делимого нулю будет исключение. Этого в данном случае достаточно.
third112
М.б. нужен будет обработчик этого исключения?
Bonart
Может быть, но не факт. Более того, в этом месте кода мы вообще можем не знать, как должны обрабатываться исключения отсюда. И если добавлять комментарий, то за ним придется следить и актуализировать, даже если код рядом не меняется. Это чистый убыток.
third112
Вы сказали:
А неочевидных мест в коде м.б. тысячи. Помнить все невозможно. А если над кодом работает команда, то только один знает почему нет проверки на ноль и обработки исключения.Bonart
Это неверно. Обычно имеются соглашения об использовании исключений в проекте плюс описание возможных исключений является частью контракта.
Например, в одном из проектов, над которым я работал, вместо выброса исключений использовалось возвращение NaN, но это не было особенностью конкретного участка кода, а являлось частью контракта.
third112
У примеров есть неприятное свойство: в формате обсуждения (вроде этого) нет возможности привести достаточно сложный пример, т.к. он был бы слишком объемным. Поэтому приходится приводить простые примеры. Вы используете это свойство и еще более упрощаете ситуацию, что со стороны, если не вдумываться, выглядит весомым доводом, хотя на деле просто уловка спора. Повторю, что даже в моем простом примере возможны неочевидные случаи:
Если посмотреть достаточно большой нетривиальный код, то там таких случаев м.б. довольно много при их большом разнообразии. Если метод реализует «в лоб» широко известный алгоритм, то достаточно комментария типа "// Алгоритм Флойда — Уоршелла". Но если в коде используется какие-то свойства матрицы расстояний, специфичные для данной задачи, то эти места кода нуждаются в комментариях. Часто бывает, что применяется оригинальный алгоритм, сделанный специально под данную задачу. Тут одних комментариев может оказаться мало и нужны будут доказательство корректности алгоритма и оценка теоретической сложности. Это уже отдельная документация, сопровождающая код. А в комментариях должна быть ссылка на соответствующий файл документации. Если алгоритм эвристический, то это нужно написать в комментарии и т.д. Отсутствие подобных пояснений превращает код в ребус и не способствует простоте. ИМХО это очевидные вещи, и мне немного странно, что мы о них спорим.
Bonart
Это и есть сложные места по определению.
Т.е. мы вернулись к тому, с чего начали — комментарии в коде полезны, когда объясняют, зачем нужна та или иная сложность. Так что никто вас специально ни на чем не ловил.
third112
Т.е. все, что неочевидно — то сложно? Пожалуй, с этим можно согласиться. Но тогда получаем, что в реальности простых задач нет, а бывают только простые участки кода сложных задач.
third112
ошибка ввода — прошу извинить
Taller
Ну, вот, вы превысили количество пунктов в статье.
Кстати, вместо 7-9, обычно говорять о 7 ± 2, т.е. от 5 до 9, также говорят, что элементы должны быть однородны, а также восприятие должно быть одновременным. С интерфейсах, параметрах и т.п. можно проводить последовательный анализ.
Bonart
Вообще-то нельзя. У интерфейса должен быть хотя бы один вариант использования, требующий всех его членов.
Параметры методу также нужны все.
Taller
Нужны все, это верно. Не все сразу, это тоже верно.
Сразу — это одновременно, в один момент. А операторы записываются слева направо, сверху вниз, не в один момент.
Если найду ссылку — будет пруф.
overmes
Для меня самое сложное, когда я просматриваю чужой код, это:
Bonart
Вы можете выделить критерии логичности-нелогичности? Вполне возможно, что для автора кода они иные.
Каковы ваши критерии обоснованности в данном вопросе? Скорее всего, автор кода уверен в обратном.
overmes
Я согласен что многие пункты субъективны.
Мы обычно одеваем сначала кофту, потом куртку, а не кофту под куртку. Программно мы можем и так и так делать.
Это про KISS. Я часто проверяю код новичков и там автор кода мало в чем уверен.
Автор должен свою уверенность и принятые решения как-то выражать в коде.
Bonart
Нередко под ними скрывается вполне объективный критерий, который сможете воспроизвести не только вы, но и ваши коллеги. Не факт, что они с ним согласятся, но по крайней мере смогут заранее понять, почему то или иное решение будет вам симпатично или наоборот.
overmes
Я сам подумывал об выступлении\статье на эту тему. И основной посыл хотел сделать в том, что сложность это не только как разделены классы, а и то что один и тот же алгоритм можно очень по разному написать.
Я пока не видел(но я не много где смотрел) ничего про эту мою так называемую логичность последовательности шагов.
Между двумя точками мы идем по кратчайшему расстоянию от начала до конца, но программно мы можем начать идти с середины до конца, а потом пройти от начала до середины.
third112
overmes
Про реализацию. Я не про CS алгоритмы, а про обычную бизнес логику.
third112
Интересный вопрос о соотношении предложенных критериев с подходами software metric. Кроме примитивного подсчета числа строк были предложены и очень сложные.
Refridgerator
Ещё вспомнилось:
«У каждой задачи есть очевидное, простое и неправильное решение» © А.Блох
overmes