Главная Материалы Новости Форум Поддержать сайт     

Объектно-ориентированное программирование (ООП)


       В настоящее время иногда всё ещё раздаются голоса по поводу того, что объектно-ориентированное программирование (ООП) это не есть что-то необходимое и даже не есть что-то полезное (см., например, здесь). Часто это из-за того, что у авторов нет чёткого понимания о том, что же такое ООП, в чём его суть и где те самые удобства, которые оно даёт. Здесь мы рассмотрим достаточно яркие примеры, иллюстрирующие пользу ООП и то, как, зачем и почему оно появилось. А вообще если кратко, то идеи ООП это всего навсего развитие такого понятия как функция (подпрограмма, процедура) в языке программирования при попытке реализовать эти самые функции максимально эффективно при работе с большими объёмами данных. Начните применять функции при работе с большими объёмами разнородных данных, попытайтесь добиться чтобы ваш код максимально экономил ресурсы компьютера и вы волей-неволей сами изобретёте все эти идеи лежащие в основе ООП. Итак.
       Рассмотрим пример работы с изображениями. Как известно в случае с компьютером изображение, картинка, фотография – это всего лишь точки (пиксели) с разной яркостью и цветом на экране монитора. В памяти компьютера яркость и цвет каждой точки изображения закодированы числами, и эти числа хранятся в таблице, где количество строк – высота картинки, количество столбцов – её ширина, а каждая конкретная ячейка таблицы содержит характеристики конкретного пикселя. Такую таблицу ещё называют массивом. Так вот, чтобы работать с изображением, нам необходимо знать имя массива в котором хранятся значения яркости и цвета каждой точки, а также нам необходимо знать размеры этого массива (высоту и ширину изображения). Так как если у нашего изображения, например, 100 столбцов, а мы попытаемся прочитать из памяти числа, там, где по нашему мнению 101 столбец, то прочитать-то мы эту память прочитаем, однако при выводе на экран этого 101 столбца получим что-то, что не относится к нашему изображению (так как, то, что относится к изображению лежит лишь в пределах 100 столбцов). Поэтому, чтобы считывать из памяти то, что относится к изображению необходимо знать где, в каких пределах памяти содержится информация об этом изображении, а для этого необходимо знать его размеры. Таким образом, практика показывает, что изображение это массив, содержащий яркости и цвета пикселей и размеры этого массива. Поэтому удобно в одном месте (под одним именем, в качестве одной структуры) хранить всю необходимую информацию для работы с данным изображением – его массив и размеры этого массива. Вот мы уже и подошли к первой идее, предшествующей ООП – это идея структуры. Как известно (см., например, здесь) структура является, по сути, предтечей класса и объединяет в себе данные разного типа. В примере с изображением мы объединили данные типа массива и данные типа переменных – размеры массива.
       Следующий пример. Пусть нам необходима функция для работы с некоторыми данными. Пусть в процессе работы этой функции ей необходимо создавать промежуточные переменные, массивы и т.п., которые нужны только лишь тогда, когда работает эта самая функция. А вот как только функция заканчивает свою работу и выдаёт результат, сразу же уничтожаются и эти промежуточные переменные, массивы и т.п., так как они нужны лишь в процессе работы функции – в них функция (которая, по сути, есть подпрограмма) хранит (запоминает на время) какие-то свои промежуточные результаты. У этого подхода есть следующий недостаток. Если мы используем одну и ту же функцию в программе множество раз, то тогда каждый раз заново создаются, а потом уничтожаются промежуточные переменные. На это создание и уничтожение отвлекаются ресурсы компьютера, в итоге программа работает медленнее. Так же нам самим постоянно необходимо следить за тем предусмотрели ли мы в функции уничтожение временно созданных переменных или нет, ведь если не предусмотрели, то память компьютера при многократном вызове одной и той же функции достаточно быстро окажется “захламлённой”. Контроль за уничтожением промежуточных данных отвлекает уже ресурсы у программиста – он дольше создаёт программу, чем хотелось бы. То есть, например, пусть каждый раз в функции написанной на c++ выделяется память следующим образом:

float F11(int Z1) {

int *x;
int *y;
int *z;
x = new int;
y = new int;
z = new int;


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

delete x;
delete y;
delete z;

...

}

Данный код имеет недостатки, описанные выше – повторное создание и уничтожение внутри функции промежуточных данных при каждом новом вызове этой функции. Решением проблемы могло бы быть следующее: мы промежуточные переменные могли бы передавать в функцию, как её аргументы. То есть переделаем код следующим образом:

float F11(int Z1, int *x, int *y, int *z) {


}

