Здравствуйте, IT, Вы писали:
IT>Несколько практических советов от настоящих индейцев.
IT>7. Ошибки происходят не только из-за неправильного кода, но и из-за неправильных данных, которые впрочем могут быть порождены неправильным кодом. Настоящие индейцы используют логи и вывод отладочной печати в отладочное окно студии. Зачастую сложную структуру данных проще напечатать и иметь сразу всю картину, чем бродить по окну Watch, заглядывая по очереди во все переменные класса.
Ммм. А если их сложно напечатать, что делать?
Например, 4-х мерная матрица 100х100х100х100 (помню, я так и не написал ту программу из-за того, что не смог нормально проанализировать её содержание)
Или, скажем, граф с представлением в виде списков следования? Это ж замучиться распечатывать! (я форму специальную делал)
IT>3. Copy/Paste vs. повторное использование. Copy/Paste — это разносчик багов, что есть плохо. Но шизиловка, когда каждые две строчки кода оформляются в виде отдельного метода ни чем не лучше. Поэтому настоящие индейцы копипейстят, но только один раз. Если некоторый фрагмент кода повторяется уже в третий раз, то это хорошая причина для оформления его в виде отдельного метода.
Хм. А вот что делать, если мне нужно реализовать процедуру умножения транспонированных матриц (очень просто, неправда ли?).
Варианты реализации:
1. Для того, что бы перемножить некоторым образом транспонированные матрицы я их транспонирую отдельной функцией и затем передаю в обычную процедуру умножения матриц
Минус: понижение производительности. Ведь я мог бы перемножить матрицы без физического транспонирования
2. Записать почти одинаковые алгоритмы для всех 4-х вариантов (т.е. AB, AtB, ABt, AtBt)
Минус: 4 раза копируется один и тот же метод
3. Записать метод с передачей в него флагов транспонированности, но тогда там тоже будет небольшой copy/paste
Минус: пользователь запутывается с флагами и становится тяжело использовать метод или необходимо написать 4 доп. метода, вызывающих этот для разных вариантов перемножений
4. Обращаться к матрице через свойство, что бы можно было не транспонировать матрицу физически
Минусы: * придётся возится с передачей указателей на соотв. методы взятия элемента матрицы, что неудобно
* небольшое (но чувствительное) снижение производительности в связи с дополнительными вызовами методов
5. Я могу писать перемножение прямо в методе (кстати, так и делаю, хоть дальше ругаюсь на пункт 4)
Минус: постоянные повторения одного и того же кода
Какой из этих вариантов предпочтительней?
И не будет ли после того, как написал два раза одинаковый код, мучительно вспоминать, а где же я его ещё писал-то?
IT>4. Настоящие индейцы не боятся длинных линейных методов, они не любят запутанных ветвистых уродцев, в которых легко прятаться багам.
Вот с этого начнём, точнее закончим.
Не согласен
Где прятаться багам, если методы до отупения просты? Представьте себе, у вас все методы из 3-5 строк. Ну 10 максимум, обычно.
При этом есть одно условие: название вызываемого метода говорит о том, что этот метод делает, а этот метод делает только то, что сказано в названии (это, кстати, рекомендация Макконнелла). Тогда получаем кучу вот этих самых методов, которые просто и в которых не где прятаться багам, так как, фактически, такой код представляет собой обычный псевдокод.
Я почему это говорю, я не так давно пробовал (на C++) писать исключительно мелкими методами, как сказал, и скорость разработки у меня повысилась приблизительно в 1,5 раза (с отладкой). При этом большое количество багов из разряда логических ошибок перекочевало в разряд "забыл вызвать метод", "при кодировании использовал неправильный алгоритм расчётов". Кстати, в последнем случае, в мелких методах гораздо лучше видно несоответствие кода используемому алгоритму и несоответствие алгоритма решаемой задачи.
IT>Существуют задачи, например, по генерации кода, которые выполняются в несколько последовательных шагов. Разбиение таких задач на несколько методов, которые используются только один раз не имеет никакого смысла, особенно учитывая, что часто приходится передавать контекст в эти методы, что выливается в длиннючий список параметров. Лучше отделить более менее независимые блоки коментариями, кратко описывающими суть действия.
О! В январе так делал. Правда на Delphi, а не на C#. Сначала я написал бо-о-о-ольшую процедуру (200-300 строк). Её предназначение было — распознавание зашумлённых шестнадцатиричных чисел, написанных от руки на неровной поверхности. Дальше у меня появились предложения по улучшению... я стал писать вложенные процедуры, по 50-70 строк. Дальше это всё отказалось стабильно и хорошо работать

