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) { } }
Очевидно, что эти два класса теперь связаны (если бы они не были, тест не прошел бы). Но это соединение хорошо или плохо?
Я могу видеть три четыре вида схождения между тестом и рабочим кодом:
- Connascence of Name, потому что тесту известны имена методов, вызываемых для объекта извлечения. Это уровень 1 (из 9) по шкале коннасценции — самая слабая и наименее разрушительная форма связи.
- Connascence of Type, потому что тест знает, какой класс создавать. Это уровень 2 по шкале, и, таким образом, он также относительно мягкий.
- Connascence of Value, потому что оба класса знают, что мы представляем денежные значения, используя целые числа. (Я пропустил это в первый раз — черт!)
- Connascence of Value, потому что как тест, так и Checkout знают цену предмета «A»:
Connascence of Value здесь означает, что тесты будут прерваны, если я изменю цену предмета «A»; Я определенно не хотел бы выпускать это в производство.
Connascence of Value — уровень 8 по шкале из 9 видов коннасценции . Шкала определяет семь более слабых форм сцепления и только один более серьезный вид. Я могу использовать эту модель, чтобы помочь мне расставить приоритеты в шаге Refactor в моем цикле TDD: Connascence of Value — серьезная проблема, и ее нужно исправить, прежде чем я сделаю что-либо еще. Единственный вопрос: как?
Первое, что я хочу отметить, это то, что коннасценция слабее с близостью, что означает, что любой из следующих вариантов будет предпочтительнее:
Таким образом, если я смогу перевести знание о цене «А» так, чтобы оно было только у одного из моих классов, то эффекты связывания значительно уменьшились.
Я могу получить некоторую помощь от 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.
В следующем посте я расскажу больше о нахождении в этом коде.