Pull to refresh

Comments 89

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

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

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

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

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


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

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

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

Почему вы не указали лишь главного преимущества, а указали плюсы от применения каждого из критериев?

Теперь собственно по недостаткам и областям применения.
1. Всякого рода конвертеры часто содержат в себе десятки методов. Есть еще перегрузки типа AddParameter(long parameter), AddParameter(int parameter),… Есть DTO и прочие классы для обмена данными, регулярно (де)сериализуемые и содержащие десятки полей.

2.
Избегайте наследования реализаций

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

И наслаждайтесь инициализацией тестовых классов. В каждом тесте.

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

Статический метод частенько зависит от состояния своего класса.

5.
комментарии в коде реализации почти всегда вредны.

Особенно при поддержке какой-либо числодробилки, с алгоритмами Кнута-Матиясевича-Хаммурапи-и-еще-1000-людей. Особенно если вы реализуете нечто оптимальное на указателях. Особенно если вы вставляете какой-нибудь грязный хак перед дедлайном.

6. А как же YAGNI? Развернутый ответ — тут.

И это еще lair не высказался.

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

Не могу сказать что это хорошо.


Надеюсь, вы не предлагаете избегать использования абстрактных классов?

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


И наслаждайтесь инициализацией тестовых классов. В каждом тесте.

Как будто что-то плохое.


Есть еще перегрузки типа AddParameter(long parameter), AddParameter(int parameter)

Для таких вещей предпочитаю обобщения и методы расширения.


Есть DTO и прочие классы для обмена данными, регулярно (де)сериализуемые и содержащие десятки полей.

Такое предпочитаю разбивать.


Особенно при поддержке какой-либо числодробилки, с алгоритмами Кнута-Матиясевича-Хаммурапи-и-еще-1000-людей. Особенно если вы реализуете нечто оптимальное на указателях. Особенно если вы вставляете какой-нибудь грязный хак перед дедлайном.

Все это входит в "почти".


А как же YAGNI?

Сам по себе не нужен. Он защищает от перепроектирования, но SRP с этой задачей справляется лучше.


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

Большим количеством маленьких типов и, возможно, недостатком производительности там, где это критично. Специально отбирал средства без серьезных побочных эффектов.


И это еще lair не высказался

С ним традиционно заруба, иногда на трехзначное число комментариев.

-Есть еще перегрузки типа AddParameter(long parameter), AddParameter(int parameter)
-Для таких вещей предпочитаю обобщения и методы расширения.


Ok, есть класс для формирования запросов к AzureStorage, в нем
12 публичных методов
public AzureStorageFilter In(string column, IEnumerable<string> values)
public AzureStorageFilter In(string column, IEnumerable<int> values)
public AzureStorageFilter In(string column, IEnumerable<long> values)
public AzureStorageFilter In(string column, IEnumerable<DateTime> values)
public AzureStorageFilter In(string column, IEnumerable<Guid> values)
public AzureStorageFilter In(string column, IEnumerable<double> values)

public AzureStorageFilter In(string column, params string[] values)
public AzureStorageFilter In(string column, params int[] values)
public AzureStorageFilter In(string column, params long[] values)
public AzureStorageFilter In(string column, params DateTime[] values)
public AzureStorageFilter In(string column, params Guid[] values)
public AzureStorageFilter In(string column, params double[] values)



Всего около 10 групп перегрузок: In, Between, Column, Const, Equal, etc.
Как именно вы раскидаете все перегрузки по методам расширения?

Переделаю на что-то вроде


public AzureStorageFilter In(this Column column, IEnumerable<string> values)
Это один метод. А у нас 10 групп по 6 методов. Что делать с оставшимися 59 методами?

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

Отлично, мы выяснили, что в методах расширения может быть сколько угодно методов. Точнее, сколько нужно.
Вопрос: чем методы расширения лучше обычных методов? У них есть один недостаток: они не имеют доступа к приватным полям обрабатываемых объектов (если они не nested, что в нашем случае бессмысленно).
Если задача — сократить количество методов, то можно сделать так:
public AzureStorageFilter In<T>(string column, IEnumerable<T> values) { }
public AzureStorageFilter In<T>(string column, params T[] values) { }

Впрочем, применимость такого подхода зависит от того, насколько разнится реализация всех этих перегрузок.
У нас T ограничено (как правило) int, double, long, Guid, DateTime. Generic не сработает.
Вопрос: чем методы расширения лучше обычных методов? У них есть один недостаток: они не имеют доступа к приватным полям обрабатываемых объектов (если они не nested, что в нашем случае бессмысленно).

