суббота, 9 марта 2013 г.

Контракты, состояние и юнит-тесты

На примере кода Боба Мартина из его книги «Принципы, паттерны и методики гибкой разработки»

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

Описывая TDD Боб Мартин пишет о том, что TDD – это не просто вначале тесты, а потом код, это способ обдумывания дизайна класса через призму тестов, что делает его более обдуманным и слабосвязанным. Этот подход вполне оправдан и юнит тесты и правда являются отличной лакмусовой бумажкой качественного дизайна.

Я не берусь судить о том, кому и когда писать юнит тесты. Лично мне подход обдумывания дизайна через тесты не подходит, поскольку мне проще думать о дизайне в терминах контрактов и лишь потом проверять свои предположения с помощью тестов. Это не говорит о том, что подходы типа test first не работают, я говорю лишь о том, что на вкус и цвет все фломастеры разные.

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

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

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

Ревью класса LoadPaymentMethodOperation

Подозрения по поводу проблем в коде у меня возникли после чтения следующего примера:

private DataRow LoadData()
{
   
if(tableName != null
)
       
return LoadEmployeeOperation
.LoadDataFromCommand(Command);
   
else
        return null;
}

Возвращение null в большинстве случаев является серьезным запахом и может приводить к неочевидным ошибкам времени исполнения. Не зря ведь Тони Хоар считает изобретение нулевых ссылок своей ошибкой на миллиард долларов, а Бертран Мейер специально ввел в языке Eiffel понятие non-nullable reference типов, чтобы четко разделить ссылочные типы, способные и не способные принимать null.

Довольно часто возвращение null скрывает ошибку в совершенно другой части программы и может говорить о невыявленных предусловиях класса. В каком случае tableName будет равен null? Нормально ли вообще, что поле tableName равняется null и какая связь между этим полем и свойством Command?

Встретив подобный код, я ожидаю проверки этого граничного условия в коде (и в тесте), но проверяет ли вызывающий метод значение на null? Не совсем:) Вызывается метод LoadData всего из одного места (что хорошо), но возвращаемое значение не проверяется:

public void Execute()
{ 
    Prepare();
   
DataRow row = LoadData();
    CreatePaymentMethod(row); }

Возвращаемое значение метода LoadRow передается другому методу. Теперь, анализируя код этого класса нужно помнить о том, что аргумент метода CreatePaymentMethod может получить null. Идем дальше.

public void CreatePaymentMethod(DataRow row)
{ 
    paymentMethodCreator(row); }

Метод почему-то открытый, ну да ладно; все, что он делает, это вызывает делегат paymentMethodCreator, который инициализируется следующим образом:

public void Prepare()
{ 
   
// methodCode инициализируется в конструкторе     if(methodCode.Equals("hold"
))
        paymentMethodCreator =
new PaymentMethodCreator
(CreateHoldMethod);
   
else if(methodCode.Equals("directdeposit"
))
    {
        tableName =
"DirectDepositAccount"
;
        paymentMethodCreator =
new PaymentMethodCreator
(CreateDirectDepositMethod);
    }
   
else if(methodCode.Equals("mail"
))
    {
        tableName =
"PaycheckAddress"
;
        paymentMethodCreator =
new PaymentMethodCreator(CreateMailMethod);
    } }

Хорошо. Метод Prepare – это некая форма фабричного метода, но вместо возврата создаваемого объекта, он изменяет состояние. Данный код достаточно запутанный, но при его анализе становится ясно, что мое исходное предположение о возможной генерации NullReferenceException (NRE) при вызове метода CreatePaymentMethod(null) не подтвердился (во всяком случае в текущей реализации; насколько вам это очевидно? ;)), но проблема все равно остается.

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

Ну и последнее. Детская ошибка все равно осталась, и находится она в методе Prepare: поле paymentMethodCreator остается неинициализированным, если methodCode невалиден (не равен "hold", "directdeposit" или "mail"). Это значит, что при передаче неверного аргумента в конструкторе объекта (methodCode инициализируется в конструкторе объекта), мы получим ошибку времени исполнения лишь при вызове метода Execute.

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

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

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

