Постепенно перемещать кодовую базу в контейнер внедрения зависимостей

9

У меня есть большая кодовая база с большим количеством «анти-шаблонных» синглетонов, служебных классов со статическими методами и классов, создающих свои собственные зависимости с помощью newключевого слова. Это делает код очень сложным для тестирования.

Я хочу постепенно перенести код в контейнер внедрения зависимостей (в моем случае это Guiceпотому, что это GWTпроект). Из моего понимания внедрения зависимости это все или ничего. Либо все классы управляются Spring / Guice, либо нет. Поскольку кодовая база велика, я не могу преобразовать код за ночь. Поэтому мне нужен способ сделать это постепенно.

Проблема состоит в том, что когда я начинаю с класса, который нужно внедрить в другие классы, я не могу использовать простой @Injectв этих классах, потому что эти классы еще не управляются контейнером. Таким образом, это создает длинную цепочку до «верхних» классов, которые нигде не вводятся.

Единственный способ, который я вижу, - сделать Injectorконтекст / application глобально доступным через одноэлементное время, чтобы другие классы могли получать из него управляемые bean-компоненты. Но это противоречит важной идее не раскрытия composition rootприложения.

Другой подход будет восходящим: начать с «высокоуровневых» классов, включить их в контейнер внедрения зависимостей и медленно перейти к «меньшим» классам. Но потом мне придется долго ждать, так как я могу тестировать те небольшие классы, которые все еще зависят от глобальных параметров / статики.

Каким был бы способ добиться такой постепенной миграции?

PS Вопрос постепенного подхода к внедрению зависимости похож в заголовке, но он не отвечает на мой вопрос.

damluar
источник
1
Вы хотите перейти от жестко связанных классов к использованию контейнера для внедрения зависимостей? Сначала вы должны сделать некоторый рефакторинг, чтобы отделить классы, используя интерфейсы, прежде чем даже думать о браке с какой-либо структурой или инструментом внедрения зависимостей.
Тулаинс Кордова
Справедливо. Итак, у меня есть некоторый служебный класс A, который используется в 30 других классах (один из них - класс B), что делает их непроверяемыми. Я думал сделать класс A зависимостью конструктора класса B. Я должен изменить все вызовы конструкторов и передать A внутрь. Но откуда мне его взять?
Дамлуар
Если B - наименьшая единица измерения, к которой вы можете применить DI, чтобы начать, то я не вижу никакого вреда в построении A там, где вы строите B, пока A не имеет зависимостей. Как только шарик катится, вы можете вернуться и провести рефакторинг.
Энди Хант
@damluar Завод для начала?
Тулаинс Кордова
1
@damluar класс не становится непроверенным только потому, что он опирается на служебный класс. Это просто означает, что ваш юнит немного больше, чем вы хотите. Если вы можете протестировать свой служебный класс самостоятельно, то можете без проблем использовать его в тестах для всех остальных 30 классов. Читайте блог Мартина Фаулерса о модульном тестировании.
gbjbaanb

Ответы:

2

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

Данный:

public class MyUI
{
    public void SomeMethod()
    {
        Foo foo = new Foo();
        foo.DoStuff();
    }
}

public class Foo
{

    public void DoStuff()
    {
        Bar bar = new Bar();
        bar.DoSomethingElse();
    }

}

public class Bar
{
    public void DoSomethingElse();
}

может быть легко реорганизован для использования DI без использования контейнера IOC - и вы можете даже разбить его на несколько этапов:

(потенциальный) Шаг первый - взять зависимости, но без изменений в вызывающем (UI) коде:

public class MyUI
{
    public void SomeMethod()
    {
        Foo foo = new Foo();
        foo.DoStuff();
    }
}

public class Foo
{

    private IBar _iBar;

    // Leaving this constructor for step one, 
    // so that calling code can stay as is without impact
    public Foo()
    {
        _iBar = new Bar();
    }

    // simply because we now have a constructor that take's in the implementation of the IBar dependency, 
    // Foo can be much more easily tested.
    public Foo(IBar iBar)
    {
        _iBar = iBar;
    }

    public void DoStuff()
    {
        _iBar.DoSomethingElse();
    }

}

public interface IBar
{
    void DoSomethingElse();
}

public class Bar
{
    public void DoSomethingElse();
}

Рефакторинг 2 (или первый рефакторинг, если реализует контейнер IOC и немедленно изменяет код вызова):

public class MyUI
{
    public void SomeMethod()
    {
        Foo foo = null // use your IOC container to resolve the dependency
        foo.DoStuff();
    }
}

public class Foo
{

    private IBar _iBar;

    // note we have now dropped the "default constructor" - this is now a breaking change as far as the UI is concerned.
    // You can either do this all at once (do only step 2) or in a gradual manner (step 1, then step 2)

    // Only entry into class - requires passing in of class dependencies (IBar)
    public Foo(IBar iBar)
    {
        _iBar = iBar;
    }

    public void DoStuff()
    {
        _iBar.DoSomethingElse();
    }

}

Шаг 2 технически может быть выполнен сам по себе - но (потенциально) будет намного больше работы - в зависимости от того, сколько классов в настоящее время «обновляют» функциональность, которую вы ищете для DI.

Подумайте о переходе с шага 1 -> шаг 2 - вы сможете создавать модульные тесты Foo, независимо от Bar. Принимая во внимание, что до рефактора шага 1 это было нелегко осуществить без использования фактической реализации обоих классов. Выполнение шага 1 -> шага 2 (а не шага 2 сразу) позволит со временем вносить меньшие изменения, и у вас уже будет запуск тестового жгута, чтобы обеспечить бесперебойную работу вашего рефакторинга.

Kritner
источник
0

Концепция одинакова, используете ли вы Java, PHP или даже C #. Этот вопрос довольно хорошо решен Джеммой Аннибл в этом видео на YouTube:

https://www.youtube.com/watch?v=Jccq_Ti8Lck (PHP, извините!)

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

Кевин Г.
источник