ООП и структурки

В очередной раз прочитал в качестве определения «объектно-ориентированности» что-то типа «это структурки и код для работы с ними«. Удивительно, как популярная (но не единственная) реализация так повлияла на понимание термина. Речь идет прежде всего о том, как «объектно-ориентированность» реализована в C++.

Для начала — попытаемся понять, что же такое «объектно-ориентированность» в программировании. Мне очень нравится определение из SICP:

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

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

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

Эта модель в явной форме была реализована в языках типа Simula или Smalltalk. Из более современного можно назвать, например, VRML — это не просто «формат файлов» типа Autocad или 3D Studio, а практически полноценная реализация вышеназванного принципа. Из «настоящих» языков программирования — можно привести в качестве примера Objective C. Очень похожи на модель с обменом сообщениями «сигналы» и «слоты» библиотеки Qt — но в данном случае речь идет о диком смешении двух подходов к ООП, и вряд ли это стоит считать подходящим примером.

Па-а-азвольте, скажут тут знатоки современных языков программирования — где же тут private и public переменные, где здесь методы? И вот тут придется вспомнить про то, что C++ — первый широко используемый «объектно-ориентированный» язык, но создавался он исключительно как «C с классами», причем эти «классы», как и все объектно-ориентированные возможности раннего C++, были лишь имитацией таких возможностей в «настоящих» объектно-ориентированных языках.

Важно понимать, что «вызов метода» в C++ — это лишь реализация «передачи сообщения» от одного объекта другому. Фактически, в C++ вместо передачи сообщений используются три разных «примитива». Например, возьмем вот такой класс:

class A {
public:
	int x;
	int f(int y);
private:
	int z;
};

… и вот такой код, работающий с ним:

A a;
int b;
a.x = 2;
b = a.x;
b = a.f(3);

В С++ эти три строчки — принципиально разные операции. На самом деле все они могут быть рассмотрены, как частный случай «посылки сообщения» (а ее мы будем записывать в виде адресат.сообщение(аргументы)):

class A {
public:
	int get_x();
	void set_x(int new_x);
	int f(int y);
private:
	int x, z;
};

... 

A a;
int b;
a.set_x(2);
b = a.get_x();
b = a.f(3);

Замечу, что «накладные расходы» второго способа оказываются несколько больше. Но «идеологически» и присваивание, и чтение значения, и вызов метода — это одна и та же операция. Все переменные-члены класса перемещаются в секцию private — и состояние объекта действительно становится «внутренним».

В «настоящих» объектно-ориентированных языках, типа Smalltalk, с помощью модели передачи сообщений реализовывались даже такие вещи, как индексация массивов или циклы. В принципе, несложно подобным образом реализовать и арифметические вычисления — с помощью каких-нибудь специально созданных объектов, типа «сумма» (я тут перейду на «русский» псевдокод):

объект Формула {
методы:
	Число вычислить() - чисто виртуальный;
}

объект Сумма : Формула {
методы: 
	Число вычислить();
	задатьПервоеСлагаемое();	
	задатьВтороеСлагаемое();
переменные состояния:
	Формула первоеСлагаемое, второеСлагаемое;
}

объект Константа : Формула {
методы:
	Число вычислить();
	задатьЗначение(Число);
переменные состояния:
	Число значение;
}

Думаю, что реализации расписывать не надо? А вот как это можно использовать:

Константа к1, к2, к3;
Сумма с1, с2;
Число результат;
к1.задатьЗначение(1);
к2.задатьЗначение(2);
к3.задатьЗначение(3);
с1.задатьПервоеСлагаемое(к1);
с1.задатьВтороеСлагаемое(к2);
с2.задатьПервоеСлагаемое(с1);
с2.задатьВтороеСлагаемое(к3);
результат = с2.вычислить();

Естественно, что вышеописанное — это некоторое извращение, и все соответствующие объекты должны создаваться сразу, как только мы напишем результат = (1+2)+3;. «Метод» в нашей терминологии — это не более чем описание «формата» получаемого сообщения и реакции на него.

С++ — это попытка «приделать» объекты к существующему «необъектному» языку C. В C уже были и структурки, и функции — именно поэтому в C++ «классы» — это некоторое обобщение структур C. После того, как C++ стал действительно распространен — аналогичный подход «взяли на вооружение» разработчики других языков. Именно поэтому в «современном» ООП практически нельзя встретить «чистую» передачу сообщений — только подход «объект — это данные плюс функции».

Жутким монстром на фоне этого всего выглядит Qt — где умудрились объединить и обмен сообщениями (в виде сигналов и слотов), и «объекты в стиле C++». Получилось что-то в духе чудовища Франкенштейна — хотя иногда и довольно удобное в использовании.

ООП и структурки: 13 комментариев

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

    1. Разница есть. Человечество за свою историю изобрело некоторое количество «вычислительных моделей». Всякие там машины Тьюринга и фон Неймана — это далеко не все. Программисты пользуются куда большим количеством разных моделей. Тут и поддерживаемая ассемблером модель с ячейками памяти, операциями над ними и условными переходами, и модель «классических» языков программирования, со структурками, вызовом функций, операторами ветвления и цикла, и модель рекурсивных функций (Lisp и РЕФАЛ), и модель «объектов, обменивающихся сообщениями». «Вызов функции» — термин из одной модели, «передача сообщения» — из другой. Существование C++ и ООП-языков типа Java — это фактически доказательство эквивалентности двух моделей. Просто в разговоре «метод класса — это типа как функция» утверждается существование только одной из них, более знакомой, а вторая благополучно забывается.

      Подход C++ я не осуждаю, он нормальный. Просто понимание того, что на самом деле есть «вызов метода», может оказаться очень полезным.

      1. А вот скажи: вызов метода происходит в момент выхова метода или ставится в очередь на исполнение?

        1. Где? В языках типа C++ или Java — никакой «очереди на исполнение» в явном виде нет. В UML, на «диаграмме кооперации» есть два типа стрелочек — асинхронная и синхронная передача сообщений. В «теории» — можно делать и так, и так.

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

    1. Вот делаешь ту же ошибку, подменяешь представление единственно известной реализацией. Не говоря уже о том, что слово «прилинковать» относится уже к второму слою реализации.

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

      1. И – боже вас сохрани – не читайте до обеда советских газет.
        – Гм… Да ведь других нет.
        – Вот никаких и не читайте.

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

        Кстати, столкнулся недавно с Objective C, проблевался конкретно. :)

          1. на уровне отдельного ключа слАбо.
            на уровне «All write operations are read-modify-write operations. So a write to a port implies that the port pins are first read, then this value is modified and written to the port data latch» вполне приемлимо.

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

    1. Нет. Определение «сообщения» этого не подразумевает. В UML на диаграмме кооперации предусмотрено два типа изображающих сообщения стрелок — синхронные и асинхронные.

Добавить комментарий для Шура Люберецкий Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *