Поиск

Любая сущность — объект.

В настоящем объектно-ориентированном языке все элементы так называемой предметной области (problem domain) выражаются через концепцию объектов. [В этой книге использовано определение Коуда-Йордо-на (Coad/Yourdon), согласно которому под предметной областью понимают решаемую задачу с учетом ее сложности, терминологии, подходов к ее решению и т. д.] Как вы уже, наверное, поняли, объекты — это центральная идея объектно-ориентированного программирования. Многие из нас, обдумывая какую-то проблему, вряд ли оперируют понятиями "структура", "пакет данных", "вызов функций" и "указатели", ведь привычнее применять понятие "объекты". Возьмем такой пример.

Допустим, вы создаете приложение для выписки счета-фактуры, в котором нужно подсчитать сумму по всем позициям. Какая из двух формулировок понятней с точки зрения пользователя?

  • Не объектно-ориентированный подход Заголовок счета-фактуры представляет структуру данных, к которой я получу доступ. В эту структуру войдет также дважды связанный список структур, содержащих описание и стоимость каждой позиции. Поэтому для получения общего итога по счету мне потребуется объявить переменную с именем наподобие totallnvoiceAmount и инициализировать ее нулем, получить указатель на головную структуру счета, получить указатель на начало связанного списка, а затем "пробежать" по всему этому списку. Просматривая структуру для каждой позиции, я буду брать оттуда переменную-член, где находится итог для данной позиции, и прибавлять его к totallnvoiceAmount.
  • Объектно-ориентированный подход У меня будет объект "счет-фактура", и ему я отправлю сообщение с запросом на получение общей суммы. Мне не важно, как информация хранится внутри объекта, как это было в предыдущем случае. Я общаюсь с объектом естественным образом, запрашивая у него информацию посредством сообщений. (Группа сообщений, которую объект в состоянии обработать, называется интерфейсом объекта. Чуть ниже я объясню, почему в объектно-ориентированном подходе вместо термина "реализация" правильней употреблять термин "интерфейс".)

Очевидно, что объектно-ориентированный подход естественнее и ближе к тому способу рассуждений, которым многие из нас руководствуются при решении задач. Во втором варианте объект "счет-фактура", наверно, просматривает в цикле совокупность (collection) объектов, представляющих данные по каждой позиции, посылая им запросы на получение суммы по данной позиции. Но если требуется получить только общий итог, то вам все равно, как это реализовано, так как одним из основных принципов объектно-ориентированного программирования является инкапсуляция (encapsulation). Инкапсуляция — это свойство объекта скрывать свои внутренние данные и методы, представляя наружу только интерфейс, через который осуществляется программный доступ к самым важным элементам объекта. Как объект выполняет задачу, не имеет значения, главное, чтобы он справлялся со своей работой. Имея в своем распоряжении интерфейс объекта, вы заставляете объект выполнять нужную вам работу. (Ниже я остановлюсь на понятиях "инкапсуляция" и "интерфейс".) Здесь важно отметить, что разработка и написание программ моделирования реальных объектов предметной области облегчается тем, что представить поведение таких объектов довольно просто.

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

ПРИМЕЧАНИЕ Фрагменты кода в этой главе представляют концепции объектно-ориентированного программирования. Помните: хотя я привожу много примеров кода на С#, сами концепции универсальны для ООП и не присущи какому-либо языку программирования. В этой главе будут также представлены для сравнения примеры на С, не являющемся объектно-ориентированным языком.

Допустим, вы пишете приложение для расчета зарплаты служащей вашей фирмы по имени Эми (Amy). Код на С, представляющий данные о служащем, будет выглядеть примерно так:

Struct EMPLOYEE {
char szFirstName[25];
char szLastName[25];
int iAge; double dPayRate;
};
Вот код для расчета зарплаты Эми Андерсон, в котором используется структура EMPLOYEE:
void main(){
double dTotalPay;
struct EMPLOYEE* pEmp;
pEmp = (struct EMPLOYEE*)malloc(sizeof(struct EMPLOYEE));
if (pEmp) {
pEmp->dPayRate = 100;
strcpy(pEmp->szFirstName, "Эми"); strcpy(pEmp->szLastName, "Андерсон"); pEmp->iAge = 28;
dTotalPay = pEmp->dPayRate * 40; printf("Bce выплаты Xs Xs составляют X0.2f",
pEmp->szFirstName, pEmp->szLastName, dTotalPay); >
free(pEmp);
}

Код этого примера основан на данных, содержащихся в структуре, и на некотором внешнем (по отношению к структуре) коде, обрабатывающем эту структуру. И что же здесь не так? Основной недостаток — в отсутствии абстрагирования: при работе со структурой EMPLOYEE необходимо знать чересчур много о данных, описывающих служащего. Почему это плохо? Допустим, спустя какое-то время вам потребуется определить "чистую" зарплату Эми (после удержания всех налогов). Тогда пришлось бы не только изменить всю клиентскую часть кода, работающую со структурой EMPLOYEE, но и составить описание (для других программистов, которым может достаться этот код впоследствии) изменений в функционировании программы.

Теперь рассмотрим тот же пример на С#: using System;
class Employee {
public Employee(string firstName, string lastName, int age, double payRate)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.payRate = payRate; }
protected string firstName; protected string lastName;
 protected int age; protected double payRate;
public double CalculatePay(int hoursWorked)
{
// Здесь вычисляется зарплата.
return (payRate * (double)hoursWorked);
}
}
class EmployeeApp {
public static void MainQ
{
Employee emp = new Employee ("Эми", "Андерсон", 28, 100);
 Console.WriteLine("n3apnflaTa Эми составляет $" +
emp.CalculatePay(40)); }
}

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

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

Помните: модифицировать переменные объекта следует только методами этого же объекта. Как видно из нашего примера, все переменные-члены в Employee объявлены с модификатором доступа protected, a метод CalculatePay — с модификатором public. Модификаторы доступа применяются для задания уровня доступа, который получают производные классы к членам исходного класса. Модификатор protected указывает, что производный класс получит доступ к члену, а клиентский код — нет. Модификатор public делает член доступным и для производных классов, и для клиентского кода. Подробнее на модификаторах доступа я остановлюсь в главе 5, пока же запомните, что модификаторы позволяют защитить ключевые члены класса от нежелательного использования.