Рефакторинг статических классов

Использование статических классов со статическими методами внутри обладает серией недостатков. Одним из недостатков является появление в коде скрытых зависимостей, так как нельзя понять по интерфейсу либо конструктору некоторого класса зависит ли он от статики или нет. Статические классы нельзя использовать полиморфно. Также подобные классы часто выполняют роль таких себе Helper’ов, Manager’ов, Doer’ов, которым свойственно отвечать за все на свете и разрастаться до немыслимых размеров. Со всеми перечисленными минусами еще можно сосуществовать некоторое время. Однако когда дело доходит до необходимости написания юнит-тестов, от использования статики приходится отказаться в пользу других решений. Рассмотрим их.

Пример кода и проблема

public class Logic
{
    public int Calculate()
    {
        var values = DBHelper.GetValues();
        return values.Sum(v => v);
    }
}

public static class DBHelper
{
    public static List<int> GetValues() =>
        new List<int>() { 1, 2, 3 };
}

Класс Logic занимается выполнением неких расчетов, обращаясь напрямую в базу за входными данными через статический класс DBHelper. Метод Calculate может быть покрыт тестом, но только не юнит, а интеграционным. Написанный тест будет тестировать логику метода Calculate вместе с базой данных. Запуск такого теста требует установки SQL Server’а, подготовку схемы базы и заполнением таблиц данными. Подобный расклад затруднит жизнь при необходимости прогонки юнит-тестов машинах, на которых отсутствует база данных. Другой минус заключается во времени выполнения юнит-тестов, завязанных на базу данных. Они будут выполняться медленно, что явно противоречит первому принципу юнит-тестов из пятерки F.I.R.S.T. В добавок, запуски подобного теста станут недетерминированными, так как изменение данных в базе будет влиять на результаты выполнения теста.

Выделение интерфейса

Необходимость разорвать жесткую зависимость между классами Logic и DBHelper наталкивает на самое очевидное решение — убрать все ключевые слова static из класса DBHelper, выделить интерфейс и внедрить его в конструктор класса Logic.

public class Logic
{
    private IDBHelper _db;
    public Logic(IDBHelper db) => _db = db;
    public int Calculate()
    {
        var values = _db.GetValues();
        return values.Sum(v => v);
    }
}

Теперь класс Logic зависит только от абстракции IDBHelper, а не от конкретного типа. Появилась возможность инициализировать Logic объектом класса NullDBLogger, возвращающий фейковые данные для написания полностью изолированных от внешнего окружения юнит-тестов.

Шаблон Ambient Context

Вторым способом решения проблемы будет использование шаблона Ambient Context, который позволит не внедрять зависимость DBHelper в конструкторы классов и при этом обеспечит гибкость необходимую для написания юнит-тестов.

public class DB
{
    public static IDBHelper Current { get; private set; }

    public static void Init(IDBHelper db) => Current = db;
}

Осталось создать объект DBHelper где-нибудь поближе к точке входа в приложение (Composition Root)

DB.Init(new DBHelper());

и использовать в любом месте проекта подобным образом

DB.Current.GetValues();

а перед запуском юнит-тестов использовать заглушку

DB.Init(new NullDBHelper());

В нашем случае с базой данных я бы остановился на использовании варианта с внедрением зависимостей. Шаблон Ambient Context имеет смысл использовать для так называемых сквозных задач (cross-cutting concerns), которыми являются логирование, проверка прав доступа, системное время, доступ к данным конфигурационного файла и тд. Перечисленные зависимости обычно используются в большом количестве в проекте на разных его уровнях, а следовательно постоянное внедрение классов Logger, User, SystemDateTime, AppConfig и других привело бы к засорению огромного количества конструкторов классов идентичными списками параметров.

Почитать дальше

Volatile Dependencies

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s