Pull to refresh

Comments 123

UFO just landed and posted this here
UFO just landed and posted this here
Если это режет блокиратор рекламы, то что-то не так:
image
UFO just landed and posted this here
> Использование базы данных как очереди задач.
а что в этом плохого? в конечном итоге standalone решение тоже должно использовать БД для хранения задач иначе они могут потеряться

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

Так что думаю, любой список подобных правил должен начинаться Правилом 0 с наивысшим приоритетом: «Если возможно, воспользуйтесь здравым смыслом».
False. Тому кто еще не прочувствовал эти правила может показаться что вот именно в его случае можно чем-то пренебречь. С точки зрения его «здравого» смысла конечно.
Похоже на то. Но, возможно, источник проблемы лежит глубже. Это психологические страхи, так же как и в оффлайн. Страх что твой код кому то не понравится, страх что будет ошибка и посыпятся шишки, перфекционизм. Или как в торговле на бирже — есть стратегия, но не можешь ее реализовать когда смотришь на прыгающий график. Сам стараюсь отключать психологию, но это как раз и есть самое трудное, т.к. не поддается программированию.
9. Что тут не понятного? Это реализация адаптера. С практической точки зрения, удобно прятать все внешние зависимости за адаптерами — так мы не зависим от внешнего api и можем легко и просто менять реализации.
Ещё такая ситуация может возникнуть, если класс реализует несколько интерфейсов, один из которых какой-нибудь стандартный контейнер (типа Map, List, etc). Самому реализовывать такие интерфейсы в продакшене вредно.
Например, вот класс Document из дравера для 3-й монги.
И это явно не бесполезный класс.
Плохой пример, так как этот класс не просто создаёт обёртку методов вокруг LinkedHashMap, но и определяет некоторую дополнительную логику по конструированию объекта, доступа к полям разных типов и т.п. Это немного сложнее, чем пример из статьи.
Но, тем не менее, можно ж было использовать обычный LinkedHashMap, а логику вынести в какой-нибудь DocumentManager. Но это было бы достаточно костыльным решением — как раз для того, чтобы этого избежать, и был сделан класс-обертка с дополнительной логикой.
Какая там такая особенная логика? Проброс исключения другого типа? Было бы о чем говорить.
Вы же сами про нее написали ;)
Плохой пример, так как этот класс не просто создаёт обёртку методов вокруг LinkedHashMap, но и определяет некоторую дополнительную логику по конструированию объекта, доступа к полям разных типов и т.п. Это немного сложнее, чем пример из статьи.
А вообще да, особенной логики там нет, поэтому этот класс как раз хорошо подходит в качестве примера.
Прошу прощения не дочитал, решил что речь про пример из статьи.
В данном случае это мало похоже на адаптер. Что к чему он адаптирует?
Адаптирует List к использованию в качестве стэка. Скрывая рандомный доступ, например.
Тогда это защищающий прокси, а не адаптер.
Не всё ли равно как это называть? Впрочем, это не прокси, так как меняет интерфейс. А прокси служит для добавления функционала без изменения интерфейса.
Абстрагирует от интерфейса конкретной реализации.
1. Архитектурную оптимизацию нужно проводить как можно раньше, иначе после «получения эмпирических данных» весь написанный код придётся выбрасывать, ибо смена архитектуры — это с высокой вероятностью переписывание всего кода.
«Как известно», правильно выбранная архитектура — это та, в которой все решения с наибольшим влиянием приняты в начале, так что стоимость внесения всех прочих изменений сравнительно невысока.

(типичный идеальный мир, впрочем)
Что подразумевается под «архитектурной оптимизацией»?
Любые не локальные оптимизации, меняющие интерфейсы модулей, их роли, взаимоотношения, форматы данных и тп.
Не могу не согласиться.

Самый банальный пример такой архитектурной оптимизации — это выбор между поэлементной обработкой и обработкой пачками (batch-processing).

Как правило, batch-processing обладает большей эффективностью, но при этом код гораздо более запутан. А поэлементная обработка даёт возможность продемонстрировать все красоты ООП. Желательно на самом начальном этапе сделать выбор между этими двумя подходами, даже если это и кажется преждевременной оптимизаций (мол, потом переделаю) — ведь batch-processing может сильно отличаться от обычной поэлементной обработки. К примеру, переписать процессинг логов с использованием map-reduce может потребовать выкинуть весь красивый ООП-код поэлементной обработки, и написать его заново.

Сколько раз я слышал от борцов с преждевременной оптимизаций, что писать надо красиво, а потом профилировать! И сколько раз потом наблюдал, как они выкидывают свой код в помойное ведро, потому что поддались соблазну написать красиво, и не оценили, сколько ресурсов потребует обработка 20+ гигабайт логов :-)
Лучше вообще все делать параллельно: проектирование, код, тесты, профилирование, рефакторинг и т.д. Это даст и скорость, и качество.
Но проблема в том, что это — самый дорогой способ разработки. Да и годится лишь для прожжёных профи-единомышленников. И милионных проектов.

А если в проекте сидят аутсорсеры или квалификации хотя бы у кого-то из команды пока недостаточно, то step-by-step — неизбежный, но довольно эффективный путь. Результат может и не быстр, кода лишнего может и много, но в головах у народа порядок и все УЧАТСЯ его поддерживать.

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


Ну а кто предлагает переписать все-все? Просто надо знать, где сразу надо писать эффективно, а где можно и через годик вернуться, да переделать, если припрет.

