Почему иногда полезно не думать о том, для чего нужна программа. ТРИЗ и программирование
Иногда наивное не отличается от серьезного, а сложные задачи "на уровне парадигм" возникают из-за простых ошибок. Последние же воспринимаются не ошибками, а достижениями. Поэтому и допускаются. Судите сами...
ИСТОРИЯ 1. НАИВНАЯ. "СТОЛЫ И ИХ НОЖКИ"
ИСТОРИЯ 2. КЛАССИЧЕСКАЯ. "ТЕПЛИЦЫ И МЕТЕОСТАНЦИИ"
ИСТОРИЯ 3. ДЕМОКРАТИЧНАЯ. "ВСЕ ЕДИНО (ЕСЛИ ВСЕ ДЕЛАТЬ НЕПРАВИЛЬНО)"
РЕЗЮМЕ
ИСТОРИЯ 1. НАИВНАЯ. "СТОЛЫ И ИХ НОЖКИ"
Один Заказчик обратился в аутсорсинговую компанию с просьбой разработать программу, которая позволяет создавать и хранить характеристики различных столов. Зачем Заказчику понадобилась такая программа, мы не знаем. Будем считать эту потребность out of scope.
Программист, которому поручили заказ, решил сэкономить время. Зайдя на www.codeproject.com, он без труда нашел пример, в котором для описания стола имелся класс CTable:
// Столешница.
class CTableTop {...};// Ножка.
class CTableLeg {...};// Стол.
class CTable
{
public:// ...
private:
// ...
CTableTop m_TableTop;
CTableLeg m_Legs[4];
};
Класс был предназначен для описания обеденного стола на 4-х ножках. Программист легко "прикрутил" его к программе и через некоторое время отослал программу Заказчику.
Получив дистрибутив и исходный код, Заказчик по условиям договора сделал первый платеж. Затем, запустив программу, он решил внести в базу данных информацию о своем фамильном трехногом столе.
Не стоит говорить, что попытка закончилась сообщением “Assertion failed”.
Заказчик, предварительно запив гнев корвалолом, написал письмо компании-исполнителю.
Получив ругательное письмо от Заказчика, менеджер компании немного взгрел программиста, и последний, не долго думая, "прикрутил" к "столу" возможность стоять на трех ногах. При этом получился такой код:
class CTable
{
public:// ...
private:
// ...
bool m_bHasThreeLegs;
CTableTop m_TableTop;
CTableLeg m_Legs[4];
};
То есть в класс CTable был добавлен флаг m_bHasThreeLegs ("имеет три ноги"), а во все функции, касающиеся стола, был добавлен следующий оператор if:
if (m_bHasThreeLegs) // если "три ноги"
{
// делаем одно,
}
else // иначе
{
// делаем другое.
}
Программа была отослана Заказчику, и тот, добавив в базу данных трехногий фамильный стол, успокоился и перевел компании оставшуюся часть денег.
Долгое время программа не вызывала никаких нареканий, пока не попалась в руки иному дотошному пользователю. Находясь с ноутбуком в кафе, он решил занести в базу данных информацию про одноногий "барный" стол, за которым он стоял. Не трудно догадаться, что попытка закончилась очередным “Assertion failed”.
Огорчившийся пользователь написал продавцу программы, который и был тем самым Заказчиком, а он, в свою очередь, послал гневное письмо в аутсорсинговую компанию. Поскольку гарантийный срок на программу к тому времени еще не закончился, баг пришлось исправлять.
Вторично взгретый менеджером программист убрал из класса флаг m_bHasThreeLegs и вместо него "прикрутил" поле m_iLegsCount:
class CTable
{
public:// ...
private:
// ...
int m_iLegsCount;
CTableTop m_TableTop;
CTableLeg m_Legs[4];
};
То есть добавил Пользователю возможность указывать количество ножек.
Не трудно понять, что подобный "дизайн" привел к разрастанию кода, и все функции, работающие с ножками стола, стали содержать 4 оператора if:
if (m_iLegsCount == 1)
{
// Делаем одно.
}
else if (m_iLegsCount == 2)
{
// Делаем другое.
}
else if (m_iLegsCount == 3)
{
// Делаем третье.
}
else if (m_iLegsCount == 4)
{
// Делаем четвертое.
}
Фикс был отослан Заказчику, а тот разослал его пользователям. Однако это не остановило поток жалоб на "глюки":
- Один пользователь ехал в поезде и не смог внести в базу данных откидной купейный стол.
- Другой пользователь летел в самолете и не смог занести информацию о столике, прикрепленном к спинке переднего кресла.
- Третий пользователь попытался занести в базу компьютерный стол.
- Четвертый – "стол-книгу".
- И т.д.
К счастью для аутсорсинговой компании, гарантийный срок к тому времени закончился, и баги исправлять не пришлось. А если бы и пришлось, то мы не сомневаемся в том, что программист смог бы еще что-нибудь "прикрутить и исправить". Только:
- Во что превратился бы код?
- И разве это гарантировало бы от очередных "падений программы со стола"? Например, когда пользователь решил бы внести в базу данных подвесной стол или каменный стол без ножек?
Тут бы самое время отложить Историю 1, а то многие Коллеги, возможно, уже рассердились на столь наивный пример в статье, которая позиционируется как серьезная.
К серьезному и перейдем.
ИСТОРИЯ 2. КЛАССИЧЕСКАЯ. "ТЕПЛИЦЫ И МЕТЕОСТАНЦИИ"
Как и положено, все серьезное предваряется цитатами из классиков. Не будем нарушать традицию и мы.
Гради Буч. Объектно-ориентированный анализ и проектирование с примерами приложений на C++, 2-е изд./Пер с англ. – М.: "Издательство Бином", СПб: "Невский диалект", 1998 г. – стр. 58.
Или www.helloworld.ru/texts/comp/other/oop/ch02.htm
"Управление режимом работы парниковой установки - очень ответственное дело, зависящее как от вида выращиваемых культур, так и от стадии выращивания.
Нужно контролировать целый ряд факторов: температуру, влажность, освещение, кислотность (показатель рН) и концентрацию питательных веществ. В больших хозяйствах для решения этой задачи часто используют автоматические системы, которые контролируют и регулируют указанные факторы. Попросту говоря, цель автоматизации состоит здесь в том, чтобы при минимальном вмешательстве человека добиться соблюдения режима выращивания.
Одна из ключевых абстракций в такой задаче - датчик. Известно несколько разновидностей датчиков. Все, что влияет на урожай, должно быть измерено, так что мы должны иметь датчики температуры воды и воздуха, влажности, рН, освещения и концентрации питательных веществ.
С внешней точки зрения датчик температуры - это объект, который способен измерять температуру там, где он расположен. Что такое температура? Это числовой параметр, имеющий ограниченный диапазон значений и определенную точность, означающий число градусов по Фаренгейту, Цельсию или Кельвину. Что такое местоположение датчика? Это некоторое идентифицируемое место в теплице, температуру в котором нам необходимо знать; таких мест, вероятно, немного.
Для датчика температуры существенно не столько само местоположение, сколько тот факт, что данный датчик расположен именно в данном месте и это отличает его от других датчиков. Теперь можно задать вопрос о том, каковы обязанности датчика температуры? Мы решаем, что датчик должен знать температуру в своем местонахождении и сообщать ее по запросу. Какие же действия может выполнять по отношению к датчику клиент? Мы принимаем решение о том, что клиент может калибровать датчик и получать от него значение текущей температуры".
Конец цитаты.
Далее Гради Буч разбирает пример программирования одного из датчиков (температуры воздуха), и тем самым, по нашему мнению, рискует превратить программу в задачу про ножки и столы.
В самом деле, датчиков много, они разных типов, и каждый контролирует ряд разных параметров. В свою очередь, поведение каждого из них зависит от изменений определенных параметров. Задача, прямо скажем, многофакторная. И что нам в таком случае за дело до отдельного датчика?
Из-за этого фрагмента книги разгорелся "сыр-бор" на одной из встреч программистов, который мы невольно услышали и потому включили данную задачу в статью.
Кто-то из участников встречи (не критикуя того, как в цитируемой книге спроектирован фрагмент программы, ответственный за отдельно взятый датчик температуры) обратил внимание Коллег на то, что начинать проектирование системы надо было не с одного датчика, А (в силу указанной многофакторности) СРАЗУ С ГРУППЫ или даже чего-то большего.
На что получил возражение от Коллег более опытных:
- Когда бы мы имели группу похожих объектов, тогда и начали бы проектирование с группы (хоть с интерфейса базового класса). А имеем ли мы в данном случае похожие объекты?
- Но мы могли бы работать с разными объектами одинаковым образом, – возразил первый спорщик. И сразу получил ответ:
- А зачем нам нужно одинаково работать с датчиком температуры и датчиком кислотности? Вернее с этими показателями, измеренными в определенных местах?
Тут появился третий спорщик и заявил:
- Имеем противоречие: "некоторых полезных сущностей должно быть несколько, ибо они разные, и не должно быть несколько (лучше бы вообще иметь одну), чтобы не усложнять программу".
Согласно, правилу, приведенному здесь, мы должны группировку выполнить и далее работать с группой. Но для группировки нужен адекватный контекст. А в данной книге он не задан – что теперь гадать…
На этом спор затих, и потому мы высказали свою точку зрения:
- Что бы ни писал Гради Буч, программисту незачем думать не только над тем, как датчик устроен, но и над тем, что он на самом деле измеряет и, тем более, куда его прицепят. К тому же вряд ли в этой книге Гради Буч рассматривал задачу проектирования датчиков "с нуля". Скорее всего, датчики уже есть, и их можно подключить к ПК, например, через порт USB.
"Как-то" датчики умеют преобразовывать градусы в кулоны (например, с помощью эффектов Зеебека, Пельтье и т.п.). Но для решения обсуждаемой задачи программисту это знать не обязательно, хотя для общего развития, конечно, полезно. Ему даже незачем (для решения данной задачи) знать, что такое "температура" или "pH". Точно так же, как ему незачем знать, как устроен монитор или принтер и где их поставили, если надо вывести данные на печать или на экран.
Помнить надо одно: на порт приходят "какие надо" сигналы "откуда надо". С этой точки зрения, они приходят "из одного места". И поэтому они принадлежат одной общности ("то, что пришло на порт"). Это и есть абстрагирование, за которое ратует Автор книги, но сам (в данном случае) почему-то его не делает. Различать надо лишь то, с какого конкретного внешнего датчика пришел сигнал".
С этой точки зрения модель задачи традиционна:
- получение данных "откуда надо";
- обработка по правилам (диспетчирование, обсчет и т.д.);
- возвращение ответных данных "куда надо":
- внешнему тепличному устройству (датчику или иному), которое, получив сигнал, выполнит свою функцию, никак не зависящую от программы;
- на экран пользователю;
- "куда надо, туда и отправим" (в файлы, на печать, …).
Эта "модель" (1-2-3) ничем особо не отличается ни от каких других программ.
Оставим спорщиков и вернемся к "первичке".
Действительно, Автор книги, по нашему мнению, понял эти разные датчики именно как непохожие объекты.
Судите сами. Гради Буч пишет:
"Известно несколько разновидностей датчиков. Все, что влияет на урожай, должно быть измерено, так что мы должны иметь датчики температуры воды и воздуха, влажности, рН, освещения и концентрации питательных веществ. С внешней точки зрения датчик температуры - это объект, который способен измерять температуру там, где он расположен. Что такое температура? Это числовой параметр, имеющий ограниченный диапазон значений и определенную точность, означающий число градусов по Фаренгейту, Цельсию или Кельвину. Что такое местоположение датчика? Это некоторое идентифицируемое место в теплице, температуру в котором нам необходимо знать; таких мест, вероятно, немного. Для датчика температуры существенно не столько само местоположение, сколько тот факт, что данный датчик расположен именно в данном месте и это отличает его от других датчиков".
Автор видит разнообразие, концентрируется на нем и размножает сущности.
Но для целей решения рассматриваемой задачи имеет смысл написать так:
"Датчики – это "периферийные устройства", преобразующие разные измеряемые сущности в одинаковые электрические сигналы".
И хотя всегда трудно быть святее римского папы, даже, если это ООПапа, мы хотим смотреть на общее, концентрироваться на нем и сущности сокращать.
Так удобнее и писать, и сопровождать программу.
Этой задачей из книги [9] мы не ограничимся. Почему? – будет видно из дальнейшего.
ПРИМЕР 2
В главе 8-ой Автор рассматривает еще одну задачу, где требуются датчики (www.helloworld.ru/texts/comp/other/oop/ch08.htm).
В этой задаче нужно автоматизировать работу метеорологической станции. В простейшем случае программа должна собирать данные (температуру и влажность воздуха, атмосферное давление, скорость и направление ветра) от разных устройств (датчиков) и выводить их на экран компьютера.
Автор вновь приступает к проектированию программы "от подсистемы", а именно с анализа датчиков. Результатом проектирования становится такая иерархия классов:
Назначения классов даны в таблице:
Мы видим, что классы данной иерархии можно условно разбить на две группы:
-
К первой группе следует отнести классы, ассоциированные с конкретными физическими устройствами (датчиками). Это – TemperatureSensor, PressureSensor, HumiditySensor, WindSpeedSensor и WindDirectionSensor.
-
Ко второй группе относятся классы, предназначенные для обработки и преобразования измеренных значений. Они не имеют физических (материальных) аналогов. Речь идет о CalibratingSensor, HistoricalSensor и TrendSensor.
Эти классы встроены внутрь иерархии и являются базовыми для "материальных датчиков", над значениями которых нужно проводить соответствующие преобразования.
На наш взгляд, такая организация порождает две проблемы, которые внимательному Читателю, быть может, уже напомнили наивную историю про ножки и столы.
Проблема первая. "Материальные датчики" и "преобразователи измеренных величин" сгруппированы в одну иерархию. Если над величиной, измеренной датчиком, нужно будет произвести преобразование, то "датчик" следует вывести из соответствующего "преобразователя".
Значит, любое добавление новых преобразований, выполняемых над измеренной величиной, приводит к появлению новых "преобразователей" и к реорганизации иерархии классов.
Например, на текущий момент за калибровку измеренной величины отвечает класс CalibratingSensor. Калибровка производится при помощи метода линейной интерполяции. Если в будущем вдруг потребуется производить калибровку данных еще и при помощи какого-нибудь другого метода, потребуется:
- завести базовый класс калибровки – BaseCalibratingSensor;
- вывести из него производные (для каждого типа калибровки) – LinearCalibratingSensor и AnotherCalibratingSensor;
- из каждого производного класса вывести свой HistoricalSensor – LinearHistoricalSensor и AnotherHistoricalSensor;
- в свою очередь, из каждого HistoricalSensor – вывести свои TrendSensor, HumiditySensor и WindSpeedSensor.
Мы видим, как изначально небольшая иерархия разрастается прямо на глазах:
class BaseCalibratingSensor
class LinearCalibratingSensor : public BaseCalibratingSensor
class LinearHistoricalSensor : public LinearCalibratingSensor
class LinearTrendSensor : public LinearHistoricalSensor
class LinearTemperatureSensor : public LinearTrendSensor
class LinearPressureSensor : public LinearTrendSensor
class LinearHumiditySensor : public LinearHistoricalSensor
class LinearWindSpeedSensor : public LinearHistoricalSensor
class AnotherCalibratingSensor : public BaseCalibratingSensor
class AnotherHistoricalSensor : public AnotherCalibratingSensor
class AnotherTrendSensor : public AnotherHistoricalSensor
class AnotherTemperatureSensor : public AnotherTrendSensor
class AnotherPressureSensor : public AnotherTrendSensor
class AnotherHumiditySensor : public AnotherHistoricalSensor
class AnotherWindSpeedSensor : public AnotherHistoricalSensor
По нашему мнению, ошибка та же, что и в случае с ножками столов, равно как и в случае с датчиками теплицы.
Проблема вторая. Для каждой измеряемой величины зачем-то заведен свой класс, и все эти классы сгруппированы в одну иерархию. На наш взгляд, в этом нет никакого смысла.
Давайте внимательно посмотрим на интерфейс класса Sensor. Мы видим, что единственно полезными функциями являются:
double currentValue();
double rawValue();
Остальные методы класса:
std::string name();
int id();
предназначены лишь для идентификации датчика.
Для того чтобы можно было работать со всеми "датчиками" одинаковым образом, вся необходимая информация должна запрашиваться у "датчика" только при помощи интерфейса базового класса:
double currentValue();
double rawValue();
Но мы видим, что это невозможно. Каждый класс иерархии добавляет к интерфейсу свои функции, без которых не обойтись:
- "датчик" WindDirectionSensor, измеряющий направление ветра, добавляет метод currentDirection;
- "датчик" CalibratingSensor, выполняющий калибровку измеренного параметра, добавляет методы setLowValue и setHighValue;
- "датчик" HistoricalSensor добавляет методы highValue и lowValue;
- "датчик" TrendSensor добавляет метод trend;
- и т.д.
Следовательно, в такой системе, для того чтобы выполнить операцию, которой нет в базовом классе, нужно преобразовать базовый "датчик" к производному классу.
Необходимость в преобразовании "датчиков" возникает, например, в момент рисования. Для вывода значений на экран используется класс DisplayManager:
class DisplayManager
{
public:...
void display(Sensor&);
...
};
Рисование осуществляет метод display этого класса. На вход он получает ссылку на Sensor. Чтобы отобразить, например, температуру, метод display должен:
- преобразовать Sensor к TemperatureSensor;
- извлечь из TemperatureSensorтекущее, минимальное и максимальное значения температуры и тренд;
- вывести все это на экран.
void DisplayManager::display(Sensor & rSensor)
{
if (typeid(rSensor) == typeid(TemperatureSensor))
{
TemperatureSensor & rTemperatureSensor = dynamic_cast(rSensor);
displayTemperature(rTemperatureSensor.currentTemperature(),
rTemperatureSensor.lowValue(),
rTemperatureSensor.highValue(),
rTemperatureSensor.trend(),
rTemperatureSensor.id());
}// ...
}
Поскольку помимо температуры существует еще множество других характеристик, которые надо отображать, функция display превратится в "портянку":
void DisplayManager::display(Sensor & sensor)
{
if (typeid(sensor) == typeid(TemperatureSensor))
{
TemperatureSensor & temperatureSensor = dynamic_cast(sensor);
// Получение и рисование параметров температуры.
}
else if (typeid(sensor) == typeid(PressureSensor)
{
PressureSensor & pressureSensor = dynamic_cast(sensor);// Получение и рисование параметров давления.
}
else if ...// И т.д.
}
В аналогичную "портянку" превратится любая другая функция для вывода конкретных данных (например, "на печать" или "в файл").
По нашему мнению, ошибка та же, что и в случае с ножками столов, равно как и в случае с датчиками теплицы.
Подобные ошибки допущены из-за анализа подсистемных элементов в отрыве от общего контекста. Грубо говоря, Гради Буч должен был сконцентрировать внимание не на том, как устроены подсистемы, а на основных функциях программы, которые было бы вполне логично (и удобно) "овеществить в контексты", которые, в частности, можно реализовать и в виде классов:
- Получальщик данных "откуда скажут" (хоть с any-датчиков, хоть с видеокамеры)
- Калибровальщик (а лучше "Справочник операций над данными", где калибровка – одна из позиций)
- Хранильщик "чего скажут" (например, экстремумов и т.д.)
- Потрендельщик (который с трендами)
- Выводильщик "куда надо, откуда укажут"
Буч нашел подсистемные сходства и различия между датчиками. Сходства вынес в родительские классы, а различия поместил в производные классы. Но в этом не было смысла, т.к. контекст использования был не учтен. Из-за того, что Гради Буч разнес различия по производным классам (которые вывел из подсистемных объектов), код функции display превратился в набор операторов if и dynamic_cast.
Кстати сказать, эти две программы (про теплицу и про метеостанцию) в книге рассматриваются как разные. А, собственно говоря, почему это две программы, а не одна?
Можно сказать: там же теплица, а здесь – метеостанция и т.д. Но эту мысль мы готовы вычитать откуда угодно, только не из учебника по абстрагированию.
ИСТОРИЯ 3. ДЕМОКРАТИЧНАЯ. "ВСЁ ЕДИНО (ЕСЛИ ВСЁ ДЕЛАТЬ НЕПРАВИЛЬНО)"
Но оставим великих в покое и демократично рассмотрим другие аналоги. Применим наш излюбленный прием и рассмотрим СРАЗУ НЕСКОЛЬКО характерных примеров, начиная с уже известного нам, и попробуем понять, что в них общего.
Такой способ изложения органичен для данного материала – ведь он посвящен тому, как обрабатывать сразу множество сущностей.
ПРИМЕР 3.1. В задаче описанной здесь:
класс "выводится из подсистемы" - объекта "Фигура", в этом и заключается "причина задачи".
Примечание: В коде (ниже) оставлены только строки, иллюстрирующие описываемую мысль.
class CFigure
class CBezier2 : public CFigure
class CWindowBezier2: public CBezier2
class CMemoryBezier2: public CBezier2
class CBezier3 : public CFigure
class CWindowBezier3: public CBezier3
class CMemoryBezier3: public CBezier3
class CEllipse : public CFigure
class CWindowEllipse: public CEllipse
class CMemoryEllipse: public CEllipse
Ситуацию можно ухудшить, если объект "Фигура" конкретизировать.
class CEllipse
class CEllipse1 : public CEllipse
class CWindowEllipse1: public CEllipse1
class CMemoryEllipse1: public CEllipse1
class CEllipse2 : public CEllipse
class CWindowEllipse2: public CEllipse2
class CMemoryEllipse2: public CEllipse2
class CBezier
class CBezier2 : public CBezier
class CWindowBezier2: public CBezier2
class CMemoryBezier2: public CBezier2
class CBezier3 : public CBezier
class CWindowBezier3: public CBezier3
class CMemoryBezier3: public CBezier3
Ситуацию можно еще ухудшить, если увеличить степень детализации.
Ниже только для эллипса
class CEllipseDekart
class CEllipseDekart1 : public CEllipseDekart
class CWindowEllipseDekart1: public CEllipseDekart1
class CMemoryEllipseDekart1: public CEllipseDekart1
class CEllipseDekart2 : public CEllipseDekart
class CWindowEllipseDekart2: public CEllipseDekart2
class CMemoryEllipseDekart2: public CEllipseDekart2
class CEllipseSimple
class CEllipseSimple1 : public CEllipseSimple
class CWindowEllipseSimple1: public CEllipseSimple1
class CMemoryEllipseSimple1: public CEllipseSimple1
class CEllipseSimple2 : public CEllipseSimple
class CWindowEllipseSimple2: public CEllipseSimple2
class CMemoryEllipseSimple2: public CEllipseSimple2
По нашему мнению, очень похоже на случаи с датчиками и со столами.
Решение см. здесь.
ПРИМЕР 3.2.
В современных программах для коммуникации с пользователем используются окна. Окна могут быть разных видов: обрамляющие, всплывающие, дочерние, диалоги, тулбары, списки и т.д.
При реализации оконной системы в одной из программ было принято решение выводить все окна из одного базового класса – CWindow.
Для управления поведением окна ему можно послать сообщение. Для этого в классе CWindow имеется функция SendMessage. В качестве параметра она принимает ссылку на сообщение – объект класса CMsg.
class CMsg {...};
class CWindow
{
public:
...
long SendMessage(const CMsg & rMsg);
};
Некоторые окна (например, главные окна приложений или диалоги) могут иметь дочерние окна, а некоторые (например, кнопки или тултипы) – нет.
Поскольку хорошим стилем в объектно-ориентированном программировании является разделение сущностей и инкапсуляция различий, разработчиком оконной системы было принято решение для окна, имеющего дочерние окна, завести отдельный класс – CParentWindow. Класс был выведен из CWindow, и в него был помещен массив указателей на дочерние окна.
class CParentWindow : public CWindow
{
...
private:std::vector m_Children;
};
Таким образом, согласно логике разработчика, все окна, которые содержат или могут содержать дочерние, должны выводиться из класса CParentWindow, а все остальные – из CWindow.
Такое решение хорошо работает до тех пор, пока не потребуется переслать какое-нибудь сообщение от окна верхнего уровня ко всем окнам нижнего уровня. Для реализации этой задачи можно добавить функцию SendMessageToChildren в класс CParentWindow.
class CParentWindow : public CWindow
{
public:
...
void SendMessageToChildren(const CMsg & rMsg);private:
std::vector m_Children;
};void CParentWindow::SendMessageToChildren(const CMsg & rMsg)
{
// Получаем количество дочерних окон.
const size_t nCount = m_Children.size();// Перебираем все дочерние окна.
for (size_t i = 0; i < nCount; i++)
{
// Получаем очередное дочернее окно.
CWindow * pWindow = m_Children[i];
ASSERT(pWindow != NULL);// Проверяем: является ли это окно
// окном, содержащим дочерние окна.
// И если является, то преобразуем указатель
// к соответствующему типу.
// Такая операция преобразования слишком дорога.
CParentWindow * pWnd =
dynamic_cast(pWindow);// В зависимости успешности или
// не успешности преобразования
// пересылаем сообщение окну и его дочерним окнам
// либо только самому окну.
if (pWnd)
pWnd->SendMessageToChildren(rMsg);
else
pWindow->SendMessage(rMsg);
}SendMessage(rMsg);
}
Код функции SendMessageToChildren очень тяжел. Во-первых, для того чтобы преобразовать указатель на объект CWindow к указателю на объект CParentWindow, в коде стоит вызов оператора dynamic_cast.
Такая операция является дорогой в плане времени выполнения и применяется к каждому указателю на дочернее окно. Во-вторых, функция вынуждена обрабатывать два противоположных случая: когда преобразование удалось и когда не получилось. Соответственно, используется оператор if.
По нашему мнению, очень похоже на случаи с датчиками, со столами и с эллипсами.
Решение: Объявляем, что все окна могут иметь дочерние.
Если кажется, что это не так (например, Вас удивляет, зачем "кнопке" иметь дочерние окна), скажите себе: "Лучше иметь 1 избыточную переменную и компактную программу, чем громоздкую программу без избыточных переменных".
Функция избыточной переменной у кнопки - "устранять лишнюю логику". Очень полезная "кнопочная избыточность".
Создаем контекст, относительно которого все окна принадлежат одной сущности. Поэтому нам не нужны if.
class CWindow
{
public:
...
void SendMessageToChildren(const CMsg & rMsg);private:
std::vector m_Children;
};void CWindow::SendMessageToChildren(const CMsg & rMsg)
{
// Получаем количество дочерних окон.
const size_t nCount = m_Children.size();// Перебираем все дочерние окна.
for (size_t i = 0; i < nCount; i++)
{
// Получаем очередное дочернее окно.
CWindow * pWindow = m_Children[i];
ASSERT(pWindow != NULL);pWindow->SendMessageToChildren(rMsg);
}SendMessage(rMsg);
}
ПРИМЕР 3.3.
Тулбар представляет собой контейнер, содержащий элементы разных типов: кнопки, окна редактирования, комбинированные списки и т.д. При изменении формы или размеров тулбара запускается алгоритм, который переразмещает дочерние элементы.
class CItem {...};
class CComboBoxItem : public CItem {...};
class CEditItem : public CItem {...};сlass CToolbar
{
public:
...
void Layout();private:
std::vector m_Items;
};
Хорошо бы реализовать алгоритм размещения таким образом, чтобы он учитывал особенности каждого элемента. Если тулбар из горизонтального становится вертикальным, необходимо спрятать широкие элементы, например комбинированные списки или окна редактирования. Но для того чтобы прятать элементы, алгоритм должен знать тип этих элементов, а это накладывает серьезные ограничения. При добавлении нового типа элемента будет недостаточно написать новый класс. Потребуется изменить и код алгоритма размещения элементов тулбара.
void CToolbar::Layout()
{
const size_t nSize = m_Items.size();for (size_t i = 0; i < nSize; i++)
{
CItem * pItem = m_Items[i];
...
CComboBoxItem * pComboBoxItem =
dynamic_cast(pItem);
if (pComboBoxItem)
{
...
}
...
CEditItem * pEditItem =
dynamic_cast(pItem);
if (pEditItem)
{
...
}
}
}
Уже сейчас при наличии всего двух исключений – комбинированного списка и окна редактирования – мы видим, что алгоритм Layout становится неуклюжим. А теперь давайте представим, что исключений не 2, а 5, 10, 20. Каким станет алгоритм, если мы будем следовать той же логике, что и разработчики предыдущих задач?!
По нашему мнению, очень похоже на случаи с датчиками, столами, эллипсами и окнами.
Решение: Вместо логики: "Если объект такой-то, делай так, а иначе – эдак…", предлагается собрать все "так, эдак, сяк и разэтак…" в общий Контекст "Способы размещения". При этом способ своего размещения сообщает алгоритму сам объект.
Т.е. группируем операции в контекст. См. код:
enum ILayoutType
{
eNormal,
eVertHide,
...
};class CItem
{
public:
...
ILayoutType GetLayoutType() const;protected:
...
ILayoutType m_iLayoutType;
};class CButtonItem : public CItem
{
public:
...
CButtonItem()
{
m_iLayoutType = eNormal;
}
};class CComboBoxItem : public CItem
{
public:
...
CComboBoxItem()
{
m_iLayoutType = eVertHide;
}
};class CEditItem : public CItem
{
public:
...
CEditItem()
{
m_iLayoutType = eVertHide;
}
};void CToolbar::Layout()
{
const size_t nSize = m_Items.size();for (size_t i = 0; i < nSize; i++)
{
CItem * pItem = m_Items[i];
...
if (pItem->GetLayoutItem() == eVertHide)
{
...
}
}
}
ПРИМЕР 3.4.
Программа, предназначенная для создания ландшафтов для авиационного симулятора, имеет пять окон редактирования:
- Редактор поверхности – создает ландшафтную поверхность.
- Редактор сцен – создает сцены, которые затем расставляются на "земле".
- Редактор сцен на земле – расставляет созданные сцены по поверхности.
- Редактор дорог – прокладывает автомобильные и железные дороги.
- Редактор рек – создает реки.
В каждый момент времени пользователь может работать только с одним окном. Остальные окна в этот момент спрятаны и неактивны. Пользователь переключается между пятью редакторами при помощи кнопок-закладок или меню. Пользователю очень неудобно.
Программист оправдывался тем, что каждый редактор имеет свой уникальный набор режимов редактирования. Только 2 режима (масштабирование и перемещение) из 50-ти режимов являются общими.
Однако полезно взглянуть на первопричину. Почему получилось столько уникальных режимов. А причина заключалась в следующем: для каждого объекта – для поверхности, для сцены, для сцены на земле, для дороги, для реки – было принято решение создать свое особое окно редактирования.
Таким образом, необходимость переключаться из окна в окно была заложена разработчиком уже на этапе проектирования. Это и привело к тому, что локальные режимы редактирования (для каждого объекта!) создавались без какой-либо системы. И потому этих режимов стало так много.
По нашему мнению, очень похоже на случаи с датчиками, столами, эллипсами, окнами и тулбаром.
Решение: Между тем правильнее было бы начать проектирование не с окон и не с объектов, а с группировки операций, которые над этими объектами выполняются, в общие контексты.
Например, операции по добавлению, вставке, перемещению и удалению точки характерны для фигур из "Редактора поверхности", для дорог из "Редактора дорог" и для рек из "Редактора рек".
Перед тем, как сделать резюме, вернемся к задаче про стол, с которой мы начали статью. Мы видим, что для каждого количества ножек, для каждого вида крепления столешницы программист пишет свой особый код и, тем самым, размножает сущности. Он видит различия и концентрируется на них.
Между тем ему следовало собрать все виды крепления – ножки, шарниры, канаты и т.п. – в единую группу "any-крепления" и, таким образом, вынести все различия за пределы класса стола:
class CTable
{
public:// ...
private:
// ...
CTableTop m_TableTop;
Any-крепление m_Крепление;
};
Любые операции, выполняемые над креплением, можно делегировать классу "any-крепление". Сам же класс можно организовать в виде справочника:
РЕЗЮМЕ
Сделаем выводы и сформулируем некоторые отличия от общепринятых трактовок (отличия, впрочем, небольшие):
1. Когда класс "выводится из подсистемы", то получается "вверх ногами" - это все равно, что писать роман "из букв" с помощью полиморфного наследования. Всегда ошибетесь с контекстом, если будете класс "выводить из подсистемы".
Большой дом не строится из маленьких домиков. Большой автомобиль не делается из маленьких автомобилей.
Назовем эту ошибку "Типовая ошибка N 1. "Дом из форточек". Мы твердо знаем, что и без нас она известна многим. Но и формализовано описать ее тоже не помешает.
2. Когда Ваш оператор if привязан к объекту, есть вероятность, что данную ошибку Вы совершили. Часто бывает и обратная ситуация: в сильноветвистых программах, оператор if привязан к объекту. Потому они и ветвистые. Хотя, конечно, их разработчики с этим не согласятся.
А если Вы сталкиваетесь с типовыми противоречиями "Много-Мало" или "Одно – Другое", то тоже проверьте, не является ли это противоречие следствием совершения данной типовой ошибки.
3. С другой стороны, мы понимаем, что такое "выведение из подсистемы" является невольным и может быть порой даже неизбежным следствием общепринятой ООП-трактовки: "Класс – это тип и объект (переменная) – экземпляр его".
Но в какой-то момент надо сделать шаг вперед. Есть более удобные, чем "тип", термины: "однородность", "контекстность". Мы будем говорить, что сущности однородны, если они принадлежат одному контексту.
Так, во всех перечисленных выше разных примерах допущена одна и та же ошибка, и в этом смысле все эти разные примеры - однородны.
А если мы заведем группу "Разный неструктурированный мусор", то все помещенное в эту группу будем считать однородным с точки зрения данного контекста1. И реализовать эту группу мы можем по-разному: в виде класса, в виде массива, в виде таблицы, в виде стека, в виде матрицы и т.д.
4. Значит, можно трактовать класс не столько как тип и не столько как общий случай нашего частного объекта, сколько как один из контекстов для функций, частный случай группировки функций - место, где функции ("методы") лежат "пачкой".
Эта трактовка совсем уж нарушает общепринятую ООП-традицию, но и не отрицает ее – просто сводит к частному случаю.
Нильс Бор, кстати, писал о том, что, по мере развития, старые теории не отрицаются, а входят в новые теории в качестве частного случая.
Иначе говоря, нельзя мыслить так:
Мысль-1: Вот есть класс
Например, "Атом водорода".
Или "Морковка"Мысль-2: Он является потомком класса
Например, "САтомы"
Или "СОвощи"Мысля так (снизу вверх), точно "залетим с контекстами".
Потому что с тем же успехом атом водорода мог бы логичнее (в зависимости от обстоятельств, а также от того, какую "фичу" попросят написать в будущем) оказаться в контексте "СВода" или в контексте "СНефть" или в контексте "СОткрытия_Кавендиша" и т.д.
А морковка – в "ССалатах", в "ССнеговиках", в "СПучках" и т.д.
Лучше мыслить так (сверху вниз):
Мысль 1: Есть функции, которые должны выполняться.
Мысль 2: Имея цель минимизировать число сущностей и сократить код (ибо хочется дружить с братом краткости) без ущерба для выполнения функций, какой контекст я для их группировки придумаю, т.е.
2.1. С какой точки зрения я посмотрю на них, чтобы сгруппировать (разнородное сделать однородным)?
2.2. Каким образом я этот контекст реализую?
- в виде класса (частный случай);
- в виде массива (другой частный случай);
- в виде стека (третий частный случай);
- в виде таблицы (четвертый частный случай);
- в виде диапазона (пятый частный случай);
- …….. ……………………………………………..
- в виде матрицы (N-ый частный случай);
- в виде формулы (N+1-й частный случай)
Мысль 3: Какие сущности (объекты?) я должен создавать (генерировать) на основе полученного контекста?
Мысль 4: Предположим, программа будет развиваться. Потребуются новые (какие - пока не знаю, any) возможности. Мысленно увеличу "поток" новых требований:
а) по количеству;
б) по разнородности."Расползаются" ли мои группировки по п.п. 2-3? Какой контекст я должен придумать, чтобы "не расползались"?
Мысль 5: В идеале созданный мной контекст должен быть таким, чтобы инкапсулированные в нем функции (методы) ничего не знали о том, кого они обслуживают. "Кого надо, того и обслужат". Обслужат любую ("any") сущность.
Введенное С.В. Сычевым слово "ANY", которые мы в дальнейших материалах часто будем использовать - это полезное идеализированное ("модельное") понятие.
Так, в физике есть понятия "абсолютно черного тела" (оно полностью поглощает все падающее на него электромагнитное излучение и его не видно по определению), "идеального газа" - газа, в котором силы взаимодействия между частицами (атомами, молекулами) пренебрежимо малы и др.
Есть также мнимые и бесконечно-малые величины - в математике, а в ТРИЗ есть "идеальная машина" - машина, которой нет, а ее функции выполняются.
Максвелл представлял себе демона, который выполнял невыполнимое, а именно нарушал второй закон термодинамики.
Все эти, казалось бы, разные понятия имеют общее: они упрощают сложные задачи. Дают ориентир в многомерных ситуациях. Даже простой икс (x) в математическом уравнении - тоже идеальное понятие, которое позволяет упростить процедуру решения этого уравнения.
Так и разработчику программных продуктов для целей абстрагирования, необходим образ предельно экономичной и максимально удобной функции, которая к тому же вовсе не обладает никакой связностью. Т.е. не зависит от контекста.
Чистая функция (чистый метод), которая "не знает", кого она обслуживает, и это незнание никак ей не мешает. Если Вы такую написали, то мы будем говорить, что Вы написали функцию "any". (Не всегда это возможно, но это идеал, к которому стоит стремиться.)
Мысль 6: Могу ли я сразу проектировать всякий проект "с поправкой на "any"?
ПРИМЕР 3.5. Сыну одного из авторов в кружке дали задание написать программу, которая смешивает цвета (методом случайных чисел) и выводит их на экран. Меняя параметры на входе, можно получать разные узоры. (Это, впрочем, стандартное задание в программистских кружках).
Но было бы полезно переформулировать эту стандартную задачу:
"Написать программу, которая смешивает "что прикажут" методом "какой укажут" и выводит "куда надо".
Или в наших терминах: "Написать программу, которая смешивает "any-things" методом "any-meth" и выводит "to_any_where".
Затраты на написание программы при обеих формулировках одинаковые. А поддерживать вторую легче. Достаточно регистрировать новую сущность в соответствующих справочниках.
И пока на этом завершим.
ЛИТЕРАТУРА:
- Г.С. Альтшуллер. Творчество как точная наука. — 2-е изд., доп. — Петрозаводск: Скандинавия, 2004.
- Сайт Официальфного Фонда Г.С. Альтшуллера
- Электронная книга "Введение в ТРИЗ. Основные понятия и подходы"
- Г. Буч. Объектно-ориентированный анализ и проектирование с примерами приложений на C++, 2-е изд./Пер с англ., М.: "Издательство Бином", СПб: "Невский диалект", 1998 г.
- Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес. Приемы объектно-ориентированного проектирования. Паттерны проектирования. СПб: Питер, 2001.368 с.ил.
- Петер Коуд, Дэвид Норт, Марк Мейфилд. Объектные модели. Стратегии, шаблоны и приложения. М.:Издательство "ЛОРИ", 1999. 434 с.ил.
- Мартин Фаулер. Рефакторинг: улучшение существующего кода., Пер. с англ., СПб: Символ-Плюс, 2003., 432 с.ил.
- Алан Шаллоуей, Джеймс Р. Тротт. Шаблоны проектирования. Новый подход к объектно-ориентированному анализу и проектированию: Пер. с англ., М.: Издательский дом "Вильямс", 2002., 288 с. ил.
Материал опубликован на сайте "Открытые методики рекламы и PR "Рекламное Измерение" 1 марта 2006 г.
1 Конечно, в известном смысле можно сказать и так: "Сущности, принадлежащие типу "Разнородное" - авт.