Статьи

Растущее волосатое ПО, руководствуясь тестами

Программное обеспечение растет органически. Одна строка за раз, одна смена за раз. Эти изменения скоро складываются. В идеальном мире они составляют целостную архитектуру с намерением раскрыть дизайн. Но иногда программное обеспечение просто становится волосатым — полно мелких деталей, которые затемняют основную логику. Что делает программное обеспечение волосатым и как мы можем его остановить?

Волосатый код

Обычно код получается чистым — новый, блестящий код. Но каждый раз, когда вы вносите изменения, которые не совсем соответствуют первоначальному дизайну, вы добавляете прическу — маленькую тонкую деталь. Это не умаляет общей цели кода, оно лишь охватывает конкретные детали, о которых изначально не думали. Один волосок сам по себе в порядке. Но потом вы добавляете еще и еще, и еще. Прежде чем вы это знаете, ваш чистый, блестящий код покрыт маленькими волосками. В конце концов, код становится таким волосатым, что вы уже не можете видеть базовый дизайн.
Посмотрим правде в глаза, мы все в основном программисты обслуживания. Сколько из нас на самом деле работает над действительно новым проектом? И в любом случае, вскоре после запуска нового проекта, вы меняете то, что было раньше, и вы снова попадаете в зону технического обслуживания. Мы проводим большую часть нашего времени, изменяя существующий код. Если мы не будем осторожны, мы проводим большую часть нашего времени, добавляя новые волосы.

Самая простая вещь

При изменении существующего кода возникает соблазн сделать наименьшее возможное изменение. Вообще, это хороший подход. Боже, TDD отлично держит тебя на этом. Напишите тест, сделайте его успешным. Напишите тест, сделайте его успешным. Сделайте самое простое, что может сработать. Но вы должны сделать шаг рефакторинга. «Красный, зеленый, рефакторинг », люди. Если вы не рефакторинг, ваш код становится волосатым. Если вы не проводите рефакторинг, то, что вы только что добавили, это клудж. Несомненно, это хорошо проверенный, красиво написанный kludge; но это все еще клудж.
Проблема в том, что себя легко простить.

Но это всего лишь небольшое заявление

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

Пример

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

01
02
03
04
05
06
07
08
09
10
11
12
13
public class OrderItem {
    public void shipIt() {
        if (stockSystem.inStock(getItem()) > getQuantity()) {
            warehouse.shipItem(getItem(),
                               getQuantity(),
                               getCustomer());
        } else {
            warehouse.addQueuedItem(getItem(),
                                    getQuantity(),
                                    getCustomer());
        }
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class OrderItem {
    public void shipIt() {
        if (getItem().isDigitalDelivery()) {
            email.shipItem(getItem(), getCustomer());
        } else if (stockSystem.inStock(getItem()) >
                       getQuantity()) {
            warehouse.shipItem(gettem(),
                               getQuantity(),
                               getCustomer());
        } else {
            warehouse.addQueuedItem(getItem(),
                                    getQuantity(),
                                    getCustomer());
        }
    }
}

В конце концов, это всего лишь небольшое «если», верно?
Это все прекрасно, пока в UAT мы не поймем, что мы показываем доставку через 3 дня для цифровых товаров. Это неправильно, поэтому мы получаем запрос на немедленную доставку цифровых товаров. В Item есть метод, который вычисляет предполагаемую дату доставки:

01
02
03
04
05
06
07
08
09
10
11
public class Item {
    private static final int STANDARD_POST_DAYS = 3;
    public int getEstimatedDaysToDelivery() {
        if (stockSystem.inStock(this) > 0) {
            return STANDARD_POST_DAYS;
        } else {
            return stockSystem.getEstArrivalDays(this) +
                       STANDARD_POST_DAYS;
        }
    }
}

Что ж, это достаточно просто — каждый элемент знает, предназначен ли он для цифровой доставки или нет, поэтому мы можем просто добавить другой, если:

01
02
03
04
05
06
07
08
09
10
11
12
13
public class Item {
    private static final int STANDARD_POST_DAYS = 3;
    public int getEstimatedDaysToDelivery() {
        if (isDigitalDelivery()) {
            return 0;
        } else if (stockSystem.inStock(this) > 0) {
            return STANDARD_POST_DAYS;
        } else {
            return stockSystem.getEstArrivalDays(getSKU()) +
                       STANDARD_POST_DAYS;
        }
    }
}

В конце концов, это еще один вопрос, верно? Где вред? Но постепенно код становится все более и более привлекательным.
Беда в том, что по всему коду наложено много маленьких связанных волосков. У тебя есть волосы здесь, еще один там. Вы знаете, что они связаны — они были сделаны как часть одного и того же набора изменений. Но будет ли кто-то еще, глядя на этот код через 6 месяцев? Что если нам нужно внести изменения, чтобы пользователи могли выбирать электронную и / или физическую доставку для товаров, которые поддерживают оба? Теперь мне нужно найти все места, на которые повлияло наше первоначальное изменение, и внести больше изменений. Но они не сгруппированы, они разбросаны по всему. Конечно, я могу быть методичным и найти их. Но, может быть, если бы я построил это лучше, во-первых, было бы проще?

Лучший способ

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

1
2
3
4
public interface DeliveryMethod {
    void shipItem(Item item, int quantity, Customer customer);
    int getEstimatedDaysToDelivery(Item item);
}

Затем я создаю две конкретные реализации этого:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class PostalDelivery implements DeliveryMethod {
    private static final int STANDARD_POST_DAYS = 3;
    @Override
    public void shipItem(Item item, int quantity,
                         Customer customer) {
        if (stockSystem.inStock(item) > quantity) {
            warehouse.shipItem(item, quantity, customer);
        } else {
            warehouse.addQueuedItem(item, quantity, customer);
        }
    }
    @Override
    public int getEstimatedDaysToDelivery(Item item) {
        if (stockSystem.inStock(item) > 0) {
            return STANDARD_POST_DAYS;
        } else {
            return stockSystem.getEstArrivalDays(item) +
                       STANDARD_POST_DAYS;
        }
    }
}
 
public class DigitalDelivery implements DeliveryMethod {
    @Override
    public void shipItem(Item item, int quantity,
                         Customer customer) {
        email.shipItem(item, customer);
    }
    @Override
    public int getEstimatedDaysToDelivery(Item item) {
        return 0;
    }
}

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

дисциплина

В конечном счете, написание чистого кода — это дисциплина . TDD — отличная дисциплина — она ​​позволяет вам сосредоточиться на поставленной задаче, только добавляя код, который необходим прямо сейчас; все время гарантируя, что у вас есть почти полное тестовое покрытие

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

Рекомендации: растущее волосатое программное обеспечение, руководствуясь тестами нашего партнера JCG Дэвида Грина в блоге Actively Lazy .