Saturday, August 28, 2010

Static constructors vs Type initializers

Недавно в обсуждении с коллегами возник вопрос: есть ли разница между инициализацией статического поля при объявлении и инициализацией его в статическом конструкторе. Т.е. отличаюся ли типы Test в примере ниже:

public class Test
{
public static object o = new object();
}

public class Test
{
public static object o;

public static Test()
{
o = new object();
}
}

Оказывается отличаются. Более того второй пример абсолютно идентичен следующему:

public class Test
{
public static object o = new object();

public static Test()
{
}
}

Подробные объяснения почему так получилось читаем здесь.

Friday, August 27, 2010

Testable Singleton Clients

Вчера послушал 19й Подкаст Петербургской Группы Alt.Net. Ребята так сильно пинали несчастный Singleton, что я решил написать пару слов в его защиту. Одним из недостатков синглтона назвали то, что очень трудно, если вообще возможно, писать юнит тесты для классов использующих синглтон. Я попробую опровергнуть это утверждение.

Давайте рассмотрим простенький пример. У нас есть класс TextProvider, который имплементит интерфейс ITextProvider и при этом является синглтоном.

public interface ITextProvider
{
    string GetText();
}

public sealed class TextProvider : ITextProvider
{
    private static readonly ITextProvider _instance = new TextProvider();

    private TextProvider()
    { }

    public static ITextProvider Instance
    {
        get { return _instance; }
    }

    public string GetText()
    {
        return "Hello from Singleton!";
    }
}

Классу Client, который использует наш синглтон, вообще говоря совсем не обязательно знать что он работает именно с синглтоном. Client просто получает в конструктор ссылку на интерфейс и дальше работает именно с ней.

public class Client
{
    private ITextProvider _textProvider;

    public Client(ITextProvider provider)
    {
        _textProvider = provider;
    }

    public void ConsumeText()
    {
        _textProvider.GetText();
    }
}

Теперь создать новый экземпляр клиента в коде можно вот так:

Client client = new Client(TextProvider.Instance);

А в тестах клиенту можно подсунуть замоканого провайдера:

ITextProvider provider = _mock.StrictMock<ITextProvider>();
Client client = new Client(provider);

Thursday, August 12, 2010

C# iterators: Compiler generated code example

В ходе обсуждения предыдущего поста возник вот такой вопрос: «Не совсем представляю, как это по произвольному классу, который реализует IEnumerable (т.е. просто должен уметь вернуть instance, реализующий IEnumerator), оно само сгенерирует реализацию IEnumerator». “Оно” – это в данном случае компилятор C# :)

Начну издалека. Для перебора элементов коллекций в C# есть удобный оператор цикла – foreach, который в общем виде выглядит вот так:

foreach (Type varName in enumerableObject)
{
...
}

Например:

IEnumerable<string> _names = new List<string> { "a", "b" };
foreach (string name in _names)
{
Console.WriteLine(name);
}

На самом деле компилятор развернет этот код вот так:

IEnumerable<string> _names = new List<string> { "a", "b" };

IEnumerator<string> ne = _names.GetEnumerator();
while (ne.MoveNext())
{
string name = ne.Current;
Console.WriteLine(name);
}

Получается, что foreach выполняет два действия:

  • вызывает у переданной ему коллекции метод GetEnumerator()
  • используя полученный объект IEnumerator, пробегается по всем элементам коллекции

Давайте посмотрим, что представляет собой IEnumerator:

public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}

public interface IEnumerator<T> : IDisposable, IEnumerator
{
T Current { get; }
}

C# реализует специальную конструкцию – итератор, которая позволяет не создавать IEnumerator руками, каждый раз, когда вам это понадобится. Давайте посмотрим как это выглядит в коде. Класс OpenFileControl (неважно откуда) получает список имен файлов и реализует метод GetStreams(), который возвращает IEnumerable<Stream> для этого списка. Это почти боевой пример. Реальный OpenFileControl получает и имена, и стримы для файлов с WCF сервиса.

public class OpenFileControl
{
private IEnumerable<string> _files = new List<string>();

public IEnumerable<Stream> GetStreams()
{
foreach (string file in _files)
{
yield return File.Open(file, FileMode.Open);
}
}
}