. В итоге вся эта первоначальная процедура с вложенными функциями занимала почти 1000 строк.
IT> Если же выполняемая операция становится действительно сложной и плохоуправляемой, а рефакторинг такого метода приводит к выделению методов с невменяемым числом передаваемых параметров, то настоящие индейцы выносят такой код в отдельный класс. В отличии от метода в отдельном классе вместо передаваемых параметров можно использовать контекст самого класса.
Число параметров у методов было довольно небольшим, но их было ОЧЕНЬ много, до того много, что я стал их просто терять, я стал не понимать вообще где используется метод, который я вижу.
Я переписал всё через несколько классов, используя маленькие логичные как можно более простые блоки (и как можно более простые классы). Заняло у меня это один день, всё заработало и даже довольно прилично смотрится.
Как вы это объясните?
Кстати, таких примеров из моей личной практики программирования до фига. Я имею ввиду, преимущества мельких методов над крупными, но с комментариями над каждой частью. Например, год назад я писал рассчёт обратной задачи кинематики плоского манипулятора с замкнутой кинематикой (на C#) и заметил ошибку в реализации метода Ньютона только когда разбил его на много мелких методов. До этого же эта ошибка существовала чуть ли не с самого начала, я о ней знал, но думал совершенно на другие части программы.
Осенью я писал расчёт деформаций шпинделя (C++) и пока не начал разбивать неожиданно разросшийся класс на два, не нашёл правильного решения. Плюс, у меня там был как раз такой метод, какой вы советуете: генерация матрицы жёсткости. В этом методе было наибольшее число ошибок, он был труден для понимания и т.п. Разбей я его на несколько методов — всё было бы хорошо.
Может быть я вас неправильно понял?
Вот этот уродец (он и сейчас такой

):
// Вычисление формы деформации конструкции и реакций опор
void Spindle::CalcShifts()
{
// Вычислить длины массива параметров опор и количество столбцов в матрице жёсткости
int SC = Iteration + 2;
int Lk = (SC + 1) * 2;
K = new double[Lk * (Lk + 1)];
double * Kf = new double[Lk * (Lk + 1)];
for (int i = 0; i < Lk * (Lk + 1); K[i++] = 0.0);
// Создать систему разрешающих уравнений
// Записать коэффициенты уравнений без учёта кинематических ограничений
for (i = 0; i < SC; i++)
{
Bars[i]->SetK(K, Lk);
}
// Записать правые части
*(K + 0 + Lk) = F;
*(K + (Lk + 1) + Lk) = M;
// Скопировать матрицу жесткостей без кинематических ограничений для дальнейших вычислений рекаций опор
for (i = 0; i < Lk * (Lk + 1); i++)
{
Kf[i] = K[i];
}
// Учесть кинематические ограничения первых двух опор обнулением столбцов, так как это упростит расчёт
for (i = 0; i < Lk; i++)
{
*(K + (Lk + 1) * i + (2) ) = 0.0;
*(K + (Lk + 1) * i + (Lk - 2)) = 0.0;
}
// Учесть кинематические ограничения всех опор
for (int j = 0; j < SC; j++)
{
for(int i = 0; i < Lk; i++)
{
*(K + (Lk + 1) * (j + 1)*2 + i) = 0.0; // j + 1, так как первый узел ни на что не опирается; *2 - так как узлу соответсвует две строчки (перемещение и поворот)
}
*(K + (Lk + 1) * (j + 1)*2 + Lk) = Sp[j + this->maxSupportCount * 2]; // Координата кинематического ограничения по y
*(K + (Lk + 1) * (j + 1)*2 + (j + 1)*2) = 1.0; // Единица на главной диагонали
// ::MessageBox(0, itoa(Sp[j + this->maxSupportCount * 2]*1000000000, new char[11], 10), "EE", 0);
}
// Решить систему
double * Y = NULL;
// Отладка
// #define DBG__
#ifdef DBG__
static int Z = 0;
Z++;
HANDLE File;
int brum = 0;
if (Z == 2)
{
File = ::CreateFileA("G:\\debug.tmp", GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
::WriteFile(File, K, Lk*(Lk + 1)*sizeof(double), (LPDWORD)&brum, 0);
}
#endif
MGauss(K, Y, Lk);
// Отладка
#ifdef DBG__
if (Z == 2)
{
::WriteFile(File, Y, Lk*sizeof(double), (LPDWORD)&brum, 0);
::CloseHandle(File);
}
#endif
#undef DBG__
// Установить эти значения в стержни
for (i = 0; i < SC; i++)
{
Bars[i]->SetY(Y);
}
// Вычислить реакции опор (только силы, моменты равны нулю)
for (i = 0, j = 2; j < Lk; i++, j += 2)
{
// Обнуление обязательно, т.к. на каждой итерации расчёта идёт запись в одну и ту же строку
this->Sp[maxSupportCount*3 + i] = 0.0;
for (int k = 0; k < Lk; k++)
{
this->Sp[maxSupportCount*3 + i] += Y[k] * Kf[k + (Lk + 1)*j];
}
}
delete [] Kf;
delete [] K;
delete [] Y;
}