Все твердят, мол, начинай писать неэффективно, потом перепишешь. Будто бы «эффективно» и «красиво/понятно» — это какие-то две крайности, разные полюса.

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

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

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

А ведь что такое фреймворк? Это сотня тысяч строк чужого кода, в котором подчас разобраться затруднительно. Если автор фреймворка при его написании тестировал его на простеньких примерах, и не профилировал на большом объеме данных — то это каюк, переписать его невозможно (это может стоить дороже, чем все приложение). А ведь можно даже и попрофилировать на терабайте данных, да не том, и все проскочит как по маслу — никогда не знаешь, для чего фреймворк решат применить!

<Лирическое отступление>
Мне довелось поработать в компании, которая сделала свой фреймворк. Нет, даже Фреймворк, с большой буквы. Решал он очень специфический задачи логистики и планирования и был, надо думать, один такой в своём роде.

С точки зрения ООП он был совершенен. Да, не без сотни слоёв абстракции внутри. Но кто сам без этого греха написал свой Фреймворк — пусть первый бросит в меня камень.

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

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

И все было ничего, пока объем данных был невелик. Но вот приходит клиент побольше, лакомый кусочек… И бамц — все еле движется… Начинается профайлинг… День, два, три — круг поиска сужается… Четыре, пять, неделя — все ниточки тянутся во Фреймворк…

И вот тут начинается Пичаль, тоже с большой буквы. Из песни слова не выкинешь, слой абстракции из Фреймворка не уберешь — где-то в другом проекте на него кто-то завязался… LinkedList на ArrayList не заменишь — кто-то ведь и к его поведению мог привязаться, применений-то у Фреймворка не счесть…

Идешь к авторам Фреймворка на поклон — «ребята, выручайте, тормозит безбожно, причем по всей площади Фреймворка сразу — нету батлнеков, все сплошняком адовое!» — а они тебе: «да мы с зимы ковыряемся с этим, в проекте ХХХ такая же шляпа, но у нас тут 44 слоя абстракции, 12 делегатов, 7 прокси, куда теперь это уберешь :-( Ну погоди, в версии 11.4 станет полегче».
</Лирическое отступление>
И вот тут начинается Пичаль, тоже с большой буквы. Из песни слова не выкинешь, слой абстракции из Фреймворка не уберешь — где-то в другом проекте на него кто-то завязался… LinkedList на ArrayList не заменишь — кто-то ведь и к его поведению мог привязаться, применений-то у Фреймворка не счесть…


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


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

Даже если они не смогут залезть внутрь и получить сам LinkedList, все равно эта особенность ведет к примеру к быстрой вставке в начало списка и медленному доступу к элементам в середине.
как люди любят тратить время на совещаниях на всякую ерунду, вместо того, чтобы обсуждать насущные проблемы. Конкретно, проектировщики атомной электростанции очень долго спорили, какой материал должен пойти на навес для велосипедов – bike-shed


Этот эпизод из «Закона Паркинсона» — он чуть о другом.

Там было заседание в мэрии, на котором обсуждались два вопроса — смета на постройку АЭС и смета на постройку сарая для велосипедов служащих мэрии. Обсуждение сметы АЭС прошло в стиле «По смете стоит X долларов. Принимаем? Да, я слышал, что строительство другой АЭС стоило больше. Принимаем». Приняли минут за пять. А вот навес для велосипедов обсуждали очень долго и в итоге вроде решили вообще не строить, точно не помню.

Суть тут не в том, что «много тратят времени на ерунду», это вообще не об этом. Суть в том, что у людей есть определенный уровень компетенции, и что такое АЭС, они не представляют, не представляют масштаб цен и поэтому их легко ввести в заблуждение и они даже ничего не поймут, т.к. некомпетентны. А вот что такое «сарай», сколько стоят жесть и гвозди, представляет всякий, поэтому готов это обусждать, и обсуждать очень долго.
Т.е. оно, конечно, выходит, что о ерунде спорят дольше, но это следствие, суть — в разном уровне компетенции. И, как следствие, то, о чем люди не имеют представления, пройдет легче, а о чем имеют — может вообще не пройти.
Это как с выборами — проголосовать за людей, о которых и о работе и компетентности которых представления мы не имеем, мы можем быстро и гладко, а вот обсуждать смету ТСЖ на ремонт подъезда будут месяцами и в итоге, может, к согласию и не придут.
Выдержки из оного также переведены неверно.

Однако статью этот факт, в принципе, не очень портит :)
Просто на этом месте меня аж передёрнуло. Переводить Zen как непонятное нерусское «Зен» — это какой-то ужас-ужас, за гранью. Остальные неточности я ещё могу простить, но изобретать новые слова, когда есть существующие канонические… Лень посмотреть в словарь?
Добавление классов уменьшает сложность.

Очень смелое заявление. Это далеко не всегда так, здесь придется положиться на «здравый смысл», определяя, где он — предел разбиения.

Встречал такие ситуации, когда новичок, находясь в эйфории от «паттернов, ООП, вот этого всего» делал разбиения там, где это совершенно не нужно. Например, есть некий метод, который на основании одного флага (т.е. вариантов работы данного метода только два и всегда будет два, больше физически невозможно сделать). Вместо того, чтобы написать код вида:
… какие-то действия…
if () {
вариант 1
} else {
вариант 2
}
… еще какие-то действия

