«Комментарии должны составлять 5% от общего количества баллов», — заявил мой коллега-преподаватель.
Ура, мудрый прохвессор наконец-то расскажет нам, как программы программировать!</sarcasm>
Вообще ту же логику можно применить и к тестам. Тесты устаревают и их приходится обновлять, некоторые программисты пишут их плохо, и вообще, совершенный код не должен всегда работать правильно. Почему же никто не кричит, что тесты надо перестать писать? А просто когда код без тестов работает неправильно, то спорить с этим трудно, придётся исправить код и признать, что надо было делать тесты. А когда код без комментариев непонятен, то, ну… ты просто не умеешь читать код, да и в конце-концов же во всём разобрался, и вообще, всё равно на комментарии сейчас нет времени.
В разделе "Порядок инициализации" есть неточность в описании порядка инициализации статических полей и вызова статических конструкторов. Среда гарантирует, что статические поля будут инициализированы, и статический конструктор будет вызван до первого использования класса, отношение наследования никак на порядок статической инициализации не влияет. Например, если в статическом конструкторе наследника как-то используется базовый класс (статические поля или создаётся экземпляр), то инициализация статических полей и вызов статического конструктора базового класса произойдёт до аналогичных действий для наследника.
Методы в ООП взаимодействуют с состоянием объекта. Когда методы перестают это делать, а состояние начинает проталкиваться через аргументы, то код превращается в обычное процедурное программирование. Разве не так?
Так, только Мартин работает не с состоянием, а со скрытыми аргументами. Допустим, я меняю класс PrimeGenerator и вызываю checkOddNumbersForSubsequentPrimes. И тут у меня всё падает, потому что, оказывается, я должен был проинициализировать (статические!) поля primes и multiplesOfPrimeFactors, причём проинициализировать их в два этапа: присвоить каждому новый экземпляр соответствующего класса и вызвать set2AsFirstPrime. Если бы я написал точно такой код в процедурном языке, например, на C, то коллеги быстро объяснили бы мне, что это полнейшее безобразие. Но если перед этим безобразием написать слово class, то оно магически превращается в "чистый код" Мартина.
Мне как-то достался такой класс, где параметры были убраны в поля. Судя по истории, изначально он был вполне компактным, но, конечно же, со временем и сам класс, и его методы разрослись, и понять что откуда берётся и где оно поменяется в следующий раз было очень сложно. Да, в том коде было много других проблем, но изменяющиеся поля вместо явных аргументов делали всё только хуже.
Может возникнуть вопрос, а как отличить состояние от скрытых аргументов? По моему мнению, состояние, во-первых, влияет на дальнейшее поведение объекта, а не только на текущий вызов, во-вторых, зависит от предыдущего состояния, в-третьих, объект переходит из одного корректного состояния в другое корректное состояние, то есть состояние полностью инициализируется в конструкторе и публичный интерфейс объекта не допускает частичной смены состояния. Если состояние не ломается при возникновении ошибок (т.е. выполняются гарантии исключений) и состояние меняется атомарно, то вообще хорошо. Скрытые аргументы наоборот, влияют только на один конкретный вызов публичного метода (ну или некой правильной последовательности вызовов), зависят только от аргументов этого вызова, а в остальное время содержат мусор.
У нас, например, есть четкое правило: заметил говнокод — заводи отдельный таск в джире с типом «рефакторинг» и делай все изменения в его ветке.
А если исправление маленькое? Завести задачу, завести ветку, а потом ещё раз описать изменение в коммите — звучит чересчур громоздко для, например, исправления опечатки в имени переменной. Не получается ли в результате, что на мелкие исправления просто забивают? Или "протаскивают" их в несвязанных по смыслу коммитах, за что потом шлются лучи добра?
Скотт Майерс высказывал интересную идею, что инкапсуляцию типа можно измерить количеством кода, который может сломаться при изменении реализации этого типа.
Видимо, я немного не понял изначальный посыл. Мне показалось, что речь была не столько про exhaustive match, сколько про неожиданное появление новых вариантов в АТД (например, другими людьми).
А так да, для исчерпывающего сравнения только делать вложенные классы private и городить визитор. Для простых случаев, правда, можно обойтись чем-то вроде
public abstract T Match<T>(T case1, T case2);
Но это уже скорее "исчерпывающее сопоставление enum`а".
К сожалению, в большом проекте у этого способа есть серьезный недостаток: никто не гарантирует отслеживание в design time расширения Ваших алгебраических типов.
Можно делать "закрытые" иерархии при помощи вложенных типов:
public abstract class Adt
{
public sealed class Case1 : Adt
{
}
public sealed class Case2 : Adt
{
}
private Adt()
{
}
}
Это один вариантов, во что превращаются алгебраические типы данных в F# в скомпилированных сборках.
Может возникнуть вопрос, почему я использовал слово "инверсия". Дело в том, что зачастую при использовании более традиционных способов разработки программного обеспечения, таких как Structured Analysis and Design получаются архитектуры, в которых высокоуровневые модули зависят от низкоуровневых, а абстракции зависят от деталей. Собственно, одна из целей при этих подходах и заключается в том, чтобы определить иерархию подпрограм, описывающую вызов низкоуровневых модулей высокоуровнеывми.… Следовательно, структура зависимостей хорошо спроектированной объектно-ориентированной программы оказывается "инвертированной" по отношению к структуре, получающейся при использовании традиционных процедурных походов.
Не скажу, что полностью согласен с таким названием, но, по крайней мере, мотивация понятна.
Это требование спецификации языка, и это выполняется, если выполнение дошло до finally, то Dispose выполняется полностью.
А барьер и деструктор просто смещают точку гонки, т.к. они довольно длительные операции.
Барьер — согласен. Финализатор же вызывается асинхронно сборщиком мусора, и смещение происходит не за счёт вызова как такового, а за счёт постановки объекта в очередь финализации. То есть это эффект не на уровне C# или IL, а на уровне среды выполнения, и ещё не факт, что будет проявляться во всех реализациях .NET.
Вопрос интересный, так как логически ни финализатор (~A()), ни барьер не должны менять поведения в данном случая.
Финализатор не вызывает Dispose. Фактически, Dispose и финализатор не связаны ничем, кроме рекомендаций и здравого смысла.
MemoryBarrier не должен влиять, так как count объявлена как volatile и JIT сам добавит нужные инструкции.
Видимо, причина различия в поведении на практике в следующем. Когда происходит создание объекта с финализатором на управляемой куче, он должен быть поставлен в очередь финализации. Поскольку надо выполнить больше действий, то больше вероятность, что прерывание выполнения потока из-за Thread.Abort произойдёт не между выполнением конструктора и Dispose, а где-то ещё. Можно в этом убедиться, немного изменив цикл и вывод в вашем примере:
int i;
for (i = 0; i < 200 && count == 0; i++)
{
var t = new Thread(() =>
{
for (; ; ) using (var a = new A()) iter++;
});
t.Start();
for (var it = iter; it == iter;) { }
t.Abort();
t.Join();
}
Console.WriteLine("count={0}, iter={1}, i={2}", count, iter, i);
У меня в варианте с раскомментироваными строками (1) и (2) точно так же count становится не 0, просто это происходит не каждый раз, а спустя 3-20 итераций.
Год, месяц, день. Как в формате ISO 8601, в JavaScript, в SQL, в Java. И потом, это часть базовой библиотеки, и с датами приходится сталкиваться достаточно часто, чтобы порядок аргументов в конструкторе запомнился. А вот в 4.May(2019) — это как раз локальный российский формат, людям из разных стран с разными локалями он не поможет.
На мой взгляд, главная проблема с написанием кода, максимально похожего на естественный язык, в том, что фразы на естественном языке многозначны и могут быть истолкованы по разному. Часть работы программиста как раз и заключается в том, чтобы описание задачи на естественном языке перевести в однозначный формальный язык и от этого "перевода" никуда не деться. Взять хотя бы ваш IfLess. Вызывающему коду предоставляются многозначные "фразы" вроде "a или b, если меньше", а внутри — строго однозначный тернарный оператор.
Чтобы понять, насколько оправдан ваш подход, надо знать специфику вашей работы. Если задачи нигде больше не фиксируются, то, наверное, зафиксировать их в коде — это хорошо. Также могу посоветовать посмотреть в сторону BDD, где требования записываются приближенно к естественному языку и превращаются в тесты.
Сейчас дешевле купить новый процессор и докупить памяти
Насколько я помню, эти выводы впервые были озвучены военными в 60х. И когда программа реализует каких-нибудь сложные вычислений по уже известному хорошему алгоритму, то, наверное, действительно дешевле докупить железо, чем убрать каждый лишний такт. А ещё железо и программы, о которых тогда шла речь, поставлялись вместе с общим ценником, так что докупить железо можно было "не отходя от кассы".
А сейчас не всегда понятно, где же взять такое железо, чтобы иной сайтик из текста и двух кнопок не тормозил.
ORM, по-идее, и должен разделять модели бизнес-логики и их представление в БД. Например, EmailAddress не требуется хранить в отдельной таблице только потому, что в коде это отдельный класс. ORM как раз обещает persistence ignorance, а его несоблюдение называют анти-паттерном .
Аналогично п. 1, придётся настраивать отображение структуры БД в модели.
Если вместо БД надо будет вызывать сервис, то ORM всё равно придётся заменять на клиент для сервиса, разве нет?
Вообще я не против явно выделенного слоя доступа к данным. Просто, на мой взгляд, если надо учитывать всю специфику БД, то ORM может быть не самым подходящим инструментом.
Ура, мудрый прохвессор наконец-то расскажет нам, как программы программировать!</sarcasm>
Вообще ту же логику можно применить и к тестам. Тесты устаревают и их приходится обновлять, некоторые программисты пишут их плохо, и вообще, совершенный код не должен всегда работать правильно. Почему же никто не кричит, что тесты надо перестать писать? А просто когда код без тестов работает неправильно, то спорить с этим трудно, придётся исправить код и признать, что надо было делать тесты. А когда код без комментариев непонятен, то, ну… ты просто не умеешь читать код, да и в конце-концов же во всём разобрался, и вообще, всё равно на комментарии сейчас нет времени.
Старые статьи на MSDN не потёрли, а хорошенько перепрятали:
Первыми всё-таки были машины Цузе.
В разделе "Порядок инициализации" есть неточность в описании порядка инициализации статических полей и вызова статических конструкторов. Среда гарантирует, что статические поля будут инициализированы, и статический конструктор будет вызван до первого использования класса, отношение наследования никак на порядок статической инициализации не влияет. Например, если в статическом конструкторе наследника как-то используется базовый класс (статические поля или создаётся экземпляр), то инициализация статических полей и вызов статического конструктора базового класса произойдёт до аналогичных действий для наследника.
Это нормально. У меня тоже чем больше опыт работы, тем больше вопросов возникает.
Я-то сделаю, проблема в том, что это приводится как пример "чистого кода" в популярной книге для новичков.
Так, только Мартин работает не с состоянием, а со скрытыми аргументами. Допустим, я меняю класс
PrimeGenerator
и вызываюcheckOddNumbersForSubsequentPrimes
. И тут у меня всё падает, потому что, оказывается, я должен был проинициализировать (статические!) поляprimes
иmultiplesOfPrimeFactors
, причём проинициализировать их в два этапа: присвоить каждому новый экземпляр соответствующего класса и вызватьset2AsFirstPrime
. Если бы я написал точно такой код в процедурном языке, например, на C, то коллеги быстро объяснили бы мне, что это полнейшее безобразие. Но если перед этим безобразием написать слово class, то оно магически превращается в "чистый код" Мартина.Мне как-то достался такой класс, где параметры были убраны в поля. Судя по истории, изначально он был вполне компактным, но, конечно же, со временем и сам класс, и его методы разрослись, и понять что откуда берётся и где оно поменяется в следующий раз было очень сложно. Да, в том коде было много других проблем, но изменяющиеся поля вместо явных аргументов делали всё только хуже.
Может возникнуть вопрос, а как отличить состояние от скрытых аргументов? По моему мнению, состояние, во-первых, влияет на дальнейшее поведение объекта, а не только на текущий вызов, во-вторых, зависит от предыдущего состояния, в-третьих, объект переходит из одного корректного состояния в другое корректное состояние, то есть состояние полностью инициализируется в конструкторе и публичный интерфейс объекта не допускает частичной смены состояния. Если состояние не ломается при возникновении ошибок (т.е. выполняются гарантии исключений) и состояние меняется атомарно, то вообще хорошо. Скрытые аргументы наоборот, влияют только на один конкретный вызов публичного метода (ну или некой правильной последовательности вызовов), зависят только от аргументов этого вызова, а в остальное время содержат мусор.
А если исправление маленькое? Завести задачу, завести ветку, а потом ещё раз описать изменение в коммите — звучит чересчур громоздко для, например, исправления опечатки в имени переменной. Не получается ли в результате, что на мелкие исправления просто забивают? Или "протаскивают" их в несвязанных по смыслу коммитах, за что потом шлются лучи добра?
Сначала подумал, что опять английское "it doesn't look like" перевели дословно, полез в оригинал, а там "it wasn't as if".
На мой взгляд, что-то вроде "Алфавит ведь не будет меняться от запуска к запуску" будет более уместно.
Скотт Майерс высказывал интересную идею, что инкапсуляцию типа можно измерить количеством кода, который может сломаться при изменении реализации этого типа.
Видимо, я немного не понял изначальный посыл. Мне показалось, что речь была не столько про exhaustive match, сколько про неожиданное появление новых вариантов в АТД (например, другими людьми).
А так да, для исчерпывающего сравнения только делать вложенные классы private и городить визитор. Для простых случаев, правда, можно обойтись чем-то вроде
Но это уже скорее "исчерпывающее сопоставление enum`а".
Можно делать "закрытые" иерархии при помощи вложенных типов:
Это один вариантов, во что превращаются алгебраические типы данных в F# в скомпилированных сборках.
Вот как объясняет термин сам Мартин:
Не скажу, что полностью согласен с таким названием, но, по крайней мере, мотивация понятна.
using (ResourceType resource = expression) statement эквивалентен следующему (если ResourceType — ссылочный тип и не dynamic):
Это требование спецификации языка, и это выполняется, если выполнение дошло до finally, то Dispose выполняется полностью.
Барьер — согласен. Финализатор же вызывается асинхронно сборщиком мусора, и смещение происходит не за счёт вызова как такового, а за счёт постановки объекта в очередь финализации. То есть это эффект не на уровне C# или IL, а на уровне среды выполнения, и ещё не факт, что будет проявляться во всех реализациях .NET.
Вопрос интересный, так как логически ни финализатор (~A()), ни барьер не должны менять поведения в данном случая.
Видимо, причина различия в поведении на практике в следующем. Когда происходит создание объекта с финализатором на управляемой куче, он должен быть поставлен в очередь финализации. Поскольку надо выполнить больше действий, то больше вероятность, что прерывание выполнения потока из-за Thread.Abort произойдёт не между выполнением конструктора и Dispose, а где-то ещё. Можно в этом убедиться, немного изменив цикл и вывод в вашем примере:
У меня в варианте с раскомментироваными строками (1) и (2) точно так же count становится не 0, просто это происходит не каждый раз, а спустя 3-20 итераций.
PizzeriaServiceTestable проверяет неявные выходные данные (indirect output) тестируемой системы. Фактически, это мок, просто специализированный.
Год, месяц, день. Как в формате ISO 8601, в JavaScript, в SQL, в Java. И потом, это часть базовой библиотеки, и с датами приходится сталкиваться достаточно часто, чтобы порядок аргументов в конструкторе запомнился. А вот в 4.May(2019) — это как раз локальный российский формат, людям из разных стран с разными локалями он не поможет.
На мой взгляд, главная проблема с написанием кода, максимально похожего на естественный язык, в том, что фразы на естественном языке многозначны и могут быть истолкованы по разному. Часть работы программиста как раз и заключается в том, чтобы описание задачи на естественном языке перевести в однозначный формальный язык и от этого "перевода" никуда не деться. Взять хотя бы ваш IfLess. Вызывающему коду предоставляются многозначные "фразы" вроде "a или b, если меньше", а внутри — строго однозначный тернарный оператор.
Чтобы понять, насколько оправдан ваш подход, надо знать специфику вашей работы. Если задачи нигде больше не фиксируются, то, наверное, зафиксировать их в коде — это хорошо. Также могу посоветовать посмотреть в сторону BDD, где требования записываются приближенно к естественному языку и превращаются в тесты.
Насколько я помню, эти выводы впервые были озвучены военными в 60х. И когда программа реализует каких-нибудь сложные вычислений по уже известному хорошему алгоритму, то, наверное, действительно дешевле докупить железо, чем убрать каждый лишний такт. А ещё железо и программы, о которых тогда шла речь, поставлялись вместе с общим ценником, так что докупить железо можно было "не отходя от кассы".
А сейчас не всегда понятно, где же взять такое железо, чтобы иной сайтик из текста и двух кнопок не тормозил.
EmailAddress
не требуется хранить в отдельной таблице только потому, что в коде это отдельный класс. ORM как раз обещает persistence ignorance, а его несоблюдение называют анти-паттерном .Вообще я не против явно выделенного слоя доступа к данным. Просто, на мой взгляд, если надо учитывать всю специфику БД, то ORM может быть не самым подходящим инструментом.