Методы расширения против методов контракта, плюсы:


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

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

1. При переносе метода из класса в «расширение» просто меняется зависимость: была зависимость от this, стала от (this ArgumentType ArgumentValue).
2. Контракты обычных классов зависят друг от друга?
3. Вместо изменения контракта основного класса меняется контракт класса с расширениями.
4. Метод основного класса не может быть переиспользован?

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

Платить за них приходится размазыванием логики по нескольким классам (что терпимо если модель это позволяет) и некоторыми проблемами с доступом.

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

Меняется две зависимости — контракт исходного типа перестает зависеть от метода, метод перестает зависеть от реализации контракта.
Двойной профит.


Контракты обычных классов зависят друг от друга?

Методы одного контракта друг от друга в общем случае зависят. И точно не зависят от методов расширения.


Вместо изменения контракта основного класса меняется контракт класса с расширениями.

Нет никакого "общего контракта класса с расширениями"!
Есть отдельные контракты — контракт класса и по одному контракту на метод расширения.


Метод основного класса не может быть переиспользован?

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


Платить за них приходится размазыванием логики по нескольким классам

Как будто что-то плохое.


и некоторыми проблемами с доступом

Это не проблема, а ограничение: если методу нужны закрытые данные — он не может быть методом расширения.


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

Ответ все тот же — обычные методы меняют контракт, методы расширения добавляют каждый (!) по новому контракту, которые можно использовать независимо друг от друга.

Меняется две зависимости — контракт исходного типа перестает зависеть от метода, метод перестает зависеть от реализации контракта.

Контракт библиотеки остался прежним, при переносе один контракт разбился на два. Далеко не факт, что это к лучшему. Если метод использовал какую-то реализацию до переноса — он её использует и после переноса, и значит зависит от нее. Вот только теперь разработчик основного класса должен поддерживать не только основной класс, но и расширения. Сомнительный профит.

Методы одного контракта друг от друга в общем случае зависят. И точно не зависят от методов расширения.

Если вы перенесли метод из основного класса в расширение — все зависящие от него методы все равно от него зависят.

Есть отдельные контракты — контракт класса и по одному контракту на метод расширения.

В итоге у нас вместо одного большого контракта куча атомарных. И я бы не сказал, что это хорошо. Малина в банке практичнее малины на полу.

Добавление метода класса — изменение контракта класса.

Добавление метода расширения — создание нового контракта (и реализации). Вы еще не забыли, что мы не можем зависеть больше чем от 3 контрактов\реализаций? И да, добавление методов к контракту совершенно необязательно влияет на переиспользование уже существующих.

— Платить за них приходится размазыванием логики по нескольким классам
— Как будто что-то плохое.

Вполне возможно, что плохое. Не очень удобно метаться по десятку классов при анализе кода. Человек, знаете ли, не может держать в голове больше 7-9 уровней вложения кода.

Ответ все тот же — обычные методы меняют контракт, методы расширения добавляют каждый (!) по новому контракту, которые можно использовать независимо друг от друга.

То есть, раньше я зависел от одного большого контракта на 60 методов, теперь завишу от 20 малых (по методу в каждом). А больше 9 зависимостей для класса — зло. Finita.
Контракт библиотеки остался прежним, при переносе один контракт разбился на два. Далеко не факт, что это к лучшему.

Да неужели? Основной контракт (требующий реализации в виде объекта) стал меньше и проще.


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

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


Вполне возможно, что плохое. Не очень удобно метаться по десятку классов при анализе кода. Человек, знаете ли, не может держать в голове больше 7-9 уровней вложения кода.

А зачем метаться по десятку классов? Методы расширения не требуют знать реализацию расширямого контракта. Реализация вообще никак не связана с методами расширения. У самого метода расширения зависимость ровно 1 (одна) — расширяемый контракт.


То есть, раньше я зависел от одного большого контракта на 60 методов, теперь завишу от 20 малых (по методу в каждом). А больше 9 зависимостей для класса — зло.

С учетом того, что метод расширения не имеет состояния и зависит только от расширяемого контракта, он вообще не является зависимостью в терминах DI. А пытаться делать вид, что зависимость от контракта с 60 методами проще — есть и более дешевые способы себе лгать.
Вы еще скажите, что IEnumerable создает зависимости от всех методов расширения LINQ.

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

Если метод поддерживается как «работает — не трогай», какая разница где его не трогать?

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