составляющий в сумме строчек 15, он создавал два разных класса с перегрузкой методов, скрывающих действия вариант1, вариант2. В итоге, когда потребовалось изменить поведение, пришлось ломать всю структуру классов и переделывать с нуля, причем в других местах потребовалась перегрузка, которая очень просто реализовалась, а в этом конкретном месте — вариант с if'ом. Ощущение было, что человек не решал проблему, а писал курсовую на тему «как написать простейший код так, чтобы он использовал все самые хитрые заморочки ООП». Разве только интерфейсов не хватало.

Или, например, работа с каким-либо файлом, например, log'ом. Реализована в виде простого метода: открыть файл, отформатировать строку, записать ее в файл, закрыть его. Вернуть признак ошибки.
Вместо этого был создан класс, в конструкторе которого файл открывался, потом отдельный метод для форматирования строки, причем с разными параметрами вместо стандартного sprintf'а (язык другой, не Си, суть та же). Закрытие в деструкторе, отдельное поле ошибки. В итоге вместо if(!DoLog(...)) do something; необходимо создать экземпляр класса, инициализировать его, вызвать запись, сохранить код ошибки, вызвать деструктор, проанализировать код ошибки.

Или когда создается отдельный класс для очень узкой, специализированной задачи, которая вызывается ровно один раз ровно в одном месте. Реализуется простой процедурой, класс не нужен вообще, но он создается. Со всеми прелестями конструирования его и деинициализации.
Подход с классом-логгером может быть оправдан. Например, что если понадобиться логировать не только в файл, но и в память, в стандартный вывод, по сети на сервер логов, в syslog? В таком случае просто нужно будет переопределить метод записи в классе логгере. А если нужно ещё различать классы сообщений (ошибка, информационное, дебаг, предупреждение…), логировать разные классы в разные места, конфигурировать это всё с помощью файлов конфигурации, плюс в зависимости от источника лог-сообщения… Тут уж без целой иерархии классов не обойтись, иначе будет просто god-class какой-то.
Может, но там не тот случай, это был именно «лог в файл», который не перенаправлялся бы ни в какой другой носитель, да и сама реализация класса не позволяла переопределеить носитель, это была по сути обертка, в которой процедуру log разодрали на части, неудачно смешав с инициализацией самого класса. Просто класс ради класса. Если у нас есть процедурка, которая работает с типом данных А, и выдает типы Б, В, вовсе необязательно создавать специальный класс-обертку из одного метода просто «чтобы было ООП».
Например, что если понадобиться логировать не только в файл, но и в память, в стандартный вывод, по сети на сервер логов, в syslog


Справедливости ради, если появились такие потребности, то не стоит изобретать велосипед, даже если это красивый абстрактный велосипед, лучше поискать готовую альтернативу. Так что в любом случае разработчик был не прав :-)
Как раз прав. Если его реализация инджектится в виде интерфейса, то поменять ее на готовую либу становится делом десяти секунд.
… после чего реализация станет просто оберткой вокруг готовой библиотеки, а это не всегда осмысленный код.
Нет-нет, я имею ввиду, что вся эта самописная либа логгирования инджектится в класс-потребитель за каким-нибудь общепринятым интерфейсом.
Это если вам повезло, и у вас был общепринятый интерфейс. А то — в случае с тем же логированием — каждая библиотека зачастую объявляет свой.
Поэтому у каждой третьей java-библиотеки своё логгирование, которое, в хорошем случае, рано или поздно становится обёрткой над slf4j, log4j, jcl, jul или osgi logging. Вместо того, чтобы сразу использовать slf4j/jcl/log4j
/Проектируя газонокосилку/ — А что, если данный узел будет использоваться не только в газонокосилках, но и в городских автомобилях, танках, машинах формулы-1, или марсоходе? Надо учесть все варианты…

Например, что если понадобиться ...

А если не понадобится? Тогда весь труд по написанию и поддержке превентивной архитектурной гибкости окажется напрасным. В конечном итоге выбор, где расположить ползунок на шкале «KISS <-> заложенная гибкость», всегда субъективен, объективные критерии априорно определить целесообразность такого выбора не существуют. На практике есть только некоторая корреляция, что с опытом удачно сделать выбор получается чаще.
А если не понадобится?

А просто надо перед тем, как браться за работу, уточнить, пишем ли мы одноразовый скрипт, или что-то другое. Если что-то другое — то надо сразу делать нормально. Здесь, коли речь вообще зашла о сохраняемом логе, видно, что речь идет о продукте. Тем более, что выше автор говорит "… когда пришлось менять поведение".

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

А еще такой псевдо-KISS приводит к тому, что в одном блоке у нас оказывается бизнес-логика, работа с ФС, работа с БД, работа с UI. А что, там же все просто, пара строк всего.

Субъективность — это для говнокода. Чистый код тем и хорош, что маскимально объективен, ибо в каждом отдельно взятом модуле делается что-то одно и спорить там не о чем.
Такое уточнение почти всегда неизвестно. Иначе задача бы звучала «реализовать логинг в файл и ХХХ»
Здесь напрашиваются определения, что такое «нормально» и «чистый код», потому как на практике почему-то оказывается, что разработчики эти определения могут понимать по-разному. Никто не ставит перед собой цель написать говно, однако иногда с одной стороны получаем «псевдо-KISS», а с другой — «астронавтов архитектуры» и истории про то, как сложный многотысячный код удалось заменить одним скриптом на пару сотен строк. А определившись с определениями, какими бы они ни были, потребуется объективное доказательство, что такой «нормальный чистый код» есть единственно верное во всех случаях решение. Доказательство по сути невозможное, потому как программирование — лишь инструмент решения класса задач, а применять любой иструмент надо с оглядкой на цели и окружающие условия (с чем очевидно, Вы тоже согласны, приводя в пример одноразовый скрипт).

