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

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

Содержание


Различное поведение фабрик
Фабрики для создания плагинов
Фабрики для создания объектов по некоторому алгоритму
Фабрики для клонирования объектов
Некоторые замечания об использовании свойств и наследования Замечания об использовании свойств
Замечания об использовании наследования. Проблема «хрупкого» базового класса.
Подобный материал:
1   ...   39   40   41   42   43   44   45   46   47

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




Фабрики нужны для того, чтобы скрыть от пользователя способ создания объекта. Пользователю достаточно потребовать от фабрики экземпляр. Причем этот экземпляр может быть вновь созданным (см . FirstType() ниже), а может быть созданным во время предыдущих обращений к фабрике (SecondType()).

public interface SimpleInterface

{

}

internal class MultipleInstances : SimpleInterface

{

}

internal class SingleInstance : SimpleInterface

{

}

public class SimpleInterfaceFactory

{

public static SimpleInterface FirstType()

{

return new MultipleInstances();

}

private static SingleInstance _instance;

public static SimpleInterface SecondType()

{

if (_instance == null)

{

_instance = new SingleInstance();

}

return _instance;

}

}


Пользователь никогда не узнает, какой именно экземпляр вернула ему фабрика и когда он был создан. Фабрики обычно нацелены на создание объектов одного типа, но возможно по-разному.

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


Фабрика для создания плагинов предназначена для создания объектов по их текстовому описанию. Это позволяет наиболее гибко отделить намерения от реализации.

С точки зрения пользователя использоваться такая фабрика должна так:

ITaxation obj = Factory.GetObject("Taxation.SwissTaxationImpl");

Для реализации такой фабрики понадобится:

Некий тип, у котором знает и сторона пользователя и сторона реализации. Это может быть интерфейс, базовый класс или некоторый generic-тип.

Отдельная сборка для реализации.

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

Сборка загружается так:

Assembly assembly = Assembly.LoadFrom( path);

Тип из загруженной сборки можно создать так:

object obj = assembly.CreateInstance( typeidentifier);

или так:

object obj = Activator.CreateInstance( Type.GetType( typedidentifier));

typedidentifier – это строка, которая может иметь вид: "{type name},{Assembly path}".

Обычно эту строку помещают в файл конфигурации приложения.


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




В примерах выше были созданы интерфейсы ITaxation и ITaxMath. В них было свойство TaxMath, через которое они должны быть связаны между собой. Но кто их свяжет? Обычно в таких случаях для создания фабрики применяют паттерн Builder (Строитель).

public class Builder

{

public ITaxation InstantiateSwiss()

{

ITaxation taxation = new SwissTaxationImpl();

taxation.TaxMath = new SwissTaxMathImpl();

return taxation;

}

}

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


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




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

class SwissTaxationImpl : ITaxation, System.ICloneable

{

ITaxMath _taxMath;

public ITaxMath TaxMath { get { ;} set { ;} }

public Object Clone()

{

SwissTaxationImpl obj = (SwissTaxationImpl)this.MemberwiseClone();

obj._taxMath = (ITaxMath)((System.ICloneable)_taxMath).Clone();

return obj;

}

}

Следует различать «мелкое» и «глубокое» копирование. При мелком копировании (MemberwiseClone()) копируются только поля, локальные для объекта. Для глубокого копирования нужно вызвать метод clone() у всех агрегированных объектов. В этот момент может возникнуть проблема, если типы A и В будут циклически пытаться скопировать друг друга (A-B-B-A). Можно посоветовать вводить специальный флаг didClone, который показывает, было проведено глубокое копирование или нет. Однако это не всегда помогает. Глубокое копирование нужно тщательно продумывать.


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



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




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


Рассмотрим пример:

class Oven // Печь

{

private int _temperature;

public int Temperature

{

get

{

return _temperature;

}

set

{

_temperature = value;

}

}

}


Пользователи такого класса будут напрямую связаны с подробностями его внутреннего устройства (через get).


Лучшее решение:

class Oven

{

private int _temperature;

public void SetTemperature(int temperature)

{

_temperature = temperature;

}

public bool IsPreHeated()

{

return false;

}

}

В этом варианте значение внутренней переменной _temperature не передается пользователям этого класса. На класс Oven перенесена ответственность по интерпретации значения температуры.

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

Чтобы решить эту задачу в .NET можно обойтись без свойств.

delegate void OnTemperature(int temperature);

class Oven

{

private int _temperature;

OnTemperature _listeners;

public void BroadcastTemperature()

{

_listeners(_temperature);

}

public void AddTemperatureListener(OnTemperature listener)

{

_listeners += listener;

}

}

class Controller

{

public Controller(Oven oven)

{

oven.AddTemperatureListener(

new OnTemperature(this.OnTemperature));

}

public void OnTemperature(int temperature)

{

Console.WriteLine("Temperature (" + temperature + ")");

}

}


В этом решении клиент узнает о температуре, однако Oven не раскрывает своей внутренней структуры. Заинтересованный в значении температуры клиент (возможен и не один) должен зарегистрировать в классе Oven свой метод через делегата. В качестве примера такого клиента выступает Controller.

Может показаться, что это решение с введением промежуточного уровня абстракции просто усложняет жизнь и проще было бы обойтись свойством. Но введение промежуточных уровней – это не просто усложнение. Это уменьшение связанности различных частей приложения. Кстати, а что если у печи несколько датчиков температуры? Вводить несколько свойств? Переложить интерпретацию их значения на клиента? Лучше возложить все обязанности, связанные с обработкой внутренних данных, на содержащий их объект.

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

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


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




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


Допустим нужно сделать класс Стек.

class Stack : ArrayList

{

private int topOfStack = 0;

public virtual void Push(Object article)

{

this.Add(article);

topOfStack++;

}

public virtual Object Pop()

{

Object retval = this[--topOfStack];

this.RemoveAt(topOfStack);

return retval;

}

public void PushMany(Object[] articles)

{

foreach (Object item in articles)

{

Push(item);

}

}

}


Обратите внимание, что метод PushMany использует Push.


Теперь допустим, что мы создаем производный класс:

class MonitorableStack : Stack

{

public int highWaterMark = 0;

public int lowWaterMark = 0;

public override void Push(Object item)

{

base.Push(item);

if (this.Count > highWaterMark)

{

highWaterMark = this.Count;

}

}

public override Object Pop()

{

Object popped = base.Pop();

if (this.Count < lowWaterMark)

{

lowWaterMark = this.Count;

}

return popped;

}

}

Его задачей является фиксирование количества добавлений и удалений элементов. Для этого переопределяются методы Push и Pop. Все работает, что доказывает следующий фрагмент:

Stack cls = new MonitorableStack();

cls.PushMany(new Object[] { 1, 2 });

Метод PushMany вызывается из объекта типа базового класса, однако методы Push и Pop вызываются из класса MonitorableStack, что нам и нужно.


Теперь допустим, что по каким-то причинам мы поменяли реализацию метода PushMany в базовом классе:

public void PushMany(Object[] articles)

{

foreach (Object item in articles)

{

//Push(item);

this.Add(item);

topOfStack++;

}

}


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

Чтобы в фрагменте

Stack cls = new MonitorableStack();

cls.PushMany(new Object[] { 1, 2 });

методы Push и Pop гарантировано вызывались из класса Stack нужно использовать ключевое слово new в определении методов в производном классе:

class Stack : ArrayList

{

private int topOfStack = 0;

public void Push(Object article)

{

this.Add(article);

topOfStack++;

}

public Object Pop()

{

Object retval = this[--topOfStack];

this.RemoveAt(topOfStack);

return retval;

}

public void PushMany(Object[] articles)

{

foreach (Object item in articles)

{

//Push(item);

this.Add(item);

topOfStack++;

}

}

}

class MonitorableStack : Stack

{

public int highWaterMark = 0;

public int lowWaterMark = 0;

public new void Push(Object item)

{

base.Push(item);

if (this.Count > highWaterMark)

{

highWaterMark = this.Count;

}

}

public new Object Pop()

{

Object popped = base.Pop();

if (this.Count < lowWaterMark)

{

lowWaterMark = this.Count;

}

return popped;

}

}


Теперь автор базового класса может менять его реализацию, не опасаясь, что он сломает поведение производного класса. Необходимость использования ключевых слов virtual и override или new подстраховывает от проблемы «хрупкого» базового класса, поскольку если вы используете virtual в базовом классе для некоторых методов, то вы должны их обязательно переопределить в производном классе.



1 По материалам Dan Pilone and Russ Miles. Head First Software Development. Перевод И.И. Белялетдинова.


2 oop_lect1.doc

3 oop_lect2.doc

4 oop_lect3.doc

5 По материалам Рихтер. CLR via C#. Программирование на платформе Microsoft .NET Framework 2.0 на языке C#

6 oop_lect4_dotnet.doc

7 oop_lect4_dotnet_part2.doc

8 oop_unit_testing.doc

9 Рефакторинг или Реорганизация - процесс полного или частичного преобразования внутренней структуры программы при сохранении её внешнего поведения.

10 По материалам Andy Hunt Dave Thomas Matt Hargett. Pragmatic Unit Testing in C# with NUnit, 2nd Edition. Перевод И.И. Белялетдинова

11 oop_unit_testing_example.doc

12 По материалам книг: Bill Wagner. Effective C#: 50 Specific Ways to Improve Your C#. Addison-Wesley. 2004,

Билл Вагнер. «Эффективное использование C#», Лори, 2005 с редакцией И.И. Белялетдинова.

13 oop_csharp1.doc

14 oop_csharp2.doc

15 oop_csharp3.doc

16 oop_csharp4.doc

17 oop_csharp5.doc

18 Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес. Приемы объектно-ориентированного проектирования. Паттерны проектирования.

19 patterns_intro.doc

20 delegates_events.doc

21 По материалам MSDN Training. Programming with C#. Перевод И.И. Белялетдинова

22 attributes.doc

23 ссылка скрыта Глава из книги Эндрю Троелсен . “Язык программирования C# 2005 (Си Шарп) и платформа .NET 2.0”

24 generics.doc

25 По материалам «Christian Gross. Foundations of Object-Oriented Programming Using .NET 2.0 Patterns. APress, 2005». Перевод И.И. Белялетдинова.