public LoadPaymentMethodOperation(Employee employee, string methodCode, 
SqlConnection
connection) {
   
Contract.Requires(employee != null
);
   
Contract.Requires(connection != null
);
   
Contract
.Requires(IsValidMethodCode(methodCode));



    
this
.employee = employee;
   
this
.connection = connection;

   
// Метод сгенерирует исключение, если methodCode невалиден.     // Я переименовал метод Prepare в CreateCommandAndPaymentMethod     // и сделал его закрытым.     CreateCommandAndPaymentMethod(methodCode); }
Аргументы методов vs состояние объекта

В другой своей книге под названием «Чистый код», Боб Мартин дает такой совет: если метод принимает 3 и более параметров, то его нужно срочно рефакторить путем протаскивания параметров через поля класса, аргументируя это тем, что такие методы сложнее понять и тестировать.

В моем понимании, поля класса предназначены прежде всего для выражения состояния объекта, необходимого для нормальной его работы на протяжении всего времени его жизни. Поля же, которые находятся в корректном состоянии после вызова метода A и до вызова метода B являются очень подозрительными. Такой код сложнее понять и намного сложнее тестировать, чем статический метод с несколькими аргументами.

Многие «объектно-ориентированные» книги по кодированию проповедуют небольшие методы (вплоть до нескольких строк). В этом есть смысл, но чрезмерное увлечение этим принципом приводит к множеству методов, анализировать которые становится слишком сложно (ок, а для чего нужно поле valid? А, да оно же устанавливается в false в начале метода parse и изменяется методом ParseArgs).

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

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

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

// Вариант Боба Мартина
public void CreateDirectDepositMethod(DataRow row)
{
    
string bank = row["Bank"
].ToString();
    
string account = row["Account"
].ToString();
     method =
new DirectDepositMethod
(bank, account); }
 



// Вариант без побочных эффектов private static PaymentMethod CreateDirectDepositMethod(DataRow
row) {
    
string bank = row["Bank"
].ToString();
    
string account = row["Account"
].ToString();
    
return new DirectDepositMethod(bank, account); }

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

Хороший пример ужасных тестов

Существуют разные подходы к юнит-тестированию. Кто-то считает, что для юнит-тестов лучше всего подходит подход "белого ящика", когда тест, по сути, знает о внутренней реализации тестируемого кода и использует для проверки соответствующие детальные утверждения (assert statements).

С другой стороны, есть сторонники тестирования на основе "черного ящика", когда мы тестируем не реализацию, а предполагаемое поведение. Лично я нахожусь в этом вопросе скорее на «темной стороне» и тестирую именно ожидаемое поведение. Если в реализации где-то присутствует условное ветвление, то я не пытаюсь написать тест типа Test_that_specific_condition_would_false, а подберу такие входные условия, которые *семантически* будут отражать ложность условия в тестируемом классе.

Возвращаясь к коду Боба Мартина это означает, что у меня будет тест вида: Test_Unknown_Method_Code_Will_Throw_Exception или же я сделаю один параметризованный юнит-тест, который покроет все внутренние условия благодаря различным входным данным.

Одним из недостатков (ИМХО) TDD является то, что тесты начинают слишком много знать о внутренней реализации класса, что делает их более хрупкими, чем мне хотелось бы, нарушая, по сути, инкапсуляцию класса. Но иногда даже без TDD можно встретить такие тесты, на которые без содрогания смотреть невозможно. К сожалению, в книге Боба Мартина такие присутствуют:

[Test]
public void LoadingEmployeeDataCommand()
{
	operation = new LoadEmployeeOperation(123, null);
	SqlCommand command = operation.LoadEmployeeCommand;
	Assert.AreEqual("select * from Employee " +
		"where EmpId=@EmpId", command.CommandText);
	Assert.AreEqual(123, command.Parameters["@EmpId"].Value);
}

Я считаю такие тесты не только бесполезными, но и весьма вредными. У команды появляется уверенность, что код покрыт тестами, хотя «зеленый цвет» таких тестов ничего не говорит. Никакие изменения в базе данных не будут отловлены этим тестом; да он даже не говорит нам, является ли текущая реализация работоспособной или нет.

При этом такой тест усложняет сопрвождаемость, поскольку он дублирует код тестируемого класса, не добавляя ничего взамен. Запросы могут усложняться с течением времени, хотя с точки зрения внешнего пользователя они будут вести себя одинаково. С таким подходом мы даже спокойно не сможем изменить имя колонки или поменять запрос с “select * …” на “select id, name” без изменения соответствующего теста.

