Проектирование классов в шутку и всерьез

Проектирование классов в шутку и всерьез

Евгений Каратаев

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

Как проектировать классы?

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

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

Шаг 1

Следует взять и потрясти заказчика (или самого себя, если сам все придумал) на тему получить побольше описания темы на русском языке. Надо именно на русском. Пусть пишет. Или даст статью в журнале. Тоже бывает. Увидит шеф статью и загорится на тему "а мне тоже надо". После этого следует просмотреть текст и выбросить из него все, что к делу не относится. После чего дописать к нему то, что не упомянуто, а по идее должно быть. После таких исправлений русскоязычного текста его следует перечитать и немножко погордиться, поскольку первый этап выполнен. 

Пример, что может получиться в результате: 

В лесу родилась елочка, в лесу она росла. 

Зимой и летом стройная, зеленая была. 

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

Шаг 2

Второй шаг состоит из того, что выписываем отдельно слова, являющиеся существительными, прилагательными, глаголами, союзами и другими частями речи. Кто давно ничего не писал на других языках кроме C++, Object Pascal, Java или других, тот может проконсультироваться у своих младших товарищей, какое слово чем является. В нашем примере получится: 

существительные:    лес, елочка, зима, лето 

глаголы:            родиться, рости, быть 

прилагательные:     стройная, зеленая 

союзы:              и 

местоимение "она" позорно отнесем к существительным, а предлог "в" - к союзам. 

Шаг №2 закончен. 

Шаг 3

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

Каждому имени нарицательному ставим в соответствии имя класса и называем его наследником некой иерархии. 

Каждому имени собственному ставим в соответствие объект как экземпляр некоего нарицательного класса. В нашем примере имени собственного нет, поэтому я, насупив бровь, могу смело заявить, что разбор примера с именем собственным есть домашнее задание и что вообще "легко видеть, что". 

Каждому прилагательному ставим в соответствие класс и называем его базовым. Еще лучше - абстрактным. Если язык позволит. Тем, кто пишет на Java, вообще повезло - прилагательным следует сопоставлять интерфейс в том виде, в котором он определен в Java. 

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

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

В нашем примере получим: 

Базовые классы: 

TСтройный, TЗеленый. 

Классы - наследники, которые можно завести: 

ТЛес, ТЕлочка, ТЗима, ТЛето. 

Функции: 

Родиться, Рости, Быть. 

Союз "и" однозначно указывает на наличие набора. 

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

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

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

Существительные, имена нарицательные, отображаются в классы, по которым реально могут быть заведены объекты. Основным отличием существительных есть понятие "существование". Если нечто является существительным, то это нечто может существовать или не существовать. В программировании прямым аналогом является вписывание в архитектуру классов однозначно выделенного класса, отвечающего именно за возможность заведения объекта. В Delphi это класс TObject, в MFC - CObject, в Java - тоже что-то типа Object. Классы, которые могут иметь представителей (по которым можно создать объект), практически всегда есть наследники такого рода базового класса с добавлением собственного поведения и / или с множественным наследованием ранее объявленных интерфейсов. При этом интерфейс базового объекта, скажем TObject, реализует именно и только интерфейс существования. Для некоторых средств программирования такой объект действительно выглядит как некое почти материальное ядро, обрастающее другими интерфейсами. Но это необязательно так. Например, в том же COM функции существования выполняются через интерфейс IUnknown. 

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

Глаголы отображаются на функции. Функции есть реализация алгоритма, последовательности неких действий. Это не данные и не определения. Функции отличаются от иных понятий тем, что они выполнимы и при выполнении воздействуют на что-то. Функция может что-то преобразовать, перевести объект из одного состояния в другое, либо передать информацию без изменений. Прилагательное характеризуется набором глаголов. Верно и обратное - набор глаголов определяет прилагательное, поскольку понятие определяется набором отношений, в котором оно участвует. Например, прилагательное "объемный" может характеризоваться набором глаголов "вмещать", "сравнить". При этом, что интересно, если человеку перечислить набор глаголов, который характеризует некое прилагательное, то он достаточно быстро отгадает это прилагательное. Набор глаголов, входящих в описание прилагательного, есть те ниточки, за которые можно дергать объект, к которому это прилагательное относится. Глагол во многих случаях программистами не относится явно ни к какому прилагательному, и при описании класса программист просто наследует класс TObject и добавляет ему функции. На самом деле это так же есть определение прилагательного, но, если можно так сказать, явно не именованного. При этом программист мог бы существенно выиграть, если бы добавляемые функции явно оформил бы в виде интерфейса. Пригодилось бы на будущее. 

Наличие союзов вида "и", "или", "не", "либо" явно свидетельствует о существовании предикативных условий или наборов данных. Если союз связывает аргументы некоего действия, то это формулировка предиката, если результата действия - то формулировка набора. 

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

Шаг 4

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

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

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

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

Довольно интересным является вопрос о союзе "и". По исходному тексту, функция "быть" возвращает набор из существительных "зима" и "лето". Если отнести эти существительные к разным классам, то получим набор разнотипных данных. Это на мой взгляд некрасиво и лучше провести модификацию схемы классов. Проведем укрупнение классов "зима" и "лето" путем отнесения их к одному классу и различением их признаком "время года". Исходной иерархией является: 

class ТЗима : public TObject {}; 

class TЛето : public TObjct {}; 

Результирующей иерархией является укрупненная или агрегированная: 

enum ЕВремяГода = { зима, лето}; 

class ТВремяГода : public TObject 

{ 

    public: 

        ЕВремяГода ВремяГода; 

}; 

Если целевой язык программирования позволяет, то следует определиться со спецификатором константности аргументов, передаваемых по ссылке, и константности функций по признаку - изменяет функция состояние объекта или нет. Если функция не изменяет состояние объекта, то, очевидно, она может быть объявлена константной. Кстати, если в результате проектирования классов мы получим классы с только константными функциями, то смело можно сказать, что исходный текст содержал описание статического состояния объектов. Если же в иерархии полученных классов все функции только константные, то значит схема классов безжизненная. И после этого можно идти обратно к постановщику и громко вопрошать: "И???" 

Шаг 5

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

В нашем примере может получиться что-то типа: 

#include ... 

class TЛес : public TObject {}; 

enum ЕВремяГода { зима, лето}; 

class ТВремяГода : public TObject 

{ 

        ЕВремяГода ВремяГода; 

    public: 

        const ЕВремяГода GetВремяГода() 

        { return ВремяГода}; 

        ТВремяГода( const ЕВремяГода& _ВремяГода) 

        : TObject(), ВремяГода(_ВремяГода) 

        {}; 

}; 

// здесь есть два варианта и второй в том, чтобы объявить 

// время года как typedef ЕВремяГода ТВремяГода; 

// какой вариант выбрать - дело вкуса. 

// я выбрал первый потому, что он позволяет запретить 

// изменение времени года для созданного объекта. 

typedef vector <ТВремяГода> ВременаГода; 

enum Фигура { никакая, стройная}; 

enum Цвет { никакой, зеленый}; 

class TФигурный 

{ 

    public: 

        // получение фигуры, зная времена года 

        const virtual Фигура Быть( ВременаГода& ) = 0; 

        // получение времени года, зная фигуру 

        const virtual ВременаГода Быть( Фигура) = 0; 

}; 

class TЦветной 

{ 

    public: 

        // получение цвета, зная времена года 

        const virtual Цвет Быть( ВременаГода& ) = 0; 

        // получение времени года, зная Цвет 

        const virtual ВременаГода Быть( Цвет) = 0; 

}; 

class ТЕлочка 

: public TObject, 

  public ТФигурный, 

  public ТЦветной 

{ 

    public: 

        const virtual Фигура Быть( ВременаГода& ); 

        const virtual ВременаГода Быть( Фигура); 

        const virtual Цвет Быть( ВременаГода& ); 

        const virtual ВременаГода Быть( Цвет); 

        const ТЛес Родиться(); 

        const ТЛес Рости(); 

        ТЕлочка();  // конструктор вызывается, когда елочка родится 

        ~ТЕлочка(); 

        // деструктор вызывается не тогда, когда елочку срубят, 

        // а тогда, когда после Нового Года выбросят на помойку 

        // и бедный дворник должен будет ее сжечь, поскольку 

        // елки в контейнеры грузить нельзя ;) 

}; 

// получили две функции Быть с одинаковым аргументом. 

// подобные проблемы следует решать по правилам применяемого 

// языка. В нашем случае придется поменять 

// имена функций на менее читабельные. 

В примере я хотел сначала писать на Object Pascal. Мысленно разговаривая с читателем, услышал шум и топот ног и выкрики с мест - "А почему не С++?". Подумал и согласился - пусть будет на C++. Конечно же, приведенная метода не есть абсолют. Как говорится, если победишь в бою с нарушением устава - молодец, творчески мыслишь, отбрасывая устаревшие догмы. Если проиграл соблюдая устав - негодяй, который не смог постичь вековые истины. Если данная метода кому поможет - то-то я порадуюсь... Мне лично иногда помогает, особенно когда голова уже ничего не соображает и приходится действовать на автопилоте. Или когда приходится разбираться в исходниках и отгадывать - что же автор хотел сказать. Осуществлять своего рода модный ныне реинжиниринг. Что интересно, потом на свежую голову просмотришь что понаписал и удивляешься своей прозорливости и вообще тому, что это работает.