Рейтинговые книги
Читем онлайн The Programmers Stone (Программистский камень) - Alan Carter

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 7 8 9 10 11 12 13 14 15 ... 42

Определение границ атома познания может происходить циклически, а квалифицированный архитектор укажет для них правильное место, где верхний уровень, где нижний, а что посередине. На начальных стадиях это может быть огромный, единый атом познания, который нужно передать в руки ответственного работника и сказать: «Попробуй разобрать эту мешанину, пожалуйста!»

По определению мы не знаем, каков наилучший подход к атому познания. Если бы мы знали, то он не был бы атомом. Из этого следует, что это не может планироваться на основе диаграмм планирования проектов (диаграмм Ганта) в терминах подцелей. Это должно быть одной задачей, а о длительности можно только догадываться. Опытные картостроители поднаторели в догадках, но они не могут объяснить, почему проблема тянет на два дня, неделю, полгода. Поэтому у того, кто дал наилучший прогноз, очень мало аргументов в его защиту. Боязнь последующих объяснений — важный фактор, который часто отбивает у картостроителей охоту проявлять свои интуитивные способности и выдавать необходимые для планирования проекта цифры.

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

Плато качества

Когда принята стратегия формирования мысленной карты проблемной области и попыток упростить ее, обычно сталкиваются с проблемой определения момента окончания работы над картой. Эта проблема возникает на каждом уровне проектирования. Сверхъестественно, но почти всегда есть глубокое решение, которое значительно проще всех остальных и очевидно минимальное. (Есть много способов это выразить, но потом этот вывод станет очевидным.) Хотя проповеди типа: «Ты узнаешь это, когда увидишь!» — несомненная истина, но они не говорят, куда посмотреть.

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

