1. Вы находитесь в архивной версии форума xaker.name. Здесь собраны темы с 2007 по 2012 год, большинство инструкций и мануалов уже неактуальны.
    Скрыть объявление

Хрупкая красота программного кода

Тема в разделе "Разное", создана пользователем Koller, 11 ноя 2010.

  1. Koller

    Koller Исчез... Глобальный модератор

    Регистрация:
    3 ноя 2006
    Сообщения:
    401
    Симпатии:
    234
    Баллы:
    0
    Хрупкая красота программного кода:
    десять приёмов программирования, способных её разрушить

    Для меня программирование — это не только технология, но и, во многом — искусство. И, поэтому, большое значение имеет красота кода.

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

    1. Объявление всех переменных в начале программы;
    2. Возврат результата функции через её параметр;
    3. Отсутствие локальных функций;
    4. Отсутствие else if;
    5. Использование параллельных массивов;
    6. Обязательное хранение размера массива в отдельной переменной;
    7. Доступ к свойствам объекта через obj.getProperty() и obj.setProperty(value);
    8. Использование рекурсии для вычисления факториалов и Чисел Фибоначчи;
    9. Отсутствие именованных параметров функции;
    10. Невозможность объявления объектов «на лету».

    • Объявление всех переменных в начале программы
    В двух словах:
    Переменные должны объявляться в начале логического блока, в котором они используются, а НЕ в начале функции или программы.
    [​IMG]
    Все программные системы иерархичны. Программы делятся на пакеты, пакеты — на классы, классы разбиваются на отдельные функции.

    Данные, относящиеся к тому или иному модулю программы, принято объявлять в начале этого модуля. Локальные переменные объявляются в начале функции; свойства, относящиеся ко всему классу, объявляются в начале определения класса и т.д.

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

    Однако эти блоки — полноценные элементы в иерархии программы. И они тоже имеют право на собственные «локальные» переменные! Которые объявляются в начале этого блока и используются только в его пределах.

    И поэтому:
    Объявление всех переменных в начале функции — страшное зло [прим. 1]


    Это приводит к смешению переменных, относящихся ко всей функции, с переменными, относящимися только к её отдельному блоку.

    Это разрывает блок на две части: объявления данных (в начале функции) и использования этих данных (в самом блоке).

    Это усложняет комментирование блока: в одном месте мы комментируем переменные, но не знаем, как их использовать; в другом месте мы комментируем алгоритм, но не знаем, с какими данными он работает.
    Хуже всего, что существуют языки, которые считают себя умнее разработчика и заставляют объявлять все переменные в начале функции. Например, такой уважаемый язык как Pascal/Delphi. Чего я ему простить не могу…
    • Возврат результата функции через её параметр
    В двух словах:
    Функция должна возвращать результат, зависящий от её параметров, а НЕ принимать результат в качестве аргумента.
    [​IMG]

    Понятие функции (как в математике, так и в программировании) имеет чёткий смысл: вычисление результата, зависящего от аргументов.

    В нормальном программном коде ясно видно, что является результатом, а что аргументами: результат = функция (аргумент1, аргумент2).

    Однако часто встречается приём, при котором возвращаемое значение передаётся в качестве аргумента функции: функция (аргумент1, аргумент2, &результат).
    Этот приём ужасен. При его использовании не видно, от чего функция зависит, а что возвращает.​


    Чаще всего, в применении этого приёма виноваты не сами разработчики, а языки программирования.

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

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

    В данном случае от использования этого приёма можно избавиться, храня результат в том виде, который функция может возвратить.
    Код станет менее лаконичным (из-за объявления дополнительных структур), но, зато, гораздо более красивым.

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

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

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

    Слава богу, в других языках классы можно описывать прямо внутри функции, а, например в JavaScript можно просто возвратить объект, нигде отдельно не описывая его структуру.
    Вот это настоящая красота!

    • Отсутствие локальных функций
    В двух словах:
    Локальная функция должна объявляться внутри функции, которой она логически подчиняется, а НЕ в глобальном контексте.
    [​IMG]

    Как уже говорилось, программные системы (как объектно-ориентированные, так и процедурные) иерархичны и делятся на вложенные друг в друга модули (впрочем, это очевидно).

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

    Например, локальные переменные являются ресурсами модуля функции.

    Однако ресурсами функции являются не только переменные! Подфункции, классы, структуры и т.д. также являются полноправными ресурсами функции, подчинёнными ей, и используемыми только в её рамках.
     
    Последнее редактирование: 11 ноя 2010
    4 пользователям это понравилось.
  2. Koller

    Koller Исчез... Глобальный модератор

    Регистрация:
    3 ноя 2006
    Сообщения:
    401
    Симпатии:
    234
    Баллы:
    0
    • Использование параллельных массивов
    В двух словах:

    При работе с вложенными данными следует соблюдать правила иерархии: свойства должны храниться внутри объекта, а НЕ объект — внутри свойства.
    [​IMG]

    При работе с вложенными данными следует соблюдать правила иерархии.

    Во-первых, связанные данные должны иметь общий контейнер.

    Благодаря этому объект выглядит именно как объект, и не распадается на множество независимых свойств.

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

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

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

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

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

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

    Во-первых, обращение к «свойствам» происходит не через осмысленное имя: leaders[2].firstName, а через номер свойства: leaders[2][1]. Для нормального обращения через осмысленные имена приходится объявлять лишние константы, что тоже достаточно уродливо.

    Во-вторых, «объект» не может иметь «свойства» разных типов. Пожалуй, это самый серьёзный недостаток многомерных массивов.

    Разумеется, самым правильным и красивым решением является создание массива объектов или структур.
    Код:
    [B]Пример:[/B]
    
                People *leaders [7] = 
                  {
                  new People ("Ленин",    "Владимир",   "Ильич"),
                  new People ("Сталин",   "Иосиф",      "Виссарионович"),
                  new People ("Хрущёв",   "Никита",     "Сергеевич"),
                  new People ("Брежнев",  "Леонид",     "Ильич"),
                  new People ("Андропов", "Юрий",       "Владимирович"),
                  new People ("Черненко", "Константин", "Устинович"),
                  new People ("Горбачёв", "Михаил",     "Сергеевич")
                  };
    
                //Эти константы – только для удобства чтения примера. В реальном коде их не будет
                const lenin = 0, stalin = 1, brezhnev = 2, gorbachev = 6;
    
    
                //Печатаем: "Владимир Ленин"
                cout<<leaders [lenin].firstName<<" "<< leaders [lenin].lastName;
    
                //Печатаем: "Леонид Брежнев"
                cout<<leaders [brezhnev].firstName<<" "<< leaders [brezhnev].lastName;
    
                //Печатаем: "Михаил Горбачёв"
                cout<<leaders [gorbachev].firstName<<" "<< leaders [gorbachev].lastName;
    • Обязательное хранение размера массива в отдельной переменной
    В двух словах:

    Размер массива должен быть доступен через его свойство, а НЕ только из отдельной переменной.
    [​IMG]

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

    В процедурных языках эти правила нередко нарушаются, причём не только при использовании параллельных массивов.

    Ещё одним примером некрасивой работы с данными является обязательное хранение размера массива в отдельной переменной. ​


    При этом, так же, нарушаются все возможные правила работы с иерархичными данными:

    Во-первых
    , массив перестаёт быть единым объектом и распадается на две независимые сущности: собственно данные и размер массива.
    Во-вторых, нарушается иерархический порядок работы с данными.



    Размер массива должен быть доступен через его свойство length (или, в крайнем случае, через функцию len (array), как в Python, например).

    Благодаря этому:

    Во-первых, мы можем работать с массивом как с единым объектом.
    Во-вторых, обращение к свойству массива идёт в иерархическом порядке.
    Разумеется, нет ничего плохого в том, чтобы временно закешировать размер массива в отдельной переменной (например, в целях производительности).

    Главное, чтобы размер массива при этом всегда был доступен через свойство и без всяких внешних переменных.
    • Доступ к свойствам объекта через
      object.getProperty () и object.setProperty (value)

    В двух словах:

    Для доступа к данным должны использоваться свойства, а НЕ методы или открытые поля.

    [​IMG]

    У полей и методов объектов есть своё чёткое предназначение:

    Поля — хранят данные;

    Методы — реализуют поведение объекта.

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

    Работа с данными: objObject.property1 = "value1";

    Поведение объекта: objObject.doSomething (param1, param2);
    Использование методов в качестве акцессора и мутатора поля — уродство. ​

    Во-первых, смешиваются два различных понятия: данные объекта и его поведение.

    Нарушается естественная конструкция для доступа к данным через оператор присваивания. Оператор присваивания самой своей сутью подразумевает присваивание.
    Код:
              objObject.property1 = "value1";
    
              strValue = objObject.property1;
    Вызов метода, само использование круглых скобок, подразумевает реализацию поведения объекта.

    Во-вторых, чтение и запись свойства реализуется по-разному, что противоречит сути поля.
    К полю не будет возможности применять стандартные операторы работы с данными, такие как ++, += и др.
    Для обращения к защищённым полям объекта как к данным, используются свойства. (Внимание: не «открытые поля», а именно — «свойства», см примечание [4] в конце темы).
    Свойство — это интерфейс для красивого и безопасного доступа к данным объекта [прим. 5].
    При его использовании вызываются методы доступа к данным (например, для выполнения проверки на правильность записываемых в свойство данных).

    Однако, вызов этих методов происходит «прозрачно» для разработчика, что позволяет красиво обращаться к данным: value = object.property и object.property = value.
    Свойства поддерживает большое количество современных языков: Delphi, C#, Python, Ruby и др.

    Однако немало языков свойства не поддерживают: C++, Java и даже гибкий и красивый JavaScript [прим. 6]

    Знаете, есть две вещи, которые обязательно надо добавить в JavaScript. Но это не классы и строготипизированные переменные, как думают многие. Отсутствие классов и строгих типов — это не баг, а фича, дающая JavaScript такую гибкость.

    Две возможности, которых действительно не хватает в JavaScript — это перегрузка операторов и поддержка свойств [прим. 7].

    • Использование рекурсии для вычисления факториалов и Чисел Фибоначчи
    В двух словах:

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

    [​IMG]

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

    Ибо рекурсия, или даже философия рекурсии, штука не такая простая.

    И вопрос, когда следует (вернее, когда красиво) применять рекурсию, а когда нет, не столь однозначен.

    Ну, в функциональных языках (таких как Lisp или Haskell) всё понятно: рекурсия применяется всегда, когда надо выполнить любые повторяющиеся действия. Там даже сумма элементов массива (там он называется списком) вполне может определяться рекурсивно как сумма первого элемента + сумма оставшейся части. В этих языках такой подход гармонирует с философией языка и, потому, красив.
    В императивных же языках всё сложнее.

    Мне кажется, что в этих языках смысл рекурсии состоит в разбиении задачи на несколько подобных подзадач более низкого ранга. А тех в свою очередь — на насколько под-подзадач, и так, в геометрической прогрессии, пока мы не дойдём до тривиального случая.

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

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

    Самым вопиющим примером уродского применения рекурсии является её использование при вычислении факториала и чисел Фибоначчи.​


    Я даже не говорю, что это страшно не эффективно. Я говорю, что вычисление факториала и чисел Фибоначчи — чисто итерационная задача, и рекурсивное её решение — это извращение самого смысла, самой сути рекурсии в императивных языках [прим. 8].

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

    • Отсутствие именованных параметров функции
    В двух словах:

    Параметры любой нетривиальной функции должны задаваться по осмысленному имени, а НЕ положению в списке аргументов.
    [​IMG]

    Никто не будет спорить с тем, что имена должны отражать суть переменённых. И что использование имён переменных вроде a0, a1, a2 — не самый понятный и красивый приём.

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

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

    Параметры функции являются такими же полноценными переменными. Однако при вызове функции мы задаём параметр не по его осмысленному имени, а по положению в списке параметров, т.е. по номеру. Это ещё хуже, чем переменные a0, a1, a2.
    Есть только два случая, когда можно использовать неименованные параметры функции.

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

    К сожалению, лишь немногие языки поддерживают именованные параметры функций (я могу вспомнить только динамические Perl, Python и Ruby, может быть есть ещё).
    Что же делать в остальных языках?

    В процедурных языках (вроде C или Pascal) проблема вызова функций с большим количеством малопонятных параметров стоит особенно остро.

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

    Гораздо больше кода, но зато более понятно и красиво. Хотя, объявление лишних структур для каждой функции, тоже не очень красивое решение.

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

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

    И, наконец, в динамических языках, не поддерживающих именованные параметры (например JavaScript), в качестве параметра функции можно передать созданный на лету объект с осмысленными свойствами.
    Лаконично, понятно и красиво!
     
    Последнее редактирование: 11 ноя 2010
  3. Koller

    Koller Исчез... Глобальный модератор

    Регистрация:
    3 ноя 2006
    Сообщения:
    401
    Симпатии:
    234
    Баллы:
    0
    • Заключение
    Пока всё на этом.

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

    Ну и не стоит забывать, что отсутствие в коде приведённых «уродских приёмов» — лишь идеальная ситуация, к которой надо стремиться, но приходится жертвовать в определённых ситуациях (например, в целях производительности). Ведь умение приносить жертвы при решении практических задач — один из элементов профессионального программирования.
    • Примечания

    1. Стив Макконнелл ещё более категоричен, чем я. В книге «Совершенный код» он пишет:
    2. Вообще, надо стараться избегать длинных функций и методов. Одно из правил рефакторинга: «если текст метода не умещается на экране без прокрутки — это уже повод разбить его на несколько более коротких методов».

      Однако, это далеко не всегда удаётся, например при генерации отчётов или при реализации сложных вычислений.
    3. Есть и противоположенная точка зрения. Например, Ховик Меликян в статье «Клиника плохого кода» пишет, что возврат корней квадратного уравнения через параметр функции: num_roots = qe_solve(10, 20, 2, &x1, &x2); — гораздо красивее, чем использование объектов.
    4. Не надо путать методы, открытые поля и свойства (как, например, в этом самоуверенном комментарии).

      Открытое поле — это просто переменная внутри объекта, доступ к которой извне ничем не ограничен.

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

      Поэтому, использование открытых полей — нежелательно.

      Для контроля обращения к данным можно использовать методы object.getProperty() и object.setProperty(value).

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

      Однако, использование методов — очень некрасиво, о чём я и пишу в статье.

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

      Вместо открытых полей и методов, при наличие такой возможности, следует использовать свойства.

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

      При обращении к свойствам вызываются всё те же методы, однако вызов их происходит «прозрачно» для разработчика.

      Это позволяет красиво обращаться к данным: value = object.property и object.property = value.
    5. Разумеется, свойствами, как и любой другой возможностью, не стоит злоупотреблять, применяя их где только можно.

      Свойства следует использовать для реализации доступа к данным объекта. Ведь использование свойств, само использование оператора присваивания подразумевает доступ к данным.

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

      В этом случае, красиво и правильно, как раз, использовать методы. Ведь вызов метода, само наличие круглых скобок предполагает реализацию поведения объекта. О чём я и писал в самом начале этой главы.
    6. Формально поддержка свойств появилась ещё в JavaScript 1.5:

      Код:
                    object.__defineGetter__ ("property", function ()      {/*Код аксессора*/})
                    object.__defineSetter__ ("property", function (value) {/*Код мутатора*/})
      Однако она только относительно недавно стала работать в «нормальных браузерах» и, несмотря на все обещания, до сих пор не работает в Internet Explorer.
    7. Перегрузка операторов появится только в JavaScript 2.0.
    8. Для вычисления чисел Фибоначчи использование рекурсии имеет какой-то смысл: всё-таки основное определение этой функции является рекурсивным:

      Fib (n) = Fib (n−1) + Fib (n−2); Fib (0) = 0; Fib (1) = 1

      Однако, наличие же у факториала рекурсивной формы записи:

      n! = n × (n−1)!

      нисколько не приближает нас ни к пониманию смысла рекурсии, ни к пониманию смысла факториала как произведения чисел от 1 до n.
    (c) Алик Кириллович ​
     
  4. Nosaer

    Nosaer Модератор

    Регистрация:
    30 ноя 2009
    Сообщения:
    96
    Симпатии:
    78
    Баллы:
    0
    Занятно было почитать, в чем то очень даже согласен, а в чем то нет.
    Во всяком случае статья написана под язык Си(мои познания в нем находятся в рамках программирования микроконтролеров), потому за то что я что то не так написал,сильно не пинать.
    статья получилась так сказать очень узконаправленная по моему мнению.
    Мне не понятно почему автор, так относится к результатам функции, которые задаются вместе с параметрами. ПО мне так это на много удобнее при выводе нескольких результатов. Да и в Python, это вообще ни в какие рамки не лезет=).

    "Использование рекурсии для вычисления факториалов и Чисел Фибоначчи;" - а это кому то вообще надо? =)
     
  5. ~|~евто|-|

    ~|~евто|-| Silentium ..::V.I.P::..

    Регистрация:
    26 май 2007
    Сообщения:
    30
    Симпатии:
    377
    Баллы:
    0
    Статейка весьма противоречивая) Но в целом, разумное зерно конечно есть)))
    Но далеко не все советы, положительно скажутся на производительности :)

    Ознакамливаемся тынц и тынц
    Далеко не все занимаются рисованием окошек и прочей прикладухой :)
     
  6. rijy

    rijy Команда форума Админ

    Регистрация:
    5 авг 2006
    Сообщения:
    521
    Симпатии:
    224
    Баллы:
    0
    ~|~евто|-|, а ты по профессии кто?
    и если программист, ты в прикладном больше работаешь? или теория?
     
  7. ~|~евто|-|

    ~|~евто|-| Silentium ..::V.I.P::..

    Регистрация:
    26 май 2007
    Сообщения:
    30
    Симпатии:
    377
    Баллы:
    0
    rijy, я по профессии программист.
    А сфера деятельности, как прикладуха(чтобы было что кушать и выпить, оплачивается это шибко хорошо. В частности я занимаюсь учетными системами), плюс я сейчас все больше ухожу в сторону администрирования и разработки средних и больших БД.(Oracle DB)

    И, что менее доходно, по крайней мере пока, занимаемся реализацией алгоритмов под анализ данных... А там... Там 99% математика) 1% красивые окошки под эту математику)

    [ADD]
    Хотя справедливости ради, нужно сказать, что и среди проектов по учетным системам встречаются очень-очень-очень интересные задачи, решения, походы... :)
     
    Последнее редактирование: 17 ноя 2010
    1 человеку нравится это.

Поделиться этой страницей