С моей точки зрения, хороший код — это:
— код, в котором есть необходимость; если задачу эффективнее решить без программирования, например организационно, такой вариант определенно стоит рассмотреть;
— код, который работает;
— код, который написан быстро (дешево);
— код, который дешево поддерживать и дополнять (если есть такая задача);

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

Поэтому, к слову, когда видишь нечто, напоминающее говнокод, порой стоит задуматься, в каких условиях он был написан (дедлайны, наличие других задач с высшим приоритетом?), и какие задачи он должен был решить, и лишь потом хаять программиста. Вполне возможно на момент написания это был действительно хороший код, просто со временем условия и требования к нему изменились.
Тем более, что выше автор говорит "… когда пришлось менять поведение".

Это о другом случае, там ряд примеров.

ибо в каждом отдельно взятом модуле делается что-то одно и спорить там не о чем.

Так то модуль, а то — класс, я о другом говорил.

Да вы идеалист, батенька. Обычно ответ на такие уточнения: «надо как получится, только быстрее-быстрее-быстрее и вчера», а потом выясняется, что нужно это расширять, хотят такую же штуку ещё где-то, и т.п.
Или когда создается отдельный класс для очень узкой, специализированной задачи, которая вызывается ровно один раз ровно в одном месте. Реализуется простой процедурой, класс не нужен вообще, но он создается. Со всеми прелестями конструирования его и деинициализации.

Как раз специализированная задача и должна быть вынесена в отдельный класс. С единственной целью избавить класс-потребитель от знания деталей этой специализации.

Отдельным бонусом идет возможность протестировать специализированный класс отдельно, а его потребитель — отдельно.

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

вызывается ровно один раз ровно в одном месте

Уже много лет дубликация не является единственным, и даже основным, поводом выделения юнита. Гуглите SRP, и заодно IoC.

P.S. Сорри за резкость, но лог джуниор сделал правильно (включая выделение отдельного метода для форматирования), а Вы — нет. Логгирование, реализованное отдельным классом — это стандарт, даже если это просто обертка над записью в файл. Если хотите, могу подробно расписать, почему это так, без упоминания абрревиатур и ссылок на Гугл.

Мне вот интересно как вы видите идеальное логирование. Желательно с примерами :-)
В идеале хочу указывать, что надо залоггировать, в аннотации к методу :) Чтобы в коде не было ни одной строки, без которой он не мог бы обойтись, чтобы сделать свою главную (и единственную задачу).

Ну а в неидеальном мире мне хватает инъекции через конструктор интерфейса с методами info, warning и error. В продакшене этот интерфейс реализовывается логгером в GrayLog, в CI — записью в файл, а во время юнит-теста в IDE — заглушкой с выводом в никуда, если все ок, или на экран, если что-то не хочет заводится и мне нужен этот лог. Конкретная реализация выбирается бутстрапом приложения на основе переменной окружения, при этом бизнес-код не меняется, не ломается и вообще ничего не знает о логгере, кроме того, что он есть.

Все просто и комфортно :)
Как раз специализированная задача и должна быть вынесена в отдельный класс. С единственной целью избавить класс-потребитель от знания деталей этой специализации.


Зачем? «чтобы был класс»? Это ООП ради ООП.
Такую задачу можно вынести в отдельную процедуру в отдельном модуле. Которая будет тестироваться отдельно и далее по тексту.

Просто создавать класс-обертку с 1 статическим методом или пустыми конструктором-деструктором + необходимость объект конструировать там, где нам просто надо 1 раз вызвать процедуру — бессмыслица. Это бездумное следование мантрам.

Уже много лет дубликация не является единственным, и даже основным, поводом выделения юнита.

Я разве спорю? Вот есть отдельная процедура (пусть даже разбитая еще на части) в отдельном модуле.
Класс зачем?

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

Вы не знаете исходных данных задачи, поэтому не можете полностью судить о правоте. Я лишь привел пример, на самом деле, там была работа с файлом, но это был не лог. Я просто упростил для изложения здесь.
Класс — чтобы объявить его реализующим интерфейс, и в потребитель передать именно этот интерфейс. Думаю, не надо перечислять преимущества зависимости от инъектируемого интерфейса перед зависимостью.
… (сорри) перед зависимостью от жестко прописанной процедурой в другом модуле.
Даже и в этом случае, с другим модулем: в чем разница между зависимостью от интерфейса класса, прописанном в модуле X и зависимостью от реализации в виде процедуры в классе Y? Учитывая, что процедура перегружаема в зависимости от типа данных, с которыми она работает и то, что она может быть просто однострочной оберткой (тот же интерфейс по сути)?
В том, что вбрасывание зависимостей, выраженных в процедурах, несколько сложнее, нежели вбрасывание зависимостей, выраженных в интерфейсах. А это, в свою очередь, затрудняет тестирование.
Без конкретных случаев говорить не о чем, слишком общая фраза. Да и как писал выше: если процедура — обертка, то это тот же интерфейс по сути.
Хотите конкретный случай? Легко. Вот у вас есть простенький C#-ный «модуль» (не важно, класс ли, метод ли). Внутри него делается два логирования — одно с уровнем Info, другое с уровнем Error. Каждый уровень логирования — отдельный метод.