В качестве примера возьмем код из прекрасной книги Джеффри Рихтера (Jeffrey Richter's Advanced Windows). Эта книга — полезное чтение для любого, кто пытается писать программы для Win32 API (Application Programming Interface) (поскольку иначе у вас не появится мысленная карта семантики системы).

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

DWORD WINAPI SecondThread (LPVOID lpwThreadParm) {

BOOL fDone = FALSE;

DWORD dw;

while (!fDone) {     // Wait forever for the mutex to become signaled.

  dw = WaitForSingleObject(g_hMutex, INFINITE);

  if (dw == WAIT_OBJECT_0) { // Mutex became signalled.

   if (g_nIndex >= MAX_TIMES) {

       fDone = TRUE;

    } else {

       g_nIndex++;

      g_dwTimes[g_nIndex — 1] = GetTickCount():

    } // Release the mutex.

  ReleaseMutex(g_hMutex);

} else { // The mutex was abandoned.

break; // Exit the while loop.

}

}

return(0);

}

Для начала просто упростим стиль скобок, уберем пробел между ключевым словом и открывающей скобкой, а также многословный комментарий к ReleaseMutex. Мы в курсе, что идет религиозная война между последователями Кернигана и Ритчи (K&R) и последователями Вирта (Wirth) по поводу стиля скобок, но симметрия обрамления блока действительно позволяет лучше увидеть некоторые вещи. Дополнительная строка, которая при этом появляется, даст выигрыш чуть позднее — следуйте за нами!

DWORD WINAPI SecondThread(LPVOID lpwThreadParm) { BOOL fDone = FALSE; DWORD dw; while(!fDone) { // Wait forever for the mutex to become signaled. dw = WaitForSingleObject(g_hMutex, INFINITE); if(dw == WAIT_OBJECT_0) { // Mutex became signalled. if(g_nIndex >= MAX_TIMES) { fDone = TRUE; } else { g_nIndex++; g_dwTimes[g_nIndex — 1] = GetTickCount(): } ReleaseMutex(g_hMutex); } else { // The mutex was abandoned. break; // Exit the while loop. } } return(0); }

Очень легко можно избавиться от одной локальной переменной: dw присваивают значение, а в следующей операции тестируют. Инвертирование смысла проверки помогает локализовать ссылку (проверка, затем изменение g_nIndex). А пока мы здесь, нет смысла инкрементировать g_nIndex просто для того, чтобы вычесть 1 из текущего значения в следующей операции! Мы уже использовали постфиксную форму оператора инкремента языка Cи, который как раз для этого и предназначен.

DWORD WINAPI SecondThread (LPVOID lpwThreadParm) { BOOL fDone = FALSE; while (!fDone) { // Wait forever for the mutex to become signaled. if (WaitForSingleObject(g_hMutex, INFINITE)==WAIT_OBJECT_0) { // Mutex became signalled. if (g_nIndex < MAX_TIMES) { g_dwTimes[g_nIndex++] = GetTickCount(); } else { fDone = TRUE; } ReleaseMutex(g_hMutex); } else { // The mutex was abandoned. break;// Exit the while loop. } } return(0); }

Прерывание цикла (break) зависит только от результата WaitForSingleObject, поэтому естественно переместить проверку в управляющее выражение, избавляясь от прерывания цикла и одного уровня вложенности:

DWORD WINAPI SecondThread (LPVOID lpwThreadParm) { BOOL fDone = FALSE; while (!fDone && WaitForSingleObject(g_hMutex, INFINITE)==WAIT_OBJECT_0) { // Mutex became signalled. if (g_nIndex < MAX_TIMES) { g_dwTimes[g_nIndex++] = GetTickCount(); } else { fDone = TRUE; } ReleaseMutex(g_hMutex); } return(0); }

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

DWORD WINAPI SecondThread (LPVOID lpwThreadParm) { BOOL fDone = FALSE; while (!fDone && WaitForSingleObject(g_hMutex, INFINITE)==WAIT_OBJECT_0) { if (g_nIndex < MAX_TIMES) g_dwTimes[g_nIndex++] = GetTickCount(); else fDone = TRUE; ReleaseMutex(g_hMutex); } return(0); }

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

Ересь в том, что если мы знаем, для чего наши переменные, то мы знаем их типы. Если мы не знаем, для чего предназначена переменная, знание ее типа мало поможет. В любом случае, компилятор все равно сделает проверку типов. Поэтому избавимся от венгерской записи, а заодно и от переопределений типов, которые просто определены (#define), но не для нас. Сокрытие разыменования используя typedef — другое бесцельное упражнение, поскольку хотя и позволяет выполнить некоторую инкапсуляцию валюты, этого совершенно недостаточно, чтобы избавиться от беспокойства по этому поводу, поэтому аккуратные программисты вынуждены держать настоящие типы в голове. Поддержка концепции дальних указателей в именах переменных для 32 битного API с плоской адресацией — тоже довольно глупое занятие.

DWORD SecondThread (void *ThreadParm) { BOOL done = FALSE; while (!done && WaitForSingleObject(Mutex, INFINITE)==WAIT_OBJECT_0) { if (Index < MAX_TIMES) Times[Index++] = GetTickCount(); else done = TRUE; ReleaseMutex(Mutex); } return(0); }

Теперь смотрите. Мы достигнем Плато Качества…

DWORD SecondThread(void *ThreadParm) { while(Index < MAX_TIMES && WaitForSingleObject(Mutex, INFINITE) == WAIT_OBJECT_0) { if (Index < MAX_TIMES) Times[Index++] = GetTickCount(): ReleaseMutex(Mutex); } return(0); }

Одиннадцать строк против 26. На один уровень меньшая вложенность, но структура полностью прозрачна. Две локальных переменных исчезли. Нет блоков. Совсем нет вложенных else. Меньше мест, где могут скрываться ошибки.

(Если вы еще не программировали используя потоки (threads), то повторная проверка значения Index внутри тела цикла кажется грубой и ненужной. Если же программировали, то повторная проверка естественна и очевидна. Это очень важно: пока текущий поток приостановлен в WaitForSingleObject, другой поток скорее всего будет активен и изменит значение. То, что для вас очевидно, зависит от вашего опыта: еще одна мораль из этого примера — рассмотрения только структуры куска кода недостаточно.)

Наконец, текст делает совершенно ясным, что разные потоки выполняют функции в разных контекстах. Поэтому совершенно не нужно определять функцию с именем FirstThread(), в точности такую же, как SecondThread(), и вызывать их так:

hThreads[0] = CreateThread(…, FirstThread…); hThreads[1] = CreateThread(…, SecondThread…);

Когда можно просто

hThreads[0] = CreateThread(…, TheThread…); hThreads[1] = CreateThread(…, TheThread…);

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

1 ... 7 8 9 10 11 12 13 14 15 ... 42
На этой странице вы можете бесплатно читать книгу The Programmers Stone (Программистский камень) - Alan Carter бесплатно.
Похожие на The Programmers Stone (Программистский камень) - Alan Carter книги

Оставить комментарий