Инкапсуляция в C#

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

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

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

Однако существуют веши, которые никогда не должны делаться, и описанное выше открытие доступа — одна из них.

Необходимо сделать так, чтобы клиенты объекта общались с ним только управляемым образом. Обычно это означает организацию взаимодействия с объектом только через методы этого объекта (или свойства, которые, по сути, являются вызовами методов). Таким образом, внутренности объекта рассматриваются как “черный ящик”.

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

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

1
2
3
4
5
class MyRectangle
{
public uint width;
public uint height;
}

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

Поэтому два поля — widthnheight — объявлены общедоступными. Возможно, это вызвано спешкой при проектировании этого базового маленького класса. Но как вы вскоре убедитесь, лишь небольшая дополнительная работа обеспечила бы существенно большую гибкость.

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

Во времена ANSI С и других простых процедурных императивных языков понадобилось бы создать функцию с именем вроде ComputeArea, которая принимала бы параметр — указатель на экземпляр MyReсtangle. Строгие принципы объектно-ориентированного программирования склоняют к тому, что лучший способ сделать это — позволить экземплярам MyRectangle самостоятельно сообщать клиенту значения площади. Поэтому давайте так и поступим:

1
2
3
4
5
6
7
8
class MyRectangle
{
public uint width;
public uint height;
public uint GetArea()
{
return width * height;
}

Как видите, в класс добавлен новый член — метод GetAreа. При вызове на экземпляре заслуживающий доверия MyRectangle вычислит свою площадь и вернет результат. Теперь есть базовый маленький класс прямоугольника, имеющий одну вспомогательную функцию, которая призвана немного облегчить жизнь клиентам, если они пожелают узнать площадь прямоугольника.

Но предположим, что возникла причина предварительно вычислить площадь прямоугольника, чтобы каждый раз при вызове метода Get Area не пришлось вычислять ее заново. Возможно, так нужно сделать потому, что известно, что Get Are а будет вызываться многократно с одним и тем же экземпляром на протяжении всего времени его существования. Игнорируя неразумность ранней оптимизации, представим, что это все-таки сделано.

Теперь класс MyRectangle выглядит примерно так:

1
2
3
4
5
6
7
8
9
class MyRectangle
{
public uint width;
public uint height;
public uint area;
public uint GetArea()
{
return area;
}

Присмотревшись внимательнее, вы заметите ошибки. Обратите внимание, что все поля являются общедоступными. Это позволяет потребителю экземпляров класса MyRectangle иметь прямой доступ к его внутренностям. Какой смысл в предоставлении метода Get Area, если потребитель может напрямую обратиться к полю area? Да, возможно, стоило бы сделать поле area приватным.

Таким образом, для получения площади прямоугольника клиенты были бы вынуждены вызывать Get Area. Это определенно шаг в правильном направлении, как иллюстрирует следующий код:

1
2
3
4
5
6
7
8
9
10
11
class MyRectangle {
public uint width;
public uint height;
private uint area;
public uint GetArea()
{
if ( area == 0 )  {
area = width * height;
}
return area;
}

Поле area сделано приватным, заставляя потребителя вызывать метод Get Area, чтобы получить площадь прямоугольника. Однако во время работы над примером стало ясно, что в какой-то момент должна быть вычислена площадь прямоугольника. По причине лени было решено проверять значение поля area перед возвратом, и если оно равно 0, то это значит, что оно должно быть предварительно вычислено.

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

Разумеется, в этом несколько надуманном примере оптимизация подобного рода будет весьма незначительной. Но если вы немного задумаетесь, то наверняка представите пример, который существенно выигрывает от применения отложенного вычисления.

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

В классе прямоугольника все еще присутствует вопиющая проблема. Поскольку поля width и height являются общедоступными, что произойдет, если пользователь изменит одно из значений после вызова GetArea на экземпляре? В этом случае будет получен наихудший пример несогласованности внутренностей объекта. Целостность состояния объекта будет нарушена.

Это определенно нехорошая ситуация. Таким образом, в коде по-прежнему присутствует ошибка. Поля width и height также должны быть сделаны приватными:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyRectangle {
private uint width;
private uint height;
private uint area;
public uint Width {
get {
return width;
}
set {
width = value;
ComputeArea();
}
public uint Height {
get
{
return height;
}
set {
height = value;
ComputeArea() ;
}
public uint Area {
get {
return area;
}
private void ComputeArea() {
area = width * height;
}

Последняя версия MyRectangle выглядит намного лучше. После того, как поля width и height были сделаны приватными, стало ясно, что потребителю этих объектов нужен какой-нибудь способ установки и получения значений ширины и высоты. И здесь пригодятся свойства С#.

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

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

В данном примере объекту точно известно, когда изменяются поля width и height, поэтому он может предпринять необходимые действия для вычисления новой площади. Если в объекте используется подход с отложенным вычислением, сохраняя кэшированное значение площади, вычисленное при первом чтении свойства Area, то значение кэша должно быть объявлено недействительным, как только будет вызван блок set любого из свойств Width или Height.

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

Другими словами, интерфейс, видимый потребителю (также называемый контрактом), не изменяется. Например, в финальной реализации класса MyRectangle площадь вычисляется заново, как только устанавливается новое значение свойства Width или Height. Может быть позже, когда программное обеспечение будет почти готово, запуск профилировщика обнаружит, что предварительное вычисление площади действительно сильно загружает процессор при работе приложения. Никаких проблем!

Можно изменить модель, ориентируя ее на применение кэшированного значения площади, которое вычисляется только при первом обращении, и поскольку соблюдались принципы инкапсуляции, потребителям объектов даже не нужно знать об этом. Они не будут знать о том, что внутренняя реализация объекта изменилась.

В этом и состоит мощь инкапсуляции. Когда изменяется внутренняя реализация объекта, а использующие его клиенты могут не меняться — значит, инкапсуляция работает так, как должна.

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

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100