Статьи

Сознание смысла

Это вторая часть короткой серии постов, в которой я исследую TDD, используя в качестве руководства только соблазн. В  предыдущей статье  я написал тест, сделал его успешным, а затем провел рефакторинг, отбросив самую сильную связь. Эта связь приобрела форму некоторого Connascence of Value между тестом и Checkout. Позже, после того, как волнение от публикации поста угасло, я понял, что в коде все еще есть некоторое нетривиальное потрясение. Сегодня пришло время исправить это.

Вот тест, который я закончил в прошлый раз:

public class CheckoutTests {
 
  @Test
  public void basicPrices() {
    int price = randomPrice();
    Checkout checkout = new Checkout();
    checkout.scan("A", price);
    assertEquals(price, checkout.currentBalance());
  }
}

Во-первых, давайте рассмотрим намеки в этом коде. (И, пожалуйста, дайте мне знать, если я тоже пропустил это время!)

  1. Connascence of Name:  тест знает имена методов, которые нужно вызвать для объекта извлечения. Это уровень 1 (из 9) по шкале коннасценции — самая слабая и наименее разрушительная форма связи.
  2. Connascence of Type:  тест знает, какой класс создавать, и знает типы различных параметров метода. Это уровень 2 по шкале, и, таким образом, он также относительно мягкий.
  3. Connascence of Value:  оба класса знают, что мы представляем денежные значения, используя целые числа, а продукты — строки. Это уровень 3 по шкале; хотя он все еще относительно безвреден, его стоит удалить, прежде чем код станет слишком большим.

Я думаю, что стоит уделить время, чтобы понять, почему я чувствую, что это Connascence of Value, а не чисто Connascence of Type. Да, тест и Checkout должны согласовать типы параметров и возвращаемых значений, но я думаю, что есть и другое: оба также должны договориться о том, что означают эти значения. Это int представляет пенс, фунты, евро? Может ли эта строка продукта в будущем стать ориентиром или штрих-кодом? Тест и оформление заказа связаны их общим знанием того, как эти понятия предмета (денежные значения и коды продуктов) представлены и интерпретируются. Если я позволю этому знанию размножаться, изменить их может быть сложно. Я могу столкнуться с небольшими ошибками, если в будущем они будут интерпретированы или представлены по-другому в приложении. Это  ошибка Ariane 5 в ожидании.

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

Поэтому, хотя у меня есть «только» Connascence of Value, я хочу пресечь это в зародыше пораньше. Стоимость этого сейчас невысока, а стоимость бездействия может быстро возрасти позже. Итак, по пестрому…

Сначала я разберусь с деньгами.

cov3

Как и  прежде , я могу ослабить коннасценцию, собрав «концы» муфты в одном месте. Но на этот раз я не могу легко ввести значение в Checkout, потому что проблема здесь в одном из типов. Поэтому вместо этого я создаю новый тип и скрываю внутри него int. Деньги будут единственным классом, который знает, как хранятся денежные значения:

cov4

Смена теста проста:

public class CheckoutTests {
 
  @Test
  public void basicPrices() {
    Money price = randomPrice();
    Checkout checkout = new Checkout();
    checkout.scan("A", price);
    assertEquals(price, checkout.currentBalance());
  }
 
  private Money randomPrice() {
    int pence = new Random().nextInt(1000);
    return Money.fromPence(pence);
  }
}

Точно так же при оформлении заказа теперь используются денежные объекты вместо целых:

public class Checkout {
  private Money balance;
 
  public Checkout scan(String sku, Money price) {
    balance = price;
  }
 
  public Money currentBalance() {
    return balance;
  }
}

И, наконец, вот новый класс Money:

public class Money {
  private int pence;
 
  private Money(int pence) {
    this.pence = pence;
  }
 
  public static Money fromPence(int pence) {
    return new Money(pence);
  }
}

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

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

@Test
public void basicPrices() {
  Money priceOfA = randomPrice();
  Checkout checkout = new Checkout();
  Money priceOfB = randomPrice();
  checkout.scan("A", priceOfA).scan("B", priceOfB);
  assertEquals(priceOfA.add(priceOfB), checkout.currentBalance());
}

Это простое изменение заставляет деньги приобретать немного больше богатства:

public class Money {
  public static final Money ZERO = new Money(0);
  private int pence;
 
  private Money(int pence) {
    this.pence = pence;
  }
 
  public static Money fromPence(int pence) {
    return new Money(pence);
  }
 
  public Money add(Money other) {
    return new Money(pence + other.pence);
  }
 
  @Override
  public boolean equals(Object other) {
    Money m = (Money) other;
    return pence == m.pence;
  }
 
  @Override
  public int hashCode() {
    return new Integer(pence).hashCode();
  }
}

Мне это нравится. Путем исправления некоторого Connascence of Value, я «открыл» концепцию предметной области, и она быстро воплотилась в совершенно разумный набор поведений. Это было бы прекрасно, если бы только Java допускала перегрузку операторов…

Теперь я взгляну на те строки, которые представляют продукты. Возможно, Connascence of Value здесь менее серьезное, потому что пока Checkout не использует переданный ему код продукта. Я мог бы оставить этот, пока у меня нет тестов, которые заставляют Checkout проверять эту строку. Я решил сделать именно это, потому что я не могу предсказать, когда это произойдет, или как код получится в этом неопределенном будущем.

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

Это похоже на большую работу только для одного теста. Было ли это оправдано? Ну, во-первых, я отмечаю, что это два теста, потому что я переработал один. Во-вторых, я назвал концепцию домена, и очень простой тест, который у меня уже был, привел к тому, что он стал более детальным. Я доволен этим кодом, и мне удобно, что он удовлетворяет  Extreme Normal Form . Я также ожидаю, что некоторые люди не согласятся.

В  следующем посте  я добавлю скидки и создам некое Connascence of Position…