Опять-таки, это сейчас метод не зависит не зависит от внутренней реализации. А если зависимость появится? Рефакторить, перенося кучу кода из класса в класс?

Но в целом, вы во многом правы. Спасибо.

Если метод поддерживается как «работает — не трогай», какая разница где его не трогать?

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


Опять-таки, это сейчас метод не зависит не зависит от внутренней реализации. А если зависимость появится? Рефакторить, перенося кучу кода из класса в класс?

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

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

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

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

Вот тут ошибка. Есть сценарий: основной контракт, и потенциальное расширение. Один контракт — одна реализация (тесты не считаю). Предположим, мы добавили кеш в основную реализацию. Соответственно, юзание кеша ускоряет методы. Кеш приватный, содержит специфические Func<...> экземпляры.
Тут у нас 2 варианта.
1. Мы вынесли «потенциальное расширение» в методы расширения. После появления кеша у нас выбор: либо переносить все методы обратно (и менять контракты), либо оставить методы как есть, но тормознутыми.
2. Мы не выносили «потенциальное расширение». После появления кеша мы на него спокойно переключаемся, получаем профит в скорости без изменения интерфейсов.
3. Вариант 3: сделать кеш публичным (или внутренним) — нарушение инкапсуляции.

Если же откинуть данный пример — получается, что чем вероятнее изменения в контракте, тем больше смысла не выносить методы в расширения. А если у нас одна «боевая» реализация на контракт — смысл выделения методов расширения еще меньше.

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

Я вижу три сценария, при которых затрагивается метод контракта:


  1. Изменение реализации (самый частый случай)
  2. Добавление реализации (реже, но при TDD — распространенная практика)
  3. Изменение контракта (самый редкий случай)

На метод расширения действует только третий сценарий.


Есть сценарий: основной контракт, и потенциальное расширение. Один контракт — одна реализация (тесты не считаю). Предположим, мы добавили кеш в основную реализацию. Соответственно, юзание кеша ускоряет методы. Кеш приватный, содержит специфические Func<...> экземпляры.
Тут у нас 2 варианта.
  1. Мы вынесли «потенциальное расширение» в методы расширения. После появления кеша у нас выбор: либо переносить все методы обратно (и менять контракты), либо оставить методы как есть, но тормознутыми.

Откуда берется "тормознутость" для методов расширения при условии неизменности контракта?

Добавление реализации (реже, но при TDD — распространенная практика)

Вы имеете ввиду тестовые реализации? В большинстве случаев ими можно пренебречь, ибо они проще «боевых».

Откуда берется «тормознутость» для методов расширения при условии неизменности контракта?

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

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


По сценарию, кеш ускоряет методы «основного» класса, но не расширения

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

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

Дать пример, увы, не могу, NDA. Но суть была в том, что изначальная реализация была прототипная (что характерно для TDD).

Можете глянуть тут: по сути, AzureStorageFilter класс вообще не нужен, всю функциональность можно вынести в методы расширения для string. Но если придется код ускорять — скорее всего потребуется StringBuilder, или те же деревья выражений строить.

И да, сейчас прикинул — десяток метод расширений зло в смысле автокомплита, засоряют окошко с возможными методами.
Это не проблема, а ограничение: если методу нужны закрытые данные — он не может быть методом расширения.

А в чем проблема? Nested static class — и все закрытые члены к вашим услугам.
— Есть DTO и прочие классы для обмена данными, регулярно (де)сериализуемые и содержащие десятки полей.
— Такое предпочитаю разбивать.

Окей, есть класс
Такие вроде в Magento любят
public class DTO
{
    [XmlElement]     public string Value1{get; set;}
    [XmlElement]     public string Value2{get; set;}
    [XmlElement]     public string Value3{get; set;}
    [XmlElement]     public string Value4{get; set;}
    [XmlElement]     public string Value5{get; set;}
    [XmlElement]     public string Value6{get; set;}
    [XmlElement]     public string Value7{get; set;}
    [XmlElement]     public string Value8{get; set;}
    ...
    [XmlElement]     public string Value80{get;set;}
}


Как вы его разобьете?

Такой класс предпочту порождать кодогенерацией.

Само собой. Но половину классов Magento после кодогенерации надо тюнить руками. После тюнинга остается 75 полей. И тут выбор — либо тюнинг выносить в кодогенератор (40 полей изменять), либо после первичной генерации изменять его только руками.

