Как правильно писать тесты 46 Цикл разработки 46 Структура проекта с тестами 51 Утверждения (Asserts) 52 Утверждения в форме ограничений 54 Категории 56

Вид материалаТесты

Содержание


Неизменяемые (Immutable) атомарные типы-значения
Подобный материал:
1   ...   14   15   16   17   18   19   20   21   ...   47

Неизменяемые (Immutable) атомарные типы-значения




Суть неизменяемых типов очень проста: после создания они остаются константами. Если параметры, использованные при создании объекта, были правильны, то можно быть уверенным, что объект будет находиться в допустимом (правильном) состоянии. Невозможно изменить внутреннее состояние такого объекта. В итоге сократится число необходимых проверок на ошибки в моменты использования такого объекта. Неизменяемые типы по своей природе являются безопасными в многопоточной среде: к одному объекту может одновременно обращаться любое количество клиентов. Можно без опаски возвращать из объектов переменные неизменяемых типов. Клиент не может случайно изменить внутреннее состояние объектов.


Не каждый тип может быть неизменяемым. Если он является таковым, то для того чтобы все-таки получить измененное значение, потребуется клонировать объекты. Вот почему следующая рекомендация применима и для атомарных, и для неизменяемых типов значений. Разложите типы на структуры, которые составляют единую сущность. Например, это можно сделать для типа Address. Адрес является единым понятием, состоящим из нескольких связанных полей. Изменение одного из полей, очевидно, предполагает изменение и других полей. Тип Customer не является атомарным. Этот тип содержит много различных фрагментов информации: адрес, имя заказчика и один или несколько телефонных номеров. Любой из этих независимых фрагментов информации может подвергнуться изменению. Номер телефона заказчика может измениться, даже если он никуда не переезжает. Заказчик может переехать в другое место, но при этом сохранить прежние номера телефонов. Заказчик может изменить собственное имя, при этом и адрес, и номера телефонов останутся неизменными. Объект Customer не является атомарным; он построен с использованием композиции нескольких неизменяемых типов: адреса, имени и коллекции пар номер телефона/тип номера. Атомарные типы — это единые объекты: их контент должен заменяться полностью. В исключительном случае можно изменить содержимое одного из его компонентов.


Вот типичная реализация адреса, являющаяся изменяемой:

// Mutable Address structure.

public struct Address

{

private string _line1;

private string _line2;

private string _city;

private string _state;

private int _zipCode;


// Rely on the default system-generated

// constructor.


public string Line1

{

get { return _line1; }

set { _line1 = value; }

}

public string Line2

{

get { return _line2; }

set { _line2 = value; }

}

public string City

{

get { return _city; }

set { _city= value; }

}

public string State

{

get { return _state; }

set

{

ValidateState(value);

_state = value;

}

}

public int ZipCode

{

get { return _zipCode; }

set

{

ValidateZip( value );

_zipCode = value;

}

}

// other details omitted.

}


// Example usage:

Address a1 = new Address( );

a1.Line1 = "111 S. Main";

a1.City = "Anytown";

a1.State = "IL";

a1.ZipCode = 61111 ;

// Modify:

a1.City = "Ann Arbor"; // Zip, State invalid now.

a1.ZipCode = 48103; // State still invalid now.

a1.State = "MI"; // Now fine.


Изменение внутреннего состояния означает, что возможна потеря согласованности. После того, как было изменено значение поля City, экземпляр объекта а1 был переведен в неверное состояние. После изменения значения City (город) оно перестало соответствовать значениям полей state (штат) и Zip code (почтовый индекс). Приведенный выше программный код кажется вполне безобидным, но представьте себе, что он является частью многопоточной программы. Любое переключение между потоками после изменения поля City и до того, как будет изменено значение State, может привести к тому, что один из потоков увидит несогласованные данные.