Всё! Так как *x, *y, *z – это теперь аргументы функции, то их необходимо создать единожды до запуска функции, как и любые другие аргументы, что подаются на вход функции. Таким образом, внутри функции F11() теперь не будут каждый раз создаваться и удаляться эти самые переменные. Таким образом, резервировать память для данной функции мы будем до её запуска единожды создав соответствующие переменные. Более того, целесообразно данный подход развить. В нашем примере наша функция возвращает некоторое число типа float. Это значит, что каждый раз внутри функции создаётся переменная типа float, куда записывается результат работы этой функции, а потом по завершении работы функции эта переменная уничтожается. То есть на самом деле в теле функции имеет место следующее:

float F11(int Z1, int *x, int *y, int *z) {

float vix;

return vix;
}

Опять имеем лишние действия создания и уничтожения. Здесь у нас создаётся внутри функции переменная vix вида float, значение которой функция возвращает, а саму переменную vix уничтожает - чистит участок памяти, что был под неё внутри функции выделен. Таким образом если функцию запускаем несколько раз, то несколько раз по сути в пустую создаём и уничтожаем переменную vix. Таким образом, с учётом вышеизложенного целесообразно сделать так:

void F11(int Z1, int *x, int *y, int *z, float *vix) {

}

В итоге внутри нашей функции не создаются никакие промежуточные переменные, а сама функция ничего не возвращает. Всё необходимое для её работы здесь передаётся функции в качестве её аргументов. В итоге при каждом запуске функции нет нужды каждый раз выделять и чистить память. Вся эта память, по сути, выделяется единожды перед первым запуском функции. Функция лишь модифицирует данные в этой памяти – в аргументах, что подаются на её вход. Сама функция память не выделяет и не очищает. Таким образом, мы отделили процесс выделения памяти и процесс работы с данными (памятью). Функция теперь только работает с данными в памяти, а за её выделение и очистку не отвечает. Всё это прекрасно, но, во-первых, здесь переменные, с которыми работает функция – это её аргументы, а это значит, что они не создаются внутри функции – они внешние по отношению к функции. А раз так, то их может поменять не только сама функция, но и кто-либо ещё помимо данной функции. В то время, как переменные создаваемые внутри функции доступны лишь самой функции и больше никому. И во-вторых, если нам необходимо огромное количество самых разнообразных промежуточных переменных, массивов и т.п. для работы функции? Ведь часто функция – это достаточно большая подпрограмма. Тогда писать все промежуточные переменные в виде аргументов функции не удобно. Как минимум хорошо бы объединить все эти переменные в единую структуру и подавать на вход функции ссылку на эту самую структуру. В итоге разработчики языков и сделали нечто подобное, но при этом пошли ещё дальше – они создали особую структуру, которую назвали классом. Класс – это такая форма организации кода, где под одним именем (идентификатором) находятся и функция и данные, которые ей обрабатываются. То есть функцию поместили в одну структуру вместе с данными, которые должна обрабатывать эта функция и которые теперь нет нужды создавать внутри этой самой функции при каждом её запуске. Мы в классе заранее описываем все переменные, массивы и т.п. – всю ту память, что нам понадобится, а также в классе мы описываем функции, которые и будут работать с этими переменными (с этой памятью). И даже если у функций нет входных аргументов, то тот факт, что переменные, массивы и т.п. и функции принадлежат одной структуре (одному классу) – это сообщает компьютеру, что функции этого класса могут работать с переменными, массивами и т.п. этого класса, как со своими собственными аргументами. Более того, данные разделяются в классе на private и public. Если вы описываете, например, переменную, как private, то с ней могут работать только функции данного класса – и ничто другое вне данного класса поменять эту переменную не может. То есть данные типа private – это, по сути, как внутренние переменные функций – к ним может обращаться лишь сама функция и ничто другое! Ну а данные типа public доступны и тем, кто не является членом данного класса – т.е. в эти данные мы по сути пишем результат работы функции которые интересны уже вне данного класса – т.е. это то, что возвращала бы наша функция если бы мы не использовали концепцию класса, а работали бы только с функциями. Так вот, такое объединение данных и функций для их обработки в одной структуре (получившей название класса) – это и есть суть такого понятия, как инкапсуляция. Как мы уже выяснили – именно практика подсказала этот “ход” разработчикам языков программирования. Оказалось, что это оптимально, когда функции лишь выполняют наборы операций над данными, а описания данных и сами данные (память) для этих функций находятся в отдельном от функций месте. И вместе с тем функции и память-данные для них образуют единую конструкцию – класс. Объект, к слову, – это конкретная реализация класса. Так же, как в команде вида:

int i;

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

int i;
int j, k;