В любом случае в итоге либо здоровенный DTO, либо здоровенный кодогенератор. И перед нами снова вопрос: как это разделять? И вообще, стоит ли?
Но половину классов Magento после кодогенерации надо тюнить руками

Partial классы?


либо здоровенный кодогенератор

Вам изнутри оно конечно виднее, но на первый взгляд при вашей вводной встроенный шаблонизатор + partilal должны помочь.

Окей, будем считать у нас есть шаблонизатор.
Вопрос: что мы выигрываем за счет partial? Был один файл с DTO классом, стало 10. Навигация усложнилась.
— Особенно при поддержке какой-либо числодробилки, с алгоритмами Кнута-Матиясевича-Хаммурапи-и-еще-1000-людей. Особенно если вы реализуете нечто оптимальное на указателях. Особенно если вы вставляете какой-нибудь грязный хак перед дедлайном.
— Все это входит в «почти».


Все это входит в разделы «Оптимизация», «Грязные хаки», «Костыли», и, самое главное, «Cложная предметная область». Немного многовато для «почти», не находите?

Нет, не нахожу. Ибо с кодом, состоящим из «Оптимизация», «Грязные хаки», «Костыли» со ссылкой на «Cложная предметная область» поработал изрядно и всякий раз оказывалось, что большая часть рефакторилась в простое и понятное.

Шикарно.
Мы таки подошли к главному: какая часть не рефакторилась в простое и понятное? Было ли у этих кодовых кусков что-то общее?

Они явно не были простыми и как следствие, несколько выбивались из темы статьи.

А было ли у этих кусков какое-то более формализуемое общее? В каких именно реализациях нужны таки комментарии?
Согласно первому критерию, их оптимальное количество должно быть 3, максимум 9, а у вас их аж 11. Противоречие.

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

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

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

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

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

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

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

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

1. Да (точнее, не я лично, а электрик).
2. Похоже это может быть на что угодно. Сама система и описание этой системы — несколько разные вещи.
  1. Это противоречит вашим словам про одинаковую сложность гирлянд разной длины. По крайней мере для электрика.
  2. Так программисты имеют дело как раз с описанием системы на языке программирования.
1. Не вижу противоречия. Исходя из вашего же определения, которое я уже процитировал, согласно которому сложность вы определяете от количества элементов, способных к хранению и различению в мозгу человека.
2. Ну да. Но всё же нужно разделять сложность системы и сложность описания системы.
способных к хранению и различению в мозгу человека.

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


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

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

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


Раскройте пожалуйста это утверждение, мысль где-то далеко и я за ней не поспел

Есть такая точка зрения, что DTO, объекты-значения, неизменяемые объекты и т.п. не являются полноценными и не должны использоваться true-ООП программистами.
Я полагаю ее в корне неверной и считаю процедурное программирование частным случаем объектно-ориентированного. Если вам не нужны данные экземпляра, то это не делает ваш статическим метод неполноценным.
Да, у него нет класса-владельца, он сам себе является таковым. Просто у этой разновидности классов есть ограничение — отсутствие состояния.

Есть такая точка зрения, что DTO, объекты-значения, неизменяемые объекты и т.п. не являются полноценными и не должны использоваться true-ООП программистами.


видели, знаем :D

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


т.е с этой точки зрения static это такой синтаксический сахар чтобы не писать new и не протягивать везде обьект?
UFO just landed and posted this here
Комментарии в хорошем коде практически всегда вредны, особенно в контракте

Выбрасываемые исключения надо определять телепатией?
Возможность использования в многопоточке — ей же?
Влияние одних вызовов на результаты других — ясновидением?
Любые ограничения, кроме тех, что ловит компилятор — вне закона?
Расскажите, как понять без комментария, что IEnumerable не гарантирует повторную итерацию?

UFO just landed and posted this here
Список throws уже отменили?

В дотнете и не применяли. Да и в яве у списка слишком неприятные побочки.


Возможность использования чего в многопоточке? Кастомной структуры данных? Думаю, если она thread-safe, т.е. тормозная, это полезно указать.

Thread-safe необязательно тормозная, но обязательно thread-safe. Факт о thread-(un)safe желательно знать у же при первом взгляде на интерфейс.


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

Методы помещения значения в очередь и извлечения их оттуда.


Во-первых, что это за исключения, которые ловит компилятор? Это вы так называете checked exceptions? Если да, то они-то тут при чем?

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


Никак

Вот вы и сами дали ответ о полезности комментариев в контракте.

