Статьи

Устаревший код для тестируемого кода № 5: извлечение класса

55592992_zpsa03c9fa3 Этот пост является частью серии « Устаревший код для тестируемого кода ». В этой серии мы поговорим о том, как выполнить шаги по рефакторингу перед написанием тестов для унаследованного кода, и о том, как они облегчают нашу жизнь.

Несколько лет назад я получил это от Эрика Тальбума : «Частный метод — это запах дизайна». Мне потребовалось некоторое время, чтобы полностью понять это и применить это.

Там есть хорошая логика.

Если в какой-то момент мы извлекаем приватный метод из публичного метода, это, вероятно, означает, что публичный метод сделал слишком много. Это было долго и процедурно, и, вероятно, сделал ряд операций. В этих обстоятельствах имело смысл извлечь часть логики в приватный метод. Имеет смысл, чтобы мы могли назвать его более четко, поскольку извлеченный метод был проще.

Это также означает, что публичный метод нарушил принцип единой ответственности. Ведь мы просто разбили его пополам (как минимум). В этом случае извлеченный нами закрытый метод содержит функции, отличные от остальной части открытого метода.

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

Тестирование и простота идут рука об руку, и выделение в отдельный класс имеет смысл во многих случаях. Жаль, что это не часто применяется.

Вот простой пример:

01
02
03
04
05
06
07
08
09
10
public void UpdateStreet(string newStreet)
{
    if(string.IsNullOrEmpty(street) &&
            !street.StartsWith(" ") &&
            !street.StartsWith("@"))
    {
        Address address = new Address(this);
        address.SetStreet(newStreet);
    }
}

Имеет смысл извлечь валидацию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public void UpdateStreet(string newStreet)
{
    if (ValidateStreet(newStreet))
    {
        Address address = new Address(this);
        address.SetStreet(newStreet);
    }
}
private bool ValidateStreet(string street)
{
    return string.IsNullOrEmpty(street) &&
            !street.StartsWith(" ") &&
            !street.StartsWith("@");
}

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

1
2
3
4
5
6
7
8
public void UpdateStreet(string newStreet)
{
    if (StreetValidator.Validate(newStreet))
    {
        Address address = new Address(this);
        address.SetStreet(newStreet);
    }
}

Теперь мы можем протестировать метод Validate , а затем отдельно оригинальный метод UpdateStreet .

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

Предположим, что наша проверка теперь включает сравнение с улицей текущего адреса:

01
02
03
04
05
06
07
08
09
10
11
public void UpdateStreet(string newStreet)
{
    if(string.IsNullOrEmpty(street) &&
            !street.StartsWith(" ") &&
            !street.StartsWith("@")) &&
            currentAddress.GetStreet().CompareTo(street) == 0)
    {
        Address address = new Address(this);
        address.SetStreet(newStreet);
    }
}

currentAddress — это поле в нашем классе, поэтому его легко извлечь в приватный метод:

1
2
3
4
5
6
7
private bool ValidateStreet(string street)
{
    return string.IsNullOrEmpty(street) &&
            !street.StartsWith(" ") &&
            !street.StartsWith("@") &&
            currentAddress.GetStreet().CompareTo(street) == 0;
}

Однако, извлекая это в отдельный класс, мы должны передать currentAddress в качестве параметра. Мы можем сделать это в два этапа. Сначала мы изменим сигнатуру метода и добавим параметр с тем же именем, что и у поля:

1
2
3
4
5
6
7
private bool ValidateStreet(string street, Address currentAddress)
{
    return string.IsNullOrEmpty(street) &&
            !street.StartsWith(" ") &&
            !street.StartsWith("@") &&
            currentAddress.GetStreet().CompareTo(street) == 0;
}

Теперь, когда мы «затеняли» поле, мы отделили метод от его класса. Теперь метод может быть извлечен в отдельный класс.

Я считаю, что люди принимают извлечение класса (если это безопасно и легко) больше, чем раскрытие методов или создание методов доступа. Эффект тот же (и риск того, что кто-то его назовет, тот же), но простота делает его более терпимым, я думаю.

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

Трюк, который мы сделали, когда добавили параметр? Мы обсудим это с более подробной информацией в следующем.

Ссылка: Устаревший код для тестируемого кода № 5: Извлеките класс из нашего партнера JCG Гила Зильберфельда в блоге Geek Out of Water .