Даже если не брать в расчет (пока!) многопоточную программу, все равно остается вероятность появления неприятностей. Представьте, что индекс оказался неверным, и по этой причине была возбуждена исключительная ситуация. Вы выполнили только часть планировавшихся изменений, после чего система оказалась в противоречивом (несогласованном) состоянии. Для исправления ситуации придется добавить внушительный код проверок. А этот код значительно увеличит размеры и сложность программы в целом. Чтобы полностью защититься от исключительных ситуаций, придется создать страховочные копии во всех местах программы, где изменяется более одного поля. Безопасность при работе с потоками потребует добавления проверок при синхронизации потоков при использовании свойств — и для set, и для get. В конечном счете, все это выльется в значительный по размерам код, который будет, скорее всего, расти в размерах по мере того, как будут добавляться новые возможности.


Вместо этого можно использовать структуру Address неизменяемого типа. Начать следует с того, чтобы сделать все поля экземпляра доступными только для чтения:

public struct Address

{

private readonly string _line1;

private readonly string _line2;

private readonly string _city;

private readonly string _state;

private readonly int _zipCode;


// remaining details elided

}


Кроме того, желательно удалить все части set для каждого из свойств:

public struct Address

{

// ...

public string Line1

{

get { return _line1; }

}

public string Line2

{

get { return _line2; }

}

public string City

{

get { return _city; }

}

public string State

{

get { return _state; }

}

public int ZipCode

{

get { return _zipCode; }

}

}

Теперь у нас имеется неизменяемый тип. Чтобы сделать его полезным, требуется добавить все необходимые конструкторы для полной инициализации структуры Address. Для структуры Address нужен только один конструктор, в котором будет специфицировано каждое поле. Конструктор копирования не требуется, поскольку достаточно эффективным будет и обычный оператор присваивания. Ниже приводится реализация конструктора, в котором все строки оставлены пустыми (null), а значение почтового индекса равно 0:

public struct Address

{

private readonly string _line1;

private readonly string _line2;

private readonly string _city;

private readonly string _state;

private readonly int _zipCode;


public Address( string line1,

string line2,

string city,

string state,

int zipCode)

{

_line1 = line1;

_line2 = line2;

_city = city;

_state = state;

_zipCode = zipCode;

ValidateState( state );

ValidateZip( zipCode );

}


// etc.

}


При использовании неизменяемых типов для модификации их состояния требуется несколько иной подход. Вместо того чтобы модифицировать существующий экземпляр объекта, создается его новый экземпляр:

// Create an address:

Address a1 = new Address( "111 S. Main",

"", "Anytown", "IL", 61111 );


// To change, re-initialize:

a1 = new Address( a1.Line1,

a1.Line2, "Ann Arbor", "MI", 48103 );


Значение al находится в одном из двух состояний: либо в исходном состоянии с размещением в городе Anytown, либо в обновленном состоянии с местонахождением в городе Ann Arbor. Вы не модифицируете существующий адрес, и потому не возникает противоречивых состояний (см. пример выше). Объект будет находиться в неполном состоянии только в течение того времени, пока выполняется конструктор Address, и это состояние невидимо вне конструктора. Как только будет сконструирован (создан с помощью конструктора) новый объект Address, его значение будет зафиксировано раз и навсегда. Оно защищено от исключительных ситуаций: а1 будет иметь либо свое первоначальное, либо новое значение. Если во время создания нового объекта Address будет сгенерировано исключение, а1 останется в исходном значении.


При создании неизменяемого типа следует убедиться в том, что отсутствуют «дыры», позволяющие клиентам изменить внутреннее состояние объекта. Типы-значения не поддерживают производных типов, так что нет необходимости защищаться от модификации полей «при помощи» производных типов. Но нужно наблюдать за любыми полями неизменяемого типа, которые относятся к изменяемым ссылочным типам. При реализации конструкторов для переменных подобных типов необходимо создавать страховочную копию. В наших примерах принимается, что Phone является неизменяемым типом-значением:

// Almost immutable: there are holes that would

// allow state changes.

public struct PhoneList