UFO just landed and posted this here
Интересно даже. Какие?

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


Жесть какая-то. Неужели из названия интерфейса «Queue» и методов «void push(E elem)», «E pull()» не очевидно, как это работает?

Вы просили пример влияния одних вызовов на результаты других, кроме сеттеров-геттеров, потому что не могли придумать сами, а теперь вас смущает его очевидность? В том или ином вашем собственном контракте влияние вполне может быть совсем не очевидным для других разработчиков.


«Публичный API» значит, что IEnumerable — не очень подходящий пример.

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

UFO just landed and posted this here
Вы уже таки прочитали главу Мартина про комментарии? Мне уже начинает надоедать ее пересказывать.
Да. На С.79 прочел, что комментарии — неизбежное зло. И далее, что совсем без них не обойтись. Дальше предлагаются примеры типовых случаев хороших и плохих комментариев. Формулируются правила, нпр.:
Короткие функции не нуждаются в долгих описаниях.
В такой осторожной формулировке с этим правилом мало кто не согласится. Тем более, что через несколько страниц (С.100) автор приводит исключение из этого правила: тело функции determineIterationLimit содержит всего две строки кода и целых три строки необходимых, по мнению автора, комментариев. Хорошо, интересно и убедительно написано. ИМХО нужно писать полезные комментарии и не писать вредные. Ok.
UFO just landed and posted this here
Вы считаете, что в списке throws в Java можно объявлять только checked exceptions?

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


В этих редких случаях комментарии не помешают. Вот только этого надо по возможности избегать.

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


Коллеги внутри компании должны создавать нормальные интерфейсы, которые не нуждаются в комментариях.

IEnumerable — нормальный интерфейс?


А еще внутренний код часто меняется

Часто меняющиеся внутренние интерфейсы без комментариев? Это как же надо ненавидеть собственных коллег.

UFO just landed and posted this here
Уважаемый автор,
Комментарии в контракте — почти всегда полезны, комментарии в коде реализации почти всегда вредны.

но ранее Вы писали:
4 Комментарии
Первая строка комментария перед объявлениями процедур, классов и т.п. должна давать понять их назначение. Последующие строки описывают те или иные особенности реализации.
Комментарии к виртуальным методам должны описывать обстоятельства вызова, а не реализацию — она может быть перекрыта в наследниках.
Комментарии в теле процедур и методов не должны описывать, ЧТО делает тот или иной оператор или блок, а должны указывать ЦЕЛЬ, для которой он (оператор) используется.

Цели меняются гораздо реже, чем средства ее реализации
Понять что делает код можно из него самого, понять ЗАЧЕМ по коду бывает нереально.

Пересмотрели свое отношения к комментариям? или я чего-то не понял?
Первая строка комментария перед объявлениями процедур, классов и т.п. должна давать понять их назначение.

Это комментарий контракта


Комментарии в теле процедур и методов не должны описывать, ЧТО делает тот или иной оператор или блок, а должны указывать ЦЕЛЬ, для которой он (оператор) используется.
Цели меняются гораздо реже, чем средства ее реализации
Понять что делает код можно из него самого, понять ЗАЧЕМ по коду бывает нереально.

А это — то самое "почти" для комментариев в коде.


Пересмотрели свое отношения к комментариям? или я чего-то не понял?

Просто в данной статье про комментарии ровно одна строка. И это неудивительно — в коде комментарии разъясняют сложные места, а статья о том, что я считаю простым.

ИМХО не только сложные, но и неочевидные места. Тривиальный пример. Переменную Х делим на переменную Y при этом нет проверки, что Y не равно нулю. Не очевидно: м.б. кодер забыл поставить проверку, а м.б. Y не равно нулю по условию подзадачи, проверялось ранее и т.д. Комментарий-пояснение ИМХО и в таком тривиальном случае необходим.

Очевидно, что в случае равенства делимого нулю будет исключение. Этого в данном случае достаточно.

М.б. нужен будет обработчик этого исключения?

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

Вы сказали:
Особенности мозга человека таковы, что он плохо хранит и отличает более 7-9 элементов в одном списке при оптимальном их количестве 1-3.
А неочевидных мест в коде м.б. тысячи. Помнить все невозможно. А если над кодом работает команда, то только один знает почему нет проверки на ноль и обработки исключения.
Все может быть!
Быть может, вы умрете,
Вас выгонят,
Сгорите на работе,
Или на базе вас задавит свеклой в таре…
Товарищи! Пишите комментарий!


