Внедрение зависимостей или статический класс для вспомогательного класса?

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

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

public static class Helpers
{
    public static void CopyFile(string sourcePath, string destPath)
    {
        Log.Verbose("Start copy from {SourcePath} to {DestPath}", sourcePath, destPath);
        if (!Directory.Exists(Path.GetDirectoryName(destPath)))
        {
            Directory.CreateDirectory(Path.GetDirectoryName(destPath));
        }
        File.Copy(sourcePath, destPath, true);
        Log.Verbose("End of copy from {SourcePath} to {DestPath}", sourcePath, destPath);
    }
}

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

Я попытался использовать System.IO.Abstraction и внедрение зависимостей, чтобы решить эту проблему, и тогда у меня есть следующий код:

public interface IHelpers
{
    public void CopyFile(string sourcePath, string destPath);
}

public class Helpers(IFileSystem fileSystem) : IHelpers
{
    private readonly IFileSystem FileSystem = fileSystem;

    public void CopyFile(string sourcePath, string destPath)
    {
        Log.Verbose("Start copy from {SourcePath} to {DestPath}", sourcePath, destPath);
        if (!FileSystem.Directory.Exists(Path.GetDirectoryName(destPath)))
        {
            FileSystem.Directory.CreateDirectory(Path.GetDirectoryName(destPath));
        }
        FileSystem.File.Copy(sourcePath, destPath, true);
        Log.Verbose("End of copy from {SourcePath} to {DestPath}", sourcePath, destPath);
    }
}

Теперь мне просто нужно внедрить свой IHelper туда, куда мне нужно вызвать CopyFile, и все кажется хорошо. За исключением того, что у меня есть редкий случай, когда кажется неправильным внедрять этого помощника внутри класса.

public class Component
{
    public string Name;
    public string Path;

    public void ExportOutput()
    {
        // ...
        // much calculation then...
        // multiple calls to Helpers.CopyFile(src,dest);
        // ...
    }
}

Чтобы решить эту проблему, я передал класс IHelper в качестве параметра ExportOutput, но это кажется совершенно отвратительным.

Единственное решение, которое я нашел для этой проблемы, — это удалить функцию ExportOutput и поместить ее туда, где я могу полагаться на внедрение зависимостей для использования класса Helper.

Это правильно или я совсем упускаю суть?

Как вы обычно обрабатываете эти небольшие вспомогательные функции в своем коде?

Разве вы не можете внедрить хелпер в свой класс вместо ExportOutput-функции? И все же, почему вы находите это «отвратительным»? Это более или менее то, чего я ожидал.

MakePeaceGreatAgain 17.07.2024 13:48

Класс компонента — это модель данных, с которыми я работаю. Мне кажется странным использовать внедрение зависимостей в этом классе, поскольку я чаще всего создаю его экземпляр следующим образом: ComponentComponent = new();

Philippe Balleydier 17.07.2024 13:56

ну, с DI-контейнером вам почти никогда не следует ничего делать, особенно когда эти вещи вводят зависимости вне вашего контроля.

MakePeaceGreatAgain 17.07.2024 14:02
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
3
88
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

Альтернативой вашему подходу было бы создание вашего CopyFile как расширения файловой системы, т.е.

public static class Helpers
{
    public static void CopyFile(this IFileSystem fileSystem, string sourcePath, string destPath)
    {
        ...
    }
}

При таком подходе реализация CopyFile будет рассматриваться как часть тестируемой вещи, и это может быть желательным или нежелательным в зависимости от обстоятельств. Возможным преимуществом является то, что это позволит тестируемому методу предоставить собственную реализацию CopyFile без необходимости изменения зависимостей или тестов.

За исключением того, что у меня есть редкий случай, когда кажется неправильным внедрять этого помощника внутри класса.

Если ваш метод ExportOutput зависит от файловой системы и вы хотите, чтобы эту зависимость можно было тестировать, вам нужно будет каким-то образом внедрить эту зависимость. Я бы, вероятно, использовал внедрить зависимость в конструктор Component или переместил ExportOutput в отдельный класс. Но могут быть случаи, когда использование параметра метода может быть уместным.

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

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

public interface IFileCopier
{
    public void CopyFile(string sourcePath, string destPath);
}

public class FileCopier:IFileCopier
{
    private readonly IFileSystem FileSystem;

    public void CopyFile(string sourcePath, string destPath)
    {
        Log.Verbose("Start copy from {SourcePath} to {DestPath}", sourcePath, destPath);
        if (!FileSystem.Directory.Exists(Path.GetDirectoryName(destPath)))
        {
            FileSystem.Directory.CreateDirectory(Path.GetDirectoryName(destPath));
        }
        FileSystem.File.Copy(sourcePath, destPath, true);
        Log.Verbose("End of copy from {SourcePath} to {DestPath}", sourcePath, destPath);
    }
}

public class Helpers
{
    private readonly IFileCopier FileCopier;

    public void CopyFile(string sourcePath, string destPath)
    {
      FileCopier.CopyFile(sourcePath,destPath);
    }
}

Затем вы можете перейти к макетированию или внедрению зависимости в экземпляр IFileCopier. Это может показаться простым переименованием (поскольку Helper пока не имеет других функций), но оно подчеркивает, какой функционал вы хотите внедрить и почему.

Ответ принят как подходящий

Как уже отмечалось, вы можете использовать интерфейс IFileSystem именно для этого сценария.

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

delegate void CopyFunction(string sourcePath, string destinationPath);

Вы можете зарегистрировать его с помощью DI так же, как и интерфейс или класс:

services.AddSingleton<CopyFunction>(Helpers.CopyFile);

...а затем ввести его.

public class Component
{
    private readonly CopyFunction _copyFunction;

    public Component(CopyFunction copyFunction)
    {
        _copyFunction = copyFunction;
    }
}

Вы вызываете ее так же, как и любую другую функцию.

_copyFunction(source, destination);

Что особенно приятно, для того, чтобы поглумиться, не нужен Moq. Вы можете использовать анонимную функцию.

Возможно

CopyFunction copyFunction = (source, destination) => {}; // does nothing

Или, если вы хотите подтвердить, что были переданы определенные значения, вы можете сделать что-то вроде этого:

string copiedSource = null;
string copiedDestination = null;

CopyFunction copyFunction = (source, destination) => 
{
    copiedSource = source;
    copiedDestination = destination;
};

... чтобы после вызова функции вы могли утвердить значения copiedSource и copiedDestination.

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

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

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


Вы также можете использовать Action<string, string> вместо делегата. Риск, хотя и маловероятный, заключается в том, что вы захотите внедрить в два места два метода, которые имеют одну и ту же сигнатуру, но делают разные вещи. Делегат позволяет вам указать использование внедряемого метода.

Другие вопросы по теме