Re[2]: Можно парочку дерзких вопросов?
От: FDSC Россия consp11.github.io блог
Дата: 29.03.07 16:28
Оценка:
Здравствуйте, 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;
}





 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.