Приложения ТРИЗ к программированию
Приложения ТРИЗ к программированию. Пример 2. Неважно где рисовать
Для решения некоторых задач программирования в качестве "инструмента", облегчающего разработчику абстрагирование от конкретики, предлагается использовать "абсолютно тупого героя", которому поручается некая деятельность. Поскольку этот герой непроходимо туп (мы даже пишем его с маленькой буквы, чтобы и тени величия в нем не наблюдалось), то задача поручить ему деятельность, которая, тем не менее, должна быть выполнена — нетривиальна.
Тем не менее, надо описать решение так, чтобы тупой (несколько тупых) смогли качественно и незатратно порученную задачу выполнить.
Остальное понятно из контекста разбираемых примеров.
ПРИМЕР 2. "НЕВАЖНО ГДЕ РИСОВАТЬ..."
С.В. Сычев, идея, текст про тупого
К.А. Лебедев, задача
Шаг 0. Что мы имеем? (описание языком программиста)
Шаг 1. Абстрагируемся (с помощью тупого героя)
Шаг 2. Пишем абстрактное решение (понятное любому)
Шаг 3. Переводим "абстрактное решение" на привычный для программиста язык
ШАГ 0
ЧТО МЫ ИМЕЕМ?
В графический редактор, который "умеет" работать с линиями, прямоугольниками и многоугольниками, понадобилось добавить новую фигуру – кривую Безье второго порядка.
Это вот такая штуковина.
Кривая Безье 2-го порядка.
Согласно требованиям, фигура должна делать следующее:
- Во-первых, рисовать себя в окне.
- Во-вторых, проверять, попадает ли заданная точка на линию контура.
Поскольку вторая задача для кривых Безье аналитически неразрешима, она выполняется в два прохода:
- Сначала кривая Безье преобразуется в ломаную линию,
- а затем (уже для ломаной линии!) осуществляется проверка на попадание точки.
Получается, что для рисования фигуры в окне и для проверки попадания точки необходимо совершить одинаковое действие – растеризацию. Понятно, что это действие удобно выполнять при помощи одной и той же процедуры. В противном случае, возникает дублирование кода, которое, как известно опытным разработчикам, потенциально чревато ошибками.
Для устранения дублирования кода программист инкапсулировал алгоритм растеризации в базовом классе, а конкретные функции "рисования", вызываемые из алгоритма, поместил в производные классы. В результате, получились три класса: базовый и два производных. Базовый класс ответственен за алгоритм, производные – за рисование в окне и формирование набора точек.
// Базовый класс: содержит алгоритм растеризации
class CBezier2
{
public:
CBezier2(const POINT points[3]);
void Draw();
protected:
virtual void MoveTo(const POINT & pt) = 0;
virtual void LineTo(const POINT & pt) = 0;
private:
POINT m_points[3];
};
// Производный класс: реализует функции MoveTo и LineTo для рисования в окне
class CBezier2InWindow : public CBezier2
{
public:
CBezier2InWindow(HDC hDC, const POINT points[3]);
protected:
virtual void MoveTo(const POINT & pt);
virtual void LineTo(const POINT & pt);
private:
HDC m_hDC;
};
// Производный класс: реализует функции MoveTo и LineTo для рисования в памяти
class CBezier2InMemory : public CBezier2
{
public:
CBezier2InMemory(const POINT points[3]);
bool Contains(const POINT & pt) const;
protected:
virtual void MoveTo(const POINT & pt);
virtual void LineTo(const POINT & pt);
private:
std::vector m_PolyLine;
};
После того, как данное решение было реализовано, в программу потребовалось внести еще одно изменение. По просьбе Заказчика нужно было добавить кривую Безье 3-го порядка.
Это вот такая штуковина.
Кривая Безье 3-го порядка.
Если такое добавление делать аналогично, то придется снова создавать три класса. Как и в предыдущем случае, один из них будет отвечать за алгоритм, а два других – за рисование в окне и формирование набора точек.
// Базовый класс: содержит алгоритм растеризации
class CBezier3
{
public:
CBezier3(const POINT points[4]);
void Draw();
protected:
virtual void MoveTo(const POINT & pt) = 0;
virtual void LineTo(const POINT & pt) = 0;
private:
POINT m_points[4];
};
// Производный класс: реализует функции MoveTo и LineTo для рисования в окне
class CBezier3InWindow : public CBezier3
{
public:
CBezier2InWindow(HDC hDC, const POINT points[4]);
protected:
virtual void MoveTo(const POINT & pt);
virtual void LineTo(const POINT & pt);
private:
HDC m_hDC;
};
// Производный класс: реализует функции MoveTo и LineTo для рисования в памяти
class CBezier3InMemory : public CBezier3
{
public:
CBezier3InMemory(const POINT points[4]);
bool Contains(const POINT & pt) const
protected:
virtual void MoveTo(const POINT & pt);
virtual void LineTo(const POINT & pt);
private:
std::vector m_PolyLine;
};
Данное решение является трудоемким. При добавлении одной фигуры мы вынуждены добавлять три класса. Как же быть?
ШАГ 1
АБСТРАГИРУЕМСЯ
Один раз решили научить тупого молоть уголь и рисовать угольной крошкой по трафаретам на поверхностях и чуть не наломали дров. Сначала научили тупого рисовать углем "бабушку" на "окошке". Для этого, велели ему:
- "Молоть уголь" для "бабушки на окошке";
- Затем брать трафарет "бабушка" и рисовать "бабушку" на "окошке".
Кто придумал такую тупую инструкцию вспомнить уже невозможно — так, что материться глупо — надо работать. Поэтому с помощью воплей, дубины, «соцпакета» и 10-ти тупых администраторов кое-как упомянутый художественный результат достигался.
Причем 10 тупых администраторов периодически посещали семинары по мотивации тупых, где их учили более правильным "соцпакетам", в результате применения которых тупой должен был бы «раскрыться» и стать Личностью. А уж Личность смогла бы рисовать не только бабушек, но и птичек.
Однако, в связи с развитием рыночных отношений, задание усложнилось. Потребовалось рисовать «бабушку» уже не только на "окошке", но и на "заборе". Новая инструкция для тупого, развившись из старой, приняла такой вид:
- Намолоть угля для "бабушки на окошке";
- Взять трафарет "бабушка" и нарисовать "бабушку" на "окошке";
- Намолоть угля для "бабушки на заборе";
- Взять трафарет "бабушка" и нарисовать "бабушку" на "заборе"...
От такой сложной работы тупой запутался, закатил истерику и сказал, что "за такие бабки" он не будет рисовать бабушек, пусть администраторы рисуют бабушек, а он пойдет к девкам.
Администраторы с горя направились на новый семинар по соцпакетам, где напились, как свиньи.
И все бы закончилось плохо, если бы раздосадованный Клиент, устав ждать заказа, не ворвался на "художественный участок", не прочитал инструкцию, которая валялась рядом с тупым (который тоже валялся) и не усовершенствовал ее на свой лад за свои же деньги.
Клиент догадался (оно со стороны-то всегда видней), что функцию помола угля можно централизовать (да и трафарет используется один и тот же) и переписал инструкцию так:
- Намолоть угля для "бабушки" (не думая о том, где "бабушку" нарисуют);
- Взять трафарет "бабушка" и нарисовать "бабушку" на "окошке";
- Перейти к забору и нарисовать "бабушку" на "заборе".
Затем, дождавшись заказа, Клиент забожился на зуб не обращаться больше в такую тупую контору.
Тут, как раз, тупые администраторы вернулись с семинарского бодуна, увидели, что дело как-то само уладилось и увеличили тупому соцпакет, после чего он перестал вопить.
Шли годы. Кризис управления почти миновал, жизнь наладилась. Ибо, предполагалось, что, когда тупому потребуется изобразить «бабушку» на какой-либо иной поверхности, то тупому эту поверхность укажут и он также перейдет к ней и проделает ранее освоенные манипуляции.
Но, как водится, всегда есть люди, которым чужая радость, как трафарет в глазу. После какого-то очередного семинара на фирме завели маркетолога. И он развил бурную деятельность.
Стали поступать заказы не только на "бабушек", но и на "дедушек", "внучек обычных", "внучек эротических" и проч. Мало того. Рисовать надо было уже не только на "окнах" и "заборах", но и на "прохожих" и "животных".
Тупые администраторы вынуждены были творчески переработать инструкцию и вот, что у них получилось:
1.1. Намолоть угля для «бабушки»
1.2. Взять трафарет "бабушка" и нарисовать "бабушку" на "окошке";
1.3. Перейти к забору и нарисовать "бабушку" на "заборе";
1.4. Подбежать к прохожему и нарисовать "бабушку" на "прохожем";
1.5. Поймать животное и нарисовать "бабушку" на "животном";
2.1. Намолоть угля для "дедушки"
2.2. Взять трафарет "дедушка" и нарисовать "дедушку" на "окошке";
2.3. Перейти к забору и нарисовать "дедушку" на "заборе";
2.4. Подбежать к прохожему и нарисовать "дедушку" на "прохожем";
2.5. Поймать животное и нарисовать «дедушку» на "животном";
3.1. Намолоть угля для "внучки обычной"
3.2. Взять трафарет "внучка обычная" и нарисовать "внучку обычную" на "окошке";
3.3. Перейти к забору и нарисовать "внучку обычную" на "заборе";
3.4. Подбежать к прохожему и нарисовать "внучку обычную" на "прохожем";
3.5. Поймать животное и нарисовать "внучку обычную" на "животном";
4.1. Намолоть угля для "внучки эротической"
4.2. Взять трафарет "внучка эротическая" и нарисовать "внучку эротическую" на "окошке";
4.3. Перейти к забору и нарисовать "внучку эротическую" на "заборе";
4.4. Подбежать к прохожему и нарисовать "внучку эротическую" на "прохожем";
4.5. Поймать животное и нарисовать "внучку эротическую" на "животном"...
Тут даже тупой понял, что такое «дублирование кода»… И что же нам делать?
ШАГ 2
ОПИШЕМ АБСТРАКТНОЕ РЕШЕНИЕ
А вот что.
- Все "поверхности" поймали, собрали и построили рядом на площадке и пронумеровали (площадку назвали «Все поверхности»).
- Один тупой ("молотильщик") теперь там же централизованно молотит уголь, не важно для кого.
- Второй тупой ("носильщик") тупо его подносит к поверхностям.
- Третий тупой ("художник-примитивист") берет «неважно какой» трафарет из ящика ("Все трафареты"), смотрит какой у трафарета номер, орет (чтобы его услышал тупой носильщик): "Восемь, мать Вашу!" и идет к поверхности с таким же номером, где и рисует нетленное.
ШАГ 3
ПЕРЕВЕДЕМ "АБСТРАКТНОЕ РЕШЕНИЕ" НА ПРИВЫЧНЫЙ ЯЗЫК
"Вместо того чтобы мешать "фигуры" и "устройства" в одной иерархии классов, можно:
1) Отделить "фигуры" от "устройств".
class CDeviceContext;
class CBezier2
{
public:
CBezier2(const POINT points[3]);
void Draw(CDeviceContext & rDC);
private:
POINT m_points[3];
};
class CBezier3
{
public:
CBezier3(const POINT points[4]);
void Draw(CDeviceContext & rDC);
private:
POINT m_points[4];
};
2) Завести для всех "устройств" базовый класс. В нашем случае под устройством можно понимать окно или набор точек в памяти. Базовый класс будет задавать общий интерфейс работы с ними.
class CDeviceContext
{
public:
virtual void MoveTo(const POINT & pt) = 0;
virtual void LineTo(const POINT & pt) = 0;
};
class CWindowContext : public CDeviceContext
{
public:
CWindowContext(HDC hDC);
virtual void MoveTo(const POINT & pt);
virtual void LineTo(const POINT & pt);
private:
HDC m_hDC;
};
class CMemoryContext : public CDeviceContext
{
public:
CMemoryContext();
bool Contains(const POINT & pt) const;
virtual void MoveTo(const POINT & pt);
virtual void LineTo(const POINT & pt);
private:
std::vector m_PolyLine;
};
3) Получать желаемые изображения путем рисования фигуры на устройстве.
CWindowContext WindowContext(hDC);
CBezier3 Bezier3(points);
Bezier3.Draw(WindowContext);
Примечание.
Разбор этой же задачи иным способом - см. в статье: С.В. Сычев, К.А. Лебедев "Как вспомнить "и так известное".
Другие примеры раздела TStupid - см. здесь и здесь.
Материал опубликован на сайте "Открытые методики рекламы и PR "Рекламное Измерение" 1 июня 2005 г.