Здесь — полная авторская версия статьи, опубликованной в журнале «Хабрахабр» в сокращённом виде из-за ограничений на размер материала.
Тема красоты кода для меня очень важна, поэтому буду благодарен всем, кто поставит ссылочку, и за любой другой «пиар» статьи. Если интересно — можете подписаться на RSS.
Для меня программирование — это не только технология, но и, во многом — искусство. И, поэтому, большое значение имеет красота кода.
Последние несколько лет я собирал приёмы программирования, разрушающие в программном коде его утончённую красоту:
Обязательное хранение размера массива в отдельной переменной;
Доступ к свойствам объекта через obj.getProperty() и obj.setProperty(value);
Использование рекурсии для вычисления факториалов и Чисел Фибоначчи;
Переменные должны объявляться в начале логического блока, в котором они используются, а НЕ в начале функции или программы.
Все программные системы иерархичны. Программы делятся на пакеты, пакеты — на классы, классы разбиваются на отдельные функции.
Данные, относящиеся к тому или иному модулю программы, принято объявлять в начале этого модуля. Локальные переменные объявляются в начале функции; свойства, относящиеся ко всему классу, объявляются в начале определения класса и т.д.
Однако функции не являются последним уровнем в иерархии программы. Любая нетривиальная функция состоит из блоков, реализующих отдельны шаги выполнения алгоритма. Тех самых блоков, которые никак не обособляются в коде, разве что отделяются друг от друга парочкой пустых строк.
Однако эти блоки — полноценные элементы в иерархии программы. И они тоже имеют право на собственные «локальные» переменные! Которые объявляются в начале этого блока и используются только в его пределах.
Предположим, нам надо написать функцию сортировки массива методом слияния.
Алгоритм сортировки слиянием состоит из следующих шагов:
просто меняем местами эти два элемента в случае неправильного их расположения.
разбиваем массив на две половины: левый и правый подмассивы;
отдельно сортируем каждый подмассив (рекурсивно);
сливаем эти два отсортированных подмассива в один отсортированный результирующий массив.
Блоки кода, реализующие шаги этого алгоритма, являются неотъемлемыми частями функции, и не имеют отдельного самостоятельного смысла (может быть, за исключением, блока слияния, который, в принципе, можно преобразовать в отдельную функцию).
Однако, они являются полноправными элементами иерархии программы, и решают свои собственные маленькие подзадачи.
Поэтому:
В начале функции объявляются переменные, относящиеся ко всей функции, например:
intLeftIndex, intRightIndex, intLength.
А переменные, относящиеся к отдельным её блокам, объявляются в начале соответствующего блока, например:
elTemp объявляется в начале блока сортировки массива из 2-х элментов;
intCenterIndex — в начале блока разбиения;
arSortedSubarray и intLeftElIndex — в начале блока слияния.
/*
Эта функция реализует сортировку слиянием
*/
function mergeSort (arArryay, intLeftIndex, intRightIndex)
{
/*
Здесь объявляются переменные, относящиеся ко ВСЕЙ ФУНКЦИИ.
Переменные, относящиеся к отдельным её блокам,
объявляются в начале соответствующего блока.
*/
intLeftIndex = intLeftIndex || 0;
intRightIndex = intRightIndex || arArryay.length-1;
var intLength = intRightIndex - intLeftIndex + 1;
if (intLength == 2)
{
/*
БЛОК КОДА: сортировка массива из 2-х элементов
*/
if (arArryay [intRightIndex] < arArryay [intLeftIndex])
{
var elTemp = arArryay [intRightIndex];
arArryay [intRightIndex] = arArryay [intLeftIndex];
arArryay [intLeftIndex] = elTemp;
}
}
else if (intLength > 2)
{
/*
БЛОК КОДА: разбиение массива на два подмассива
*/
var intCenterIndex = intLeftIndex + Math.ceil (intLength / 2) - 1;
var intLeftSubarrayLength = intCenterIndex - intLeftIndex + 1;
var intRightSubarrayLength = intRightIndex - intCenterIndex;
/*
БЛОК КОДА: рекурсивная сортировка каждого подмассива
*/
mergeSort (arArryay, intLeftIndex, intCenterIndex);
mergeSort (arArryay, intCenterIndex+1, intRightIndex);
/*
БЛОК КОДА: слияние
*/
var arSortedSubarray = [];
var intLeftElIndex = intLeftIndex;
var intRihtElIndex = intCenterIndex+1;
while (intLeftElIndex <= intCenterIndex || intRihtElIndex <= intRightIndex)
{
if (intRihtElIndex <= intRightIndex && (arArryay [intRihtElIndex] <= arArryay [intLeftElIndex] || intLeftElIndex > intCenterIndex))
{
arSortedSubarray [arSortedSubarray.length] = arArryay [intRihtElIndex];
intRihtElIndex++;
}
if (intLeftElIndex <= intCenterIndex && (arArryay [intLeftElIndex] <= arArryay [intRihtElIndex] || intRihtElIndex > intRightIndex))
{
arSortedSubarray [arSortedSubarray.length] = arArryay [intLeftElIndex];
intLeftElIndex++;
}
}
for (var i = 0; i < intLength; i++)
{
arArryay [intLeftIndex + i] = arSortedSubarray [i];
}
}
}
И поэтому:
Объявление всех переменных в начале функции — страшное зло [1].
Это приводит к смешению переменных, относящихся ко всей функции, с переменными, относящимися только к её отдельному блоку.
Это разрывает блок на две части: объявления данных (в начале функции) и использования этих данных (в самом блоке).
Это усложняет комментирование блока: в одном месте мы комментируем переменные, но не знаем, как их использовать; в другом месте мы комментируем алгоритм, но не знаем, с какими данными он работает.
В случае объявления всех переменных в начале функции, переменные, относящиеся ко всей функции, перемешаются с переменными, имеющими смысл только внутри отдельных её блоков.
function mergeSort (arArryay, intLeftIndex, intRightIndex)
{
/*
Угадайте: какие из этих переменных относятся ко всей функции,
а какие — к отдельным её блокам?
*/
var intLength,
elTemp,
intCenterIndex,
intLeftSubarrayLength,
intRightSubarrayLength,
arSortedSubarray,
intLeftElIndex,
intRihtElIndex,
i;
intLeftIndex = intLeftIndex || 0;
intRightIndex = intRightIndex || arArryay.length-1;
intLength = intRightIndex - intLeftIndex + 1;
if (intLength == 2)
{
if (arArryay [intRightIndex] < arArryay [intLeftIndex])
{
elTemp = arArryay [intRightIndex];
arArryay [intRightIndex] = arArryay [intLeftIndex];
arArryay [intLeftIndex] = elTemp;
}
}
else if (intLength > 2)
{
intCenterIndex = intLeftIndex + Math.ceil (intLength / 2) - 1;
intLeftSubarrayLength = intCenterIndex - intLeftIndex + 1;
intRightSubarrayLength = intRightIndex - intCenterIndex;
mergeSort (arArryay, intLeftIndex, intCenterIndex);
mergeSort (arArryay, intCenterIndex+1, intRightIndex);
arSortedSubarray = [];
intLeftElIndex = intLeftIndex;
intRihtElIndex = intCenterIndex+1;
//...код сокращён...
}
}
Вы только представьте:
У нас есть функция в 300 строк кода
[2].
Где-нибудь на 200-й строке нам надо поменять две переменные местами.
Для этого мы лезем на 200 сток выше в начало функции,
объявляем переменную temp, которая не имеет никакого отношения ко всей функции,
а используется только один раз в одном месте,
потом опять возвращаемся к 200-й строке и меняем переменные местами…
По-моему, это просто кошмар.
Хуже всего, что существуют языки, которые считают себя умнее разработчика и заставляют объявлять все переменные в начале функции. Например, такой уважаемый язык как Pascal/Delphi. Чего я ему простить не могу…
Функция должна возвращать результат, зависящий от её параметров, а НЕ принимать результат в качестве аргумента.
Понятие функции (как в математике, так и в программировании) имеет чёткий смысл: вычисление результата, зависящего от аргументов.
В нормальном программном коде ясно видно, что является результатом, а что аргументами:
результат = функция (аргумент1, аргумент2).
Однако часто встречается приём, при котором возвращаемое значение передаётся в качестве аргумента функции:
функция (аргумент1, аргумент2, &результат).
Этот приём ужасен. При его использовании не видно, от чего функция зависит, а что возвращает.
Чаще всего, в применении этого приёма виноваты не сами разработчики, а языки программирования.
Существуют две основные причины, по которым языки заставляют нас прибегать к этому приёму.
Первая причина состоит в том, что в некоторых языках функции не могут создавать и возвращать сложные объекты.
Предположим, что мы хотим на C++ написать функцию, перемножающую две матрицы и возвращающую получившуюся матрицу в качестве результата. Матрицы мы решили представлять в виде двумерного массива.
Но мы не можем объявить в функции результирующий двумерный массив, а затем вернуть его:
int mtxResult [10][10] = mult (mtxA, mtxB);
Поэтому нам придётся сначала вне функции объявить результирующий массив, а затем вызвать функцию перемножения, передав результат в качестве аргумента:
mult (mtxA, mtxB, mtxResult);
В данном случае от использования этого приёма можно избавиться, храня результат в том виде, который функция может возвратить.
Можно хранить матрицу не в виде двумерного массива, а в виде структуры или объекта:
Matrix mtxResult = mult (mtxA, mtxB);
Код станет менее лаконичным (из-за объявления дополнительных структур), но, зато, гораздо более красивым.
Вторая причина состоит в том, что в большинстве языков программирования функция не может возвращать несколько значений.
Предположим, что мы хотим на C++ написать функцию, решающую квадратное уравнение.
Функция принимает в качестве аргумента коэффициенты
a, b, c и возвращает три результата:
число корней, x1 и x2.
Однако вернуть сразу три значения в C++ невозможно:
intRootsCount, numX1, numX2 = quadraticEquation (numA, numb, numC)
Поэтому нам придётся часть результатов выполнения функции передать через указатель в качестве аргументов:
intRootsCount = quadraticEquation (numA, numB, numC, &numX1, &numX2);
Здесь, опять же, от этого приёма можно избавиться, возвращая объект или структуру, хранящую результаты выполнения функции в виде полей.
Можно возвращать результаты решения квадратного уравнения в виде структуры с тремя свойствами [3]:
QuadrEqResult qerResult = quadraticEquation (numA, numB, numC);
intRootsCount = qerResult.count;
numX1 = qerResult.x1;
numX2 = qerResult.x2;
Однако и в первом, и во втором случае, при использовании структур или объектов, мы можем столкнуться с ещё одной проблемой: необходимостью отдельно описывать эти структуры или классы.
Причём во многих языках, например на C++, мы не можем описать структуру или класс внутри самой функции (см. следующий раздел). Нам придётся описывать их отдельно от функции, делая их неподчиненными функции сущностями, что уродует иерархию программы.
Слава богу, в других языках классы можно описывать прямо внутри функции, а, например в JavaScript можно просто возвратить объект, нигде отдельно не описывая его структуру.
function quadraticEquation (numA, numb, numC)
{
//...
return ({
count: intRootsCount,
x1: intX1,
x2: intX2
});
}
var objResult = quadraticEquation (numA, numB, numC);
intRootsCount = objResult.count;
numX1 = objResult.x1;
numX2 = objResult.x2;
Вот это настоящая красота!
Локальная функция должна объявляться внутри функции, которой она логически подчиняется, а НЕ в глобальном контексте.
Как уже говорилось, программные системы (как объектно-ориентированные, так и процедурные) иерархичны и делятся на вложенные друг в друга модули (впрочем, это очевидно).
Каждый из модулей располагает своими локальными ресурсами, используемыми только в рамках этого модуля.
Например, локальные переменные являются ресурсами модуля функции.
Однако ресурсами функции являются не только переменные! Подфункции, классы, структуры и т.д. также являются полноправными ресурсами функции, подчинёнными ей, и используемыми только в её рамках.
Предположим, нам надо написать функцию printDossier, печатающую досье.
Сверху и снизу досье должен находиться его номер,
выровненный по центру и обрамлённый декоративными свасти звёздочками:
****************** 666 ******************
Имя: Макс Отто фон Штирлиц
Звание: штандартенфюрер
Национальность: истинный ариец
Характер: нордический, выдержанный
****************** 666 ******************
Чтобы не дублировать код, печатающий верхний, и печатающий нижний номер,
мы решили вынести его в отдельную функцию printNumber.
Функция, печатающая номер досье, является подчинённой по отношению к функции, печатающей всё досье,
используется только в её рамках и не имеет самостоятельного смысла.
Поэтому, функция printNumber должна быть локальной по отношению к printDossier,
и объявляться в её теле.
#
#Эта функция печатает досье:
#
def printDossier (people):
#
#Эта локальная функция печатает номер досье:
#
def printNumber ():
#Вычисляем количество звёздочек слева и справа, так, чтобы номер оказался по центру
stringLength = 32
numberLength = len (people.number)
starsCount = (stringLength - numberLength) / 2
#Печатаем звёздочки слева, номер досье, и звёздочки справа
print ('*' * starsCount) + people.number + ('*' * starsCount)
#
#Печатаем досье
#
#Печатаем номер сверху
printNumber ()
#Печатаем: имя, звание, национальность и характер
print 'Имя: '+ people.name
print 'Звание: '+ people.rank
print 'Национальность: '+ people.race
print 'Характер: '+ people.character
#Печатаем номер снизу
printNumber ()
Поэтому:
Это уродует иерархию программы, делая подчинённую функцию независимой, и иерархически неподчиненной родительской функции сущностью.
Это нарушает принцип сокрытия информации, вынося наружу детали внутренней реализации родительской функции — подчинённую функцию.
Это затрудняет понимание кода, засоряя глобальный контекст лишними сущностями.
Это приводит к возникновению ошибок, делая возможным случайный вызов подчинённой функции не в родительском, а в глобальном контексте.
Но многие языки программирования, например C++, не поддерживают локальные функции, классы, структуры и т.д.
Поскольку C++ не поддерживает локальные функции, функцию, печатающую номер досье, нам придётся сделать глобальной, независимой и иерархически неподчинающейся функции, печатающей всё досье:
/*
Эта функция печатает номер досье.
Мы вынуждены сделать её глобальной,
хотя она является деталью внутренней реализации функции printDossier
и не имеет самостоятельного смысла.
*/
void printNumber ()
{
//Вычисляем количество звёздочек слева и справа, так, чтобы номер оказался по центру
int stringLength = 32;
int numberLength = strlen (people.number);
int starsCount = (stringLength - numberLength) / 2;
//Печатаем звёздочки слева, номер досье, и звёздочки справа
for (int i = 0; i < starsCount; i++) cout<<"*";
cout<<people.number;
for (int i = 0; i < starsCount; i++) cout<<"*";
}
/*
Эта функция печатает досье
*/
void printDossier (People people)
{
/*
Печатаем досье
*/
//Печатаем номер сверху
printNumber ();
//Печатаем: имя, звание, национальность и характер
cout<< "Имя: " << people.name;
cout<< "Звание: " << people.rank;
cout<< "Национальность: " << people.race;
cout<< "Характер: " << people.character;
//Печатаем номер снизу
printNumber ();
}
Язык Pascal/Delphi поддерживает локальные функции, но, заставляет их объявлять только в начале функции. Это не так страшно, как объявлять только в начале все переменные, но тоже, иногда, бывает достаточно некрасиво.
Предположим, у нас есть функция из нескольких блоков кода, каждый из которых выполняет отдельный шаг всего алгоритма.
Мы решили переписать один их блоков в рекурсивной форме.
Для этого мы переделали его в рекурсивную локальную функцию… после чего вынуждены перетащить этот блок кода (ставший теперь локальной функцией) с того места, где он используется, в начало основной функции.
program main ();
function block3 (param: integer): integer; // (4)──┐
begin // ↑ ↓
Рекурсивный блок кода #3 // ↑ ↓
end; // ↑ ↓
{} // ↑ ↓
procedure block5 (param: integer); // ↑ ↓ (7)──┐
begin // ↑ ↓ ↑ ↓
Рекурсивный блок кода #5 // ↑ ↓ ↑ ↓
end; // ↑ ↓ ↑ ↓
{} // ↑ ↓ ↑ ↓
begin // ↑ ↓ ↑ ↓
блок кода #1 // (1) ↑ ↓ ↑ ↓
{} // ↓ ↑ ↓ ↑ ↓
блок кода #2 // (2) ↑ ↓ ↑ ↓
{} // ↓ ↑ ↓ ↑ ↓
{Прокручиваем на самый верх // ↓ ↑ ↓ ↑ ↓
и находим код рекурсивной функции block3} // ↓ ↑ ↓ ↑ ↓
block3 (param); // (3)──┘ ↓ ↑ ↓
{} // ↓ ↑ ↓
блок кода #4 // (5) ↑ ↓
{} // ↓ ↑ ↓
{Прокручиваем на самый верх // ↓ ↑ ↓
и находим код рекурсивной функции block5} // ↓ ↑ ↓
block5 (param); // (6)──┘ ↓
{} // ↓
блок кода #6 // (8)
end.
Как теперь прикажите читать эту функцию?
Первый блок кода, второй, третий, пока, всё нормально и понятно.
Вдруг, хлоп, вызов рекурсивной подфункции, для чтения которой прокручиваем код к самому началу.
Затем опять возвращаемся, назад и продолжаем читать обычные блоки.
Не намного лучше, чем читать спагетти-код с goto.
К счастью, поддержка локальных функций есть почти во всех «новых» языках, как динамических (JavaScript, Python), так и классических (Java). И надо только воспользоваться этой возможностью.
else if
Уровень вложенности блока должен соответствовать его иерархическому положению в программе.
Блоки кода, имеющие одинаковый уровень вложенности, относятся к одному уровню иерархии программы. Соответственно, блоки кода с большим уровнем вложенности являются подчинёнными по отношению к блокам с меньшей вложенностью.
Вроде бы, это очевидный факт.
Тем не менее, иногда, особенно в учебной литературе, я встречаю нарушение этого правила:
Когда блоки, логически находящиеся на одном уровне иерархии в программе, тем не менее, имеют разный уровень вложенности. Или наоборот, имеют разный уровень иерархии, но одинаковый уровень вложенности.
Предположим, что нам надо приветствовать пользователя, в зависимости от времени суток. Приветствия «Спокойной ночи», «Доброе утро», «Добрый день», и «Добрый вечер» совершенно равноправны и относятся к одному уровню иерархии программы.
Поэтому этот код, где приветствия имеют разный уровень вложенности, страшно уродлив:
if (numHour >= 0 && numHour < 6)
{
print ("Спокойной ночи!");
}
else
{
if (numHour >= 6 && numHour < 12)
{
print ("Доброе утро!");
}
else
{
if (numHour >= 12 && numHour < 18)
{
print ("Добрый день!");
}
else
{
print ("Добрый вечер!");
}
}
}
Его надо переписать так, чтобы равноправные блоки имели равный уровень вложенности:
if (numHour >= 0 && numHour < 6)
{
print ("Спокойной ночи ");
}
else if (numHour >= 6 && numHour < 12)
{
print ("Доброе утро!");
}
else if (numHour >= 12 && numHour < 18)
{
print ("Добрый день!");
}
else
{
print ("Добрый вечер!");
}
Предположим теперь, что мы должны проверить, зарегистрирован ли пользователь в системе, и, если зарегистрирован, то поприветствовать, а если нет — послать вон.
В данном случае, блоки кода приветствия имеют подчинённый уровень по отношению к блоку «зарегистрирован», а блок «пошёл вон» — подчинённое к блоку «не зарегистрирован». Поэтому уродлив код, в котором все блоки имеют одинаковый уровень вложенности:
if (!isRegistred ())
{
print ("Вы не зарегистрированы в системе. Идите вон!");
}
else if (numHour >= 0 && numHour < 6)
{
print ("Спокойной ночи ");
}
else if (numHour >= 6 && numHour < 12)
{
print ("Доброе утро!");
}
else if (numHour >= 12 && numHour < 18)
{
print ("Добрый день!");
}
else
{
print ("Добрый вечер!");
}
Его надо переписать так, чтобы уровень вложенности блоков соответствовал их иерархическому положению:
if (!isRegistred ())
{
/*
Не зарегистрирован
*/
print ("Вы не зарегистрированы в системе. Идите вон!");
}
else
{
/*
Зарегистрирован
*/
if (numHour >= 0 && numHour < 6)
{
print ("Спокойной ночи!");
}
else if (numHour >= 6 && numHour < 12)
{
print ("Доброе утро!");
}
else if (numHour >= 12 && numHour < 18)
{
print ("Добрый день!");
}
else
{
print ("Добрый вечер!");
}
}
В некоторых старых языках программирования (по-моему, в каких-то древних версиях Паскаля или что-то в этом роде)
были операторы if и else, но отсутствовал оператор else if.
Они навязывали приём, при котором каждый последующий блок в цепочке сравнений
имел всё больший уровень вложенности (как в примере №1).
Но сейчас эти языки вымерли как динозавры, и все нормальные языки поддерживают else if.
Поэтому надо писать код, в котором уровень вложенности соответствует уровню иерархии блока, а также не поддаваться на провокации всяких вредных учебников.
При работе с вложенными данными следует соблюдать правила иерархии: свойства должны храниться внутри объекта, а НЕ объект — внутри свойства.
При работе с вложенными данными следует соблюдать правила иерархии.
Во-первых, связанные данные должны иметь общий контейнер.
Благодаря этому объект выглядит именно как объект, и не распадается на множество независимых свойств.
Также, мы можем работать с объектом как с единым целым, например, передавать его в качестве параметра функции и т.д.
В данном примере видно, что lastName, firstName, surName
относятся к одному объекту: objPeople1.
objPeople1.lastName = "Пупкин";
objPeople1.firstName = "Василий";
objPeople1.surName = "Иванович";
Мы можем работать с этим объектом не как с набором свойств, а как с единым целым:
doSomething (objPeople1);
Во-вторых, при обращении к вложенным данным следует соблюдать иерархический порядок: сначала обращаться к корневому элементу, затем к вложенному в него элементу, затем к элементу с ещё большей вложенностью и т.д.
За время написания статьи Путин успел переехать:
World.Russia.Moscow./*Kremlin*/WhiteHouse.Putin
А в данном примере только с помощью иерархического порядка можно определить, что
синий пакет находится в красном:
redBag.blueBag.myThing
…или же красный — в синем:
blueBag.redBag.myThing
В программах на объектно-ориентированных языках это правила иерархии почти всегда соблюдается.
Однако соблюдать правила иерархии при обращении к данным надо не только в объектно-ориентированных языках! Тем не менее, в программах на процедурных языках это правило нередко нарушается.
Одним их вопиющих примеров уродского обращения к данным является использование так называемых параллельных массивов.
Предположим нам надо сохранить данные о советских лидерах, содержащие следующие свойства: фамилию, имя и отчество.
Каждое из этих свойств хранится в отдельном массиве:
char *lastNames [3] = {"Ленин", "Сталин", "Хрущёв"};
char *firstNames [3] = {"Владимир", "Иосиф", "Никита"};
char *surNames [3] = {"Ильич", "Виссарионович", "Сергеевич"};
const lenin = 0, stalin = 1, khrushchev = 2;
И обращение к данным происходит следующим образом:
//Печатаем: "Никита Хрущёв"
cout<<firstNames[khrushchev]<<" "<<lastNames[khrushchev];
При использовании параллельных массивов нарушаются все возможные правила работы с иерархичными данными.
Во-первых, записи распадаются на множество несвязанных полей. Мы не можем работать с записью как с единым объектом.
Вместо работы с объектом как с единым целым, нам приходится работать со множеством полей:
doSomething(lastNames [lenin], lastNames [lenin], surNames [lenin]);
Во-вторых, обращение к вложенным данным происходит «задом-наперёд».
Запись
firstNames[khrushchev]
означает не Хрущёва, хранящего свойство firstName,
а, наоборот, свойство, хранящее внутри себя Хрущёва!
Мне не раз приходилось встречаться с ещё одним приёмом, очень похожим на параллельные массивы: хранение данных в многомерном массиве, где первый индекс отвечает за номер свойства, а второй — за номер записи.
char *leaders [3][3] =
{
{"Ленин", "Сталин", "Хрущёв"},
{"Владимир", "Иосиф", "Никита"},
{"Ильич", "Виссарионович", "Сергеевич"}
}
const lenin = 0, stalin = 1, khrushchev = 2;
const lastName = 0, firstName = 1, surName = 2;
Обращение к данным происходит следующим образом:
leaders [propertyNumber][peopleNumber]
Например:
//Печатаем: "Владимир Ленин"
cout<< leaders [firstName][lenin]<<" "<< leaders [lastName][lenin];
//Печатаем: "Никита Хрущёв"
cout<< leaders [firstName][khrushchev]<<" "<< leaders [lastName][khrushchev];
//Печатаем: "Иосиф Сталин"
cout<< leaders [firstName][stalin]<<" "<< leaders [lastName][stalin];
Этот приём имеет только одно преимущество по сравнению с параллельными массивами — список всех записей объединён в единый объект. Однако все остальные недостатки параллельных массивов никуда не деваются:
Во-первых, записи, по прежнему, распадаются на множество несвязанных полей.
Вместо работы с объектом как с единым целым, нам опять приходится работать со множеством полей:
doSomething(leaders [lastName][lenin], leaders [firstName][lenin], leaders [surName][lenin]);
Во-вторых, обращение к вложенным данным происходит «задом-наперёд».
Запись
leaders [propertyNumber][peopleNumber]
означает
не человека peopleNumber хранящего свойство propertyNumber,
а, наоборот, свойство, propertyNumber хранящее внутри себя человека!
Использовать красивые приёмы работы с данными не так сложно.
Вместо параллельных массивов можно даже воспользоваться предыдущим приёмом с многомерными массивами, просто переставив индексы местами, так чтобы: сначала шёл номер записи, а затем — номер свойства.
char *leaders [7][3] =
{
{"Ленин", "Владимир", "Ильич"},
{"Сталин", "Иосиф", "Виссарионович"},
{"Хрущёв", "Никита", "Сергеевич"},
{"Брежнев", "Леонид", "Ильич"},
{"Андропов", "Юрий", "Владимирович"},
{"Черненко", "Константин", "Устинович"},
{"Горбачёв", "Михаил", "Сергеевич"},
};
const lenin = 0, stalin = 1, brezhnev = 2, gorbachev = 6;
const lastName = 0, firstName = 1, surName = 2;
//Печатаем: "Владимир Ленин"
cout<<leaders [lenin][firstName]<<" "<< leaders [lenin][lastName];
//Печатаем: "Леонид Брежнев"
cout<<leaders [brezhnev][firstName]<<" "<< leaders [brezhnev][lastName];
//Печатаем: "Михаил Горбачёв"
cout<<leaders [gorbachev][firstName]<<" "<<leaders[gorbachev][lastName];
Этот способ уже имеет ряд преимуществ перед параллельными массивами.
Во-первых, «свойства» «объекта» объединены в единый контейнер. Мы можем работать с «объектом», как с единым целым.
Делаем что-то с Хрущёвым:
doSomething (leaders [khrushchev]);
Во-вторых, обращение к данным идёт в иерархическом порядке.
Все советские лидеры:
leaders
Лидер — Хрущёв:
leaders
[khrushchev]
Имя Хрущёва:
leaders
[khrushchev]
[firstName]
Однако, использование многомерных массивов имеет и ряд серьёзных недостатков.
Во-первых, обращение к «свойствам» происходит не через осмысленное имя:
leaders[2].firstName, а через номер свойства: leaders[2][1].
Для нормального обращения через осмысленные имена приходится объявлять лишние константы, что тоже достаточно уродливо.
Во-вторых, «объект» не может иметь «свойства» разных типов. Пожалуй, это самый серьёзный недостаток многомерных массивов.
Разумеется, самым правильным и красивым решением является создание массива объектов или структур.
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;
Размер массива должен быть доступен через его свойство, а НЕ только из отдельной переменной.
Итак, при работе с вложенными данными следует соблюдать правила иерархии: связанные данные должны иметь общий контейнер; и при обращении к ним следует соблюдать иерархический порядок.
В процедурных языках эти правила нередко нарушаются, причём не только при использовании параллельных массивов.
Ещё одним примером некрасивой работы с данными является обязательное хранение размера массива в отдельной переменной.
Предположим, нам надо считать в массив фамилии из текстового файла.
Поскольку число фамилий нам заранее неизвестно,
мы объявляем массив names с заведомо большим количеством элементов (например, 1000),
а число реально используемых элементов храним в отдельной переменной length.
var
//names - массив фамилий с заведомо большим количеством элементов
names : array [0..1000] of string;
//length - число реально использованных элементов
length: integer;
begin
assign (input, 'input.txt'); reset (input);
{/*
Считываем данные из файла в массив
*/}
length := 0;
while not SeekEof (input) do
begin
length := length + 1;
read (names [length]);
end;
end.
При этом, так же, нарушаются все возможные правила работы с иерархичными данными:
Во-первых, массив перестаёт быть единым объектом и распадается на две независимые сущности: собственно данные и размер массива.
Вместо работы с массивом как с единым целым, нам приходится передавать его по частям:
собственно данные — names и размер — length.
doSomething (names, length);
Во-вторых, нарушается иерархический порядок работы с данными.
Размер массива должен быть доступен через его свойство length
(или, в крайнем случае, через функцию len (array), как в Python, например).
Благодаря этому:
Во-первых, мы можем работать с массивом как с единым объектом.
Делаем что-то с массивом:
doSomething (names);
Во-вторых, обращение к свойству массива идёт в иерархическом порядке.
Весь массив:
array;
Длина массива:
array.length;
Разумеется, нет ничего плохого в том, чтобы временно закешировать размер массива в отдельной переменной (например, в целях производительности).
Главное, чтобы размер массива при этом всегда был доступен через свойство и без всяких внешних переменных.
Предположим, нам надо написать на JavaScript функцию, суммирующую в цикле элементы массива.
Чтобы в каждой итерации цикла не выполнять
достаточно ресурсоёмкое для динамического интерпретируемого языка обращение к свойству length,
мы можем сохранить размер массива во временной переменной intLength.
function summ (arNumbers)
{
var intSumm = 0;
//intLength - размер массива, закешированный во временной переменной
var intLength = arNumbers.length;
for (var i = 0; i < intLength; i++)
{
intSumm += arNumbers [i];
}
return (intSumm);
}
Однако, при этом, размер массива нам всегда остаётся доступен через свойство length.
И мы всегда можем обойтись без внешней переменной:
//Передаём массив как единое целое, без отдельной передачи его размера
var intSumm = summ (arNumbers);
object.getProperty () и object.setProperty (value)
У полей и методов объектов есть своё чёткое предназначение:
Поля — хранят данные;
Методы — реализуют поведение объекта.
В нормальном коде ясно видно, где идёт работа с данными, а где реализуется логика поведения объекта:
Работа с данными: objObject.property1 = "value1";
Поведение объекта: objObject.doSomething (param1, param2);
Использование методов в качестве акцессора и мутатора поля — уродство.
Во-первых, смешиваются два различных понятия: данные объекта и его поведение.
Нарушается естественная конструкция для доступа к данным через оператор присваивания. Оператор присваивания самой своей сутью подразумевает присваивание.
objObject.property1 = "value1";
strValue = objObject.property1;
Вызов метода, само использование круглых скобок, подразумевает реализацию поведения объекта.
Во-вторых, чтение и запись свойства реализуется по-разному, что противоречит сути поля.
При использовании свойств, для доступа к полю, как для чтения, так и для записи мы используем одну конструкцию:
objObject.property1
intA = objObject.property1; //Чтение
objObject.property1 = intB; //Запись
При использовании методов, для чтения и для записи поля используются разные конструкции:
objObject.getProperty1 () и objObject.setProperty1 ():
intA = objObject.getProperty1 (); //Чтение
objObject.setProperty1 (intB); //Запись
К полю не будет возможности применять стандартные операторы работы с данными, такие как ++, += и др.
Мы хотим увеличить значение свойства на 1.
При использовании свойств мы можем воспользоваться естественным для этого действия оператором инкрементации:
objObject.property1++;
При использовании методов, для выполнения элементарного действия приходится писать такую кашу:
objObject.setProperty1 (objObject.getProperty1 () + 1);
Для обращения к защищённым полям объекта как к данным, используются свойства. (Внимание: не «открытые поля», а именно — «свойства», см сноску [4]).
Свойство — это интерфейс для красивого и безопасного доступа к данным объекта [5].
При его использовании вызываются методы доступа к данным (например, для выполнения проверки на правильность записываемых в свойство данных).
Однако, вызов этих методов происходит «прозрачно» для разработчика,
что позволяет красиво обращаться к данным:
value = object.property и object.property = value.
Класс Person хранит поле _money, доступ к которому осуществляется через свойство money:
class Person
{
private long _money;
public long money
{
get
{
/*
Метод, который «прозрачно» вызывается при чтении свойства
*/
return (_money);
}
set
{
/*
Метод, который «прозрачно» вызывается при изменении свойства.
Например, здесь может быть проверка на валидность данных.
*/
_money = value;
}
}
}
Теперь мы можем нормально работать с данными:
Person psnBillGates = new Person ();
lngOldRiches = psnBillGates.money; //Чтение
psnBillGates.money = lngNewRiches; //Запись
psnBillGates.money += 1000000000; //Инкрементация
Свойства поддерживает большое количество современных языков: Delphi, C#, Python, Ruby и др.
Однако немало языков свойства не поддерживают: C++, Java и даже гибкий и красивый JavaScript [6]…
Знаете, есть две вещи, которые обязательно надо добавить в JavaScript. Но это не классы и строготипизированные переменные, как думают многие. Отсутствие классов и строгих типов — это не баг, а фича, дающая JavaScript такую гибкость.
Две возможности, которых действительно не хватает в JavaScript — это перегрузка операторов и поддержка свойств [7].
Рекурсию следует использовать только в тех случаях, когда решение задачи на каждом шаге разбивается на несколько подобных подзадач более низкого ранга
Здесь, в отличие от предыдущих разделов, я не буду столь категоричен.
Ибо рекурсия, или даже философия рекурсии, штука не такая простая.
И вопрос, когда следует (вернее, когда красиво) применять рекурсию, а когда нет, не столь однозначен.
Ну, в функциональных языках (таких как Lisp или Haskell) всё понятно: рекурсия применяется всегда, когда надо выполнить любые повторяющиеся действия. Там даже сумма элементов массива (там он называется списком) вполне может определяться рекурсивно как сумма первого элемента + сумма оставшейся части. В этих языках такой подход гармонирует с философией языка и, потому, красив.
Программа вычисления факториала на функциональном языке Haskell красива,
как в интерационной форме:
factorial n = product [1..n]
…так и в рекурсивной с использованием приёма «Сопоставление с образцом»:
factorial :: Integer -> Integer
factorial 0 = 1
factorial n | n > 0 = n * factorial (n - 1)
В императивных же языках всё сложнее.
Мне кажется, что в этих языках смысл рекурсии состоит в разбиении задачи на несколько подобных подзадач более низкого ранга. А тех в свою очередь — на насколько под-подзадач, и так, в геометрической прогрессии, пока мы не дойдём до тривиального случая.
При рекурсивном обходе дерева, мы разбиваем дерево на несколько поддеревьев (ветвей), каждое из поддеревьев на под-подеревья, и так, пока не дойдём до листов.
При вычислении определителя матрицы, мы находим определители нескольких подматриц меньшего порядка (миноры), для нахождения же миноров, мы разбиваем каждую из подматриц на под-подматрицы и т.д. пока не дойдём до матриц 2×2.
При сортировке массива слиянием мы сортируем несколько (обычно два) подмассивов этого массива. Для сортировки каждого подмассива мы сортируем их под-подмассивы, пока не дойдём до подпод…подмассивов длины 2.
Поскольку задача разбивается именно на несколько подобных подзадач, то количество данных (локальных переменных) на каждом шаге рекурсии увеличивается. Из-за этого заранее красиво объявить все эти локальные переменные в итеративном алгоритме не получится. Можно, конечно, вручную организовать стек, хранящий состояния каждой «итерации», но это уже будет замаскированная рекурсия, пусть и в обычном цикле без вызова функций. Что нелаконично, малопонятно и не очень красиво. И, соответственно лаконично и красиво использовать рекурсию.
Когда же задача не требует разбиения на несколько подобных, смысл рекурсии вырождается. И применять её в таких случаях — некрасиво.
Самым вопиющим примером уродского применения рекурсии является её использование при вычислении факториала и чисел Фибоначчи.
Я даже не говорю, что это страшно не эффективно. Я говорю, что вычисление факториала и чисел Фибоначчи — чисто итерационная задача, и рекурсивное её решение — это извращение самого смысла, самой сути рекурсии в императивных языках [8].
Как ни странно, эти задачи часто приводят в учебной литературе, причём в самом начале обучения рекурсии, в качестве первого примера. Быть может, рекурсия считается столь сложным для обучения приёмом, именно из-за того, что её объяснение ведётся на совершенно противоестественных её сути примерах…
Параметры любой нетривиальной функции должны задаваться по осмысленному имени, а НЕ положению в списке аргументов.
Никто не будет спорить с тем, что имена должны отражать суть переменённых.
И что использование имён переменных вроде a0, a1, a2 — не самый понятный и красивый приём.
Точнее, не иметь осмысленного имени могут переменные, не являющиеся уникальными, входящие в состав коллекции, обрабатываемые не каждая отдельно, а вместе, в цикле.
Осмысленные имена должны быть у уникальных переменных, обрабатываемых отдельно.
Программа, выводящая имя продукта, его кодовое имя и список глюков:
println ("Имя продукта: "+ objWinVista.name); //Windows Vista
println ("Кодовое имя: "+ objWinVista.codename); //Longhorn
println ("Число глюков: "+ objWinVista.bugsCount); //1 000 000 000 :-)
println ("Список глюков:");
for (long numBugNumber = 0; numBugNumber < objWinVista.bugsCount; numBugNumber++)
{
println (objWinVista.bugs [numBugNumber]);
}
В данном примере name, codename и bugsCount
являются уникальными данными и обрабатываются отдельно, поэтому имеют осмысленные имена.
Каждый же из глюков bugs [i] уникальным не является,
поэтому имеет не осмысленное имя, а просто номер.
Параметры функции являются такими же полноценными переменными.
Однако при вызове функции мы задаём параметр не по его осмысленному имени,
а по положению в списке параметров, т.е. по номеру.
Это ещё хуже, чем переменные a0, a1, a2.
Вот примеры из официальной документации к Java 2D:
GradientPaint gp = new GradientPaint (50.0f, 50.0f, Color.blue, 50.0f, 250.0f, Color.green);
или
RotationInterpolator rotator = new RotationInterpolator (
new Alpha (-1, Alpha.DECREASING_ENABLE, 0, 0, 8000, 0, 0, 0, 0, 0),
xformGroup, axis, 0.0f, (float)Math.PI*2.0f);
Что означают эти параметры:
-1, Alpha.DECREASING_ENABLE, 0, 0, 8000,
0, 0, 0, 0, 0?
Есть только два случая, когда можно использовать неименованные параметры функции.
Первый случай, это когда параметров немного (не больше 3), и их предназначение очевидно.
Math.pow (2, 5) вряд ли можно интерпретировать иначе как 25.
Ну, разве что, как 52 :-)
Второй случай, это когда каждый из параметров не является уникальным, и не имеет собственного предназначения.
Функция, суммирующая числа: Math.summ (3, 7, 18, -2, 11, 2.3)
Во всех остальных случаях параметры надо задавать по осмысленным именам.
К сожалению, лишь немногие языки поддерживают именованные параметры функций (я могу вспомнить только динамические Perl, Python и Ruby, может быть есть ещё).
Функция перевода текста.
$strResult = translate
text => "Hello, world!",
from => $lngEnglish,
to => $lngRussian,
vocabulary => $vcblrGeneral,
quality => 10;
Что же делать в остальных языках?
В процедурных языках (вроде C или Pascal) проблема вызова функций с большим количеством малопонятных параметров стоит особенно остро.
Для её решения вместо передачи большого числа параметров, можно создать структуру, в которой поля будут соответствовать параметрам функции; затем, обращаясь через поля с осмысленными именами записать в структуру нужные данные; и вызвать функцию, передав ей структуру в качестве параметра.
//rectangle1 – структура для хранения параметров функции
Rectangle rectangle1;
//Задаём параметры функции
rectangle1.x = 80; //x
rectangle1.y = 25; //y
rectangle1.width = 50; //ширина
rectangle1.height = 75; //высота
rectangle1.rotation = 30; //угол наклона
rectangle1.borderWeight = 2; //толщина контура
rectangle1.borderColor = "red"; //цвет контура
rectangle1.backgroundColor = "blue"; //цвет заливки
rectangle1.alpha = 20; //процент прозрачности
//Вызываем функцию, передавая ей структуру с параметрами
drawRectangle (rectangle1);
Гораздо больше кода, но зато более понятно и красиво. Хотя, объявление лишних структур для каждой функции, тоже не очень красивое решение.
К счастью, сейчас процедурные языки используются достаточно редко, и главным образом в тех случаях, когда больше важна не красота и понятность, а скорость выполнения программы (например, C).
В классических объектно-ориентированных языках вместо сложной функции,
лучше всего реализовать шаблон проектирования
«Command»:
создать объект; затем, обращаясь через свойства с осмысленными именами, записать в него данные;
и вызвать метод apply ();
Rectangle rectangle1 = new Rectangle ();
rectangle1.x = 80;
rectangle1.y = 25;
rectangle1.width = 50;
rectangle1.height = 75;
rectangle1.rotation = 30;
rectangle1.borderWeight = 2;
rectangle1.borderColor = "red";
rectangle1.backgroundColor = "blue";
rectangle1.alpha = 20;
rectangle1.draw ();
Опять же, гораздо больше кода, но зато понятно, и достаточно красиво.
И, наконец, в динамических языках, не поддерживающих именованные параметры (например JavaScript), в качестве параметра функции можно передать созданный на лету объект с осмысленными свойствами.
Функция анимации из библиотеки Dojo Toolkit. В качестве аргумента функции передаём объект со свойствами-аргументами функции:
var slideLeft = dojo.fx.slideTo
({
node: divAnim,
duration: 1000,
left: 0,
top: 80
});
Функция поиска файлов. Объект с аргументами имеет сложную структуру:
var arSerchResult = searchFiles
({
text: "Пушкин",
extensions: ["txt", "rtf", "doc", "pdf", "odt"],
hidden: true,
modified:
{
from: new Date (01, 02, 2008),
to: new Date (15, 03, 2008)
},
size:
{
min: 10000,
max: 1000000
}
});
Лаконично, понятно и красиво!
Пока всё на этом.
Наверняка, в некоторых местах вы будете со мной несогласны — ведь чувство красоты у всех разное.
Ну и не стоит забывать, что отсутствие в коде приведённых «уродских приёмов» — лишь идеальная ситуация, к которой надо стремиться, но приходится жертвовать в определённых ситуациях (например, в целях производительности). Ведь умение приносить жертвы при решении практических задач — один из элементов профессионального программирования.
↑ Стив Макконнелл ещё более категоричен, чем я. В книге «Совершенный код» он пишет:
В идеальном случае сразу объявляйте и определяйте каждую переменную непосредственно перед первым обращением к ней
↑ Вообще, надо стараться избегать длинных функций и методов. Одно из правил рефакторинга: «если текст метода не умещается на экране без прокрутки — это уже повод разбить его на несколько более коротких методов».
Однако, это далеко не всегда удаётся, например при генерации отчётов или при реализации сложных вычислений.
↑
Есть и противоположенная точка зрения.
Например, Ховик Меликян в статье «Клиника плохого кода» пишет, что
возврат корней квадратного уравнения через параметр функции:
num_roots = qe_solve(10, 20, 2, &x1, &x2); — гораздо красивее, чем использование объектов.
↑ Не надо путать методы, открытые поля и свойства (как, например, в этом самоуверенном комментарии).
Открытое поле — это просто переменная внутри объекта, доступ к которой извне ничем не ограничен.
При использовании открытых полей нет никакой возможности отследить обращение к ним, или провести проверку корректности записываемых в поле данных.
Поэтому, использование открытых полей — нежелательно.
Для контроля обращения к данным можно использовать методы
object.getProperty() и object.setProperty(value).
Методы позволяют отслеживать обращение к данным, выполнять при этом сопутствующие действия и проверять правильность введённых данных.
Однако, использование методов — очень некрасиво, о чём я и пишу в статье.
Тем не менее, в языках, не поддерживающих свойства, при выборе между использованием открытых полей и методов — лучше использовать методы. Хоть это и очень некрасиво, но, зато, гибко и безопасно.
Вместо открытых полей и методов, при наличие такой возможности, следует использовать свойства.
Свойства — это интерфейс для доступа к данным, совмещающий гибкость и безопасность методов с красотой открытых полей.
При обращении к свойствам вызываются всё те же методы, однако вызов их происходит «прозрачно» для разработчика.
Это позволяет красиво обращаться к данным: value = object.property и object.property = value.
Как в этом примере с классом Person и свойством money .
↑ Разумеется, свойствами, как и любой другой возможностью, не стоит злоупотреблять, применяя их где только можно.
Свойства следует использовать для реализации доступа к данным объекта. Ведь использование свойств, само использование оператора присваивания подразумевает доступ к данным.
Если же, при обращении к данным происходят сильные побочные действия — использовать свойства нежелательно.
В этом случае, красиво и правильно, как раз, использовать методы. Ведь вызов метода, само наличие круглых скобок предполагает реализацию поведения объекта. О чём я и писал в самом начале этой главы.
↑ Формально поддержка свойств появилась ещё в JavaScript 1.5:
object.__defineGetter__ ("property", function () {/*Код аксессора*/})
object.__defineSetter__ ("property", function (value) {/*Код мутатора*/})
Однако она только относительно недавно стала работать в «нормальных браузерах» и, несмотря на все обещания, до сих пор не работает в Internet Explorer.
↑ Перегрузка операторов появится только в JavaScript 2.0.
↑ Для вычисления чисел Фибоначчи использование рекурсии имеет какой-то смысл: всё-таки основное определение этой функции является рекурсивным:
Fib (n) = Fib (n−1) + Fib (n−2); Fib (0) = 0; Fib (1) = 1
Однако, наличие же у факториала рекурсивной формы записи:
n! = n × (n−1)!
нисколько не приближает нас ни к пониманию смысла рекурсии,
ни к пониманию смысла факториала
как произведения чисел от 1 до n.
С первым и вторым пунктом НЕ согласен.
Объявление переменных в начале логического блока позволяет контролировать их использование и занятость в блоке. Сам пишу на Delphi, а в последнее время все чаще на PHP. Уже пару раз встречал подобное:
tmp = rand(1, 10); // Тут работаем с tmp tmp = random_string(); // Тут работаем с новым tmp ... tmp = tmp * 1.5;
В том же Delphi такое не реально.
По поводу возврата результата через параметр: да, по большей части такое только вредит. Но, бывают случаи, когда из функции надо вернуть 2 результата: результат работы функции и доп.данные. В таких случаях, можно воспользоваться массивом или классом, но мне это не нравится. Проще что-то, наподобии следующего:
function doSum(aParam1, aParam2: integer; out oResult: integer):boolean;
С пунктом «Доступ к свойствам объекта через object.getProperty () и object.setProperty (value)» не согласен.
Во-первых, смешиваются два различных понятия: данные объекта и его поведение.
Представьте себе ситуацию, в какой-либо программе поголовно используется код типа psnBillGates.money = lngNewRiches;
Раз этак сотню-другую. А потом вдруг при каждом изменении денег у psnBillGates, да и у любого объекта того же класса, потребуется оповестить налоговую службу об изменении денег на счету. Как вы будете поступать в этом случае, к каждой строчке psnBillGates.money = lngNewRiches; будете дописывать оповещение служб. Мне кажется проще использовать метод setMoney(value) и в нём единожды добавить оповещение и налоговой, и антимонопольной службы.
То есть на изменение данных у объекта должна быть возможность реагировать (то же поведение)! (Да хотя бы проверка на валидность нового значения)
Да по сути все верно написано.
Есть конечно кое какие притензии по поводу 6-го пункта.
Вот пример обстрактный
a = array(1,2,3);
for(i=0, len = a.len; i<len; i++)
{
...
}
ну вот как тут можно обойтись без 6-го пункта?
если не заводить отдельную переменную для длины масива то её прийдётся вычислять каждую итерацию, это не всегда хорошо да и дальше, если масив большой
думаю дальше...
Объявление всех переменных в начале функции — страшное зло.
Я с этим не согласен, ведь если есть какие-то (логические) блоки в методе значит этот метод можн разбить на совокупность других методов.
Не в стречал методов с отдельными логическими блоками, которые нельзя разбить на несколько методов.
А функции по 200 строк - ....
>Использование методов в качестве акцессора и мутатора поля — уродство.
А если нужно какуюто валидацию делать? В свойстве это не уродство?
Cheatsheet на основе этого можно было бы сделать красивый.
Но, к сожалению, универсальных coding guidelines не бывает.
А давно в lisp нельзя писать последовательно инструкции?
а что делать с do нотацией в haskell?
Чувак в общем. По порядочку.
"Переменные должны объявляться в начале логического блока, в котором они используются, а НЕ в начале функции или программы."
Тут кому как удобно... Тут сильно спорить не буду.
"Функция должна возвращать результат, зависящий от ее параметров, а НЕ принимать результат в качестве аргумента."
Ничего она не должна. Смотри сюда. Есть вектор который надо наполнить внутри ф-и... Т.е. легче сделать так
int foo(std::vector<int> &vec1)
{
//сбор вектора.
}
main
{
std::vector<int> vec;
foo(vec);
//тут уже плностью готовый вектор. И нет затрат на сборку конструирование, копирование и разрушение вектора. И т.д.
}
"Отсутствие локальных функций"
Как по мне так полный бред... В с++ есть отдельные блоки, но ты первый в них запутаешься... Если они у тя будут по 20 а то и больше строк..
"Отсутствие else if"
Тут все зависит от ситуации...
"Для доступа к данным должны использоваться свойства, а НЕ методы."
Круто! По ООПшному! Слов нет, и что такое инукапсуляция тоже никто не задумывается.. Да бес с ней! Любой объект и не только меняет проперти этого обьекта. Или иными словами меняют проперти все кому не лень.
"Параметры любой нетривиальной функции должны задаваться по осмысленному имени, а НЕ положению в списке аргументов."
Эээ, полный бред... У функции есть набор параметров такой какой считает нужным автор ф-и... И это полный бред... Извини...
З.Ы. Я не знаю может ты и мнишь из себя крутого программера, но почитай мат часть... Поможет... Про массивы и числа фибоначи просто умолчу, хотя и про это есть что сказать... Но лень.. Так шо книги почитай а потом в сеть пихай... И еще не отправляй это на rsdn. Засмеют...
deepshark, а что мешает добавить оповещение в блок set? :)
То есть на изменение данных у объекта должна быть возможность реагировать (то же поведение)! (Да хотя бы проверка на валидность нового значения)
Вы путаете поля и свойства. Свойство (property) в современных языках — это геттер и сеттер, маскируемые под обычное поле. А снаружи всё прозрачно.
За статью спасибо. Интересно и качественно написано. В большинстве вещей с вами согласен.
Именованные параметры кажется есть ещё в Groovy, если не путаю ничего, ибо знакомился поверхностно и с удовольствием забыл.
Никогда кстати не задумывался насчёт ненужности рекурсии в некоторых ситуациях. Хотя вроде и не злоупотреблял.
Спасибо за статью
Можно поставь якаря для главного списка ( чисто для новых читателей )
Представьте себе ситуацию, в какой-либо программе поголовно используется код типа psnBillGates.money = lngNewRiches;
Раз этак сотню-другую. А потом вдруг при каждом изменении денег у psnBillGates, да и у любого объекта того же класса, потребуется оповестить налоговую службу об изменении денег на счету. Как вы будете поступать в этом случае, к каждой строчке psnBillGates.money = lngNewRiches; будете дописывать оповещение служб. Мне кажется проще использовать метод setMoney(value) и в нём единожды добавить оповещение и налоговой, и антимонопольной службы.
То есть на изменение данных у объекта должна быть возможность реагировать (то же поведение)! (Да хотя бы проверка на валидность нового значения)
Ну так просто в свойство добавляеться оповещение. Свойства - это и есть объединенный set и get. Просто они позвляют получать нормальный доступ с применением операторо.
Спасибо за отличную статью. Некоторая часть подобных мыслей самому приходила в голову.
За статью огромное спасибо!!!
Полностью согласен со всеми пунктами.
Я сматрю многим не понравилось работа с методами через getProperty(),setProperty(value), кто-то даже про инкапсуляцию говарил и про опавещение кого-то там, друзья советую вам взглянуть как это сделано в C#, акцессоры и мутаторы никуда не диваються!!!
Мы просто преобритаем удобный нэмспейс и плюсы использования операторов с данными свойствами класса, отсюда красоту написания кода.
Очень надеюсь что будет продолжение. Так как для новичка это статья будет очень интересна.
Об искустве программирования можно многое что написать ;-).
СПАСИБО ещё раз!!!
к Автору.
Во многом согласен. Кроме вот этих пунктов:
Первое:
Это говорит только о том, что Вы пишите не думая вот и все. Переменные объявленные в начале программы (точнее в начале каждой подпрограммы, если быть точным) делают код однозначным, заставляют не писать спагетти-код (длиной более одного экрана). И еще Если Переменная нужна только один раз, то в 80% случаев без нее можно обойтись! А иначе код становится трудно-читаемым!
Далее по тексту:
4. Отсутствие else if;
else if - первый шажок к нечитаемому коду. Замена - синтаксис case (swich в других языках): во-первых, он читаемее, во-вторых, чаще всего работает быстрее, так как нет лишних проверок.
Пункты с которыми согласен лишь отчасти:
6. Хранение размера массива в отдельной переменной;
Иногда (когда речь идет о действительно больших массивах) затраты на много кратное вычисление длины массива не оправданы, и проще сохранить длину массива в отдельном месте.
9. Отсутствие именованных параметров функции;
Крайне спорно. При написании кода
x=func_name(param1:lala,param2:lala2);
Текст увеличивается и как следствие менее читаем. Во-вторых, возможность жонглирования порядком переменных - прямой путь к ошибке!
Michael, artyfarty, Ilya, спасибо, был невнимателен, смотрел узко.
Алик Кириллович, спасибо за статью и дискуссию.
Статья отличная, спасибо.
Возражу по поводу чисел фибоначчи: не взирая на то, что их определение рекурсивно, рассчитывать их рекурсивно - очень большая ошибка, ведь F(n) = F(n - 1) + F(n - 2) =
(F(n - 2) + F(n - 3)) + F(n - 2) - тоесть у нас уже на втором шаге два независимых вызова F(n - 2), каждый из которых в свою очередь вызовет F(n - 3) и F(n - 4). Таким образом мы получим лавинную реакцию, которая съест процессор при маленьких n, а при сравнительно небольших (~50-60) стает уже неподъемной. Для сравнения - итерационный алгоритм линейный, и скорее упирается в длину целого числа.
Можно также написать умную рекурсию, которая будет кешировать результаты - но это уже другая история.
Вобщем резюме: если рекурсивная функция вызивает себя более одного раза - подумайте о границах дерева вызовов. Оно может оказатся значительно больше, чем вы ожидаете.
Попытаюсь тоже пройтись по порядку:
1). пункт - не согласен точнее согласен лишь отчасти, в случае если пишется функция, то место под неё в стеке всё равно выделяется сразу под все локальные переменные. Если переменные стоят в начале, то сразу видно, что и как используется функцией. Если они граммотно прокомментированы, то проблем вобще не возникает. Про "поменять местами 2 переменные" - фунции рефакторинга в IDE придумали трусы?
Смысл выделать переменные есть если использовать блоки {} в C подобных языках. Но почему бы в таких случаях не использовать вложенную функцию.
2). возврат функции через результат точно нужен в функциональных языках программирования там без этого никуда :). В остальных же опять возникают вопросы иногда это действительно удобно иногда нет, например вы возвращаем результат + код ошибки(или даже список ворнингов, чтобы не было соблазна исключение кидать), тогда тут надо делать объект обёртку, что не всегда удобно особенно в языках типа C/C++. В языках где объекты делаются на лету, конечно большой проблемы не стоит.
Про матрицы был приведён ужасный пример на C. Вы создаёте объект внутри функции в локальной переменной, не подскажете, а что происходит с локальными переменными после возврата из функции? Правильно они освобождаются, а у Вас радостно передаётся ссылка на переменную "зомби", (в Яве,C# и проч языках со сборщиками мусора подобной проблемы нет, но в C/C++/Pascal надо бы об этом помнить).
3). Круто, тут ничего не скажешь :) жаль не во всех языках это реализуемо. Ну и наверное если локальная функция потом может использоваться в разных функциях, то "привет refactoring".
4). Пример хороший, по в Paskal варианты "if ( cond1) and (cond2) then" не равнозначен варианту "if (cond1) then if (cond2) then", т.к. паскаль в первом случае не прервёт проверку после проверки cond1. Вобще в этом вопросе всё сильно зависит от языка и компилятора.
Написавшему, что else if это замена switch просьба вспомнить, что в switch есть магическая команда break; и код типа switch(a) { case 1: do1; case 2: do2;break; } очень неудобно переписывается через else if. А if (cond1 && cond2){} else if (cond3) {} никак не пишутся через switch.
5). статья правильна во всех случаях когда не нужна сверхскорость. на опыте (паскаля) паралельные массивы просто быстрее (порядок уже сказать не могу давно было)
7). доступ к fields через properties. (огорчило не знаение участниками дискуссии, того что такое property). Тут вопрос опять же спорный. В принципе property это очень круто, если язык позволяет использовать разные уровни доступа. т.е. private setField; public getField; /паскаль не позволяет, в java только в следующей версии появятся свойства/, в этом случае пропертями не обойтись
8). согласен с автором. В не функциональных языках действительно лучше не использовать рекурсию там, где это возможно. Но надо вспомнить о том, что можно сохранять результат вычисления фунции в зависимости от поданных значений. Причём в c (с gcc точно) компилятор может сам позаботиться об этом - в таком случае можно получить прибавку в скорости (за счет памяти)
9). да человеку, которому лень изучать Ваши интерфейсы так будет удобно. Так же это удобно, когда можно передавать не все параметры из большого кол-ва, тогда не надо писать тысячи перегруженных функций (особенно в пхп где перегрузки нет (не уверен на счёт пхп5). Но хорошо бы подумать за счёт чего появляется это удобство и как работает такой подход :). Может получиться так, что потери слишком большие. Мне понравился вариант в python где функция задаётся как def name(a,b,c,k*,k**): где k* это список именованных параметров k** просто список параметров по номеру (пинайте больно если перепутал порядок), но общий смысл тот, что там обработаны все возможности: передача с заданным порядком, именованные параметры, передача неописанных именованных параметров, передача списка неименованных параметров.
10). возможность создания объектов это круто конечно. Но опять же нужно думать какие у этих объектов могут быть возможности и что будет стоять за этим созданием объектов. В общем в языках с использованием статистического /не путать со static/ связывания мы его уже не получим.
Все могу слова не претендуют на оригинальность или правильность.
Как говорит препод по информатике: "В программировании всегда приводятся противоположные рекомендации. Поэтому умение программировать заключается в том, чтобы выбирать среди рекомендаций правильный баланс. Следование любому набору рекомендаций доведённому до абсурда приведёт к тому, что программа будет нечитаемой, неподдерживаемой и нестабильной".
С Уважением.
Не согласен по поводу локальных функций.
Свое не согласия вырожу только одним вопросом:
При повторном использовании локальной функции в другой функции ее что придется искать(а я понял автор очень сильно не нравиться ходить по коду) и копировать или забить на поиски и плодить код, не хорошо это...
И по свойствам, да в тех языках где поддерживается синтаксис и использование понятия свойство не нужно плодить методы, но по-моему в C\C++ без функций не обойтись, ведь данные объекта(класса) должны храниться отдельно, а любые действия над ними оформляются функциями. Один из принципов ООП (инкапсуляция).
И еще примеры с факториалом и числами Фибоначчи, правильно написано приводятся в учебниках как пример, так как легче объяснить что значит рекурсия. Рекурсия сложное понятие, и не многим она дается с первого раза, да после обучения нужно писать более организованный код, но это уже остается на совести пользователя.
Спасибо за статью, полезно прочитать.
автор. респект!
сам думаю 100% также.
АХРЕНЕННО.
Последнее время начинаю думать, почему все языки не есть яваскрипт + c# ? И тогда не пришлось бы плодить адское колво 1 разовых языков
@Melis
9. Отсутствие именованных параметров функции;Крайне спорно. При написании кода
x=func_name(param1:lala,param2:lala2);Текст увеличивается и как следствие менее читаем. Во-вторых, возможность жонглирования порядком переменных - прямой путь к ошибке!
Жонглирование порядком? Насколько я понимаю, список аргументов предстает ассоциативным массивом.
Статья интересная. Но мне(да и не только мне) было бы интереснее, если бы пример кода был бы на двух языках. К примеру на вашем(я так его и неопределил) и на питоне к примеру
Все правильно сказано, я сам программист, и придерживаюсь точно таких же принципов.
Статья имеет огромное значение для начинающих программистов, вроде меня...
Сжечь еретика! У каждого своё понятие красоты, не надо приводить аксиомы, забивая молодежи головы больными стереотипами...
@phpdude
Последнее время начинаю думать, почему все языки не есть яваскрипт + c# ? И тогда не пришлось бы плодить адское колво 1 разовых языков
Вы имеете ввиду asm, c, bash,java, pascal etc.?
Почти все советы спорные и даже очень.
А для кого стараться? Для взломщиков? Ага! Чтобы им код понимать легче было! Каждый программирует так, как ему легче и понятнее.
Здрасте!
Первый же приём...
А вам не кажется, что если класс или метод можно разбить на логичесмкие блоки, то лучше было бы разбить сам класс или метод на несколько отдельных классов/методов?
И сразу же приводите пример огромного метода, который можно было бы разбить на 5 нормальных и у которых объявление всех локальных переменных начинается в начале метода. :)
З.Ы.: А на картинке что? Неужели мне не привиделось и вы объявляете стопку переменных в одной строке, через запятую? И кто говорит о чистоте кода?
var intLength, elTemp, intCenterIndex, intLeftSubarrayLength, intRightSubarrayLength, arSortedSubarray, intLeftElIndex, intRihtElIndex, i;
Вы коллекционируете самые плохие практики программирования?
Вы только представьте: У нас есть функция в 300 строк кода [2].
Вот где зло и о чём надо было писать, а не о постановке переменных.
Извините, что сразу такие комментарии. Просто сразу, первое же и задело. Видел я такой код, в нём прривлекательности ровно нуль. А, вообще, очень достойная попытка описать что-нибудь - очень добротно и многословно, прям разжевали и в рот положили.
Я не думаю что JavaScript именно тот язык программирования, который нужно приводить в пример в статье о "красоте программного кода".
Возврат результата функции через её параметр - очень нужная и важная штука.
Локальные функции лучше вообще вынести в отдельный контекст/класс *Context.
В общем-то это всё моё ИМНО.
Я раньше загонялся красотой кода, структурой и т.д. Но после того как на меня наехал препод по кодингу за то, что я использую не рациональный подход, предусмотрев слишком много лишних вариантов и код получился у меня не понятный(хотя его оптимизация заняла раз в 10 больше времени разработки),я забил.
Переменные не выношу, все должно быть к месту...
Совершая нападки на аксессоры, ты плюешь против ветра. Много ли серьезных книжек ты прочел, и, что самое главное, много ли серьезных проектов поднял и сопровождал ?..
Да ваще какая разница какой код главное результат!
@Tena: ну не скажите... Красота кода - это очень важно!
А по поводу использования параллельных массивов - многие их используют в парсерах. Когда приходится при помощи регулярных выражений вытаскивать определенные значения.
Забота о красоте кода это правильно, в конце концов, его ведь читают чаще, чем пишут. Спорные моменты есть, но в статье о красоте, к тому же не привязанной к конкретному языку без них не бывает.
О параллельных массивах скажу, что стараюсь при наличии возможности использовать ассоциативные вместо плоских в подобных случаях. Хотя конечно это не всегда лучше, и не во всех языках есть.
Про аксессоры и мутаторы полностью согласен, свойства выглядят красивее. Причем я предпочитаю проверку данных писать именно в мутаторе (будь то метод или свойство), а не где-то вовне, мне кажется это помогает сохранить некую внутреннюю целостность объекта. Иногда можно наблюдать, как ее делают в обработчике события, но смешивание интерфейса с логикой мне решительно не нравится.
Про именованные параметры можно добавить о существующих в Python (может и где-то еще, не в курсе) значениях параметров по умолчанию, позволяющих не перегружать методы и создавать логичное дефолтное поведение.
Еще можно было бы как-нибудь написать о примерах плохого оформления, но это уже несколько другой вопрос.
Спасибо за статью
Можно поставь якаря для главного списка ( чисто для новых читателей )
Да уж...не знал,что многие эти приемы настолько смертельны для программы...огромное спасибо автору!=)
Все считаю по теме, широко написано
Статья конечно неоднозначная, по крайней мере для меня. Все языки меня мало волнуют, а вот применимо к javascript интересно. Обзор стилей написания заставил проанализировать собственную технику письма. Считаю так, если логично применять тот или иной способ, то его нужно применять. Не всегда удается продумать все наперед, для этого нужен определенный опыт проектирования систем. К примеру нет смысла делать функцию локальной если ее функционал будет использоваться в дальнейшем и это сразу закладывается на будущее, с другой стороны нет смысла плодить такие функции в массовом количестве. Мне кажется здесь важно понимать такую грань, а не делать или не делать как написано в этих "10 правилах". Однако подведу черту, статья безусловно заставляет задуматься над тем что используешь в данный момент. Побольше бы таких статей.
подскажите что делать если реально устаешь от кодинга?
Сначала критика:
1)
Вы только представьте: У нас есть функция в 300 строк кода
такую функцию стоит добавить в признаки уродского кода ;)
2)
Отсутствие else if
switch / case, и будет вам счастье.
3)
Написавшему, что else if это замена switch просьба вспомнить, что в switch есть магическая команда break; и код типа switch (a) { case 1: do1; case 2: do2;break; } очень неудобно переписывается через else if. А if (cond1 && cond2){} else if (cond3) {} никак не пишутся через switch.
switch (true) { ... } и пишется всё, что угодно.
4)
Обязательное хранение размера массива в отдельной переменной
смотря какой размер массива, правда, автор сам уже уточнил:
Разумеется, нет ничего плохого в том, чтобы временно закешировать размер массива в отдельной переменной (например, в целях производительности).
5)
Доступ к свойствам объекта черезobject.getProperty () и object.setProperty (value)
Плохие примеры... billGates.getProperty('money') - бесспорное уродство. Но вот billGates.money += 100, тоже довольно плохо, потому что при прямой записи у нас нет возможности контролировать валидность данных, и совершать связанные действия.
Вот неплохая статья по теме, если интересно - читайте:
http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html?page=1
6)
Использование рекурсии ... http://www.alik.su/articles/10-ugly-programming-techniques/recursion-to-factorials.png
Картинка немного радикальная ;) может корректнее было написать "не стоит использовать рекурсию, если вы не понимаете зачем она там".
7)
↑ Перегрузка операторов появится только в JavaScript 2.0.
Жуть. Надеюсь, ни один нормальный интерпретатор не будет ее поддерживать ... так ни для чего более умного, чем обфусцирование кода, она не нужна.
П.С.: Хорошо, что есть такие статьи - они заставляют думать :) Автору - спасибо.
Почитал, хм, любопытно, и сделал такие выводы.
В статье идёт речь о персональной(для каждого отдельно взятого) манере написания кода.
Утверждать, что писать надо так, и только так, а не иначе, ну это не серьёзно. Все мы люди, и все мы разные.
Несколько дней назад, мне попалась маленькая заметка, но уж очень яростная, её смысл можно свести к одному предложению:
"Все козлы, кто пишет не так, как я, мне не удобно читать ваш код"
И человек приводит пример, как мы должны писать, чтобы ему было хорошо. Написано, всё это было на грани фола.
А здесь нам предоставили возможность дискутировать, хотя и не вижу о чём.
На одном из форумов попался вопрос начинающего, как правильно писать код, ну ему и ответили, как тебе удобно так и пиши.
ух... спс за статью, но можно было бы разбить на несколько ибо не осилил всю
Добавлю и я свое имхо. Много с чем не согласен, но особо...
Пункт 2.
Кодил я на ассемблере. А вы?
Вы в курсе каким образом передается параметр в функцию? Через стэк. А каким образом он возвращается? Тоже через стэк.
А теперь представим что вы возвращаете через стэк объект на 5мб. А теперь это в цикле на 1000+ итераций в кадр.
О да! почему у меня все висит! А почему на эту игру не хватает 4гб оперативки? Как такое возможно?!
Категорический против 7 пункта. Был такой случай что пришлось у объектов отслеживать изменение параметра(что не было изначально запланировано в проекте), что бы запретить в некоторых случаях, а в некоторых модифицировать параметр иначе, чем запрашивается. Что бы вы делали с вашим object.property++ в данном случае?
Это называется жесткая структура кода, когда изменение модели поведения ведет за собой изменение всего кода. И его категорический не советуется применять, дабы избежать загнивания проекта
Процесс творческий, кто-то любит порядок и красоту, кто-то и в хаосе себя вполне уютно чувствует.
Беру статью на заметку
У меня друг программист, думаю ему пригодятся советы
Спасибо
5
Классная статья! Некоторые моменты приходили и мне в голову. А вам +1 подписчик:)
А Вы не заметили, что Вы своими пунктами как идуальный, фактически, описываете функциональный язык программирования?
Неплохие советы начинающим, а так очень много спорных моментов, особенно если идёт речь об оптимизации на скорость работы.
Интересный пост - лично я в своих проектах тоже как-то по началу думал так же ,но потом как-то разленился, так что грустно как-то...Спасибо за единомыслие!
Стремление к красоте кода - очень-очень похвальное поведение, но уверен, что правила должны быть у каждого свои. Могу привести контрпример ко второму пункту, где при описанном подходе не только потеряется эффективность, но и красота:
Предположим, что у нас есть большой объект и набор функций, вносящих в этот объект изменения - например, матрица, которая пересчитывается. Каждая функция встречается больше одного раза - поэтому их нельзя объединять. Каждая функция может определить, что объект плохой и она не может продолжить его изменение. Технически это очень напоминает конвейер. Все функции в примере возвращают булево значение - OK или FAIL, на его основании принимается решение стоит ли дальше продолжать обработку объекта.
var object;
if (function_1(&object) &&
function_2(&object) &&
function_3(&object) &&
function_4(&object) &&
function_5(&object)
) {
...
}
Читать было интересно, но есть очень спорные моменты, где каждому своё.
1-й пункт высосан из пальца.
Вопрос возникает, что делать, если разрабатываешь на чистом Си? Как эта теория сработает? Заявления о том, что язык Си фигня и прочее - как-то смешно будет выглядеть.
Да и более верным подходом будет разбить большую мотню кода на более мелкие процедуры
Как раз изучая программирование возьму на заметку
Я правда только начинаю ПХП постигать, такое громадное количество букав не реально за раз переварить,будем возвращаться
По поводу объявления переменных могу сказать то, что кому-то удобно объявить их в начале программы и он это делает на протяжении 10 лет и ему это удобно. Может для Вас после этого переписать этот код будет невозможным заданием, а ему удобно. А в остальном согласен.
Товарищи!
Хороший код - это понятный код. А понятным его делает добротная документация, со схемами, и большое количество комментариев в нужных местах. Ибо программисты, которые будут разбираться в коде после Вас, могут иметь иные представления о красоте кода.
Очень полезные советы, раньше некоторые элементы делала не так, теперь благодаря вашему доступному объяснению, буду делать более удобно. Кстати блоки "В двух словах" очень наглядно демонстрируют основную мысль
Мне очень понравилась статья! Большое спасибо автору. Сделал для себя пару выводов.. Сейчас изучаю Delphi..
Вам не кажется что статья длинновата??:)) но я все таки осилил её до конца и пару полезных советов изьял для себя, спасибо
Задумался над вторым пунктом.. О некоторых пунктах вообще раньше не думал. Буду выводить свою систему правил))
Спасибо за интересную статью. Действительно, аккуратный код сейчас очень важною. Даже в егэ по информатике на паскале требует ОПТИМАЛЬНОЕ решение задачи! вот так)
Полезные и нужные советы, некоторые части ранее творил не так, теперь благодаря вашему доступному объяснению, буду делать более удобно.
Спасибо за статью, считаю она очень полезна для PHP программистов.Всё по порядку, подробно и понятно. Ваши статьи интересно читать!
Для любого программера это полезно. Прочитав, я задумался о своем стиле коддинга. Почти все уродские приемы я не использую давным давно, но есть один.
Спасибо за пост. Буду читать дальше ваш блог
ЗЫ первый мой коммент
Отсутствие комментариев в коде или комментарии не по делу.
Статья полнейшая лажа.
Такие вещи в первом семестре объясняют в уважающем себя инженерном ВУЗе.
Полезно для тех кто не обучается по специальности программиста,т.е. для тех кто до прочтения этой статьи являлся говнокодером.
Помню как нам преподавательница по программированию все вбивала, что программировать надо красиво, с понятной структурой. И действительно потом анализировать программу на ошибки становилось легче и код становился более читаемым.
Самый близкий мне по идеологии язык — Руби. В нём, например, доступ к параметрам осуществляется всегда через методы. В качестве примера, покажу класс, в котором доступ к параметру записывается в лог.
class ProtectedClient < Client # Все данные класса содержатся в переменных, начинающихся на @. # Они недоступны извне. def money @log.info 'Получен баланс клиента' @money # "return" можно опускать end def money=(val) # в названиях методов могут содержаться знаки равно, арифметических операций, а так же восклицательный и вопросительный знак @log.info 'Изменён баланс клиента' @money = val end end petr = ProtectedClient.new p petr.money # выведет количество денег Пети и запишет это в журнал petr.money = 500 # изменит количество денег Пети и запишет это в журнал # эквивалентно petr.money=(500)
Из примера, кстати видно ещё много приятностей Руби. Например, при вызове методов, скобки можно опускать; точки с запятой в конце строк не нужны. При изучении языка видишь ещё множество подобных штук, упрощающих написание кода и делающих его красивым.
Я за 5ть лет обучения научился красиво писать программы, работаю программистом уже несколько лет. А вот когда у жены не приняли задачу на паскале, сделанную мною, по причине большого количества функций(4 функции в программе), я просто офигел, пришлось переписывать все без функций, код увеличился в 2 раза. Потом задачу снова забраковали - переменные слишком длинные, используйте однобуквенные сокращения, сказал препод.
Я уже не первый год работаю разработчиком на Java. Хоть я и завел свой блог, я все еще пишу кое какие программы на Java. Язык передовой, но, конечно минусов он не лишен. Правда этой осенью уже будут выпускать 7ую Джаву. Может в новых версиях будет поддержка свойств.
Прочитал статью, вспомнил как в универе над преподами издевался используя в С++, обращение к массиву в виде номер_элемента[имя-массива], половина преподавателей просто не верило, что это работает.
Статья отличная! Спасибо ) Пошел читать МакКонела.
Хорошая развернутая статья. Некоторые подобные идеи приходили в голову. В большинстве вещей соглашусь. В первую очередь, что код должен быть грамотно и красиво написан, что б хотя бы самому не заблудится в нем :). И после написания другой программер понимал что к чему. Тем более если это большой проект в котором участвуют много народа. Об искусстве программирования можно многое что написать. Жаль не во всех языках реализуемы перечисленные методы.
Я так понял, что это самый популярный пост