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

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


       В настоящее время иногда всё ещё раздаются голоса по поводу того, что объектно-ориентированное программирование (ООП) это не есть что-то необходимое и даже не есть что-то полезное (см., например, здесь). Часто это из-за того, что у авторов нет чёткого понимания о том, что же такое ООП, в чём его суть и где те самые удобства, которые оно даёт. Здесь мы рассмотрим достаточно яркие примеры, иллюстрирующие пользу ООП и то, как, зачем и почему оно появилось. А вообще если кратко, то идеи ООП это всего навсего развитие такого понятия как функция (подпрограмма, процедура) в языке программирования при попытке реализовать эти самые функции максимально эффективно при работе с большими объёмами данных. Начните применять функции при работе с большими объёмами разнородных данных, попытайтесь добиться чтобы ваш код максимально экономил ресурсы компьютера и вы волей-неволей сами изобретёте все эти идеи лежащие в основе ООП. Итак.
       Рассмотрим пример работы с изображениями. Как известно в случае с компьютером изображение, картинка, фотография – это всего лишь точки (пиксели) с разной яркостью и цветом на экране монитора. В памяти компьютера яркость и цвет каждой точки изображения закодированы числами, и эти числа хранятся в таблице, где количество строк – высота картинки, количество столбцов – её ширина, а каждая конкретная ячейка таблицы содержит характеристики конкретного пикселя. Такую таблицу ещё называют массивом. Так вот, чтобы работать с изображением, нам необходимо знать имя массива в котором хранятся значения яркости и цвета каждой точки, а также нам необходимо знать размеры этого массива (высоту и ширину изображения). Так как если у нашего изображения, например, 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 и т.д., при этом вас не заботит формат этого изображения – вам для его обработки необходимо в программе совершать одни и те же операции, не зависимо от того, каков сейчас формат данного изображения. В итоге полиморфизм позволяет создавать довольно удобные интерфейсы программ. А так же, что кстати более важно, такие вот перегруженные (универсальные) функции чаще будут наследоваться – именно на основе классов с такими вот универсальными функциями удобно создавать классы-наследники, а это, как мы с вами уже знаем, способствует минимизации дублирования по сути одних и тех же функций – способствует минимизации захламления памяти.
       Таким образом, конструкции ООП – это не что-то кем-то надуманное произвольным образом. Приёмы ООП возникли из самой практики, из стремления минимизировать всевозможные повторы и дубли, как данных в памяти, так и дубли при выполнении действий (например дублирование действий по резервированию и очистке памяти – см. про инкапсуляцию выше), таким образом, это приёмы более оптимальной работы с информацией, только и всего. И особенно хорошо вы можете почувствовать плюсы ООП поработав, например, с современными библиотеками для программирования на языке си, а ещё лучше, если попробуете инструменты типа Microsoft Visual Studio, C++Builder, Qt Creator, Ultimate++ и т.д., которые обеспечивают работу с данными библиотеками на основе графического интерфейса пользователя (GUI). И вот тогда при работе с данными инструментами даже при создании простейшего окна вы в полной мере воспользуетесь и, прежде всего, наследованием, и полиморфизмом, и конечно же инкапсуляцией.



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



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



                     Комментарии



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

Сообщение:

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






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

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