Теперь мы хотим это протестировать (т.е., удостовериться, что логи пишутся в нужных случаях). Можете показать пример кода для такого случая?
Нет, на шарпе не пишу, к сожалению. Ваша задача сводится к тому, чтобы зафиксировать факт вызова этих двух процедур, выполняюших логирование. Например, вы можете использовать conditionals для перехвата. Или компиляции другого, отладочного модуля-заглушки с теми де процедурами. Или библиотеку, перехватывающую вызов любой процедуры, втч встроенной в язык (у нас в проекте используется такая).
Но это все не важно, потому что я говорил о другом, тривиальном использовании (см. пример кода, пусть утрированный, но все же, ниже).
… а в случае с классами и интерфейсами я просто задам нужный мне мок, и сделаю все проверки на нем.
Кому что удобнее. Все равно вам придется создавать класс-заглушку и подставлять либо его, либо рабочий класс через интерфейс учитывая вариант сборки.

Да и это не тот случай, как я уже написал. Я же не говорю «нет таких ситуаций, когда использование ООП оправдано», я говорю, что есть случаи, когда не оправдано.
Не «учитывая вариант сборки», а через DI. Это фундаментальная разница.
А DI надо конфигурировать… разными конфигами в зависимости от варианта сборки!
Мне кажется, тут где-то терминологическая путаница. Варианты сборки для меня — это всяческая условная компиляция, что приводит к тому, что тестируемая сборка бинарно отличается от выкатываемой. Все, что настраивается в рантайме (в том числе — конфигурация) — это не варианты сборки.
В общем и целом, похоже на то. А в принципе всё зависит от реализации DI, коих вагон и маленькая тележка. Взять тот же Cake Pattern в Скале.
Насколько я успел понять из беглого прочтения, этот паттерн тоже рекомпиляции не требует. Это в моем понимании критичное отличие.
Прочитал повнимательнее.

С одной стороны, я бы очень аккуратно подумал прежде, чем относить Cake Pattern к DI, особенно после вот этой фразы:

In plain dependency injection, we create components and we assemble these components together to form an application. Using the Cake Pattern, we create pieces of functionality and we assemble the functionality to form the application.


С другой стороны, то, как выглядит тест — это типичный DI, просто не через конструктор, а через миксины/дженерики (первыми из которых трейты и являются):

class CakeTestSpecification extends Specification with Mockito {

  trait MockEntitManager {
    val em = mock[EntityManager]

    def expect(f: (EntityManager) => Any) {
      f(em)
    }
  }

  "findAll should use the EntityManager's typed queries" in {
    val query = mock[TypedQuery[User]]
    val users: java.util.List[User] = new ArrayList[User]()

    val userService = new DefaultUserServiceComponent
                        with UserRepositoryJPAComponent
                        with MockEntitManager
    userService.expect { em =>
      em.createQuery("from User", classOf[User]) returns query
      query.getResultList returns users
    }

    userService.userService.findAll must_== users
  }
}


И здесь, кстати, хорошо видно, что рекомпиляции тестируемого кода для замены зависимостей не нужно: зависимости определяются в тесте.
Да, но нужна будет перекомпиляция теста. То есть по любому придётся что-то перекомпилировать, не сами базовые классы, так место их использования. Про то и говорю.
Перекомпиляция теста никого не пугает. Важно, что не будет перекомпиляции тестируемого кода, т.е. тот же код, который тестировался, уйдет в продакшн.
Я про это место писал если что:
  val userService = new DefaultUserServiceComponent
                        with UserRepositoryJPAComponent
                        with MockEntitManager

Вот именно про перекомпиляцию этого используемого анонимного класса я писал:
Чтобы изменить конкретную используемую реализацию, нужно изменить объявление класса.

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

(заметим, этот пойнт критичен не только для меня, Хамбл и Фарли в Continuous Delivery говорят о том же, причем вплоть до расчета хэшей файлов)
то я не могу быть уверенным, что тот, который в продуктиве — протестирован

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

Более того, вашим продуктом может быть не программа, а программный комплекс. Который полагается на другие части — например, базу данных.
Вот вы протестировали у себя свою программу с СУБД, например, MySQL 5.1.44, в требованиях у вас указано, что ПО работает с версией не ниже 5.1.44.
А у клиента — 5.1.45 и в ней, например, исправлен какой-то баг, который не вылазил у вас в предыдущей версии. И так можно продолжать долго — вплоть до ОС и самого железа. Вы не можете быть уверены, что ваш продукт будет работать у клиента так же, если только вы не отдадите ему компьютер тестировщика со всеми настройками. Это все — допущения — о том, что сторонние части будут работать так же. В ситуации с блоками программы та же логика, можно рассматривать те два блока из примера как два компонента программного комплекса.

Откуда я знаю, что еще подменила условная компиляция?

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

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

Вот я предпочитаю «варианты», где модель-под-тестированием бинарно идентичен тому, который пойдет в продуктив.

Да, я понимаю, что есть допущения. Но все допущения, которые под моим контролем, я постараюсь минимизировать.
Мы, наверно, о разном говорим.
где модель-под-тестированием бинарно идентичен тому

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

public class ClassUnderTest
{
     private readonly ILogger _logger;

     public ModuleUnderTest(ILogger logger)
     {
          _logger = logger;
     }

     public void MethodUnderTest()
     {
          _logger.Info("abc");
          ...
          _logger.Error("def");
     }
}


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

