Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Объектно-ориентированное программирование.-7

.pdf
Скачиваний:
11
Добавлен:
05.02.2023
Размер:
4.54 Mб
Скачать

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

Теперь рассмотрим тот же пример на C#:

using System;

class Employee

{

public Employee(string name, int age, double rate)

{

Name = name; Age = age; PayRate = rate;

}

private string Name; private int Age; private double PayRate;

public double CalculatePay(int hours)

{

// Здесь вычисляется зарплата return PayRate * hours;

}

}

class EmployeeApp

{

public static void Main()

{

Employee emp = new Employee("Анна", 28, 100);

Console.WriteLine("3apплата Анны составляет " + emp.CalculatePay(40));

}

}

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

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

11

всегда будут вместе.

Модифицировать переменные объекта следует только методами этого же объекта. Как видно из нашего примера, все переменные-члены в классе Employee объявлены с модификатором доступа private, а метод CalculatePay

– с модификатором public. Модификаторы доступа применяются для задания уровня доступа к членам класса. Модификатор private указывает, что доступ к члену имеет только сам класс, а клиентский код – нет. Модификатор public делает член доступным как для любых классов, так и для клиентского кода. Подробнее о модификаторах доступа поговорим в § 4.2.

Пример: Samples\2.1\2_1_1.

2.1.1.1. Объект или класс?

Программисты, начинающие осваивать ООП, часто путают термины «объект» и «класс».

Есть разные трактовки термина «класс», показывающие, в частности, чем класс отличается от объекта. Будем считать, что класс – это просто новый тип данных (как char, int или long), с которым связаны некие данные и методы для их обработки. Объект же – это экземпляр типа, или класса.

Можно понимать класс как «чертеж» объекта. Разработчик объекта сначала создает его «чертеж», так же, как инженер-строитель сначала чертит план дома. Имея такой чертеж, мы располагаем всего лишь проектом дома этого типа. Однако те, кто приобрел этот чертеж, могут по нему построить себе дом. Таким же образом на базе класса – «чертежа» набора функциональных возможностей – можно создать объект, обладающий всеми возможностями этого класса.

2.1.1.2. Реализация

Реализация (instantiation) в ООП означает факт создания экземпляра (он же объект) некоторого класса. В следующем примере мы создадим только класс, или спецификацию (specification), объекта. А поскольку это не сам объект, а лишь его «чертеж», то память для него не выделяется.

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

class EmployeeApp

{

12

public static void Main()

{

Employee emp = new Employee("Анна", 28, 100);

}

}

В этом примере объявлена переменная «emp» типа Employee, и с помощью оператора new выполнена ее реализация. Переменная «emp» представляет собой экземпляр класса Employee и является объектом Employee. Выполнив реализацию объекта, мы можем установить с ним связь через его открытые (public) члены. Например, для объекта «emp» это метод CalculatePay. Пока реально объект не существует, вызывать его методы нельзя (за исключением статических членов, о которых мы поговорим в главе 4). Посмотрим на следующий код C#:

class EmployeeApp

{

public static void Main()

{

Employee emp = new Employee("Анна", 28, 100); Employee emp2 = new Employee("Яна", 34, 120);

}

}

Здесь два экземпляра одного класса Employee – «emp» и «emp2». Оба объекта одинаковы с точки зрения программной реализации, но у каждого экземпляра свой набор данных, который может обрабатываться отдельно от другого.

2.1.2. Три основных принципа ООП

По Бьерну Страуструпу, автору языка C++, язык может называться объектно-ориентированным, если в нем реализованы три концепции: объекты, классы и наследование. Однако теперь принято считать, что такие языки должны держаться на других трех китах: инкапсуляции, наследовании и полиморфизме. Этот философский сдвиг произошел из-за того, что со временем мы стали понимать: построить объектно-ориентированные системы без инкапсуляции и полиморфизма так же невозможно, как без классов и наследования.

Пример: Samples\2.1\2_1_2.

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

Как уже было сказано выше, инкапсуляция, или утаивание информации

13

