воскресенье, 22 августа 2010 г.

ООП с применением средств С# – Инкапсуляция (Часть 1)

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

Формальное определение класса в С#

Класс в С#, как и в других языках программирования, - это пользовательский тип данных (user defined type, UDT), который состоит из данных (часто называемых атрибутами или свойствами) и функциями для выполнения с этими данными различных действий (эти функции обычно называются методами). Классы позволяют группировать в единое целое данные и функциональность, моделируя объекты реального мира. Именно это свойство классов и обеспечивает одно из наиболее важных преимуществ объектно-ориентированных языков программирования.

Как уже говорилось, для классов С# можно определить любое количество конструкторов. Любой класс С# автоматически снабжается конструктором по умолчанию. Этот конструктор в С# (в отличие от С++) при создании объекта автоматически присвоит всем переменным-членам класса безопасные значения по умолчанию. Кроме того, вы можете, как переопределить конструктор по умолчанию, так и создать такое количество пользовательских конструкторов (принимающих параметры), сколько вам необходимо.

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

Указание области видимости на уровне типа

Для каждого члена класса необходимо указать (или принять значение по умолчанию) область видимости с помощью одного из следующих ключевых слов: public, private, pritected, internal. Однако область видимости задается не только для членов класса, но и для самих классов! Разница между областью видимости для членов классов и областью видимости для самих классов заключается в том, что в первом случае мы определяем те члены класса, которые будут доступны через экземпляры объектов, а во втором - те области программы, из которых будет возможно обращение к данным классам.

Для класса в С# используется только два ключевых слова для определения области видимости: public и internal. Объекты классов, определенных как public (открытых), могут быть созданы как из своего собственного двоичного файла, так и из других двоичных файлов С# (то есть из другой сборки). Если переопределять HelloClass как public, то это можно сделать следующим образом:

public class HelloClass {...}

Если вы не указали явно область видимости для класса, то по умолчанию этот класс будет объявлен как internal (внутренний). Объекты таких классов могут создаваться только объектами из той же сборки, в которой они были определены. Внутренние классы часто рассматриваются как вспомогательные (helper classes), поскольку они используются типами данной сборки для помощи в выполнении каких-либо действий.

Классы - это не единственные пользовательские типы данных, для которых существует атрибут области видимости. Как мы помним, тип - это общий термин, который используется в отношении классов, структур, перечислений, интерфейсов и делегатов. Атрибут видимости (public или internal). может быть установлен для любого типа С#.

Столпы объектно-ориентированного программирования

С# можно считать новым членом сообщества объектно-ориентированных языков программирования, к самым распространенным из которых относятся ]аvа, С++, Object Pascal и (с некоторыми допущениями) Visual Basic 6.0. В любом объектно-ориентированном языке программирования обязательно реализованы три важнейших принципа - «столпа» объектно-ориентированного программирования:

инкапсуляция: как объекты прячут свое внутреннее устройство;

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

полиморфизм: как в этом языке реализована поддержка выполнения нужного действия в зависимости от типа передаваемого объекта

Конечно же, все три этих принципа реализованы и в С#.

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

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

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

Наследование: отношения «быть» и «иметь»

Когда ваши классы связываются друг с другом отношениями наследования, это означает, что вы устанавливаете между ними отношения типа «быть» (is-a). Такой тип отношений называется также классическим наследованием.

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

Полиморфизм: классический и для конкретного случая

