Концепции объектно-ориентированного программирования

В центре ООП находится понятие объекта. Объект– это сущность, которой можно посылать сообщения (вызывать методы) и котораяможет на них реагировать, используя свои данные. Данные объектаскрыты от остальной программы. Сокрытие данных (объявление дляних области видимости «protected» или «private») называется инкапсуляцией. Однако наличие инкапсуляции еще не означает объектнойориентированности языка, для этого требуется наличие наследования. Но в полной мере преимущества ООП проявляются только втом случае, когда в языке программирования реализован полиморфизм – возможность объектов с одинаковой спецификацией иметьразличную реализацию.

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

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

Класс, от которого произошло наследование, называетсябазовым, илиродительским. Классы, которые произошли от базового, называютсяпотомками, наследниками, или производными классами(листинг 15.8).

Листинг 15.8. Простейший пример наследования

classClass1

{

}

classClass2 : Class1

{

}

Инкапсуляция– свойство языка программирования, позволяющее объединить данные и код в объект и скрыть реализацию объекта от пользователя. При этом пользователю предоставляется только спецификация (интерфейс) объекта. Пользователь может взаимодействовать с объектом только через этот интерфейс. Инкапсуляцияможет быть достигнута простейшими организационными мерами. Знание того, что «вот так делать нельзя» иногда является самым эффективным средством инкапсуляции (листинги 15.9 и15.10).

Листинг 15.9. Класс реализации комплексного числа

// Класс комплексного числа

classComplex

{

// Целаячастьчисла

privatedoublere;

// Мнимая часть числа

privatedoubleim;

// Конструктор с инициализацией

public Complex(double i_re, double i_im)

{

re = i_re;

im = i_im;

}

// Сложение комплексных чисел

// parComplex1 – Первое комплексное число

// parComplex2 – Второекомплексноечисло

public static Complex operator+(Complex parComplex1,

Complex parComplex2)

{

return new Complex(parComplex1.re + parComplex2.re,

parComplex1.im + parComplex2.im);

}

}

Листинг15.10. Использование класса комплексного числа

Complexcomplex1 = newComplex (1,1);

Complex complex2 = new Complex (2,2);

Complexcomplex3 = complex1 + complex2;

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

Полиморфизм– взаимозаменяемость объектов с одинаковыминтерфейсом. Язык программирования поддерживает полиморфизм, если классы с одинаковой спецификацией могут иметь различнуюреализацию, например, реализация класса может быть изменена впроцессе наследования. Кратко смысл полиморфизма можно выразить фразой: «один интерфейс, множество методов».

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

Возможность приписывать разную функциональность одномуметоду (функции, операции) называетсяперегрузкой метода (листинг15.11).

В приведенном примере листинга 15.11 класс «Operation» определяет виртуальный метод, который подразумевает какое-то арифметическое вычисление с использованием двух переменных и возвращение результата. Класс «Add» является наследником «Operaion» иперегружает метод «Calculate» как операцию сложения двух переменных. Класс «Sub» также является наследником «Operation» иперегружает метод «Calculate» как операцию вычитания.

Листинг 15.11. Пример перегрузки метода для арифметическойоперации

// Класс для определения произвольной арифметической операции

// с использованием двух переменных

classOperation

{

//Вычислениепроизвольнойоперациисучастиемдвухпеременных

public virtual float Calculate(float parArgument1, float

parArgument2);

}

// Сложение двух переменных

classAdd : Operation

{

// Сложение с использованием двух переменных

public override float Calculate(float parArgument1,

floatparArgument2)

{

returnparArgument1+ parArgument2;

}

}

// Сложение двух переменных

classSub : Operation

{

// Сложение с использованием двух переменных

publicoverridefloatCalculate(floatparArgument1,

floatparArgument2)

{

returnparArgument1 – parArgument2;

}

}

В листинге 15.12 представлен простейший пример использованияполиморфизма, когда объявляется локальная переменная «operation» с типом родительского класса «Operation» и ей присваивается экземпляр класса «Add». Данное присвоение разрешается, что являетсяодним из свойств наследования: переменным с типом родительского класса можно присваивать экземпляры классов-наследников, ноне наоборот. В результате вызова метода «Calculate» переменная«result1» примет значение 2.0f, так как в данный момент переменная«operation» является экземпляром класса «Add», где вычислениеопределено как сложение. Далее переменной «operation» присваивается значение экземпляра «Sub» и вызов метода «Calculate» возвратитзначение 0.0f, как результат вызова метода из класса «Sub».

Листинг 15.12. Простейший пример использованияполиморфизма

Operationoperation = newAdd();

float result1 = operation. Calculate(1.0f, 1.0f);

operation = new Sub();

float result2 = operation. Calculate(1.0f, 1.0f);

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

- конструктор по умолчанию – конструктор, не имеющий обязательных аргументов; в отсутствие явно заданного конструктора по умолчанию его код генерируется компилятором автоматически при компиляции;

- конструктор копирования – конструктор, аргументом которого является ссылка на объект того же класса; используется для создания экземпляра класса со значениями полей аналогичными копируемому объекту;

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

Листинг 15.13. Пример объявления конструкторов различного вида

// Класс комплексного числа

classComplex

{

doublere;

doubleim;

// Конструктор по умолчанию

publicComplex()

{

re = 0.0;

im = 0.0;

}

// Конструктор копирования

public Complex(Complex obj)

{

re = obj.re;

im = obj.im;

}

// Конструктор с инициализацией (обычный конструктор)

public Complex(double i_re, double i_im)

{

re = i_re;

im = i_im;

}

}

Деструктор– специальный метод класса, служащий для деинициализации объекта (например, освобождения памяти). Имя деструктора должно совпадать с именем класса и иметь префикс~. У класса может быть только один деструктор. Деструктор не имеет модификатора доступа и параметров (листинг15.14).

Листинг 15.14. Пример объявления конструкторов различного вида

// Класс комплексного числа

classComplex

{

oublere;

doubleim;

// Конструктор с инициализацией (обычный конструктор)

public Complex(double i_re, double i_im)

{

re = i_re;

im = i_im;

}

// Деструктор

~Complex()

{

}

}

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

Листинг 15.15. Пример объявления виртуальных и невиртуальных функций

class Ancestor

{

public virtual void function1 ()

{

Console.WriteLine(“Ancestor.function1()”);

}

public void function2 ()

{

Console.WriteLine(“Ancestor.function2()”);

}

}

class Descendant:Ancestor

{

public override void function1 ()

{

Console.WriteLine(“Descendant.function1()”);

}

public void function2 ()

{

Console.WriteLine(“Descendant.function2()”);

}

}

...

Descendant descendant = new Descendant ();

Ancestor ancestor = descendant;

descendant.Function1();

descendant.Function2();

ancestor.Function1();

ancestor.Function2();

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

В примере листинга 15.15 класс «Ancestor» определяет две функции, одна из них виртуальная, другая – нет. Класс «Descendant»переопределяет обе функции, но одинаковое обращение к функциям дает разные результаты. Результат, получаемый на выходе, представлен листингом 15.16.

Листинг 15.16. Пример вызовы виртуальных и невиртуальных функций

Descendant.Function1

Descendant.Function2

Descendant.Function1

Ancestor.Function2

В случае виртуальной функции для определения реализации функции используется информация о типе объекта и вызывается «правильная» реализация, независимо от типа указателя. При вызове невиртуальной функции компилятор руководствуется типом переменной, поэтому вызываются две разные реализации «function2()», несмотря на то что используется один и тот же объект.

Наши рекомендации