(information hiding), – это возможность скрыть внутреннее устройство объекта от его пользователей, предоставив через интерфейс доступ только к тем членам объекта, с которыми клиенту разрешается работать напрямую. Поскольку в том же контексте мы говорили также об абстрагировании, то рассмотрим разницу между этими похожими понятиями. Инкапсуляция подразумевает наличие границы между внешним интерфейсом класса (открытыми членами, видимыми пользователям класса) и деталями его внутренней реализации. Преимущество инкапсуляции для разработчика в том, что он может открыть те члены класса, которые будут оставаться статичными, или неизменяемыми, скрыв внутреннюю организацию класса, более динамичную и в большей степени подверженную изменениям. Как уже говорилось, в C# инкапсуляция достигается путем назначения каждому члену класса своего модификатора доступа.

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

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

Однако язык – это один уровень абстрагирования. Если мы пойдем дальше, то, как разработчикам класса, нам нужно придумать такую степень абстрагирования, чтобы клиенты нашего класса могли сразу сосредоточиться на своей задаче, не тратя время на изучение работы класса. На вопрос – какое отношение интерфейс класса имеет к абстрагированию? – можно ответить так: интерфейс класса и есть реализация абстрагирования.

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

14

Торговые автоматы остаются без изменений (более или менее) со времени их изобретения. Это связано с тем, что их внутренняя организация совершенствовалась по мере развития технологии, а основной интерфейс не нуждался в больших переменах. Неотъемлемой частью проектирования интерфейса класса является достаточно глубокое понимание предметной области. Такое понимание поможет нам создать интерфейс, предоставляющий пользователям доступ к нужной им информации и методам, но изолирующий их от «внутренних органов» класса. При разработке интерфейса мы должны думать не только о решении текущей задачи, но и о том, чтобы обеспечить такое абстрагирование от внутреннего представления класса, которое позволит неограниченно модифицировать закрытые члены класса, не затрагивая существующего кода. Кроме того, решая, какие члены класса сделать открытыми, надо опять вспомнить о клиенте. При разработке своих классов мы всегда должны ставить себя на место программиста, которому предстоит работать либо с экземплярами наших классов, либо с производными от них классами.

2.1.2.2. Наследование

Наследованием называют возможность при описании класса указывать на его происхождение (kind-of relationship) от другого класса. Наследование позволяет создать новый класс, в основу которого положен существующий. В полученный таким образом класс можно внести свои изменения, а затем создать новые объекты данного типа. Этот механизм лежит в основе создания так называемой иерархии классов. После абстрагирования наследование

– наиболее значимая часть общего планирования системы. Производным (derived class) называется создаваемый класс, производный от базового класса (base class). Производный класс наследует все члены базового класса. Какие именно члены базового класса наследуются производными классами, решается в C# через модификаторы доступа.

Чтобы понять, когда и как применять наследование, вспомним пример EmployeeApp. Допустим, в компании есть служащие с разными типами оплаты труда: постоянный оклад (salaried), почасовая оплата (hourly) и оплата по договору (contract). Хотя у всех объектов Employee должен быть одинаковый интерфейс, их внутреннее функционирование может различаться. Например, метод CalculatePay для служащего на окладе будет работать не так, как для контрактника. Однако для наших пользователей важно, чтобы интерфейс

15

CalculatePay не зависел от того, как считается зарплата.

Нельзя ли здесь обойтись без объектов? Введем в структуру EMPLOYEE член, описывающий тип оплаты, и напишем следующую функцию:

double CalculatePay(EMPLOYEE *emp, int hours)

{

double dTotalPay;

// Проверяем тип оклада

if (emp->type == SALARIED)

{

// Вычисляем заработок для служащего на окладе

}

else if (emp->type == CONTRACT)

{

// Вычисляем заработок по контракту

}

else if (emp->type == HOURLY)

{

// Вычисляем почасовой заработок

}

else

{

// Выполняем иную обработку

}

return dTotalPay;

}

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

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

using System;

class Employee

{

public Employee(string id, string name, int age, double rate)

16

{

EmployeeId = id; Name = name;

Age = age; PayRate = rate;

}

protected string EmployeeId; protected string Name; protected int Age;

protected double PayRate;

}

class SalariedEmployee : Employee

{

public string PensionNumber;

public SalariedEmployee(string id, string name, int age, double rate, string pension) : base(id, name, age, rate)

{

PensionNumber = pension;

}

public double CalculatePay(int days)

{

//Вычисляем заработок постоянного служащего

//из расчета 8 рабочих часов в день

}

}

class ContractEmployee : Employee

