1. Базовые сведения об ООП

В рамках данной темы поднимается вопрос сложность программного обеспечения как основного вызова при разработке ПО, понятию ОО программирования, приводится пример использования объектного подхода.

Базовые сведения об ООП

1.1 Сложность программного обеспечения

"Если бы строители строили здания так же, как программисты пишут программы, первый залетевший дятел разрушил бы цивилизацию" (с) Второй закон Вейнберга

Одним из основных вызовов при разработке программного обеспечения является сложность программ. Этот вызов иногда называют «кризисом программного обеспечения».

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

Наибольшие проблемы возникают при разработке корпоративного программного обеспечения (enterprise software). Такие системы используются довольно долго (годами и десятилетиями), и от их корректной работы зависят тысячи и даже миллионы людей. Это могут быть, например, системы управления воздушным транспортом, железнодорожными перевозками, банковские системы, системы коммунальных платежей, онлайн-игры, популярные веб-сайты и веб-службы и так далее.

Важнейшей особенностью корпоративной программы является ее высокая сложность. Одному программисту не под силу решить все проблемы, связанные с проектированием такой системы. Грубо говоря, сложность промышленных программ превышает интеллектуальные возможности отдельного человека.

Некоторые специалисты считают что популярные операционные системы являются самой сложной рукотворной системой, которую когда-либо создавало человечество.

Со времени возникновения области разработки программного обеспечения, человечество накопило достаточно знаний, чтобы проектировать даже самые сложные системы программного обеспечения, но мы до сих пор сталкиваемся с огромным количеством проблем. В чем же дело?

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

Таким образом, у нас возникает проблема сложности – программное обеспечение становится всё более сложным, а способности справиться с этой сложностью остаются ограниченными. Как же решить эту проблему?

1.2 Декомпозиция программных систем

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

Одним из методов декомпозиции является алгоритмическая декомпозиция (вы сталкивались с таким видом декомпозиции на первом курсе, когда изучали дисциплину «Алгоритмизация и программирование»). Алгоритмическая декомпозиция выполняется методом «сверху вниз», где каждый модуль системы выполняет один из этапов общего процесса. На рисунке 1.1 приведена часть программы, которая обновляет содержимое основного файла.

Другим видом декомпозиции называется объектно-ориентированная декомпозиция, которая вам пока неизвестна.

При использовании этого вида декомпозиции, вместо разделения системы на этапы, например, «Прочитать отформатированное обновление» и «Добавить контрольную сумму», мы определяем такие объекты, как «Основной файл» и «Контрольная сумма», которые создаются при анализе предметной области. На рисунке 1.2 приведен пример объектно-ориентированной декомпозиции для той же части программы.

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

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

Так какой же метод декомпозиции следует использовать? Использовать оба метода одновременно нельзя – сначала следует произвести декомпозицию либо по алгоритмам, либо по объектам.

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

Преимущества объектно-ориентированной декомпозиции:

  • уменьшается размер систем за счет повторного использования общих механизмов;

  • объектно-ориентированные системы являются более гибкими и легче эволюционируют со временем;

  • снижается риск, возникающий при создании сложной программной системы;

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

1.3 Объектная модель проектирования

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

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

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

Объектная модель допускает масштабирование. В крупных системах образуются целые кластеры, образующие слои. Пример структуры крупных систем приведен на рисунке 1.4.

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

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

Объект – структура, которая объединяет данные и методы, которые эти данные обрабатывают. Фактически, объект является основным строительным блоком объектно-ориентированных программ.

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

Разница между классом и объектом такая же, как между абстрактным понятием и реальным объектом.

Объект состоит из следующих частей:

  • имя объекта;

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

  • методы (операции) – применяются для выполнения операций с данными, а также для совершения других действий. Методы определяют, как объект взаимодействует с окружающим миром.

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

В объектно-ориентированной программе весь код должен находиться внутри классов!

В классе описываются, какого типа данные относятся к классу, а также то, какие методы применяются к этим данным. Затем, в программе на основе того или иного класса создается экземпляр класса (объект), в котором указываются конкретные значения полей и выполняются необходимые действия над ними.

Структуры в языке C как прототип класса

Давайте попробуем понять, что такое объект и класс с помощью языка программирования C.

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

Мы можем воспользоваться массивами и хранить данные о книгах в нескольких массивах.

    int book_year[100];
    int book_pages[100];
    char book_author[100][50];
    char book_title[100][100];
    float book_price[100];

Теперь мы можем обратиться к i-му номеру каждого массива для получения информации об i-ой книге.

printf("%s-%s %d page", book_author[3], book_title[3], book_pages[3]);

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

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

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

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

Сначала мы должны описать структуру (новый тип данных).

struct Book {
    char author[50];
    char title[100];
    int year;
    int pages;
    float price;
};

Таким образом, некоторое понятие реального мира (то есть, книга вообще, как понятие) в программе описан структурой Book. Экземпляр этого понятия (какая-то конкретная книга) в программе будет представлен экземпляром структуры - переменной типа Book. Мы объявляем экземпляр структуры, после чего мы сможет заполнить поля структуры и работать с ней дальше в программе.