Можно сказать, что этот термин определяет возможности, заложенные в языке, по интерпретации связанных объектов одинаковым образом. Существует две основные разновидности полиморфизма: классический полиморфизм и полиморфизм «для конкретного случая» (ad hoc). Классический полиморфизм встречается только в тех языках, которые поддерживают классическое наследование (в том числе, конечно, и в С#). При классическом полиморфизме вы можете определить в базовом классе набор членов, которые могут быть замещены в производном классе. При замещении в производных классах членов базового класса эти производные классы будут по-разному реагировать на одни и те же обращения.

Вторая разновидность полиморфизма - полиморфизм для конкретного случая (ad hoc polymorphism). Этот тип полиморфизма позволяет обращаться схожим образом к объектам; не связанным классическим наследованием. Достигается это очень просто: в каждом из таких объектов должен быть метод с одинаковой сигнатурой (то есть одинаковым именем метода, принимаемыми параметрами и типом возвращаемого значения). В языках, поддерживающих полиморфизм этого типа, применяется технология «позднего связывания» (late binding), когда тип объекта, к которому происходит обращение, становится ясен только в процессе выполнения программы. В зависимости от того, к какому типу мы обращаемся, вызывается нужный метод

Средства инкапсуляции в С#

Принцип инкапсуляции предполагает, что ко внутренним данным объекта (переменным-членам) нельзя обратиться напрямую через экземпляр этого объекта. Вместо этого для получения информации о внутреннем состоянии объекта и внесения изменений необходимо использовать специальные методы. В С# инкапсуляция реализуется на уровне синтаксиса при помощи ключевых слов public, private и protected.

Следование принципу инкапсуляции позволяет защитить внутренние данные класса от неумышленного повреждения. Для этого достаточно все внутренние данные сделать закрытыми (объявив внутренние переменные с использованием ключевых слов private или protected). Для обращения к внутренним данным можно использовать один из двух способов:

создать традиционную пару методов - один для получения информации (accessor), второй - для внесения изменений (mutator);

определить именованное свойство.

Еще один метод защиты данных, предлагаемый С#, - использовать ключевое слово readonly. Однако какой бы способ вы ни выбрали, общий принцип остается тем же самым - инкапсулированный класс должен прятать детали своей реализации от внешнего мира. Такой подход часто называется «программированием по методу черного ящика». Еще одно преимущество такого подхода заключается в том, что вы можете как угодно совершенствовать внутреннюю реализацию своего класса, полностью изменяя его содержимое. Единственное, о чем вам придется позаботиться, - чтобы в новой реализации остались методы с той же сигнатурой и функциональностью, что и в предыдущих версиях. В этом случае вам не придется менять ни строчки существующего кода за пределами данного класса.

Реализация инкапсуляции при помощи традиционных методов доступа и изменения

Давайте обратимся к нашему классу Employee. Если мы хотим, чтобы внешний мир смог работать с внутренними данными этого класса (пусть это будет только одна переменная fullName, для которой используется тип данных string), то традиционный подход рекомендует создание метода доступа (accessor, или get method) и метода изменения (mutator, или set method). Набор таких методов может выглядеть следующим образом:

// Определение традиционных методов доступа и изменения для закрытой переменной

public class Employee

{

private striпg fullName;

// Метод доступа

public string GetFullName() {return fullName; }

// Метод изменения

public void SetFullName(string n) {

// Логика для удаления неположенных символов (!, @, #, $, % и прочих .

// Логика для проверки максимальной длины и прочего

fullName = n;

}

}

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

// Применение методов доступа и изменения

public static int Main(string[] args) {

Employee p = new Employee();

p. SetFul1 Name("Fred");

Consolе.WriteLine(" Employee is named: " + p.GetFull Name() ) ;

// Ошибка! К закрытым данным нельзя обращаться напрямую через экземпляр объекта!

// p.FullName;

return 0;

}

Второй способ инкапсуляции: применение свойств класса

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

get {return empID;}

set {empID = value;}

Свойство С# состоит из двух блоков - блока доступа (get block) и блока изменения (set block). Ключевое слово value представляет правую часть выражения при присвоении значения посредством свойства. Как и все в С#, то, что представлено словом value - это также объект. Совместимость того, что передается свойству как value, с самим свойством, зависит от определения свойства.

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

Свойства только для чтения, только для записи и статические

Однако при создании пользовательских свойств класса часто возникает необходимость создать свойство, которое будет доступно только для чтения. Делается это очень просто: необходимо в определении свойства пропустить блок set.

С# также поддерживает статические свойства. Как мы помним, статические переменные предназначены для хранения информации на уровне всего класса, а не его отдельных объектов. Если у нас объявлены статические данные, то обращаться к ним и устанавливать значения должны статические свойства. Предположим, что в классе Employee мы собираемся, помимо всего прочего, хранить еще и информацию об имени организации, в которой работают все сотрудники - объекты класса Employee. Для этого будет использована специальная статическая переменная. Соответствующее статическое свойство для работы с этой переменной может выглядеть так:

// Со статическими данными должны работать статические свойства

publiC class Employee

{

private static string CompName; // Статическая переменная

public static string Соmраnу // Статическое свойство

{

get { return.CompName; }

set { CompName = value; }

}

}

Обращение к статическим свойствам производится так же, как и к статическим методам.

Статические конструкторы

Само словосочетание «статический конструктор» звучит несколько странно - ведь мы знаем, что конструкторы нужны для создания объектов, а все, что имеет определение «статический», работает на уровне классов, а не отдельных объектов. Однако в С# такие конструкторы вполне имеют право на существование. Единственное их назначение - присваивать исходные значения статическим переменным. Для них нельзя использовать модификаторы области видимости, однако слово static должно присутствовать обязательно.

Псевдоинкапсуляция: создание полей «только для чтения»

Помимо свойств только для чтения, в С# также предусмотрены поля, значения которых изменять нельзя. Как мы помним, поля (fields) - это открытые данные класса (переменные, объявленные с ключевым словом public). Обычно применение полей в рабочем приложении - не самая лучшая идея, поскольку поля беззащитны - им легко присвоить ошибочное значение и тем самым испортить внутреннее состояние объекта. Однако в С# предусмотрена возможность запретить любую возможность изменять значение поля, объявив его с ключевым словом readonly:

public readonly string SSNField;

Про второй и третий столпы программирования я расскажу в следующей статье.

Комментариев нет:

Отправить комментарий