В этом, собственно, вся идея Dependency Inversion: вызывающий код зависит от абстракции, а не от реализации.
Да это ясно, вы не поняли вопроса: в чем разница-то? Тестируемый блок зависит от интерфейса ILogger, но что подставляется — логгер или заглушка — определяется на этапе линковки, верно? Это просто точка стыковки двух блоков, но сама комбинация определяется при сборке, верно? В релиз версии через ModuleUnderTest будет задан реальный логгер, в тестовой сборке — тестовая заглушка. Бинарно идентичными сборки «модуль+логгер» и «модуль+заглушка» не могут быть. А то, что сам «модуль» бинарно идентичен — так он и в процедурной реализации бинарно идентичен. Различается второй блок (логгер vs заглушка), а он и у вас отличается.
Тестируемый блок зависит от интерфейса ILogger, но что подставляется — логгер или заглушка — определяется на этапе линковки, верно?

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

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

Отнюдь. В моем случае модуль в тесте и в продуктиве бинарно идентичен, это уменьшает степень неуверенности.

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

Нет такого кода. Просто нет.

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

В тестировании composition root находится прямо внутри теста (иногда — внутри тест-сетапа), и там задаются нужные тестовые зависимости, опять-таки, напрямую.
Не работаю ни с Net, ни с Java платформами, поэтому наберитесь терпения.

Отнюдь. В моем случае модуль в тесте и в продуктиве бинарно идентичен, это уменьшает степень неуверенности.

Вы говорите про тестируемый модуль, или про всю программу? Тестируемый модуль и в процедурной парадигме бинарно идентичен.

Нет такого кода. Просто нет.

Тем не менее — через один и тот же интерфейс мы контактируем либо с логгером, либо с заглушкой. Чем это определяется, если, как вы утверждаете, не линкером и не динамически(рантайм)?

Composition root, вызываемый в продуктиве, всегда подставляет продуктивную реализацию логгера… В тестировании composition root находится прямо внутри теста

Т.е. все-таки есть внешний модуль/сущность, которая определяет связь между тестируемым модулем и логгером либо заглушкой. Причем эта связь различна в тестовой версии и в релизе?
У вас есть модуль А, который вызывает Б или В:
А-(1)>Б
А-(2)>В
если вы говорите, что связи (1) и (2) не устанавливаются во время рантайма, значит они установлены статически при сборке, соответственно, комплексы различны — либо А+Б в релизе, либо А+В в тестовой среде.
В чем тогда разница по сравнению с ситуацией, когда связь А-> прописана в виде прототипа, объектный файл модуля А бинарно идентичен в тесте и в релизе, но при сборке мы используем объектный модуль либо Б, либо В, причем это задано make файлом?
Вы говорите про тестируемый модуль, или про всю программу? Тестируемый модуль и в процедурной парадигме бинарно идентичен.

Я говорю про сборку (assembly), как единицу развертывания. Это (условно) независимый бинарный файл.

Чем это определяется, если, как вы утверждаете, не линкером и не динамически(рантайм)?

Composition root.

В чем тогда разница по сравнению с ситуацией, когда связь А-> прописана в виде прототипа, объектный файл модуля А бинарно идентичен в тесте и в релизе, но при сборке мы используем объектный модуль либо Б, либо В, причем это задано make файлом?

Если язык/платформа позволяет одновременно иметь статически верифицируемую связь с используемым компонентом, и при этом заменять реализацию этого компонента при сборке, сохраняя компонент-пользователь бинарно неизменным, то ничем.

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

Но вы же говорите, что composite root в тестовой среде определен статически. Пара «модуль+composite root(тест)» и «модуль+composite root(релиз)» бинарно различны, нет?

и при этом заменять реализацию этого компонента при сборке, сохраняя компонент-пользователь бинарно неизменным, то ничем.

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

у вас хоть вся система может быть в одном модуле, на тестирование это не повлияет

В приципе, если добавить один кросс-модуль с глобальными переменными процедурного типа, которые будут инициализироваться по-разному в разных сборках — это будет то же самое, что у вас composite root.
Так и получается — в процедурной парадигме реализация — это отдельный объектный файл, а «интерфейс» — заголовок, прототип процедуры.

Не в «процедурной парадигме», а в конкретных языках/платформах, которые это позволяют. Например, .net такого не позволяет.
На самом деле, я был не прав, фраза «на этапе рантайма» неточна. На этапе рантайма происходит определение, какую продуктивную зависимость использовать, если их несколько или если используется DI-контейнер. В противном случае зависимость прописывается статически прямо в composition root, и никакого выбора не происходит вовсе.
А, да. Если у меня нет возможности сделать нормальный рантайм-DI, я всегда могу вынести composition root в отдельный модуль, зависящий от всех остальных. Тогда этот модуль будет единственным, отличающимся между тестом и продуктивом, но это уже никого не волнует, потому что composition root все равно не тестируется юнит-тестами.
Класс — чтобы объявить его реализующим интерфейс, и в потребитель передать именно этот интерфейс.

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

Простой пример: у вас функция, реализующая некоторый алгоритм. Совершенно неважно, это функция-член класса, или отдельно стоящая функция. Часть ее, являющуюся логически законченным блоком, выделяется в отдельную функцию для повышения читаемости. Данная функция работает с одним и тем же типом данных, ее функционал никогда не будет использован повторно, более того она не вызывается не только из других модулей, но даже из других процедур. Она решает одну конкретную задачу. Попробуйте, покажите, зачем здесь нужен абстрактный интерфейс.
Упрощенно: у вас есть кусок кода (простой тип данных использую условно, just to make a point):
bool func1(){

if(a > 0) {
действие1
}

}

он преобразован в функции:

bool positive(int b){
return (b > 0);
}

bool func1(){

if(positive(a)){
действие1
}

}