Теперь воспользуемся Reflector’ом чтобы посмотреть, что сгенерил для этого примера компилятор:

public class OpenFileControl
{
private IEnumerable<string> _files;

public IEnumerable<Stream> GetStreams();

// Nested Types
[CompilerGenerated]
private sealed class <GetStreams>d__0 : IEnumerable<Stream>, IEnumerable, IEnumerator<Stream>, IEnumerator, IDisposable
{
// Fields
private int <>1__state;
private Stream <>2__current;
public OpenFileControl <>4__this;
public IEnumerator<string> <>7__wrap2;
private int <>l__initialThreadId;
public string <file>5__1;

// Methods
[DebuggerHidden]
public <GetStreams>d__0(int <>1__state);
private void <>m__Finally3();
private bool MoveNext();
[DebuggerHidden]
IEnumerator<Stream> IEnumerable<Stream>.GetEnumerator();
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator();
[DebuggerHidden]
void IEnumerator.Reset();
void IDisposable.Dispose();

// Properties
Stream IEnumerator<Stream>.Current { [DebuggerHidden] get; }
object IEnumerator.Current { [DebuggerHidden] get; }
}
}

Имена у сгенерированных объектов, конечно, не особо читабельные, но разобраться все-таки можно. Вы видите, что в OpenFileControl добавился приватный класс, реализующий одновременно и IEnumerable<Stream>, и IEnumerator<Stream>. При этом наш метод GetStreams() изменился и теперь он просто возвращает экземпляр сгенерированного класса:

public IEnumerable<Stream> GetStreams()
{
return new <GetStreams>d__0(-2) { <>4__this = this };
}

Логика по перебору элементов коллекции, как и ожидалось, переехала в метод MoveNext() сгенерированного энумератора.

private bool MoveNext()
{
try
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<>7__wrap2 = this.<>4__this._files.GetEnumerator();
this.<>1__state = 1;
while (this.<>7__wrap2.MoveNext())
{
this.<file>5__1 = this.<>7__wrap2.Current;
this.<>2__current = File.Open(this.<file>5__1, FileMode.Open);
this.<>1__state = 2;
return true;
Label_0078:
this.<>1__state = 1;
}
this.<>m__Finally3();
break;

case 2:
goto Label_0078;
}
return false;
}
fault
{
this.System.IDisposable.Dispose();
}
}

Использовать OpenFileControl можно примерно так:

OpenFileControl ofc = new OpenFileControl();
foreach (Stream s in ofc.GetStreams())
{
using (s)
{
byte[] buf = new byte[10];

s.Read(buf, 0, 10);

// ...
}
}

Обратите внимание на то, что GetStreams() не открывает все файлы одновременно. На каждой итерации цикла открыт только текущий файл.

Tuesday, August 10, 2010

DIP: real world usage sample

Вчера, просматривая очередное DnrTV! видео, я наткнулся на интересный пример реализации принципа Dependency Inversion. Я уже подробно описывал этот принцип вот здесь, но для того чтобы вы поняли, что меня заинтересовало, еще раз кратенько повторю основные моменты.

Итак. Для того чтобы убрать из класса Manager жесткую ссылку на класс Worker, мы создали интерфейс IWorker, где определили весь функционал, который требуется Manager’у от Worker’а. Затем мы заменили в Manager’е ссылку на конкретный класс Worker ссылкой на интерфейс IWorker, а в классе Worker реализовали этот интерфейс. Полученные зависимости выглядели как на рисунке ниже.

DIP2

В этой реализации есть один нюанс. Любой класс, который мы захотим использовать вместо Worker должен реализовывать интерфейс IWorker.

Давайте рассмотрим другой пример. Допустим, у нас есть два класса: Radio и TV.

public class Radio
{
    public int Volume { get; private set; }
}
public class TV
{
    public int TvVolume { get; private set; }
}

И мы хотим создать некий класс Remote, который бы управлял громкостью радио и телевизоров. Вооружившись знанием DIP, не составляет никакой проблемы определить вот такой интерфейс IDevice:

public interface IDevice
{
    int Volume { get; set; }
}

