Самое сложное в этом всем - это решиться все-таки покрыть сначала тестами, находясь под давлением того, что вы не знаете сколько времени уйдет на добавлении новой фичи. Часто в “легаси” проектах очень сложно определить сколько времени потребуется на добавление фичи. Существует несколько техник.
Когда программисту говорят добавить в приложение новую функциональность, это можно воспринимать в первую очередь, как последовательность таких действий как:
- Напиши новый метод или добавь новый кусок кода к существующему методу.
- А также вызови его из места, где нужно внедрить эту новую функциональность.
- Удостоверься, что все работает, как надо.
Обычно написать тесты, покрывающие именно места вызова нашего нового метода нелегко, но зато по крайней мере мы можем “покрыть тестами” добавленный нами новый метод.
Представьте, себе такой код.
Пример. [source,java]
public class TransactionGate { public void postEntries(List entries) { for (Iterator it = entries.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); entry.postDate(); } transactionBundle.getListManager().add(entries); } … }
Например, нам нужно добавить код для проверки того, что ни один из новых вхождений еще не находится в transactionBundle
до того, как установим дату этому вхождению и добавим в transactionBundle.getListManager()
. Глядя на postEntries
, кажется, что новое изменение нужно добавить в начало метода - до цикла. Но, на самом деле, это должно случится внутри цикла. Например, мы можем сделать так:
[source,java]
public class TransactionGate { public void postEntries(List entries) { List entriesToAdd = new LinkedList(); for (Iterator it = entries.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); if (!transactionBundle.getListManager().hasEntry(entry) { entry.postDate(); entriesToAdd.add(entry); } } transactionBundle.getListManager().add(entriesToAdd); } … }
Наша вставка кажется небольшим изменением функционала, но на самом деле это не так. Теперь, как мы можем проверить, что наше изменение делает именно, то что мы хотим? Ведь нет никакого разделителя, который бы отличал новый код от старого. Хуже еще то, что мы сделали код немного непонятней. Мы смешали две операции здесь: установка даты и проверка на дублирование. Этот метод довольно небольшой, но уже менее понятный и плюс - мы добавили временную переменную.
Добавление временной переменной - необязательно плохо, но иногда оно приносит с собой и новый код. Если изменение, которое мы сделали, включает работу со всеми уникальными вхождениями до того,как мы их добавим, то тогда существует только одно место, где изменение должно находится : прямо в этом методе. Было бы заманчиво просто положить наше новое изменение прямо в этот метод, но может мы можем сделать это каким-либо другим способом?
Да. Мы можем вынести наш код в отдельную операцию. Мы можем использовать подход TDD (link:http://en.wikipedia.org/wiki/Test-driven_development[Test-Driven-Development]) и создать новый метод uniqueEntries
:
[source,java]
public class TransactionGate { … List uniqueEntries(List entries) { List result = new ArrayList(); for (Iterator it = entries.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); if (!transactionBundle.getListManager().hasEntry(entry) { result.add(entry); } } return result; } … }
В этом случае будет легко написать код для тестирования нового добавленного метода. Теперь мы можем вернуться нашему изначальному примеру и вызвать метод. [source,java]
public class TransactionGate { … public void postEntries(List entries) { entriesToAdd = uniqueEntries(entries); for (Iterator it = entriesToAdd.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); entry.postDate(); } transactionBundle.getListManager().add(entriesToAdd); } … }
У нас еще все-таки осталась одна временная переменная, но код стал более упорядоченным. Если нам понадобится добавить еще функциональность, которой понадобится список уникальных вхождений, то мы сможем использовать этот же метод. Если в дальнейшем, наш метод будет вызываться очень часто, то мы легко сможем вынести его в новый класс. Фокус в том, чтобы держать этот метод небольшим и понятным с первого взгляда - это увеличивает читаемость все класса в целом.
Это был пример добавления функционала путем “ответвления” с помощью метода. В процессе мы выполнили следующие шаги:
. Идентифицировать место, в котором требуются изменения. . Если изменение формулируется как последовательность утверждений в одном месте в методе, написать вызов метода, который будет включать все эти утверждения. . Определить какие локальные переменные нужны для вызова из исходного метода и подставить все необходимые аргументы. . Определить должен ли ответвяющий метод возвращать какое-нибудь значение. . Написать тест на наш новый метод.
Я рекомендую использовать метод ветвления когда вы видите, что назначение нового кода отличается от уже существующего или вы не можете покрыть тестами изначальный метод. Предпочтительно, конечно, добавить этот метод в одну строчку.
Иногда, когда вы хотите использовать метод ветвления, зависимостей в вашем классе настолько много, что вы не можете создать экземпляр без создания большого количества заглушек и фейковых объектов.
Альтернатива этому подходу - это передавать везде null
.
Когда не подошли предыдущие подходы, рассмотрите возможность сделать метод public static
. Вы можете передать экземпляр исходного класса как аргумент, это позволит сделать вам ваши изменения.
Это может показаться странным - делать метод статическим для наших целей, но в устаревшем (legacy) коде это может быть полезным. Как правило, много статических методов в одном классе - это плацдарм для рефакторинга. Часто, если вы видите несколько статических методов и вы замечаете ,что они работают с одними и теме же переменными, лучше перенести эти статические методы в новый класс, и сделать их методами экземпляра. Когда понадобится вернуть их назад - это будет несложно.
=== Минусы
- Код не становится лучше, мы просто добавляем новый метод, который будет покрыт тестом.
=== Плюсы
- Четкое разделение старого кода и нового
Использована литература [bibliography]
- [[[legacy]]] Working Effectively with Legacy Code. p.51