Покажите, почему функцию positive следует реализовать в парадигме ООП «чтобы передать потребителю — функции func1 — интерфейс». Без додумывания вида «а что если потом нам потребуется работать не только с int, мы тогда сможем перегрузить». Постановка задачи именно такова — тип данных — int — зависит от внутренней реализации функции func1 и меняться не будет. Даже если и будет — positive тоже перегружаема, без ООП.
Хм. Потому что я не хочу перегружать. Я хочу прямо в конструкторе видеть все зависимости класса-потребителя (вы же знаете, чем плохи глобальные переменные? в том числе неявным контрактом того куска кода, где они используются. А если у меня нет доступа к исходникам?); я хочу в рантайме эти зависимости устанавливать на то, что мне нужно в данный момент, включая то, что в один экземпляр класса я передам одну реализацию, а в другой — другую (я не про абстрактное «вдруг мне понадобиться заменить...», а про запуск на проде и в IDE в изоляции, например); я хочу нормальное переиспользование кода за счет SRP; за счет SRP же я хочу простую поддерживаемость в дальнейшем; наконец, я не хочу хитровывертов с перегрузкой, даже если я могу их сделать.

Слушайте, ну поищите уже в интернете, зачем нужны интерфейсы… Пожалуста, не обижайтесь за такой совет, но это азы и об этом написаны просто кучи материалов и книг.

P.S. Было бы интересно узнать, для чего по Вашему мнению нужны классы/объекты (или ООП).
Какие глобальные переменные, если функция чистая? Вот о чём речь.
Глобальные переменные — это пример неявного контракта. Прямая зависомость кода от юзер-функции в другом модуле — это тоже неявный контракт. Так же плохо, как глобальная переменная.

P.S. Чистая функция, пишущая в файл (в вашем исходном примере, с которого все началось)? Вы издеваетесь? :)
P.S. Чистая функция, пишущая в файл (в вашем исходном примере, с которого все началось)? Вы издеваетесь? :)

Э, нет, началось не с записи в файл. Там было 3 разных случая, только в одном шла работа с файлом. И пример, приведенный выше — просто ящик: data in — data out.

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

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

Вам придется, если надо работать с другим типом данных.

Я хочу прямо в конструкторе видеть все зависимости класса-потребителя

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

я хочу нормальное переиспользование кода за счет SRP

Так не используется positive нигде более. SRP соблюден.

Слушайте, ну поищите уже в интернете, зачем нужны интерфейсы… Пожалуста, не обижайтесь за такой совет, но это азы и об этом написаны просто кучи материалов и книг.

Не в интерфейсах дело. Вы доказываете примерно такую точку зрения: «Существуют ситуации, когда нам требуется сделать X, Y и Z, наиболее просто и полно это реализуется в рамках ООП парадигмы».
Тогда как я говорил «Существуют ситуации, когда нам не треубется делать X, Y и Z, наиболее просто это реализуется в рамках процедурной парадигмы, ООП же излишне». Вы воююте с ветряной мельницей — так, будто я утверждал никчемность ООП, тогда как моя позиция была «создавать класс каждый раз не всегда нужно и полезно».
P.S. Было бы интересно узнать, для чего по Вашему мнению нужны классы/объекты (или ООП).

Говорить об объектах можно тогда, когда у нас есть некоторое внутреннее состояние, которое изменяется в течение времени жизни объекта.

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

Не люблю глобальные переменные, но, справедливости ради, если переменная глобальна в рамках модуля (область видимости — данный модуль), то это терпимо. Потому что если какой-то модуль реализует какой-либо класс, доступ к полю данных класса по сути то же самое (с точки зрения кода, не рантайма, конечно — объектов может быть много, в отличие от г.п., но доступ к этой г.п. можно получить так же только в рамках модуля).
Попробуйте, покажите, зачем здесь нужен абстрактный интерфейс.
Тут многое зависит от платформы. Он может оказаться необходим хотя бы для целей тестирования: пишем отдельные тесты на positive, а в тестах основной функции подставляем стаб. Не на каждой платформе можно легко подставить стаб без использования абстракций типа интерфейса.
UFO just landed and posted this here
В нормальной горизонтальной команде эти все вещи просто не могут возникнуть, т.к. это всё требует жутких усилий для развития и поддержки.

Зато это не требует усилий для написания. Поэтому если конкретный автор (вне зависимости от того, в какой он команде), думает о «сейчас», а не о «потом» — будут и god classes, и все остальное.

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

Есть: отсутствие рефакторинга.
UFO just landed and posted this here
Согласен, но если в команде больше 2х человек, то то что кто-то думает о «сейчас», а кто-то о «потом» вполне нормально.

Ну вот чтобы из первых получались вторые, и нужны подобные статьи.

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

Я всегда думал, что основной инструмент взаимодействия разработчиков — это код и речь. А рефакторинг — всего лишь инструмент.
UFO just landed and posted this here
Рефакторинг позволяет же привлекать объективные критерии, вроде соответствия SOLID.

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

Потому что код должен быть хорошим, вы не поверите.

Если зажимать разработчиков по каким-то критериям кроме способности выполнять свои задачи

Согласитесь, что важно не только, что задача выполнена, но и как она выполнена, нет?

Проблема в том что и речь, и код это всё субъективные вещи.

Если код субъективен, то и SOLID, на который вы ссылаетесь далее, субъективен. Но на самом деле, не забывайте, что речь — это первичный способ взаимодействия людей. Если вы отказываете ему в эффективности, то вы никуда не придете.

Рефакторинг позволяет же привлекать объективные критерии, вроде соответствия SOLID.