Реализовать этот интерфейс в классах Radio и TV. И затем определить класс Remote следующим образом:

public class Remote
{
    private IDevice _device;

    public Remote(IDevice device)
    {
        _device = device;
    }

    public void IncreaseVolume()
    {
        _device.Volume +=1;
    }
}

А что делать, если мы не хотим чтобы все классы, которыми можно управлять из Remote, обязательно реализовывали IDevice? Причины могут быть разные. Например, мы не хотим расширять API наших девайсов, добавляя реализацию IDevice. Или, как в случае с классом TV, мы не хотим изменять его API. Там уже есть свойство TvVolume, и нет смысла его переименовывать в Volume или добавлять свойство Volume рядом.

Мы пойдем другим путем. Вместо выделения общего для всех устройств интерфейса IDevice мы создадим интерфейс IRemote, в который включим всю функциональность нужную для управления громкостью устройства.

public interface IRemote
{
    void IncreaseVolume();
}

Теперь класс Radio можно переписать так:

public class Radio
{
    public int Volume { get; private set; }

    public IRemote GetRemote
    {
        return new Remote(this);
    }

    private ctass Remote : IRemote
    {
        private Radio _radio;

        public Remote(Radio radio)
        {
            _radio = radio;
        }

        public void IncreaseVolume()
        {
            _radio.Volume +=1;
        }
    }
}

Переписав аналогичным образом класс TV, мы получим возможность написать нечто подобное:

Radio radio = new Radio();
Console.Write(radio.Volume);
IRemote radioRemote = radio.GetRemote();
radioRemote.IncreaseVolume();
Console.Write(radio.Volume);

TV tv = new TV();
Console.Write(tv.TvVolume);
IRemote tvRemote = tv.GetRemote();
tvRemote.IncreaseVolume();
Console.Write(tv.TvVolume);

Не будем останавливаться и определим еще один интерфейс:

public interface IRemotable
{
    IRemote GetRemote();
}

Реализовав его в классах Radio и TV, мы сможем переписать клиентский код вот так:

IRemotable radio = new Radio();
IRemote radioRemote = radio.GetRemote();
radioRemote.IncreaseVolume();

IRemotable tv = new TV();
IRemote tvRemote = tv.GetRemote();
tvRemote.IncreaseVolume();

Как видите, мы успешно развернули зависимости. Теперь клиентский код может управлять любым устройством реализующим IRemotable, вообще не зная конкретный тип этого устройства.

Ну как? Ничего не напоминает? Наши IRemote и IRemotable по смыслу – это один в один IEnumerator и IEnumerable из .Net BCL, про интенсивность использования которых, я думаю, не стоит даже говорить.

Вот такой вот нашелся интересный пример реализации DIP сразу же с отличным примером практического использования. Кому интересно, рекомендую посмотреть оригинал.

Sunday, August 1, 2010

Устойчивость. Принципы проектирования модулей (часть 2)

Давайте рассмотрим последние три принципа проектирования списка Роберта Мартина, касающиеся проектирования модулей. Эти принципы позволяют управлять связностью (coupling) ваших модулей. Как вы, наверно, помните из поста про метрики кода, чем меньше связность, тем лучше. Однако, полностью избавиться от зависимостей между модулями все равно нельзя. Если модули должны взаимодействовать, то между ними должна быть связь. Давайте разберемся, каких зависимостей следует избегать, а какие стоит использовать.

ADP. The Acyclic Dependencies Principle

Граф зависимостей между модулями приложения не должен содержать циклов.

Давайте рассмотрим пример. Мы разрабатываем приложение, которое состоит из нескольких модулей. В соответствии с принципом REP, каждый модуль и ничего не знает про исходники модулей, от которых он зависит.

DAG

Когда, например, разработчики модуля Task1 выпустят новую версию, то достаточно просто определить какие модули теперь нуждаются в обновлении: Task Window и Application. Разработчики этих модулей должны будут решить, когда они проинтегрируют свои модули с новой версией Task1.

Кроме того процесс сборки всего приложения тоже не вызывает затруднений. Достаточно пройтись по графу зависимостей снизу вверх и собрать последовательно каждый модуль.