{

private readonly Phone[] _phones;


public PhoneList( Phone[] ph )

{

_phones = ph;

}


public IEnumerator Phones

{

get

{

return _phones.GetEnumerator();

}

}

}


Phone[] phones = new Phone[10];

// initialize phones

PhoneList pl = new PhoneList( phones );


// Modify the phone list:

// also modifies the internals of the (supposedly)

// immutable object.

phones[5] = Phone.GeneratePhoneNumber( );

Тип массивов (array) является ссылочным типом. Массив, находящийся внутри структуры PhoneList, ссылается на ту же область в памяти (phones), которая была выделена ранее вне объекта и ссылка на которую была передана через параметр конструктора. Клиенты такого класса вполне могут модифицировать вашу «неизменяемую» структуру с помощью другой переменной, ссылающейся на ту же самую область памяти. Для устранения этой возможности необходимо создать страховочную копию массива. Обратите внимание, что в этом примере были показаны «подводные камни» изменяемой коллекции. Еще больше неприятностей может возникнуть, если тип Phone является изменяемым ссылочным типом. Клиенты могут модифицировать значения коллекции, даже если она защищена от любых модификаций. Всякий раз, когда в состав неизменяемого типа включается изменяемый ссылочный тип, следует предусмотреть во всех конструкторах создание страховочной копии:

// Immutable: A copy is made at construction.

public struct PhoneList

{

private readonly Phone[] _phones;


public PhoneList( Phone[] ph )

{

_phones = new Phone[ ph.Length ];

// Copies values because Phone is a value type.

ph.CopyTo( _phones, 0 );

}


public IEnumerator Phones

{

get

{

return _phones.GetEnumerator();

}

}

}


Phone[] phones = new Phone[10];

// initialize phones

PhoneList pl = new PhoneList( phones );


// Modify the phone list:

// Does not modify the copy in pl.

phones[5] = Phone.GeneratePhoneNumber( );


Тем же самым правилам необходимо следовать, когда возвращается изменяемый ссылочный тип.


Степень сложности типа диктует, какую из трех стратегий следует использовать для инициализации неизменяемого типа.

Во-первых, структура Address определяет один конструктор для того, чтобы позволить клиентам инициализировать адрес. Часто наиболее простым подходом является определение разумного набора конструкторов. Помните, что для структур остается доступным и конструктор по умолчанию. В этом структуры отличаются от классов, для которых конструкторы по умолчанию создаются только в том случае, когда нет других конструкторов.


Во-вторых, для инициализации структуры можно создать так называемые фабричные методы (factory methods). Фабрики значительно облегчают создание объектов на основе некоторых общих данных. Тип .NET Framework Color следует этой стратегии при инициализации системных цветов. Статические методы Color.FromKnownColor() и Color.FromName() возвращают копию значения цвета, представляющего текущее значение заданного системного цвета.


В-третьих, можно создать изменяемый класс-помощник для создания экземпляров, в которых необходимо выполнять многошаговые операции для полного создания неизменяемого объекта. Класс строк (string) в .NET следует этому подходу, используя System.Text.StringBuilder. Класс StringBuilder используется для создания строки при помощи нескольких операций. После выполнения всех необходимых операций вы получаете из StringBuilder неизменяемую строку.


Неизменяемые типы проще кодировать и сопровождать. Не следует вслепую реализовывать get и set для каждого свойства. При выборе между struct и class, в котором хранятся данные, на первом месте должны стоять неизменяемые, атомарные типы-значения. Из этих объектов можно с легкостью построить более сложные объекты.



Содержание 1

Процесс разработки программного обеспечения 3

Основные принципы объектно-ориентированного программирования (начало) 12

История 14

Основные принципы объектно-ориентированного программирования (продолжение) 15

Главные понятия 15

Основные принципы 15

Абстракция данных 15

Инкапсуляция 16

Наследование 16

Основные принципы объектно-ориентированного программирования (окончание) 18

Полиморфизм 18

Отношения 19

Основы .NET Framework 22

Введение 22

Обзор выполнения кода в среде CLR 26

Компиляция исходного кода в управляемые модули 26

Части управляемого модуля 28

Объединение управляемых модулей в сборку 29

Загрузка CLR при выполнении программы 31

Исполнение кода сборки 31

IL и верификация 36

Небезопасный код 36

IL и защита интеллектуальной собственности 37

NGen.exe — генератор объектного кода 38

Библиотека классов .NET Framework 38

Общая система типов (Common Type System, CTS) 40

Общеязыковая спецификация 42

Модульное тестирование (unit testing) 44

Предпосылки 44

Преимущества 44

Поощрение изменений 45

Упрощение интеграции 45

Документирование кода 45

Отделение интерфейса от реализации 45

Ограничения 46

Как правильно писать тесты 46

Цикл разработки 46

Структура проекта с тестами 51

Утверждения (Asserts) 52

Утверждения в форме ограничений 54

Категории 56

Настройка среды выполнения тестов 57

Дополнительные утверждения 59

Тесты и исключения 60

Правила тестирования 62

Юнит-тестирование 70

Пример 70

Два вида констант 98

Операторы is, as и приведение типов 101

Метод ToString() 109

Типы-значения и ссылочные типы 117

Неизменяемые (Immutable) атомарные типы-значения 122

0 в типах-значениях 132

Методы ReferenceEquals(), Equals(), статический метод Equals() и operator== 134

Циклы foreach 140

Управление ресурсами в .NET 143

Инициализаторы переменных 149

Инициализация статических полей классов с помощью статических конструкторов 155

Цепочки конструкторов 157

Применение операторов using и try/finally для освобождения ресурсов 160

О минимизации мусора 167

Упаковка и распаковка 169

Наследование классов и реализация интерфейсов 171

Отличие реализации методов интерфейса от переопределения виртуальных методов 178

Введение в паттерны проектирования 184

Что такое паттерн проектирования 186

Паттерны проектирования в схеме MVC 188

Описание паттернов проектирования 190

Каталог паттернов проектирования 192

Как решать задачи проектирования с помощью паттернов 195

Механизмы повторного использования 204

Сравнение структур времени выполнения и времени компиляции 210

Проектирование с учетом будущих изменений 212

Как выбирать паттерн проектирования 220

Как пользоваться паттерном проектирования 221

Делегаты и события 226

Делегаты 226

События 230

Параметры событий 233

Атрибуты 237

Синтаксис 237

Создание атрибута 240

Составляющие класса атрибута 242

Получение значений атрибута 244

Задание 1 245

Задание 2 246

Обобщения 251

Проблемы создания объектных образов и восстановления значений 252

Типовая безопасность и строго типизованные коллекции 253

Проблемы создания объектных образов и строго типизованные коллекции 258

Пространство имен System.Collections.Generic 259

Тип List 260

Создание обобщенных методов 263

Пропуск параметров типа 265

Создание обобщенных структур (и классов) 267

Ключевое слово default в обобщенном программном коде 269

Создание пользовательских обобщенных коллекций 271

Установка ограничений для параметров типа с помощью where 274

Отсутствие поддержки ограничений при использовании операций 279

Создание обобщенных базовых классов 280

Создание обобщенных интерфейсов 282

Создание обобщенных делегатов 284

Несколько слов о вложенных делегатах 286

Задачи 286

Примеры реализации по шаблонам Мост+Фабрика 289

Пример 1 289

Пример 2 292

Пример 3. Контроллер в виде интерфейса, а не класса. 294

Различное поведение фабрик 298

Фабрики для создания плагинов 299

Фабрики для создания объектов по некоторому алгоритму 300

Фабрики для клонирования объектов 300

Некоторые замечания об использовании свойств и наследования 302

Замечания об использовании свойств 302

Замечания об использовании наследования. Проблема «хрупкого» базового класса. 304