3 Именование, назначение и использование

      Комментарии к записи 3 Именование, назначение и использование отключены

Главная Форумы Программирование Программирование на С++ Заметки о С++ 3 Именование, назначение и использование

Помечено: ,

В этой теме 1 ответ, 1 участник, последнее обновление  Васильев Владимир Сергеевич 2 мес. назад.

  • Автор
    Сообщения
  • #3574

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

    Основная идея

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

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

    Предположим, пользователя интересует некоторое множество объектов. Например, он хочет последовательно проитерировать некоторый контейнер, выполнив какие-то действия над его содержимым, и вы предоставляете ему такую возможность. При этом совершенно не зачем информировать пользователя о том, что же это за контейнер, std::vector, std::list или boost::array. Эта информация является лишней. Она только собьет пользователя с мысли. Ему гораздо важнее что хранится в контейнере, а не то, как оно там хранится. Представьте себе, что вы пришли в ресторан, заказали блюдо из говядины, сидите, ждете заказ, и тут вас вдруг заставляют изучить весь сельскохозяйственный процесс по выращиванию и забою крупного рогатого скота вместо того, чтобы дать спокойно насладиться едой.

    Старайтесь брать в typedef все шаблонные типы с явно указанными шаблонными параметрами. Это даст возможность скрыть от пользователя детали реализации и слишком обобщенные имена, позволив вам присвоить сущности более верное с точки зрения семантики имя.

    Плохо:

    struct node
    {
        boost::shared_ptr<node> next;
        std::vector<boost::shared_ptr<node> > children;
    };
    node root;
    std::vector<boost::shared_ptr<node> >::iterator it = root.children.begin();

    Гораздо лучше:

    struct node;
    typedef boost::shared_ptr<node> node_ptr;
    struct node
    {
        typedef std::vector<node_ptr> container;
        node_ptr next;
        container children;
    };
    node root;
    node::container::iterator it = root.children.begin();

    Назначение

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

    Здесь мы хотели установить заголовок окна, но потеряли назначение по дороге:

    void on_init(window& win)
    {
        win.send_message(window::set_title, "My first application");
    }

    Гораздо лучше:

    void on_init(window& win)
    {
        win.set_title("My first application");
    }

    Использование

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

    Запись в лог выглядит ужасно:

    std::stringstream stream;
    
    stream << get_current_date()
           << get_current_time()
           << get_current_thread()
           << "Application was successfuly initialized"
           << std::endl;
    
    log::instanse().write(log::info, stream.str());

    Уже лучше:

    // Сбор контекста спрятан внутри log::write
    log::instance().write(log::info, "Application was successfuly initialized");

    Прекрасно:

    // Сбор контекста и интерфейс синглтона спрятаны внутри log_message
    log_message(log::info, "Application was successfuly initialized");

    Подведем итоги

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

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

    Не создавайте сущностей, решающих сразу несколько задач. Каждая сущность должна решать одну конкретно поставленную задачу. Если на вопрос «Что делает ваша сущность?» вы не можете дать ясный ответ, состоящий их одного глагола и одного существительного, то, скорее всего, сущность нуждается в доработке.

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

    Помимо всего прочего, умение правильно именовать сущности вам особенно пригодится, если вам придется работать с языками, не имеющими статической типизации (например php, javascript, visualbasic и так далее).

    Напоследок

    Не самая удачная функция из Win32 Platform SDK:

    BOOL AccessCheckByTypeResultListAndAuditAlarmByHandle
    (
        LPCTSTR SubsystemName,
        LPVOID HandleId,
        HANDLE ClientToken,
        LPCTSTR ObjectTypeName,
        LPCTSTR ObjectName,
        PSECURITY_DESCRIPTOR pSecurityDescriptor,
        PSID PrincipalSelfSid,
        DWORD DesiredAccess,
        AUDIT_EVENT_TYPE AuditType,
        DWORD Flags,
        POBJECT_TYPE_LIST ObjectTypeList,
        DWORD ObjectTypeListLength,
        PGENERIC_MAPPING GenericMapping,
        BOOL ObjectCreation,
        LPDWORD GrantedAccess,
        LPDWORD AccessStatusList,
        LPBOOL pfGenerateOnClose
    );

  • #3959

    Дополнение…

    В языке Си есть несколько типов констант: целые константы, такие как 10, или 01C; константы с плавающей точкой, как 1.0 и 6.022e+23; символьные константы, такие как a, \x10. Так же Си поддерживает строковые литералы, такие как Hello World и \n. Все их можно назвать литералами.

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

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

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

    Объекты отмеченные const

    Такие объекты подчиняются правилам пространств имен и их типо-совместимость может быть проверена компилятором. Поскольку это именованные объекты (в отличие от макросов) некоторые инструменты отладки могут показывать их имя. Но объекты потребляют память.

    Для константных объектов можно указывать точный тип, как ниже:

    const unsigned int buffer_size = 256;

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

    — размер битового поля в структуре
    — размер массива (кроме массивов переменной длинны)
    — значение константы перечисления
    — значение константы в case

    В любом из этих случаев в качестве rvalue значения должен использоваться литерал.

    Зато можно получить адрес константного объекта.

    const int max = 15;
    int a[max]; /* неверно вне функции! */
    const int *p;
     
    /* можно получить адрес константного объекта */
    p = &max;

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

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

    enum  max = 15 ;
    int amax; /* Корректно вне функции */
    const int *p;
     
    p = &max; /* ошибка: нельзя получить адрес */

    Объекто-подобные макросы

    Программисты на Си часто определяют символические константы с помощью объектно-подобных макросов. Например:

    #define buffer_size 256

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

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

    Объектные макросы не хранятся в памяти как переменные, следовательно на них нельзя сделать указателя. Так же они не позволяют производить контроль типов, так как у них просто нет типа.

    Обобщим

    В следующей таблице собраны выше обозначенные различия между разными видами констант.

    МакросыПеречисленияconst объекты
    ПодставляетсяПрепроцессоромПри компиляцииВ рантайме
    Потребляет памятьНетНетДа
    Видно в отладчикахНетДаДа
    Проверка типаНетНетДа
    Вычисление во время компиляцииДаДаНет

    Смысл числа 18 не совсем ясен в данном примере:

    if age >= 18 
       /* Совершаем действия */
    
    else 
      /* Совершаем другое действие */


    Правильное решение

    Правильным решением будет замена целочисленного литерала на константу ADULT_AGE, которая сама говорит о своем назначении:

    enum  ADULT_AGE=18 ;
    /* ... */
    if (age >= ADULT_AGE )
       /* Совершаем действие */
    
    else 
      /* Совершаем другое действие */

    Пример с ошибкой №2

    Если вы надеетесь, что будете всегда помнить порядок следования параметров функций типа fgets, то можете писать и так:

    char buffer[256];
    /* ... */
    fgets(buffer, 256, stdin);

    Правильное решение (enum)

    Но гораздо понятнее будет с именованной константой:

    enum  BUFFER_SIZE=256 ;
     
    char buffer[BUFFER_SIZE];
    /* ... */
    fgets(buffer, BUFFER_SIZE, stdin);

    Правильное решение (sizeof)

    Справедливости ради нужно отметить, что читабельность кода можно повысить не только применением именованных констант, но и другими методами. Так выше приведенный пример можно переписать с использованием sizeof:

    char buffer[256];
    /* ... */
    fgets(buffer, sizeof(buffer), stdin);

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

    Пример с ошибкой №3

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

    LDAP *ld = ldap_init("localhost", 1234);
    if (ld == NULL) {
      perror("ldap_init");
      return 1;
    }

    Правильное решение

    Правильным решением будет определение макросов, которые можно передать с помощью опци -D командной строки компилятора.

     /* может быть передано через командную строку компилятора */
    #ifndef PORTNUMBER
    #  define PORTNUMBER 1234
    #endif
     
     /* может быть передано через командную строку компилятора */
    #ifndef HOSTNAME
    #  define HOSTNAME "localhost"
    #endif
     
    /* ... */
     
    LDAP *ld = ldap_init(HOSTNAME, PORTNUMBER);
    if (ld == NULL) {
      perror("ldap_init");
      return 1;
    }
    

    Исключение

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

    x = -b + sqrt(b*b) - 4*a*c / 2*a;

    читается намного удобнее, чем

    enum  TWO = 2 ;     /* a scalar */
    enum  FOUR = 4 ;    /* a scalar */
    enum  SQUARE = 2 ;  /* an exponent */
    x = -b + sqrt(pow(b)), SQUARE - FOUR*a*c/ TWO * a;

    То есть в первую очередь нужно руководствоваться здравым смыслом.

Для ответа в этой теме необходимо авторизоваться.