Это выделяет в памяти соотвествующее место для переменных i, j, k, куда мы можем записать данные целого типа. Однако, если у нас есть, например класс K1, в котором есть, например, переменные a1, b1, c1 типа int и функция обработки этих переменных funk1():

class K1 {
       public:
              int a1, b1, c1;
              void funk1();

};

то описание вроде такого:

K1 X;
K1 Y, Z;

приведёт к тому, что будут созданы объекты X, Y, Z в которых будет выделена память под их переменные a1, b1, c1, куда мы можем записать соотвествующие данные. Таким образом теперь мы можем записать какие-то целые числа в участки памяти с именами X.a1, X.b1, X.c1, Y.a1, Y.b1 и так далее т.е. оказалось, что выделено место в памяти под соотвествующие 9 чисел: 3 числа (a1, b1, c1) для объекта X, 3 числа для объекта Y, 3 числа для объекта Z – тут всё как в ситуации с объвлением типа int, где память выделяется под соотвествующие переменные. А вот будет ли выделено место в памяти для каждой из функций funk1() каждого объекта? Будет ли выделена память под X.funk1(), Y.funk1(), Z.funk1()? Оказывается, что нет и это логично, ведь иначе мы бы три раза выделили память под по сути одну и ту же функцию (под функции что работают одинаково)! А это бестолковые траты ресурсов компьютера. Таким образом в c++ память под функции для каждого из объектов одного и того же класса не выделяется – функции не тиражируются при создании объектов класса. То есть при введении понятия класса мы не просто объединяем в одну структуру (класс) данные и функции для их обработки, но также эта концепция подразумевает отказ от дублирования одной и той же функции в конкретных объектах одного и того же класса.
       Следующее понятие ООП – наследование. Опять пример из обработки изображений. Пусть до нас кто-то создал класс для работы с изображениями. Нас этот класс полностью устраивает, однако нам бы хотелось, чтобы помимо тех возможностей по обработке изображений, что предоставляет данный класс, в нём появились бы дополнительные функции. Процедура наследования и позволяет создать наш собственный класс для работы с изображениями на основе существующего класса – наш класс унаследует все функции предыдущего, плюс мы туда добавим нужные нам функции. Вот и всё. Это очень удобно, так как нам нет необходимости повторять работу по разработке функций, которые уже существуют в уже созданном до нас классе. Мы эти функции просто наследуем создавая класс-наследник уже существующего класса. Причём в нашей программе мы можем использовать одновременно и класс-предок и класс-наследник и несмотря на то, что у нас есть два очень похожих друг на друга класса в классе-наследнике мы не создаём повторно функции, что уже есть в классе предке. Таким образом нам не приходится эти самые функции заново переписывать в класс-наследник – мы просто пользуемся функциями класса-предка и в итоге не занимаем повторно память под одни и те же функции в разных классах.
       Ну и, наконец, полиморфизм. Здесь всё достаточно просто и это понятие есть, по сути, развитие такого явления, как перегрузка функций (раньше прегрузка называлась переопределением). Это когда одна и та же функция может работать по-разному в зависимости от того, какого типа данные вы подали на её вход. Например, у вас есть некая функция перемножения, и когда вы в качестве аргументов передаёте ей целые числа, то она их просто перемножает, а когда в качестве аргументов даёте ей матрицы, то тогда эта функция перемножает матрицы по правилу перемножения матриц. То есть здесь одна и та же функция, один и тот же интерфейс (способ взаимодействия, отображения) предназначен для перемножения данных разного типа. Для того, чтобы организовать код таким образом, чтобы один и тот же интерфейс использовался для работы с данными разного типа существуют специальные команды языка. Ещё один пример – из обработки изображений. Это когда одна и та же программа может одинаково обрабатывать изображения разных форматов – bmp, jpeg, gif и т.д., при этом вас не заботит формат этого изображения – вам для его обработки необходимо в программе совершать одни и те же операции, не зависимо от того, каков сейчас формат данного изображения. В итоге полиморфизм позволяет создавать довольно удобные интерфейсы программ.
       Таким образом, конструкции ООП – это не что-то кем-то надуманное произвольным образом. Приёмы ООП возникли из самой практики – это приёмы более оптимальной работы с информацией только и всего.



p.s. Для того, кто интересуется устройством компьютера можно посоветовать вот эту книгу и в частности главу из неё – "Что такое компьютер" (саму книгу или отдельные главы из неё вы можете приобрести здесь).



Обсудить на форуме



                     Комментарии



Представтесь (не менее 2-х символов):

Сообщение:

Далее функция антиспама.
Ответьте на вопрос:
Восемь умножить на сто будет равно? (введите числом):






Читаем книгу "Что людей объединяет или обо всём понемногу"

Что людей объединяет ...