Допустим, у нас появляются новые требования, и для их реализации мы добавляем в Task2 ссылку на Application.

DCG

В графе зависимостей у нас появился цикл. Теперь все модули, которые зависят от Task2, зависят еще и от Application. Теперь не получится просто пройтись по графу, чтобы собрать все приложение. Получается для того чтобы собрать Application нужно собрать все модули под ним, включая Task2. И одновременно, чтобы собрать Task2, нужно иметь уже собранный Application.

Одновременно – здесь ключевое слово. Получилось, что циклическая ссылка объединила все модули, попавшие в цикл, в один большой виртуальный модуль. Объединение их в один реальный модуль – это один из вариантов избавления от циклических ссылок. Но этот вариант не самый удачный.

Второй вариант избавления от циклов – вынесение функционала, от которого зависят Task2 и Application в отдельный новый модуль.

И наконец, третий вариант борьбы с циклами – применение принципа инверсии зависимостей (DIP). Для этого надо объявить в Task2 интерфейс, описывающий все, что Task2 хочет получать из Application. А в Application создать имплементацию этого интерфейса. Это позволит развернуть зависимость между Task2 и Application.

Стабильность

На картинке ниже изображена схема зависимостей между классами. Вы видите, что здесь применен принцип инверсии зависимостей. Вместо того чтобы явно ссылаться на KeyboardReader и PrinterWriter из класса Copy, мы объявили два интерфейса IReader и IWriter, поместили ссылки на них в класс Copy, и реализовали эти интерфейсы в KeyboardReader и PrinterWriter соответственно.

DIP

Как вы думаете, для каких из приведенных на рисунке типов вероятность изменения будет наименьшей? Наименьшей она будет для интерфейсов IReader и IWriter. По двум причинам.

Во-первых, интерфейсы приведенные на схеме ни от кого не зависят. Т.е. в нашей системе нет таких типов внесение изменений в которые, вызвало бы необходимость изменять IReader или IWriter. Типы, которые ни от кого не зависят, Мартин называет «независимыми» (independent).

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

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

SDP. The Stable Dependencies Principle

Зависимость должна быть в сторону большей стабильности. Модуль должен зависеть только от модулей более стабильных, чем он сам.

А как определить кто более стабильный, а кто нет? Как известно, для того чтобы что-то оценить, надо его изменить. Нестабильность любого пакета можно посчитать достаточно просто:

Instability

  • Сe (Efferent couplings) – число типов внутри пакета, которые зависят от типов из других пакетов.
  • Ca (Afferent couplings) – число типов в других пакетах, которые зависят от типов внутри пакета.
  • I (Instability) – Нестабильность пакета. Как видно из формулы эта метрика имеет диапазон [0, 1]. При 0 мы имеем идеально стабильный пакет, абсолютно независимый, и при этом ответственный. А при 1, наоборот, максимально нестабильный пакет, от которого никто не зависит, но который сам имеет много зависимостей.

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

SAP. Stable abstraction principle

Абстракция увеличивает стабильность. Уровень абстракции определяет уровень стабильности модуля.

Стабильные модули должны быть абстрактными. При этом их стабильность не будет препятствовать их расширению. И наоборот. Нестабильные модули должны быть конкретными. Это будет позволять изменять их.

Вспомните наш предыдущий пример. Интерфейсы IReader и IWriter – это абстракции. Они очень стабильны, т.к. ни от кого не зависят, и наоборот много кто зависит от них. Они могут быть легко расширены за счет новых реализаций и при этом останутся стабильными.

Стабильные пакеты более абстрактны, то есть имеют больше абстрактных классов и интерфейсов, редко изменяются. От них все зависят, а сами они мало зависят от остальных пакетов.

Принципы SDP и SAP вместе являются аналогом принципа Dependency Inversion. SDP рекомендует создавать связи только от менее стабильных пакетов к более стабильным, а SAP говорит, что стабильность подразумевает абстрактность.

В отличие от типов, которые однозначно либо абстрактные, либо нет, для модулей существует градация уровня абстрактности. Абстрактность модуля определяется отношением определенных в нем абстрактных типов к общему числу его типов.