понедельник, 14 марта 2011 г.

Частичные классы

Частичные классы (partial classes) (*) – это весьма простая конструкция языка C#, которая позволяет определить один класс в нескольких файлах (**). Это далеко не rocket science, но существует пара интересных сценариев их применения, которые являются не столь очевидными с первого взгляда. Но об этом позже, а начнем мы лучше с простого объяснения, что это такое и с чем его едят.

Общие сведения

Давайте вспомним старые добрые дни C# 1.0, когда спецификация языка была вдвое тоньше, возможностей языка было на порядок меньше, да и не было никаких LINQ-ов и всех остальных dynamic-ов (и вопросов на собеседовании, соответственно, тоже). Но даже тогда компилятор языка старался скрасить наши с вами серые будни и усердно выполнял всякую унылую работу, но делал он это не втихаря, путем генерации всяких там анонимных классов или методов, а путем генерации C# кода с помощь дизайнеров. И все бы ничего, но с этим подходом зачастую приходили и некоторые проблемы, которые, в основном сводились к ярому желанию разработчика расширить этот код и к ярому сопротивлению этого кода от подобного ручного вмешательства. Другими словами, внесение любых изменений в автосгенерированный код всегда вели к беде, ибо тут же прибивались компилятором при повторной генерации.

В те далекие дни существовало два простых способа борьбы с этой проблемой. Первый способ активно применялся, например, дизайнером Windows Forms, когда весь сгенерированный код формы помещался в отдельный регион, изменять который было чревато, а весь остальной пользовательский код располагался в оставшейся части файла (и, соответственно, класса). Другой подход наиболее часто применялся при работе с датасетами (да, это ORM для бедных, точнее и не ORM вовсе, но это уже и не важно), когда для расширения функционала нужно было создать класс-наследник и добавить в него весь необходимый дополнительный функционал.

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

Итак, вот простой пример:

// CustomForm.cs
// За "содержимое" этого файла отвечает пользователь
public partial class CustomForm : Form
{
    public CustomForm()
    {
        InitializeComponent();
    }
}
// CustomForm.Designer.cs
// А вот этим файлом "занимается" дизайнер форм
partial class CustomForm
{
    // Код, сегенерированный компилятором
    private void InitializeComponent()
    { }
}

Обратите внимание, что только в одном файле указан наследник текущего класса (в данном случае, класс Form), и область видимости этого класса (ключевое слово public); это упрощает контроль этих аспектов пользователем, оставляя код, сгенерированный компилятором в нетронутом виде. Кроме того, как не сложно заметить, класс CustomForm все еще остается одним классом, что позволяет вызывать функции, определенные в одном файле (в данном случае, это функция InitializeComponent) из другого файла.

Частичные классы и выделение абстракций

Не пугайтесь страшного названия, rocket science в этой заметке так и не появится, просто более удачного названия подобрать не смог. Дело все в том, что частичные классы умеют делать нечто не совсем интуитивно понятное с первого взгляда. А именно, объявления частичных классов совпадать не должны, это значит, что в одном файле вы можете «объявить» реализацию классом некоторого интерфейса, а во втором файле – реализовать этот интерфейс, путем определения всех его методов (хотя можно часть методов реализовать в одном файле, а часть в другом; я думаю, что идея понятна):

// IFoo.cs
// Простой интерфейс, с одним единственным методом
interface IFoo
{
    void DoSomething();
}
// Foo.cs
// В этом файле мы объявляем, что класс Foo реализует интерфейс IFoo
partial class Foo : IFoo
{ }
// Foo.IFoo.cs
// А в этом файле, мы этот интерфейс реализуем
partial class Foo
{
    public void DoSomething() { }
}

И что в этом такого, скажете вы? Да, в общем-то, ничего особенного, но иногда это может очень здорово помочь при работе с автосгенерированным кодом. Давайте представим, что у нас есть некоторые сгенерированные компилятором классы (будь-то, классы, генерируемые по XSD, классы-сущности базы данных или что-то еще), и так уж вышло, что каждый из этих, совершенно не связанных между собой классов, содержит некоторую общую функциональность в виде набора свойств или методов, которые, в общем-то, делают одно и то же. Но поскольку все они генерируются компилятором, у вас (и у нас, кстати, тоже) нет возможности обрабатывать их обобщенным образом с помощью некоторого базового класса или интерфейса, в результате ваш код (и наш иногда тоже) начинает обрастать неприглядными конструкциями следующего вида:

ProcessType1(objectOfType1);
ProcessType2(objectOfType2);

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

// IName.cs
// Вводим собственный интерфейс, содержащий общие свойства и методы
// некоторого семейства автосгенерированных классов.
interface IName
{
    string Name { get; }
}
 
// AutogeneratedObject.Designer.cs
// Автосгенерированный код содержит свойство Name.
partial class AutogeneratedObject1
{
    public string Name { get; private set; }
}
 
// AutogeneratedObject.cs
// Добавляем свой вклад в "абстракцию" этого класса.
// Теперь он реализует интерфейс IName, хотя настоящая реализация
// этого интерфейса находится в автосгенерированном коде.
partial class AutogeneratedObject1 : IName
{}
 
// Где-то еще.cs
// Теперь мы можем обрабатывать любое количество автоматически сгенерированных
// типов, до тех пор пока они содержат нашу "выделенную" абстракцию в виде интерфейса IName.
void ProcessIName(IName name) {}

Частичные классы в юнит-тестировании

Чисто теоретически применение частичных классов не ограничивается автоматически сгенерированным кодом. Это самая обычная возможность языка программирования, и никто не мешает вам разбивать ваши собственные классы на несколько файлов. Почему «чисто теоретически»? Потому что, в большинстве случаев, если вы видите преимущества от использования частичных классов для реализации ваших собственных бизнес-сущностей, возникает некоторое подозрение, что эти самые сущности делают слишком много и, скорее всего, будет не лишним разбить их на несколько составляющих. (Хотя, нужно признать, из этого правила бывают и исключения.)

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

// FooTest.cs
// "Основная часть" класса FooTest, в данном случае предназначенная для тестирования
// метода DoSomething
[TestClass]
public partial class FooTest
{
    [TestMethod]
    public void DoSomethingTest()
    {
        // Тестирование метода DoSomething
        Console.WriteLine("DoSomethingTest");
    }
}
// Foo.ConstructorTest.cs
// "Часть" класса FooTest, отвечающая за тестирование конструктора класса Foo
partial class FooTest
{
    [TestMethod]
    public void ConstructorTest()
    {
        // Тестирование конструктора класса Foo
        Console.WriteLine("ConstructorTest");
    }
}

«Объединение» частичных классов в Solution Explorer-е

Внимательный разработчик наверняка давно уже заметил, что наша с вами любимая среда разработки, когда речь заходит об автоматически сгенерированном коде, умеет хитрым образом «объединять под одним крылом» частичные классы. Это выражается в том, что в Solution Explorer-е файлы, сгенерированные компилятором, находится «под» основным файлом, с которым работает разработчик. Но раз так, то наверняка мы сможем применять эту же технику для группировки наших собственных частичных классов.

И действительно, для этого достаточно немного поменять файл проекта и для зависимого файла добавить элемент с именем DependentUpon:

<Compile Include="Foo.cs" />
<Compile Include="Foo.IFoo.cs">
  <DependentUpon>Foo.cs</DependentUpon>
</Compile>

Помимо ручного редактирования файла проекта, можно порыться в Visual Studio Gallery и поискать расширение с подобной функциональностью. Как минимум одно расширение, с незамысловатым названием VSCommands 2010 поддерживает группировку нескольких файлов именно таким образом. После установки этого расширения в контекстном меню в Solution Explorer-е появятся дополнительные пункты меню “Group Items” и “Ungroup Items”, соответственно. Но не зависимо от того, каким именно способом вы будете «объединять» файлы, результат будет следующий (рисунок 1):

PartialClasses

Рисунок 1 – Группировка файлов в Solution Explorer

Заключение

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

------------------------------

(*) Да, как правильно заметили в комментариях, частичными бывают не только классы, но еще и структуры и интерфейсы. Так что все, что здесь говорится о классах, применимо еще и для структур с интерфейсами.

(**) Вообще-то, частичные классы могут объявляться и в одном файле:

//Foo.cs
partial class Foo { }

//Foo.cs
partial class Foo { }

Но вот польза в таком объявлении весьма сомнительна.

7 комментариев:

  1. А как же частичные интерфейсы ?
    http://msdn.microsoft.com/en-us/library/wa80x488.aspx

    It is possible to split the definition of a class or a struct, an interface or a method over two or more source files.

    Об этом стоило бы упомянуть :)

    ОтветитьУдалить
  2. >>когда для расширения функционала >>нужно было создать класс-наследник >>и добавить в него весь необходимый >>дополнительный функционал.

    логично было бы про
    Generation Gap упомянуть

    ОтветитьУдалить
  3. Есть еще и частичные структуры :)

    ОтветитьУдалить
  4. От себя хочу добавить - это мегаполезная возможность при коллективной работе над проектом. Например, в проекте есть ряд "популярных" форм, которые приходится править одновременно многим членам проекта. До того, как мы в проекте начали использовать Partial классы, шла постоянная торговля типа "Ты когда отпустишь форму SyperMainForm??? Мне там необходимо подправить метод MyGeniusMethod!". Теперь все подобные формы (классы...) разбиты на файлы (в каждом находятся методы сгруппированные по функционалу типа SyperMainFormPrint, SyperMainFormSaveData...). И каждый из участников разработки проекта правит свой файл.

    ОтветитьУдалить
  5. @NN: Да, действительно частичными не только классы, но и структуры с интерфейсами. Сейчас упомяну.

    @Anatoliy: спасибо за дополнение.

    ОтветитьУдалить
  6. @Anatoly: Без обид, но если вам приходится править ФОРМУ сообща, то, что-то не так в королевстве датском. В форме вообще код до неприличия простой. Если это не так - надо курить в сторону MVC/MVP.
    @Sergey: Мне показалась несколько искусственной статья. Partial классы - по-моему меньшее, что может волновать в процессе разработки. Как правильно замечено - в основном касается автосгенерированного кода. Во всех остальных случаях можно без них обойтись. Крайне редко возникает необходимость разбивать класс на несколько файлов (я это вообще один раз видел), потому что удобства это не доставляет. ИМХО, это сделано для борьбы с автосгенерированными файлами. Про Process(IFoo foo) все понятно, но тоже ВЕСЬМА частный случай и опять же из-за автосгенерированного кода.

    ОтветитьУдалить
  7. @eugene: ну, я же говорил, что это не rocket science. Ну а вообще, пример с добавлением функциональности в автосгенерированный код - не выдумка. Я правда сталкивался с этим на практике. Ну а разбивка тестов на частичные классы мне, ИМХО, нравится и я это тоже потихоньку использую на практике.

    ОтветитьУдалить