Рефакторинг для DI на крупных проектах
Я работаю над крупномасштабным проектом платформы, поддерживающим около 10 продуктов, которые используют наш код.
До сих пор все продукты использовали полную функциональность нашей платформы:
- Получение данных конфигурации из базы данных
- Удаленный доступ к файловой системе
- Разрешение на безопасность
- Базовая логика (то, что нам платят)
Для нового продукта нам было предложено поддерживать меньшую часть функциональности без инфраструктуры, которую приносят платформы. Наша архитектура старая (начало кодирования с 2005 года или около того), но достаточно прочная.
Мы уверены, что можем сделать это с помощью DI на наших существующих классах, но расчетное время для этого варьируется от 5 до 70 недель, в зависимости от того, с кем вы разговариваете.
Там много статей, рассказывающих вам, как делать DI, но я не могу найти что-то, что расскажет вам, как реорганизовать DI на наиболее эффективный способ? Есть ли инструменты, которые делают это вместо того, чтобы проходить через 30 000 строк кода и ударить CTRL + R для расширения интерфейсов и слишком много раз добавлять их в конструкторы? (у нас есть resharper, если это помогает). Если нет, то, что вы находите, является идеальным рабочим процессом для быстрого достижения этого?
Ответы
Ответ 1
Спасибо за все ответы. Прошло уже почти год, и я думаю, что могу в основном ответить на собственный вопрос.
Мы, конечно же, конвертировали только те части нашей платформы, которые должны были повторно использоваться в новом продукте, как указывает lasseeskildsen. Поскольку это было лишь частичное преобразование базы кода, мы пошли с подходом DIY к инъекции зависимостей.
Наша цель заключалась в том, чтобы сделать эти части доступными без привлечения нежелательных зависимостей, а не для их модульного тестирования. Это влияет на то, как вы подходите к проблеме. В этом случае реальных изменений дизайна нет.
Работа связана с обыденностью, поэтому вопрос о том, как сделать это быстро или даже автоматически.
Ответ заключается в том, что он не может быть автоматизирован, но с помощью некоторых сочетаний клавиш и повторной настройки это можно сделать довольно быстро. Для меня это оптимальный поток:
-
Мы работаем с несколькими решениями. Мы создали временное "главное" решение, которое содержит все проекты во всех файлах решений. Хотя инструменты рефакторинга не всегда достаточно умны, чтобы поднять разницу между бинарными и проектными ссылками, это, по крайней мере, сделает их частично частью нескольких решений.
-
Создайте список всех зависимостей, необходимых для вырезания. Группируйте их по функциям. В большинстве случаев мы могли решать сразу несколько взаимосвязанных зависимостей.
-
У вас будет много небольших изменений кода во многих файлах. Эта задача лучше всего выполняется одним разработчиком или двумя максимум, чтобы избежать необходимости постоянно сменить ваши изменения.
-
Сначала избавиться от одиночек:
После преобразования их из этого шаблона извлеките интерфейс (resharper → refactor → extract interface)
Удалите одноэлементный аксессуар, чтобы получить список ошибок сборки. На шаге 6.
-
Чтобы избавиться от других ссылок:
а. Извлеките интерфейс, как указано выше.
б. Прокомментируйте первоначальную реализацию. Это дает вам список ошибок сборки.
-
Теперь Resharper становится большой помощью:
- Alt + shift + pg down/up быстро перемещает неработающие ссылки.
- Если несколько ссылок имеют общий базовый класс, перейдите к его конструктору и нажмите ctrl + r + s ( "подпись метода изменения" ), чтобы добавить новый интерфейс к конструктору. Resharper 8 предлагает вам возможность "разрешить по дереву вызовов", что означает, что вы можете заставить классы наследования изменить свою подпись автоматически. Это очень аккуратная функция (новая в версии 8 кажется).
- В теле конструктора назначьте вложенный интерфейс к несуществующему свойству. Хит alt + enter, чтобы выбрать "создать свойство", переместить его туда, где он должен быть, и вы сделали. Раскомментировать код из 5b.
-
Test! Промыть и повторить.
Чтобы использовать эти классы в исходном решении без серьезных изменений кода, мы создали перегруженные конструкторы, которые извлекают свои зависимости через локатор службы, как упоминает Бретт Вентра. Это может быть анти-шаблон, но работает для этого сценария. Он не будет удален, пока весь код не поддержит DI.
Мы конвертировали примерно четверть нашего кода в DI примерно через 2-3 недели (1,5 человека).
Еще год, и теперь мы переводим весь наш код в DI. Это другая ситуация, когда фокус переходит к единичной тестируемости. Я думаю, что общие шаги, описанные выше, будут по-прежнему работать, но для этого требуются некоторые фактические изменения дизайна.
Ответ 2
Я предполагаю, что вы хотите использовать инструмент IoC, такой как StructureMap, Funq, Ninject и т.д.
В этом случае работа по рефакторингу действительно начинается с обновления ваших точек входа (или Roots of the...) в кодовой базе. Это может иметь большое влияние, особенно если вы используете широко распространенное использование статики и управляете временем жизни ваших объектов (например, кеширование, ленивые нагрузки). Когда у вас есть инструмент IoC на месте, и он прокладывает диаграммы объектов, вы можете начать распространять свое использование DI и пользоваться преимуществами.
Сначала я хотел бы сосредоточиться на настройках, подобных зависимостям (которые должны быть объектами простого значения) и начать делать вызовы с разрешением с помощью инструмента IoC. Затем создайте классы Factory и добавьте те, которые управляют временем жизни ваших объектов. Будет ощущение, что вы идете назад (и медленно), пока не достигнете гребня, где большинство ваших объектов использует DI, и, следовательно, SRP - оттуда он должен быть вниз. Когда у вас будет лучшее разделение проблем, гибкость вашей кодовой базы и скорость, с которой вы можете вносить изменения, значительно увеличится.
Осторожно: не позволяйте себе одурачить мысль о том, чтобы разбрызгивать "Локатор сервисов" повсюду - ваша панацея, на самом деле это DI antipattern. Я думаю, что вам нужно будет сначала использовать это, но затем вы должны закончить работу DI с помощью инъекций конструктора или сеттера и удалить Locator службы.
Ответ 3
Вы спрашивали о инструментах. Одним из инструментов, который может помочь в таком большом рефакторинге, является nDepend. Я использовал его, чтобы помочь определить места для целей рефакторинга.
Я не хочу упоминать об этом, потому что я не хочу создавать впечатление, что для этого проекта нужен инструмент, такой как nDepend. Однако полезно визуализировать зависимости в базе кода. Он поставляется с 14-дневной полнофункциональной пробной версией, которая может быть достаточной для ваших нужд.
Ответ 4
Не думайте, что есть какой-либо инструмент для этого преобразования кода.
Потому что →
Использование DI в существующей базе кода будет включать в себя
-
использование интерфейса/абстрактного класса. Опять же, здесь нужно сделать правильный chioce, чтобы облегчить преобразование, сохраняя принцип DI и функциональность кода.
-
Эффективная сегрегация/унификация существующих классов в нескольких/отдельных классах, чтобы сохранить код модульных или небольших восстанавливаемых единиц.
Ответ 5
То, как я приближаюсь к конверсии, - это посмотреть на любую часть системы, которая постоянно изменяет состояние; файлы, базу данных, внешний контент. После изменения и перечитания изменилось ли оно навсегда? Это первое место, чтобы посмотреть, чтобы изменить его.
Итак, первое, что вы делаете, - найти место, которое модифицирует источник следующим образом:
class MyXmlFileWriter
{
public bool WriteData(string fileName, string xmlText)
{
// TODO: Sort out exception handling
try
{
File.WriteAllText(fileName, xmlText);
return true;
}
catch(Exception ex)
{
return false;
}
}
}
Во-вторых, вы пишете unit test, чтобы убедиться, что вы не нарушаете код во время рефакторинга.
[TestClass]
class MyXmlWriterTests
{
[TestMethod]
public void WriteData_WithValidFileAndContent_ExpectTrue()
{
var target = new MyXmlFileWriter();
var filePath = Path.GetTempFile();
target.WriteData(filePath, "<Xml/>");
Assert.IsTrue(File.Exists(filePath));
}
// TODO: Check other cases
}
Далее, Извлеките интерфейс из исходного класса:
interface IFileWriter
{
bool WriteData(string location, string content);
}
class MyXmlFileWriter : IFileWriter
{
/* As before */
}
Повторите тесты и надейтесь, что все будет хорошо. Соблюдайте первоначальный тест, поскольку он проверяет вашу более старую реализацию.
Затем напишите фальшивую реализацию, которая ничего не делает. Мы хотим реализовать здесь очень простое поведение.
// Put this class in the test suite, not the main project
class FakeFileWriter : IFileWriter
{
internal bool WriteDataCalled { get; private set; }
public bool WriteData(string file, string content)
{
this.WriteDataCalled = true;
return true;
}
}
Затем unit test это...
class FakeFileWriterTests
{
private IFileWriter writer;
[TestInitialize()]
public void Initialize()
{
writer = new FakeFileWriter();
}
[TestMethod]
public void WriteData_WhenCalled_ExpectSuccess()
{
writer.WriteData(null,null);
Assert.IsTrue(writer.WriteDataCalled);
}
}
Теперь, когда все тестируемые модули и обновленные версии все еще работают, мы должны убедиться, что при вводе вызывающий класс использует интерфейс, а не конкретную версию!
// Before
class FileRepository
{
public FileRepository() { }
public void Save( string content, string xml )
{
var writer = new MyXmlFileWriter();
writer.WriteData(content,xml);
}
}
// After
class FileRepository
{
private IFileWriter writer = null;
public FileRepository() : this( new MyXmlFileWriter() ){ }
public FileRepository(IFileWriter writer)
{
this.writer = writer;
}
public void Save( string path, string xml)
{
this.writer.WriteData(path, xml);
}
}
Так что же мы сделали?
- Создайте конструктор по умолчанию, который использует обычный тип
- У конструктора, который принимает тип
IFileWriter
- Используется поле экземпляра для хранения указанного объекта.
Тогда это случай записи unit test для FileRepository
и проверки того, что метод вызывается:
[TestClass]
class FileRepositoryTests
{
private FileRepository repository = null;
[TestInitialize()]
public void Initialize()
{
this.repository = new FileRepository( new FakeFileWriter() );
}
[TestMethod]
public void WriteData_WhenCalled_ExpectSuccess()
{
// Arrange
var target = repository;
// Act
var actual = repository.Save(null,null);
// Assert
Assert.IsTrue(actual);
}
}
Хорошо, но здесь мы действительно тестируем FileRepository
или FakeFileWriter
? Мы тестируем FileRepository
, поскольку наши другие тесты тестируют FakeFileWriter
отдельно. Этот класс - FileRepositoryTests
был бы более полезен для проверки входящих параметров для нулей.
Подделка не делает ничего умного - без проверки параметров, без ввода-вывода. Он просто сидит, так что FileRepository может сохранять содержимое любой работы. Его назначение в два раза; Чтобы значительно ускорить тестирование устройства и не нарушить состояние системы.
Если этот FileRepository также должен был прочитать файл, вы также могли бы реализовать IFileReader (что немного экстремально) или просто сохранили последний файл filePath/xml в строке в памяти и вместо этого извлекли.
Итак, с общими принципами - как вы подходите к этому?
В большом проекте, который требует много рефакторинга, всегда лучше включать модульное тестирование в любой класс, который подвергается изменению DI. Теоретически ваши данные не должны быть привязаны к сотням мест [в вашем коде], но пробиты через несколько ключевых мест. Найдите их в коде и добавьте для них интерфейс. Один трюк, который я использовал, - это скрыть каждый DB или индексный источник за интерфейсом, подобным этому:
interface IReadOnlyRepository<TKey, TValue>
{
TValue Retrieve(TKey key);
}
interface IRepository<TKey, TValue> : IReadOnlyRepository<TKey, TValue>
{
void Create(TKey key, TValue value);
void Update(TKey key, TValue);
void Delete(TKey key);
}
Что позволяет вам извлечь из источников данных очень общий способ. Вы можете переключиться с XmlRepository
на DbRepository
, только заменив туда, где он был введен. Это может быть чрезвычайно полезно для перехода проекта из одного источника данных в другой, не затрагивая внутренности системы. Это может быть небольшим изменением манипуляции с XML для использования объектов, но гораздо проще поддерживать и внедрять новые функции при таком подходе.
Единственный другой совет, который я могу дать, это сделать 1 источник данных за один раз и сделать это. Сопротивляйтесь соблазну делать слишком много за один раз. Если вам действительно нужно сохранять файлы, DB и веб-службы за один удар, используйте Extract Interface, подделывайте вызовы и ничего не возвращайте. Это настоящий жонглинговый акт, чтобы делать много за один раз, но вы можете сложить их более легко, чем начинать с первых принципов.
Удачи!
Ответ 6
Эта книга, вероятно, была бы очень полезной:
Эффективная работа с устаревшим кодом - Michael C. Feathers - http://www.amazon.com/gp/product/0131177052
Я бы предложил начать с небольших изменений. Постепенно перемещайте зависимости, которые нужно вводить через конструктор. Всегда держите систему в рабочем состоянии. Извлеките интерфейсы из зависимых от конструктора зависимостей и начните упаковку с модульных тестов. Принесите инструменты, когда это имеет смысл. Вам не нужно сразу начинать использование инъекций зависимостей и насмешек. Вы можете сделать много улучшений, вручную введя зависимости через конструктор.
Ответ 7
То, что вы описали, является важной частью процесса; проходя через каждый класс, создал интерфейс и зарегистрировал его. Это наиболее проблематично, если вы сразу же передаете рефакторинг корневому составу, в случае MVC, что означает, что вы собираетесь вводить в контроллер.
Это может быть очень много работы, и если код делает много прямого создания объекта, может быть очень сложно попытаться сделать все сразу. В этих случаях я считаю приемлемым использовать шаблон Locator Service и вручную вызывать решение.
Начните с замены некоторых ваших прямых вызовов на конструкторы с помощью вызова разрешения локатора службы. Это уменьшит количество необходимых рефакторингов и начнет давать вам преимущества DI.
С течением времени ваши вызовы будут все ближе и ближе к корню композиции, а затем вы можете начать удалять использование локатора службы.