вторник, 29 октября 2013 г.

Форвардинг типов в .NET

Когда речь заходит об абстракциях и инкапсуляциях, то обычно у нас в голове появляется образ класса, с разделением его интерфейса и реализации. Однако мы значительно реже используем тот же самый подход для более крупных строительных блоков, таких как модули (пакеты в Java или сборки в .NET). У сборки, также как и у класса, есть открытый интерфейс (абстракция) состоящий из набора открытых типов и закрытая часть (реализация), представленная в виде внутренних (internal) типов.

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

Именно по этой причине Мартин Фаулер в своей статье "Public versus Published Interfaces" вводит подмножество открытых интерфейсов, который он называет "опубликованными интерфейсами". До тех пор, пока открытый тип не используется за пределами нашего проекта, то он не слишком сильно отличается от закрытого типа. У нас все еще развязаны руки, и мы довольно легко можем "рефакторить" открытый интерфейс, изменяя имена, добавляя или удаляя методы или классы. Все что нам потребуется, это пересобрать нашу систему и прогнать тесты.

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

Теперь давайте рассмотрим такую ситуацию. У нас есть библиотека CustomLibrary с единственным типом CustomType, которые используются из нашего приложения.

static void Main(string[] args)
{
   
//System.Func<int> foo = null;     var d = new Library.CustomType
();
   
Console.WriteLine("CustomType created from assembly - {0}",
        d.GetType().Assembly.GetName().Name); }

image

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

Но можно ли каким-то образом перенести тип CustomType в другую сборку, чтобы клиенты этого не заметили и не потребовали перекомпиляции? Именно для этой цели служит атрибут TypeForwardedToAttribute.

image

Чтобы воспользоваться этой возможностью достаточно внутри CustomLibrary добавить следующую строку:

[assembly: TypeForwardedTo(typeof(Library.CustomType))]

И перенести определение CustomType в AnotherLibrary. В результате, в метаданных нашей сборки (Library.dll) будет содержаться не сам тип, а лишь информация о перенаправлении в другую сборку:

image

И теперь, если мы предоставим новую версию нашей библиотеки (теперь в виде двух сборок – Library.dll и AnotherLibrary.dll), то наше приложение будет нормально работать без перекомпиляции.

Применимость форварда типов

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

Представьте себе такую ситуацию. Разработчики фреймворка тоже люди, а значит их решения со временем могут изменяться. Так, многие типы при переходе от .NET 3.5 к .NET 4.0 переехали из одной сборки в другую. Одним из таких типов является System.Func<T>, в .NET 3.5 он "жил" в System.Core, а в .NET 4.0 он "переехал" в mscorlib.

Теперь, представим, что у нас есть приложение, скомпилированное под .NET 3.5, в котором используется делегат System.Func<T>. Это же приложение должно без перекомпиляции уметь работать и под .NET 4.0.

class Program
{ 
   
static void Main(string
[] args)
    {
        System.
Func<int> foo = null;
    } }

Поскольку компилятор указывает всю информацию о типе, включая имя сборки, то если мы откроем сгенерируемый IL, то увидим ссылку на System.Core (ведь в .NET 3.5 System.Func жил именно там):

image

Так как же мы сможем запустить это же приложение под .NET 4.0, в котором System.Func переехал в mscorlib? Именно благодаря форвардингу типов. Если открыть сборку System.Core рефлектором, то мы увидим декларацию форвардинга типов:

image

При запуске под .NET 4.0 CLR увидит, что System.Func переехал в mscorlib и продолжит его поиски уже в mscorlib.dll. В новой сборке также может находиться перенаправление в другую сборку, тогда CLR продолжит поиски, пока не будет найдена сборка, в котором этот тип определен.

Заключение

Да, форвардинг типов – это не сверх часто используемая возможность, ни именно благодаря ей у нас есть возможность запуска приложения под разными CLR без перекомпиляции. Еще одним примером применения Type Forwarding является Portable Class Libraries, так что эта штука применяется чаще, чем может показаться с первого взгляда.

1 комментарий: