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

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

Содержание


Наследование классов и реализация интерфейсов
Подобный материал:
1   ...   22   23   24   25   26   27   28   29   ...   47

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




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


Следует различать: обычный класс, абстрактный класс, интерфейс.


Абстрактные базовые классы — используются как общие прародители иерархии классов. Интерфейс описывает один атомарный кусочек функциональных возможностей, которые могут быть реализованы классом. Интерфейсы являются способом так называемого проектирования по соглашениям (design by contract). Проектирование по соглашениям — это метод разработки программного обеспечения, при котором элементы системы проектируются таким образом, что их взаимодействие основывается на точно определенных соглашениях (contract). Класс, реализующий интерфейс, должен обеспечивать реализацию ожидаемых методов. Абстрактные базовые классы описывают общую абстракцию для набора производных классов. Они могут определять часть состояния и некоторое поведение, но экземпляры абстрактных классов создать нельзя. Ожидается, что производный от абстрактного класс дополнит его до законченного состояния и уже экземпляр производного класса можно будет создать.

Наследование означает «является разновидностью», а интерфейсы означают «ведет себя как». Базовые классы описывают, каким является объект, а интерфейсы описывают способы взаимодействия с этим объектом.


Интерфейсы содержат описание функциональных возможностей. В интерфейсе могут присутствовать описания: методов, свойств, индексаторов и событий. Любой тип, реализующий интерфейс, должен предоставить конкретные реализации всех определенных в интерфейсе элементов. Необходимо реализовать все методы, обеспечить все средства доступа и индексаторы свойств, определить все события, заданные в интерфейсе. Следует выявить и выделить повторяющиеся элементы поведения в интерфейсы. Интерфейсы применяются как типы параметров и типы возвращаемых значений. Кроме того, появляется больше шансов повторно использовать программный код, так как несвязанные наследованием классы могут реализовывать одни и те же интерфейсы. А это означает возможность заменяемости друг на друга даже для не связанных наследованием классов. Более того, другим разработчикам легче реализовать интерфейс, чем строить производный класс от вашего базового класса.


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


Абстрактные базовые классы в дополнение к описанию общего поведения могут предлагать некоторую реализацию полезную для производных классов. Можно ввести элементы данных (поля), конкретные методы, реализации виртуальных методов, свойств, событий и индексаторов. Базовый абстрактный класс может предложить реализацию для некоторых из методов, обеспечивая тем самым повторное использование общих методов. Любой из элементов может быть виртуальным, абстрактным или невиртуальным.


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


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


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


Две описанные модели могут быть смешаны для повторного использования программного кода в случае реализации нескольких интерфейсов. Одним из примеров этого является System.Collections.CollectionBase. Этот класс можно использовать как базовый класс, который реализует несколько интерфейсов: IList, ICollection и IEnumerable. Вдобавок он предлагает protected методы, которые можно переопределять для более точной настройки поведения. Интерфейс IList содержит метол Insert(), который служит для добавления в коллекцию новых объектов. Вместо того чтобы предлагать собственную реализацию Insert, вы переопределяете лишь виртуальные методы Onlnsert() или OnlnsertComplete() класса CollectionBase.


Примеченание.

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

public virtual double Area()

{

return x * y;

}

Реализацию виртуального члена можно изменить путем переопределения члена в производном классе.

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


По умолчанию методы не являются виртуальными. Их нельзя переопределить.


Модификатор virtual нельзя использовать с модификаторами static, abstract, private или override.


public class IntList : System.Collections.CollectionBase

{

protected override void OnInsert( int index, object value )

{

try

{

int newValue = System.Convert.ToInt32( value );

Console.WriteLine( "Inserting {0} at position {1}",

index.ToString(), value.ToString());

Console.WriteLine( "List Contains {0} items",

this.List.Count.ToString());

}

catch( FormatException e )

{

throw new ArgumentException(

"Argument Type not an integer",

"value", e );

}

}


protected override void OnInsertComplete( int index,

object value )

{

Console.WriteLine( "Inserted {0} at position {1}",

index.ToString( ), value.ToString( ));

Console.WriteLine( "List Contains {0} items",

this.List.Count.ToString( ) );

}

}


public class MainProgram

{

public static void Main()

{

IntList l = new IntList();

IList il = l as IList;

il.Insert( 0,3 );

il.Insert( 0, "This is bad" );

}

}


В этом примере для добавления в коллекцию двух различных значений создается список целых (integer) и используется ссылка на переменную типа IList, полученная с помощью преобразования типов. Благодаря переопределению метода OnInsert(), класс IntList проверяет тип вставляемого значения и порождает исключение, если этот тип не является целым (integer). Итак, базовый класс предлагает реализацию по умолчанию и дает способ для уточнения поведения в производных классах.


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

public class CollectionBase: IEnumerable, ICollection и IList

{



}

Т.е. класс CollectionBase обеспечивает общую реализацию этих интерфейсов, пригодную для повторного применения.


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


Эти два метода выполняют одинаковую задачу:

public void PrintCollection( IEnumerable collection )

{

foreach( object o in collection )

Console.WriteLine( "Collection contains {0}",

o.ToString( ) );

}


public void PrintCollection( CollectionBase collection )

{

foreach( object o in collection )

Console.WriteLine( "Collection contains {0}",

o.ToString( ) );

}


Второй метод является менее пригодным для повторного применения. Он не может быть использован с классами Arrays, ArrayLists, DataTables, HashTables, ImageLists и со многими другими классами коллекций. Объявление метода с применением интерфейсов в качестве типа его параметров является более общим и удобным для повторного использования.


Кроме того, применение интерфейсов для определения API класса обеспечивает большую гибкость. К примеру, многие приложения используют класс DataSet для переноса данных между компонентами приложения. Совсем несложно включить в программу «на постоянной основе» следующее:

public DataSet TheCollection

{

get { return _dataSetCollection; }

}

Но при этом возможны проблемы в будущем. В какой-то момент может понадобиться перейти от использования DataSet к DataTable или DataView или даже к созданию собственного специального объекта. Любое из подобных изменений приводит к разрушению программы. Конечно, можно изменить тип параметра, но при этом общедоступный интерфейс заменится классом. Замена общедоступного интерфейса классом вынуждает делать намного больше изменений в системе; придется вносить изменения везде, где осуществляется доступ к общим свойствам.


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


using System.ComponentModel;


public IListSource TheCollection

{

get { return _dataSetCollection as IListSource; }

}


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


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

public class Employee

{

public string Name

{

get

{

return string.Format( "{0}, {1}", _last, _first );

}

}


// other details elided.

}


public class Customer

{

public string Name

{

get

{

return _customerName;

}

}


// other details elided

}


public class Vendor

{

public string Name

{

get

{

return _vendorName;

}

}

}


По смыслу у них не должно быть общего базового класса, но они имеют общие свойства: имя, адрес, телефон. Эти требования к наличию общей функциональности (в данном случае – свойств) нужно вынести в интерфейс:

public interface IContactInfo

{

string Name { get; }

PhoneNumber PrimaryContact { get; }

PhoneNumber Fax { get; }

Address PrimaryAddress { get; }

}


public class Employee : IContactInfo

{

// implementation deleted.

}

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

public void PrintMailingLabel( IContactInfo ic )

{

// implementation deleted.

}


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

public struct URLInfo : IComparable

{

private string URL;

private string description;


public int CompareTo( object o )

{

if (o is URLInfo)

{

URLInfo other = ( URLInfo ) o;

return CompareTo( other );

}

else

throw new ArgumentException(

"Compared object is not URLInfo" );

}


public int CompareTo( URLInfo other )

{

return URL.CompareTo( other.URL );

}

}

В итоге можно создать отсортированный список объектов URLInfo, потому что URLInfo поддерживает интерфейс IComparable. Структуры URLInfo упаковываются при добавлении в список. Но метод Sort() не требует выполнения распаковки обоих объектов перед вызовом CompareTo(). Необходимо распаковать аргумент (other), но не требуется распаковка левой части в выражении сравнения при вызове метода IComparable. CompareTo().