В данной статье я хочу рассмотреть подробности объектно-ориентированного программирования (ООП) с применением средств С# и более подробно остановиться на первом столпе ООП инкапсуляции. В следующей статье я опишу остальные наследование и полиморфизм.
Формальное определение класса в С#
Класс в С#, как и в других языках программирования, - это пользовательский тип данных (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;
Про второй и третий столпы программирования я расскажу в следующей статье.
Комментариев нет:
Отправить комментарий