Рефакторинг для DI на крупных проектах - программирование
Подтвердить что ты не робот

Рефакторинг для DI на крупных проектах

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

До сих пор все продукты использовали полную функциональность нашей платформы:
- Получение данных конфигурации из базы данных
- Удаленный доступ к файловой системе
- Разрешение на безопасность
- Базовая логика (то, что нам платят)

Для нового продукта нам было предложено поддерживать меньшую часть функциональности без инфраструктуры, которую приносят платформы. Наша архитектура старая (начало кодирования с 2005 года или около того), но достаточно прочная.

Мы уверены, что можем сделать это с помощью DI на наших существующих классах, но расчетное время для этого варьируется от 5 до 70 недель, в зависимости от того, с кем вы разговариваете.

Там много статей, рассказывающих вам, как делать DI, но я не могу найти что-то, что расскажет вам, как реорганизовать DI на наиболее эффективный способ? Есть ли инструменты, которые делают это вместо того, чтобы проходить через 30 000 строк кода и ударить CTRL + R для расширения интерфейсов и слишком много раз добавлять их в конструкторы? (у нас есть resharper, если это помогает). Если нет, то, что вы находите, является идеальным рабочим процессом для быстрого достижения этого?

4b9b3361

Ответ 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.

С течением времени ваши вызовы будут все ближе и ближе к корню композиции, а затем вы можете начать удалять использование локатора службы.