int main() {

    // Объявляем экземпляр структуры
    struct Book book1;

    // Заполняем поля экземпляра
    strcpy(book1.author,"Иванов А.А");
    strcpy(book1.title,"Программирование на Java");
    book1.pages = 255;
    book1.price = 350.25;
    book1.year = 2018;

    // Теперь мы можем работать с созданным экземпляром структуры
    printf("%s - %s, %d страниц", book1.author, book1.title, book1.pages);

    return 0;
}

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

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

Пример использования объектного подхода

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

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

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

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

Для указания класса в Java используется ключевое слово class, после чего идет название класса, далее мы ставим фигурные скобки и всё, что будет написано внутри фигурных скобок (переменные и функции) будет относиться к этому классу.

class Triangle {
    
}

Какие переменные и типы данных будут моделировать координаты точек и цвет? Для моделирования цвета воспользуемся обычным целым числом (очень часто цвета моделируются обычным целочисленным значением).

class Triangle {
    
    // Цвет треугольника
    int color;
}

Теперь попробуем подумать, а как нам смоделировать координаты трех точек? Если бы мы программировали на языке C, мы бы написали что-то вроде

class Triangle {

    // Цвет треугольника
    int color;

    // Координаты точек треугольника
    int x1,y1,x2,y2,x3,y3;
}

Но давайте подумаем - координаты каждой точки логически связаны между собой и когда мы будем, например, перемещать треугольник или будем менять размер треугольника, будут одновременно меняться две переменные, которые моделируют одну точку. С точки зрения объектной модели, лучшим вариантом будет предусмотреть отдельную сущность, отдельный класс, который моделирует точку на двумерной плоскости. Таким образом, мы сгруппируем данные и код, который будет эти данные изменять (например, менять значение X и Y).

class Point2D {
    
    int x;
    int y;
}

Теперь вернемся к классу Triangle и укажем, что в качестве координат у нас будут выступать три объекта класса Point2D.

class Triangle {

    // Цвет треугольника
    int color;

    // Координаты трех точек
    Point2D point1;
    Point2D point2;
    Point2D point3;
}

Теперь давайте определимся с методами классов – то есть, к тому поведению, которое будут осуществлять объекты этих классов.

Какое поведение может быть у объекта класса «точка»? Совершенно точно это будет метод «изменить координаты». То есть, наша точка как отдельный объект может вести себя следующим образом – менять свои координаты. Давайте запрограммируем этот метод.

class Point2D {

    int x;
    int y;

    void changeCoordinates(int new_x, int new_y) {
        x = new_x;
        y = new_y;
    }
}

Теперь давайте подумаем, какое поведение будет у треугольника? Что с ним можно сделать? Ну, например, можно его перекрасить, можно его передвинуть в другое место на плоскости, можно его нарисовать на каком-то полотне и так далее. Давайте опишем некоторые из этих методов.

class Triangle {

    int color; // Цвет треугольника

    // Координаты трех точек
    Point2D point1;
    Point2D point2;
    Point2D point3;

    // Меняем цвет фигуры
    void changeColor(int new_color) {
        color = new_color;
    }

    // Меняем расположение фигуры
    // Для наглядности будем передавать входные параметры
    // в виде 6 целых чисел
    void move(int x1, int y1, int x2, int y2, int x3, int y3) {
        point1.changeCoordinates(x1, y1);
        point2.changeCoordinates(x2, y2);
        point3.changeCoordinates(x3, y3);
    }
}

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

Создадим внутри метода main() несколько объектов класса Triangle. Создание объектов мы будем рассматривать подробно в следующих лекциях, на данный момент мы просто скажем, что для создания объекта используется ключевое словоnew. После создания объекта, мы можем обращаться к объекту и вызывать методы у объектов (как мы вызывали функции в C).

public class Main {

    public static void main(String[] args) {

        // Создали один объект треугольника
        Triangle triangle1 = new Triangle();

        // Создали еще один объект треугольника
        Triangle triangle2 = new Triangle();

        // Для первого треугольника поменяли цвет
        triangle1.changeColor(10000);
        // Поменяли цвет для второго треугольника
        triangle2.changeColor(20000);

        // Создали объект первой точки треугольника
        // и назначили точке какие-то координаты
        triangle1.point1 = new Point2D();
        triangle1.point1.changeCoordinates(140,180);

        // Сделаем это же для первой точки второго треугольника
        triangle2.point1 = new Point2D();
        triangle2.point1.changeCoordinates(50,80);
    }
}

Важно! Вы должны понять, что у разных объектов значения переменных будут разные!

То есть, где-то в оперативной памяти у нас создано два разных объекта. Внутри первого есть объект класса Point2D, внутри этого объекта есть две переменные типа int, у которых будут значения 140 и 180. Внутри же второго треугольника будет свой объект Point2D, внутри которого будут совершенно другие две переменные типа int, у которых будут значения 50 и 80. Это же будет касаться переменных color в двух разных объектах (рис.1.5).

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

Last updated