Я не говорю уже о том, что автор этого теста не разделяет юнит-тесты от интеграционных тестов. Тесты базы данных не отвечают критериям F.I.R.S.T., и должны быть отделены от остальных модульных тестов. Но в данном случае, это не самая большая проблема; самое плохое в этом случае то, что такие примеры могут серьезно повлиять на неокрепшие умы. Нужно всегда помнить, что тесты – не самоцель, а лишь средство достижения хорошего качества кода и дизайна. Подобные тесты не приближают нас к нашей цели, а скорее отдаляют от нее.

Заключение

Цель этого поста была не в том, чтобы показать ошибки в книге камрада Мартина, а в том, чтобы сравнить разные подходы к дизайну и реализации класса. Как мы можем думать о граничных условиях класса и его методов? Мы можем подойти со стороны проверки этих условий в юнит-тестах и посмотреть предполагаемое поведение. Или же мы можем подойти с точки зрения его «обязательств» или контракта, сделав отношения классом и его клиентами более четкими.

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

Ну и последнее: тесты – это инструмент, который в одних руках упростит сопровождаемость, а в других – сделает ее невозможной. Тесты не должны просто дублировать код класса, а должны описывать тестируемую проблему в более абстрактной форме.

Ссылки по теме

26 комментариев:

  1. Там, похоже очепятка: prepayMethodCreator вместо paymentMethodCreator.

    Ну а в целом есть ли смысл читать эту книгу "старика Боба"? Есть ли там достойные разделы?

    ОтветитьУдалить
  2. @kosmoh: спасибо, исправил. Проф деформация, однко:)

    По поводу стоит или не стоит читать эту книгу:

    Я начал читать эту книгу будучи в командировке в Штатах и показывал эти косяки своему коллеге. Он несколько раз спросил: "если книга - фигня, то зачем ты ее читаешь?", на что я ответил так, как написал выше: эта книга весьма популярна, с высокими оценками, а это значит, что мы встретим множество ребят, которые будут слепо следовать ее советам. А раз так, то лучше вооружиться зараннее.

    В результате, мой коллега тоже взял себе эту книгу для этих же целей:)

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

    ОтветитьУдалить
  3. Наконец мы узнали, что Боб Мартин тоже человек:)


    > Я предпочитаю более «функциональный» подход

    Как Вы думаете, можно ли этот подход взять по максимуму? Всегда стараясь "проектировать классы в максимально возможном функциональном стиле". Вплоть до передачи методов?

    ОтветитьУдалить
  4. @makajda:
    > Как Вы думаете, можно ли этот подход взять по максимуму? Всегда стараясь "проектировать классы в максимально возможном функциональном стиле". Вплоть до передачи методов?

    Я вообще не считаю ООП и ФП конкурентами и предпочитаю первое для высокоуровневого дизайна, а второе - для реализации.

    Но используя не-функциональные языки (как C#) перегибать палку (ИМХО) тоже не стоит. Уже сейчас можно спокойно использовать некоторые идиомы функционального программирования, но здравый смысл, как обычно, рулит. Не стоит городить лямбды на лямбдах, поскольку это далеко не всегда приведет к повышению читабельности и улучшению выразительности.

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

    ОтветитьУдалить
  5. Хорошее у Вас получилось определение ФП: "городить лямбды на лямбдах":) Согласен, что цель не парадигма, а повышение читабельности и улучшение выразительности.

    ОтветитьУдалить
  6. Затрону одну частность языка Си и ядра Linux. В нашем убогом мире^W^W^W языке Си как таковых ссылок и классов не существует, но есть указатели и структуры. Часто возникает вопрос, что выбрать в качестве возращаемого значения функции, которая должна вернуть вызывающему указатель на живущий объект? Есть два как минимум варианта прототипов:
    struct obj *get_obj(...);
    и
    void get_obj(..., struct obj**);

    Почему я выделил второй метод, хотя он кажется здесь неэффективным? А дело вот в чём. Изначально мы можем вернуть указатель на объект или null, если такового не нашлось. Однако, у любого программиста возникает вопрос: а как же исключения внутри метода? Да-да, на Диком Западе^W^W^W в языке Си мы пользуемся кодом возврата. И вот тут возникает такая картинка (см. прототипы выще и сравни):
    struct obj *get_obj(..., int *ret);
    int get_obj(..., struct obj **);

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

    Для этого было придумано нехитрое решение. Поскольку void * у нас всё-таки число, то давайте разделим возможные значения на три интервала (32 бита): 0x00..0x10, 0x11..0xffffffff - MAX_ERRNO, 0xffffffff - MAX_ERRNO + 1..0xffffffff. Первый - NULL pointer, второй - обычное адресное пространство, третий - инвалидный поинтер или код ошибки. Соответственно появилось несколько макросов для проверки ZERO_OR_NULL_PTR, IS_ERR, IS_ERR_OR_NULL. И люди начали ими пользоваться! Проблемы же вылезли из-за неправильного применения макроса IS_ERR_OR_NULL для тех частей кода, где NULL - валидный возврат, например, когда объёкт отвечает за некую функциональность в ядре, которая в данной сборке ядра выключена. Тогда возвращается NULL pointer, который надо обрабатывать по-другому!

    Так вот, мораль басни такова, что тяжёлое наследие в виде NULL pointer'ов всё ещё будоражит неокрепшие умы :-)

    ОтветитьУдалить
    Ответы
    1. Вы можете создать функцию возвращающую булевое значение для идентификации NULL pointera раз уж мы о ядре ОС говорим. В таком случае, поведение будет контролируемым и истиный null будет виден как на ладони. Поясню, примером:



      bool isNullTrue()
      {
      return true;
      }

      bool isNullFalse()
      {
      return false;
      }

      void* f()
      {
      return new SomethingClass();
      }

      void main()
      {
      bool (*p)();
      void *pointer = f();
      p = isNullTrue;
      if(p == pointer)
      {
      printf("Error"); //это истинный null
      }
      }

      Удалить
    2. Не прокатит, т.к. надо возвращать разные коды ошибок и при этом NULL может уже быть и не NULL.

      Удалить
  7. @Andy: спасибо, Андрюх! Интересный трюк и интересная проблема.

    ОтветитьУдалить
  8. Сергей, нравятся записи где Вы раскрываете, размышляете, осмысляете и переосмысляете. Хочется поблагодарить Вас за них!

    Но кое с чем особенно не могу согласиться:

    "Я не говорю уже о том, что автор этого теста не разделяет юнит-тесты от интеграционных тестов. Тесты базы данных не отвечают критериям F.I.R.S.T., и должны быть отделены от остальных модульных тестов."
    Но ведь это, по-моему, не тест базы данных. Запрос не выполняется, а результат выполнения не проверяется.
    Более того, если текст запроса генерируется, то подобный тест скорее необходим, чем излишен, а тем более вреден. Я вижу этот тест как проверку того, что определенное свойство возвращает ожидаемую строку. Сергей, Вы совсем так не считаете?

    "Нужно всегда помнить, что тесты – не самоцель, а лишь средство достижения хорошего качества кода и дизайна. Подобные тесты не приближают нас к нашей цели, а скорее отдаляют от нее."
    Пишу тесты, и все-таки для меня они прежде всего то, что дает уверенность в своём коде. Большая модульность и меньшее кол-во ошибок - это уже следствия. С этой точки зрения вполне могу понять автора - если для того, чтобы чувствовать себя уверенным он написал этот тест - пожалуйста.
    Да и скорее не реализация продублирована в тесте, а тест в реализации.
    Просто если привык писать тесты, то пишешь их для всего подряд - иногда это конечно может выглядеть странно.

    ОтветитьУдалить
  9. @the South: спасибо за благодарности и вполне можно на "ты".

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

    Вопрос такой: чем полезен этот тест? Он дает читателю хотя бы что-то? Я этого не вижу. Такой тест придется поменять даже когда меняется *деталь реализации* класса, а абстракция (т.е. внешнее поведение класса) останется неизменным.

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

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

    > Да и скорее не реализация продублирована в тесте, а тест в реализации.

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

    Свобода - это же так прекрасно! А тут появляется куча тестов, тесно связанных с нашей реализацией:)

    ОтветитьУдалить
  10. "Точно могу сказать, что если в рассматриваемых областях опыта не очень много, то книга может скорее навредить, чем помочь."

    Сорри Серёж, не соглашусь. Огрехи в книге есть, но имхо пользы все-таки больше. Я пока прочитал где-то 2/3 книги.

    ОтветитьУдалить
  11. Сереж, в "Clean code" не все так плохо, например, там отличная глава о комментах :)

    ОтветитьУдалить
  12. @nightcoder: по поводу пользы книги. Понятно, что это лишь мое предположение, которое может быть и неверным. Но, с другой стороны, у тебя же опыта тоже два ведра, чтобы уже не слепо верить советам, а анализировать их критическим взглядом. Поэтому тебе-то она не навредит:))

    @Владимир: да, клин код - тоже не совсем однозначная книга (меня там напрягает та же категоричность и любовь протаскивать состояние через поля класса). Но там полезных советов было больше (с моей точки зрения).

    ОтветитьУдалить
  13. Прикольно. Не только я заметил. Помнишь мы в G+ осуждали уже, что тест не интеграционный, плохой, но не интеграционный.
    По поводу тестирования. Немного лукавишь, когда говоришь про черное тестирование. Параметры - это корнер кейсы, которые подсматриваешь в реализации. С другой стороны - тестировать ветвление в коде, чтобы добиться лучшего покрытия - фигня полная. Поэтому правильнее бы это назвать - "серое". ИМХО, конечно.
    Я не очень понял фразу, что тестируем то, что проверяем в контрактах. Я считаю, что это разное. Смотри в функциях мы просто проверяем аргументы и возращаемые значения, это значит в тестах будем проверять именно логику работы (спецификации). Даже инварианту тестить -все случаи не переберешь.

    По поводу стратегии и тактики, то бишь ООП и ФП - терли уже не раз. Но вот что прикольно. Редко где упоминается, что "чистые" функции - удобны для использования в многопоточке. Все эти локи - костыли, чтобы скрыть переменные состояния.
    Очень нравится авторефакторинг от решарпера - сделать функцию статической. Очень удобно. Когда глаз это видит - сразу понимаешь, что функция не зависит от состояния объекта. Мне сейчас нормально, когда публичные функции не статические, а приватные - статические. Читать намного удобнее. ИМХО, конечно.

    P.S. Давно лежит в ридере книга по F#. Очень хочу, но сдерживает пока непонимание ГДЕ я это буду применять.

    ОтветитьУдалить
  14. @eugene: по поводу черно-белого тестирования, кстати, я написал довольно "обтекаемо". У меня там: "Я скорее на темной стороне", а не "я использую тестирование на основе черного ящика":)
    Так что да, скорее это серый ящик, но он "темно-серый", а не "светло-серый".

    По поводу тестирования и контрактов: я имел ввиду, что наши тесты неявно будут проверять постусловия и инварианты и если они будут нарушены при выполнении какого-то из юнит-тестов, то мы получим ContractException. Предусловия я не тестирую.

    По поводу F#: тут скорее расширить кругозор, а не применять на практике. Даже в этом случае польза будет намалая.

    ОтветитьУдалить
  15. Тесты не должны просто дублировать тест класса
    ->
    Тесты не должны просто дублировать код класса

    ОтветитьУдалить
  16. Подскажите, где исходный код примеров для книги найти? сайт авторов, objectmentor.com, не рабочий уже!

    ОтветитьУдалить
  17. По программированию через контракты есть какие-то толковые книги, кроме Б. Мейера? А то книга Мейера 1100+ страниц и в твердой копии сейчас доступна только на английском...

    ОтветитьУдалить
    Ответы
    1. Я других книг подобных не знаю.

      Насколько я помню, продается электронный вариант где-то на сайте интуита.

      Удалить
  18. Этот комментарий был удален автором.

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

    Я тоже предпочитаю такой подход, но есть одно существенное "исключение". Это readonly поля класса. Использование readonly полей в классах вида XXXCalculator или XXXParser порой помогает существенно разгрузить списки аргументов функций класса, и при этом не грозит никакими побочными эффектами.

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

    ОтветитьУдалить
    Ответы
    1. Согласен, но с небольшим уточнением: readonly поля должны быть быть неизменяемыми. В противном случае гарантировать отсутствие побочных эффектов не удастся.

      Удалить
    2. В смысле использования immutable objects, чтобы было невозможно сделать такое?
      readonly User user = new User("Вася");
      ...
      user.Name = "Петя";

      Да, всё так. Не упомянул об этом, наверное, потому, что уже просто привык думать о таких параметрах, как о неизменяемых.

      Удалить