{

public double ContractHours;

public ContractEmployee(string id, string name, int age, double rate, double hours) : base(id, name, age, rate)

{

ContractHours = hours;

}

public double CalculatePay(int percent)

{

//Вычисляем заработок для контрактника

//исходя из доли выполненной работы

}

}

class HourlyEmployee : Employee

{

public HourlyEmployee(string id, string name, int age, double rate) : base(id, name, age, rate)

{

}

public double CalculatePay(int hours)

{

// Вычисляем заработок для почасового служащего

}

}

17

Отметим важные моменты, вытекающие из данного примера:

В базовом классе Employee описана строковая переменная EmployeeId, которая наследуется и классом SalariedEmployee, классом ContractEmployee и классом HourlyEmployee. Все производные классы получили эту переменную автоматически как наследники класса Employee.

Каждый из производных классов реализует свою версию CalculatePay. Хотя реализация этих функций различна, пользовательский код останется прежним. Базовый класс этой функции лишен, т.к. если не известен тип оплаты труда, то провести расчет невозможно.

Производные классы в дополнение к членам, унаследованным из базового класса, имеют свои члены: в классе SalariedEmployee описана строковая переменная PensionNumber (номер пенсионного страхового свидетельства), а в класс ContractEmployee включено описание члена ContractHours (количество часов, прописанное в контракте).

Мы изменили модификатор доступа у полей класса с private на protected. Сделано это для того, чтобы производные классы могли получить доступ к этим полям. Внешний клиентский код доступа к ним по-прежнему не имеет.

Конструкторы производных классов явно вызывают конструктор класса базового.

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

2.1.2.3. Полиморфизм

Полиморфизм – это функциональная возможность, позволяющая старому коду вызвать новый код. Это свойство ООП наиболее ценно, поскольку дает нам возможность расширять и совершенствовать свою систему, не затрагивая существующий код.

Предположим, нам нужно написать метод, в котором для каждого объекта из набора Employee вызывается метод CalculatePay. Все просто, если зарплата рассчитывается одним способом: мы можем сразу вставить в набор тип нужного объекта. Проблемы начинаются с появлением других форм оплаты – в этом случае опять придется переписывать много кода. Объектно-

18

ориентированный язык решает эту проблему благодаря полиморфизму.

Внашем примере надо описать абстрактный базовый класс Employee,

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

using System;

abstract class Employee

{

public Employee(string id, string name, int age, double rate)

{

EmployeeId = id; Name = name;

Age = age; PayRate = rate;

}

protected string EmployeeId; protected string Name; protected int Age;

protected double PayRate;

public abstract double CalculatePay(int param);

public string AName

{

get { return Name; }

}

}

class SalariedEmployee : Employee

{

public string PensionNumber;

public SalariedEmployee(string id, string name, int age, double rate, string pension) : base(id, name, age, rate)

{

PensionNumber = pension;

}

public override double CalculatePay(int days)

{

//Вычисляем заработок постоянного служащего

//из расчета 8 рабочих часов в день

Console.Write(" :: SalariedEmployee.CalculatePay :: "); return PayRate * days * 8;

19

}

}

class ContractEmployee : Employee

{

public double ContractHours;

public ContractEmployee(string id, string name, int age, double rate, double hours) : base(id, name, age, rate)

{

ContractHours = hours;

}

public override double CalculatePay(int percent)

{

//Вычисляем заработок для контрактника

//исходя из доли выполненной работы

Console.Write(" :: ContractEmployee.CalculatePay :: "); return ContractHours * PayRate * percent / 100;

}

}

class HourlyEmployee : Employee

{

public HourlyEmployee(string id, string name, int age, double rate) : base(id, name, age, rate)

{

}

public override double CalculatePay(int hours)

{

// Вычисляем заработок для почасового служащего

Console.Write(" :: HourlyEmployee.CalculatePay :: "); return PayRate * hours;

}

}

class EmployeeApp

{

private static Employee[] employees;

private static void CalculatePay()

{

foreach(Employee emp in employees)

{

double pay;

Console.Write("Зарплата служащего " + emp.AName + " составляет");

pay = emp.CalculatePay(40); Console.WriteLine(pay);

}

}

public static void Main()

{

employees = new Employee[3];

employees[0] = new SalariedEmployee("111-111-111",

"Анна", 28, 100, "555-555-555-55"); employees[1] = new ContractEmployee("111-111-112",

"Яна", 34, 120, 100);

20