Как стать автором
Обновить
13
0

Пользователь

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

Ура, мудрый прохвессор наконец-то расскажет нам, как программы программировать!</sarcasm>


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

Старые статьи на MSDN не потёрли, а хорошенько перепрятали:


Так первый немецкий компьютер был уничтожен с помощью пилы, кирки, молотка и топора.

Первыми всё-таки были машины Цузе.

При всём уважении, принципы SOLID, TDD сейчас мне кажутся понятными намного меньше, чем 10 лет назад, когда я начинал работать.

Это нормально. У меня тоже чем больше опыт работы, тем больше вопросов возникает.

Ну так сделайте не статический класс у которого все конструкторы требуют этих аргументов. В чём проблема то?

Я-то сделаю, проблема в том, что это приводится как пример "чистого кода" в популярной книге для новичков.

Методы в ООП взаимодействуют с состоянием объекта. Когда методы перестают это делать, а состояние начинает проталкиваться через аргументы, то код превращается в обычное процедурное программирование. Разве не так?

Так, только Мартин работает не с состоянием, а со скрытыми аргументами. Допустим, я меняю класс PrimeGenerator и вызываю checkOddNumbersForSubsequentPrimes. И тут у меня всё падает, потому что, оказывается, я должен был проинициализировать (статические!) поля primes и multiplesOfPrimeFactors, причём проинициализировать их в два этапа: присвоить каждому новый экземпляр соответствующего класса и вызвать set2AsFirstPrime. Если бы я написал точно такой код в процедурном языке, например, на C, то коллеги быстро объяснили бы мне, что это полнейшее безобразие. Но если перед этим безобразием написать слово class, то оно магически превращается в "чистый код" Мартина.


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


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

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

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

Это не выглядит так, как будто алфавит будет отличаться от одного сеанса к другому.

Сначала подумал, что опять английское "it doesn't look like" перевели дословно, полез в оригинал, а там "it wasn't as if".


На мой взгляд, что-то вроде "Алфавит ведь не будет меняться от запуска к запуску" будет более уместно.

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

Видимо, я немного не понял изначальный посыл. Мне показалось, что речь была не столько про 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 получаются архитектуры, в которых высокоуровневые модули зависят от низкоуровневых, а абстракции зависят от деталей. Собственно, одна из целей при этих подходах и заключается в том, чтобы определить иерархию подпрограм, описывающую вызов низкоуровневых модулей высокоуровнеывми.… Следовательно, структура зависимостей хорошо спроектированной объектно-ориентированной программы оказывается "инвертированной" по отношению к структуре, получающейся при использовании традиционных процедурных походов.

Не скажу, что полностью согласен с таким названием, но, по крайней мере, мотивация понятна.

Но в C# на это забили и using этот механизм не использует.

using (ResourceType resource = expression) statement эквивалентен следующему (если ResourceType — ссылочный тип и не dynamic):


{
    ResourceType resource = expression;
    try {
        statement;
    }
    finally {
        if (resource != null) ((IDisposable)resource).Dispose();
    }
}

Это требование спецификации языка, и это выполняется, если выполнение дошло до finally, то Dispose выполняется полностью.


А барьер и деструктор просто смещают точку гонки, т.к. они довольно длительные операции.

Барьер — согласен. Финализатор же вызывается асинхронно сборщиком мусора, и смещение происходит не за счёт вызова как такового, а за счёт постановки объекта в очередь финализации. То есть это эффект не на уровне C# или IL, а на уровне среды выполнения, и ещё не факт, что будет проявляться во всех реализациях .NET.

Вопрос интересный, так как логически ни финализатор (~A()), ни барьер не должны менять поведения в данном случая.


  1. Финализатор не вызывает Dispose. Фактически, Dispose и финализатор не связаны ничем, кроме рекомендаций и здравого смысла.
  2. 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 итераций.

Итак, я хочу сделать так, чтобы в моём тестовом методе не было ни одного мока.

PizzeriaServiceTestable проверяет неявные выходные данные (indirect output) тестируемой системы. Фактически, это мок, просто специализированный.

Год, месяц, день. Как в формате ISO 8601, в JavaScript, в SQL, в Java. И потом, это часть базовой библиотеки, и с датами приходится сталкиваться достаточно часто, чтобы порядок аргументов в конструкторе запомнился. А вот в 4.May(2019) — это как раз локальный российский формат, людям из разных стран с разными локалями он не поможет.

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


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

Сейчас дешевле купить новый процессор и докупить памяти

Насколько я помню, эти выводы впервые были озвучены военными в 60х. И когда программа реализует каких-нибудь сложные вычислений по уже известному хорошему алгоритму, то, наверное, действительно дешевле докупить железо, чем убрать каждый лишний такт. А ещё железо и программы, о которых тогда шла речь, поставлялись вместе с общим ценником, так что докупить железо можно было "не отходя от кассы".


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

  1. ORM, по-идее, и должен разделять модели бизнес-логики и их представление в БД. Например, EmailAddress не требуется хранить в отдельной таблице только потому, что в коде это отдельный класс. ORM как раз обещает persistence ignorance, а его несоблюдение называют анти-паттерном .
  2. Аналогично п. 1, придётся настраивать отображение структуры БД в модели.
  3. Если вместо БД надо будет вызывать сервис, то ORM всё равно придётся заменять на клиент для сервиса, разве нет?

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

Поковырявшись с EF6 и NHibernate вынужден признать, что ошибся.


Оба ORM поддерживают наследование, в типы добавляются закрытые (private) сеттеры и конструкторы по умолчанию, и после этого они сохраняются в БД и загружаются обратно. Однако на смене типа после UpdatePostalAddress возникают проблемы с сохранением нового объекта вместо старого. Судя по тикету, в Entity Framework Core замена объекта возможна, но я ещё не проверял. В любом случае, задача требует больше усилий и больше зависит от конкретной ORM, чем я думал.


Извините, если ввёл в заблуждение.

1

Информация

В рейтинге
Не участвует
Откуда
Москва, Москва и Московская обл., Россия
Зарегистрирован
Активность