Статьи

Знание ценности

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

Давайте  поговорим о классическом   ката @pragdave Back to Checkout на Java. Мой первый тест проверяет, что мы можем отсканировать один элемент и правильно рассчитать сумму:

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

Теперь я делаю это самым простым способом, который я могу представить:

class Checkout {
  public int currentBalance() {
    return 50;
  }
 
  public Checkout scan(String item) { }
}

Очевидно, что эти два класса теперь связаны (если бы они не были, тест не прошел бы). Но это соединение хорошо или плохо?

Я могу видеть три четыре вида схождения между тестом и рабочим кодом:

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

cov1

Connascence of Value здесь означает, что тесты будут прерваны, если я изменю цену предмета «A»; Я определенно не хотел бы выпускать это в производство.

Connascence of Value — уровень 8 по шкале из  9 видов коннасценции . Шкала определяет семь более слабых форм сцепления и только один более серьезный вид. Я могу использовать эту модель, чтобы помочь мне расставить приоритеты в шаге Refactor в моем цикле TDD: Connascence of Value — серьезная проблема, и ее нужно исправить, прежде чем я сделаю что-либо еще. Единственный вопрос: как?

Первое, что я хочу отметить, это то, что коннасценция слабее с близостью, что означает, что любой из следующих вариантов будет предпочтительнее:

cov2

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

Я могу получить некоторую помощь от SOLID здесь, потому что  принцип инверсии зависимости  также говорит мне, что этот код имеет проблему. DIP говорит, что мы должны зависеть от абстракций, а не от деталей. И все же здесь у меня есть тест, который работает только благодаря знанию одной из деталей внутри производственного кода.

DIP (и  @jbrains ) также говорит мне, что делать дальше: я должен  сдвинуть детали к тестам . Это означает, что мне нужно изменить Checkout, чтобы тест вводил значение 50 через параметр. Я мог бы передать это через метод сканирования:

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

В качестве альтернативы я мог бы внедрить его через конструктор Checkout:

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

В любом случае, теперь я удалил Connascence of Value между Checkout и тестом: я могу изменить цену предмета «A», изменив только один метод.

Худшее сцепление теперь ушло, но я могу сделать лучше. В этом методе тестирования все еще существует Connascence of Value, хотя и очень ограниченное. Это стоит исправить?

Мне нравятся мои тесты, чтобы быть выразительными и легко читаемыми. Например, я бы не хотел извлекать значение 50 из константы, потому что тогда мне пришлось бы сканировать тестовый класс вверх и вниз, чтобы точно узнать, что делал тест. Но в равной степени это волшебное значение 50 заставляет меня немного нервничать. Имеет ли это значение для бизнеса? Не в этом случае, и новый член команды может не поднять это.

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

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

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

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

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

В  следующем посте  я расскажу больше о нахождении в этом коде.