А вы считаете, что SOLID — безусловное объективное благо? И вы можете уверенно и объективно доказать уровень соответствия того или иного решения SOLID?

Сделай и рефакторинг и представь правильное.

В свободное от работы время, я надеюсь?

Нормальная команда всегда оценит вычищенный и понятный класс.

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

Однако на практике в хорошей опытной команде даже субъективные вещи все понимают примерно одинаково.
Хорошие опытные команды когда-то были плохими и неопытными, не правда ли?
На страшилке про преждевременную оптимизацию воспитано поколение кретинов и вредителей.

Во-первых, всегда есть возможность даже примитивные действия написать оптимально и не оптимально. Надо выработать в себе рефлекс в каждой строке кода выбирать более оптимальный вариант. А не наращивать строку в цикле с мыслью «И так пойдёт для начала!»

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

Во-третьих, кэшируйте обращения к базе всегда. Особенно, если используете ORM. Особенно, если есть хоть мизерный шанс, что ваш бэкенд будут DoS'ить. Промах добавит к вызову мизерное время на проверку наличия данных в кеше и их запись, на порядок меньшее, чем время извлечения этих данных из базы. Зато хит сделает вызов на порядок же быстрее.
Во-третьих, кэшируйте обращения к базе всегда.

А что после этого делать с консистентностью данных?

(вот как раз кэш — в правильно спроектированной системе — можно добавить почти в любой момент)
Wrong.

Думать надо не об оптимизация, а об архитектуре. Конечно, ни в коем случае не стоит вместо O(N)-алгоритма пихать О(N^3), но архитектура и следование SOLID важнее микрооптимизаций, а это частая ошибка, часто встречается вместе с «сараем» — от неспособности спроектировать в целом. Скажем, вместо того, чтобы спроектировать такую архитектуру, когда кэширование можно при необходимости подключить где угодно, пишут его вручную. Или вот (пример из мира PHP) Smarty и Twig. Пока первые занимались ужасно важными вопросами типа «что быстрее, strlen($a) > 200 или isset($s[200])», гоняли искусственные бенчмарки сферических коней в вакууме, вторые занимались архитектурой проекта и целостностью его модели, в итоге получив гораздо более целостную библиотеку, которой приятно пользоваться и которую легко расширять и поддериживать, а проблемы с действительно узким местом решили написанным на С php-расширением.
В программировании есть две сложных вещи: инвалидация кэша и выбор, как правильно что-нибудь назвать.

Преждевременное создание системы кэширования «на всякий случай» может сильно задержать старт проекта как на этапе постановки и проектирования (не зная реальных узких мест можно очень много времени потратить на обсуждения выборов стратегий инвалидации и их параметров там, где кэширование вовсе не требуется), так и на этапе непосредственно реализации и поддержки.
С наездом на код из 9 вообще не согласен.
1) Это следование правилу номер 6. Студент не стал изобретать велосипед и использовал уже существующее решение.
2) Это инкапсуляция. Просто list использоваться для стэка нельзя, потому что он позволяет работать с любым элементом и не ограничен доступом только к верхнему.

Студент взял уже существующий механизм и ограничил его работу так, как требовалось в исходной задаче.
По-моему все ОК.
Есть еще проблема #10 — Overcomplcated code
С этой проблемой сталкиваюсь все чаще и чаще, да и сам ей грешу

Особенно часто эта проблема возникает среди программистов с не очень большим опытом, скажем до 3 лет. Это выражается в необоснованном уровне абстракций, желании написать общий код там, где хватило бы конкретного и многое другое
Как упоминалось в Совершенном коде — код и ПО должно быть максимально простым, но на практике это выражается в борьбе KISS и SOLID, DRY и тд :)
По поводу правил 4 и 5 — наглядный пример — Django. Со своими views, forms, ..., которые разростаются до размеров в десятки тысяч строк и потом попробуй найди то, что тебе нужно. До сих пор не могу понять эту особенность Django.
В Django можно разделять их, к примеру, вьюхи у меня разделены и лежат в папке views
При желании — можно все сделать, но фреймворк изначалано подразумевает плоскую структуру. Ну и если все переносить в папки, то нужно редактировать urlconf или же все включать в __init__.py
Например, в ASP.NET MVC изначально придумали папочки отдельные для этого
Мне кажется «божественный объект» более устоявшийся термин, чем «класс бога»
По анти-паттернам довольно много статей на хабре, по этой причине предпочел бы увидеть такое оформление статьи:

5 Страх перед добавлением классов

Редкое лучше, чем густое
Тим Питерс, Зен языка Python

Слишком длинно, не читал

Большое число классов – не признак плохого дизайна

Все остальные пункты
Что это

В чём сложность
Что за «письмо от Пола-Хенинга Кэмпа по этому поводу»?
Забыл ссылочку поставить. Спасибо, исправил.
Чем объясняется искажения нормального закона распределения оценок в примере с тестом?
Читерством же объясняется, очень много людей, которые едва не проходили тест, читерили, стараясь попасть на хотя бы минимальную оченку. По сути этот пик — «сдвинутые» случаи из левой части.
Добротой экзаменатора, очевидно :-)
Рассматривал и эту версию, и ту, которая указана выше. И честно говоря, сам склонялся именно к этой. Плюсы за Ваш ответ могут косвенно свидетельствовать, что так и было — преподаватель набрасывал пару баллов для того, чтобы испытуемые всё-таки набрали минимум баллов и прошли тест.
Sign up to leave a comment.

Articles

Change theme settings