— Леонид Бунич
А если над кодом работает команда, то только один знает почему нет проверки на ноль и обработки исключения.

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

У примеров есть неприятное свойство: в формате обсуждения (вроде этого) нет возможности привести достаточно сложный пример, т.к. он был бы слишком объемным. Поэтому приходится приводить простые примеры. Вы используете это свойство и еще более упрощаете ситуацию, что со стороны, если не вдумываться, выглядит весомым доводом, хотя на деле просто уловка спора. Повторю, что даже в моем простом примере возможны неочевидные случаи:
м.б. Y не равно нулю по условию подзадачи

Если посмотреть достаточно большой нетривиальный код, то там таких случаев м.б. довольно много при их большом разнообразии. Если метод реализует «в лоб» широко известный алгоритм, то достаточно комментария типа "// Алгоритм Флойда — Уоршелла". Но если в коде используется какие-то свойства матрицы расстояний, специфичные для данной задачи, то эти места кода нуждаются в комментариях. Часто бывает, что применяется оригинальный алгоритм, сделанный специально под данную задачу. Тут одних комментариев может оказаться мало и нужны будут доказательство корректности алгоритма и оценка теоретической сложности. Это уже отдельная документация, сопровождающая код. А в комментариях должна быть ссылка на соответствующий файл документации. Если алгоритм эвристический, то это нужно написать в комментарии и т.д. Отсутствие подобных пояснений превращает код в ребус и не способствует простоте. ИМХО это очевидные вещи, и мне немного странно, что мы о них спорим.
Но если в коде используется какие-то свойства матрицы расстояний, специфичные для данной задачи, то эти места кода нуждаются в комментариях.

Это и есть сложные места по определению.
Т.е. мы вернулись к тому, с чего начали — комментарии в коде полезны, когда объясняют, зачем нужна та или иная сложность. Так что никто вас специально ни на чем не ловил.

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

Кстати, вместо 7-9, обычно говорять о 7 ± 2, т.е. от 5 до 9, также говорят, что элементы должны быть однородны, а также восприятие должно быть одновременным. С интерфейсах, параметрах и т.п. можно проводить последовательный анализ.
С интерфейсах, параметрах и т.п. можно проводить последовательный анализ.

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

Нужны все, это верно. Не все сразу, это тоже верно.
Сразу — это одновременно, в один момент. А операторы записываются слева направо, сверху вниз, не в один момент.

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

Вы можете выделить критерии логичности-нелогичности? Вполне возможно, что для автора кода они иные.


когда алгоритм необоснованно перегружен лишними шагами и данными

Каковы ваши критерии обоснованности в данном вопросе? Скорее всего, автор кода уверен в обратном.

Я согласен что многие пункты субъективны.
Вы можете выделить критерии логичности-нелогичности? Вполне возможно, что для автора кода они иные.

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

Каковы ваши критерии обоснованности в данном вопросе? Скорее всего, автор кода уверен в обратном.

Это про KISS. Я часто проверяю код новичков и там автор кода мало в чем уверен.
Автор должен свою уверенность и принятые решения как-то выражать в коде.
Я согласен что многие пункты субъективны.

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

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

Я пока не видел(но я не много где смотрел) ничего про эту мою так называемую логичность последовательности шагов.

Между двумя точками мы идем по кратчайшему расстоянию от начала до конца, но программно мы можем начать идти с середины до конца, а потом пройти от начала до середины.
один и тот же алгоритм можно очень по разному написать
Вы про описание алгоритма или про его реализацию на конкретном языке программирования? В литературе по CS (и в той же википедии) алгоритмы часто описывают на естественном языке (русский, английский и т.д. + мат. формулы) или на псевдокоде. Еще один способ — это блок-схемы. Конечно же, и описание и реализация могут быть сделаны очень по разному.
Про реализацию. Я не про CS алгоритмы, а про обычную бизнес логику.
Интересный вопрос о соотношении предложенных критериев с подходами software metric. Кроме примитивного подсчета числа строк были предложены и очень сложные.
Ещё вспомнилось:

«У каждой задачи есть очевидное, простое и неправильное решение» © А.Блох
«Есть два подхода к программированию. Первый — сделать программу настолько простой, чтобы в ней очевидно не было ошибок. А второй — сделать её настолько сложной, чтобы в ней не было очевидных ошибок.»
Tony Hoare. Профессор, занимался реализацией Алгол 60, сейчас исследователь в Microsoft Research.
Sign up to leave a comment.

Articles