Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 19 Общие вопросы управления 443 // Обрабатываем транзакцию в зависимости от ее типа. if ( transaction.Type == TransactionType_Deposit ) { // Обрабатываем транзакциювклад. if ( transaction.AccountType == AccountType_Checking ) { if ( transaction.AccountSubType == AccountSubType_Business ) MakeBusinessCheckDep( transaction.AccountNum, transaction.Amount ); else if ( transaction.AccountSubType == AccountSubType_Personal ) MakePersonalCheckDep( transaction.AccountNum, transaction.Amount ); else if ( transaction.AccountSubType == AccountSubType_School ) MakeSchoolCheckDep( transaction.AccountNum, transaction.Amount ); } else if ( transaction.AccountType == AccountType_Savings ) MakeSavingsDep( transaction.AccountNum, transaction.Amount ); else if ( transaction.AccountType == AccountType_DebitCard ) MakeDebitCardDep( transaction.AccountNum, transaction.Amount ); else if ( transaction.AccountType == AccountType_MoneyMarket ) MakeMoneyMarketDep( transaction.AccountNum, transaction.Amount ); else if ( transaction.AccountType == AccountType_Cd ) MakeCDDep( transaction.AccountNum, transaction.Amount ); } else if ( transaction.Type == TransactionType_Withdrawal ) { // Обрабатываем снятие денег. if ( transaction.AccountType == AccountType_Checking ) MakeCheckingWithdrawal( transaction.AccountNum, transaction.Amount ); else if ( transaction.AccountType == AccountType_Savings ) MakeSavingsWithdrawal( transaction.AccountNum, transaction.Amount ); else if ( transaction.AccountType == AccountType_DebitCard ) MakeDebitCardWithdrawal( transaction.AccountNum, transaction.Amount ); } Вот транзакция перевода — TransactionType_Transfer. else if ( transaction.Type == TransactionType_Transfer ) { MakeFundsTransfer( transaction.SourceAccountType, transaction.TargetAccountType, transaction.AccountNum, transaction.Amount ); } else { // Обрабатываем неизвестный тип транзакции. LogTransactionError( “Unknown Transaction Type”, transaction ); } } Этот код сложен, но бывает и хуже. Он имеет всего четыре уровня вложенности, содержит комментарии и логические отступы, а его функциональная декомпози# ция достаточно адекватна, особенно для типа транзакции TransactionType_Transfer. И все же вы можете улучшить этот код, вынеся содержимое внутренних if#прове# рок в отдельные методы. > 444 ЧАСТЬ IV Операторы Пример правильно вложенного кода после декомпозиции на методы (C++) while ( !TransactionsComplete() ) { // Читаем транзакционную запись. transaction = ReadTransaction(); // Обрабатываем транзакцию в зависимости от ее типа. if ( transaction.Type == TransactionType_Deposit ) { ProcessDeposit( transaction.AccountType, transaction.AccountSubType, transaction.AccountNum, transaction.Amount ); } else if ( transaction.Type == TransactionType_Withdrawal ) { ProcessWithdrawal( transaction.AccountType, transaction.AccountNum, transaction.Amount ); } else if ( transaction.Type == TransactionType_Transfer ) { MakeFundsTransfer( transaction.SourceAccountType, transaction.TargetAccountType, transaction.AccountNum, transaction.Amount ); } else { // Обрабатываем неизвестный тип транзакции. LogTransactionError(“Unknown Transaction Type”, transaction ); } } Код новых методов просто был изъят из исходного фраг# мента и оформлен в виде новых методов (они здесь не по# казаны). Такой код имеет несколько преимуществ. Во#пер# вых, двухуровневая вложенность делает структуру проще и понятнее. Во#вторых, вы можете читать, исправлять и отла# живать более короткий цикл while, помещающийся на од# ном экране — не требуется переходить между экранами или страницами напечатанного текста. В#третьих, при вынесе# нии функциональности в методы ProcessDeposit() и ProcessWithdrawal() приобре# таются все остальные преимущества модульности. В#четвертых, теперь легко можно увидеть, что этот код может быть преобразован в оператор case, что еще более упростит чтение: Перекрестная ссылка Этот спо- соб функциональной декомпози- ции особенно прост, если вы из- начально строили методы по ме- тодике, описанной в главе 9. О принципах функциональной де- композиции см. подраздел «Раз- деляй и властвуй» раздела 5.4. ГЛАВА 19 Общие вопросы управления 445 Пример правильно вложенного кода после декомпозиции и использования оператора case (C++) while ( !TransactionsComplete() ) { // Читаем транзакционную запись. transaction = ReadTransaction(); // Обрабатываем транзакцию в зависимости от ее типа. switch ( transaction.Type ) { case ( TransactionType_Deposit ): ProcessDeposit( transaction.AccountType, transaction.AccountSubType, transaction.AccountNum, transaction.Amount ); break; case ( TransactionType_Withdrawal ): ProcessWithdrawal( transaction.AccountType, transaction.AccountNum, transaction.Amount ); break; case ( TransactionType_Transfer ): MakeFundsTransfer( transaction.SourceAccountType, transaction.TargetAccountType, transaction.AccountNum, transaction.Amount ); break; default: // Обрабатываем неизвестный тип транзакции. LogTransactionError(“Unknown Transaction Type”, transaction ); break; } } Используйте более объектно'ориентированный подход Самый прямой под# ход для упрощения именно этого кода в объектно#ориентированной среде состоит в создании абстрактного базового класса Transaction и его подклассов Deposit, Withdrawal и Transfer. 446 ЧАСТЬ IV Операторы Пример хорошего кода, использующего полиморфизм (C++) TransactionData transactionData; Transaction *transaction; while ( !TransactionsComplete() ) { // Читаем транзакционную запись. transactionData = ReadTransaction(); // Создаем объект транзакции в зависимости от ее типа. switch ( transactionData.Type ) { case ( TransactionType_Deposit ): transaction = new Deposit( transactionData ); break; case ( TransactionType_Withdrawal ): transaction = new Withdrawal( transactionData ); break; case ( TransactionType_Transfer ): transaction = new Transfer( transactionData ); break; default: // Обрабатываем неизвестный тип транзакции. LogTransactionError(“Unknown Transaction Type”, transactionData ); return; } transaction>Complete(); delete transaction; } В системах любого размера оператор switch можно заменить вызовом специаль# ного метода фабрики объекта, который может повторно использоваться в любом месте, где нужно создать объект типа Transaction. Если бы этот код принадлежал такой системе, то он мог бы стать еще проще: Пример хорошего кода, использующего полиморфизм и фабрику объекта (C++) TransactionData transactionData; Transaction *transaction; while ( !TransactionsComplete() ) { // Читаем транзакционную запись и выполняем транзакцию. transactionData = ReadTransaction(); transaction = TransactionFactory.Create( transactionData ); transaction>Complete(); delete transaction; } ГЛАВА 19 Общие вопросы управления 447 Чтобы вам было понятно, код метода TransactionFactory% .Create() представляет собой простую адаптацию операто# ра switch из предыдущего примера: Пример хорошего кода для фабрики объекта (C++) Transaction *TransactionFactory::Create( TransactionData transactionData ) { // Создаем объект транзакции на основе ее типа. switch ( transactionData.Type ) { case ( TransactionType_Deposit ): return new Deposit( transactionData ); break; case ( TransactionType_Withdrawal ): return new Withdrawal( transactionData ); break; case ( TransactionType_Transfer ): return new Transfer( transactionData ); break; default: // Обрабатываем неизвестный тип транзакции. LogTransactionError( “Unknown Transaction Type”, transactionData ); return NULL; } } Перепроектируйте глубоко вложенный код Некоторые эксперты утверждают, что операторы case в объектно#ориентированном программировании практически всегда сигнализируют о плохо факторизованном коде и должны использоваться как можно реже (Meyer, 1997). И показанное преобразование из операторов case, вызывающих методы, к фабрике объекта с вызовами полиморфных методов — один из таких примеров. Обобщая, можно сказать, что сложный код — это признак того, что вы недоста# точно хорошо понимаете свою программу, чтобы сделать ее простой. Глубокая вложенность — это знак, предупреждающий о том, что нужно добавить вызов метода или перепроектировать сложную часть кода. Это не значит, что вы обяза# ны переписать весь метод, но у вас должна быть веская причина не делать этого. Сводка методик уменьшения глубины вложенности Далее перечислены способы, позволяющие уменьшить вложенность. Рядом ука# заны ссылки на разделы этой книги, в которых эти способы обсуждаются: повторная проверка части условия (этот раздел); конвертирование в блоки if%then%else (этот раздел); Перекрестная ссылка Дополни- тельные полезные улучшения кода наподобие этого см. в гла- ве 24. 448 ЧАСТЬ IV Операторы преобразование к оператору case (этот раздел); факторизация глубоко вложенного кода в отдельный метод (этот раздел); использование объектной и полиморфной диспетчеризации (этот раздел); изменение кода с целью использования статусной переменной (раздел 17.3); использование сторожевых операторов для выхода из метода и пояснения номинального хода алгоритма (раздел 17.1); использование исключений (раздел 8.4); полное перепроектирование глубоко вложенного кода (этот раздел). 19.5. Основа программирования: структурное программирование Термин «структурное программирование» был введен в исторической статье «Struc# tured Programming», представленной Эдсжером Дейкстрой на конференции НАТО по разработке ПО в 1969 году (Dijkstra, 1969). С тех самых пор термин «структур# ный» применялся к любой деятельности в области разработки ПО, включая струк# турный анализ, структурный дизайн и структурное валяние дурака. Различные структурные методики не имели между собой ничего общего, кроме того, что все они создавались в то время, когда слово «структурный» придавало им большую значимость. Суть структурного программирования состоит в простой идее: программа должна использовать управляющие конструкции с одним входом и одним выходом. Такая конструкция представляет собой блок кода, в котором есть только одно место, где он может начинаться, и одно — где может заканчиваться. У него нет других входов и выходов. Структурное программирование — это не то же самое, что и структур# ное проектирование сверху вниз. Оно относится только к уровню кодирования. Структурная программа пишется в упорядоченной, дисциплинированной мане# ре и не содержит непредсказуемых переходов с места на место. Вы можете чи# тать ее сверху вниз, и практически так же она выполняется. Менее дисциплини# рованные подходы приводят к такому исходному коду, который содержит менее понятную и удобную для чтения картину того, как программа выполняется. Меньшая читабельность означает худшее понимание и в конце концов худшее качество программы. Главные концепции структурного программирования, касающиеся вопросов исполь# зования break, continue, throw, catch, return и других тем, применимы до сих пор. Три компонента структурного программирования В следующих разделах описаны три конструкции, составляющие основу структур# ного программирования. Последовательность Последовательность — это набор операторов, выполняющих# ся по порядку. Типичные последовательные операторы содер# жат присваивания и вызовы методов. Вот два примера: Перекрестная ссылка Об ис- пользовании последовательно- стей см. главу 14. ГЛАВА 19 Общие вопросы управления 449 Примеры последовательного кода (Java) // Последовательность операторов присваивания. a = “1”; b = “2”; c = “3”; // Последовательность вызовов методов. System.out.println( a ); System.out.println( b ); System.out.println( c ); Выбор Выбор — это такая управляющая конструкция, которая зас# тавляет операторы выполняться избирательно. Наиболее час# тый пример — выражение if%then%else. Выполняется либо блок if%then, либо else, но не оба сразу. Один из блоков «выбирается» для выполнения. Оператор case — другой пример управляющего элемента выбора. Оператор switch в C++ и Java, оператор select — все это примеры case. В каждом случае для выпол# нения выбирается один из вариантов. Концептуально операторы if и case похо# жи. Если ваш язык не поддерживает операторы case, вы можете эмулировать их с помощью набора if. Вот два примера выбора: Пример выбора (Java) // Выбор в операторе if. if ( totalAmount > 0.0 ) { // Делаем чтото. } else { // Делаем чтото еще. } // Выбор в операторе case. switch ( commandShortcutLetter ) { case ‘a’: PrintAnnualReport(); break; case ‘q’: PrintQuarterlyReport(); break; case ‘s’: PrintSummaryReport(); break; default: DisplayInternalError( “Internal Error 905: Call customer support.” ); } Перекрестная ссылка Об исполь- зовании выбора см. главу 15. 450 ЧАСТЬ IV Операторы Итерация Итерация — это управляющая структура, которая заставля# ет группу операторов выполняться несколько раз. Итерацию обычно называют «циклом». К итерациям относятся струк# туры For%Next в Visual Basic и while и for в C++ и Java. Этот фрагмент кода содержит примеры итераций на Visual Basic: Примеры итераций на Visual Basic ‘ Пример итерации в виде цикла For. For index = first To last DoSomething( index ) Next ’ Пример итерации в виде цикла while. index = first While ( index <= last ) DoSomething ( index ) index = index + 1 Wend ’ Пример итерации в виде цикла с выходом. index = first Do If ( index > last ) Then Exit Do DoSomething ( index ) index = index + 1 Loop Основной тезис структурного программирования гласит, что любая управляющая логика программы может быть реализована с помощью трех конструкций: пос# ледовательности, выбора и итерации (B ö hm Jacopini, 1966). Программисты иног# да предпочитают языковые конструкции, увеличивающие удобство, но програм# мирование, похоже, развивается во многом благодаря ограничению того, что мы можем делать на наших языках программирования. До введения структурного про# граммирования использовать goto представлялось очень удобным, но код, напи# санный таким образом, оказался малопонятным и не поддающимся сопровожде# нию. Я считаю, что использование любых управляющих структур, отличных от этих трех стандартных конструкций, т. е. break, continue, return, throw%catch и т. д., дол# жны рассматриваться под критическим углом зрения. 19.6. Управляющие структуры и сложность Одна из причин, по которой столько внимания уделялось управляющим структу# рам, заключается в том, что они вносят большой вклад в общую сложность про# граммы. Неправильное применение управляющих структур увеличивает сложность, правильное — уменьшает ее. Перекрестная ссылка Об ис- пользовании итераций см. гла- ву 16. ГЛАВА 19 Общие вопросы управления 451 Одной из единиц измерения «программной сложности» яв# ляется число воображаемых объектов, которые вам прихо# дится одновременно держать в уме, чтобы разобраться в программе. Это умственное жонглирование — один из са# мых сложных аспектов программирования и причина того, что программирование требует большей сосредоточенности, чем другие виды деятельности. По этой причине программисты не любят, когда их «ненадолго прерывают» — такие перерывы равносильны просьбе жонглеру продолжать под# кидывать три мяча и подержать вашу сумку с продуктами. Интуитивно понятно, что сложность программы во многом определя# ется количеством усилий, требуемых для ее понимания. Том Маккейб (Tom McCabe) опубликовал важную статью, утверждающую, что сложность про# граммы определяется ее управляющей логикой (1976). Другие исследователи об# наружили дополнительные факторы, кроме предложенного Маккейбом циклома# тического показателя сложности (например, количество переменных, использу# емых в программе), но они согласны, что управляющая логика — одна из глав# ных составляющих сложности, если не самая главная. Насколько важна сложность? Исследователи в области вычислительной техники уже на протяжении двух десятилетий осознают важность пробле# мы сложности. Много лет назад Дейкстра предупреждал об опасности сложности: «Компетентный программист полно# стью осознает строго ограниченные размеры своего чере# па, поэтому подходит к задачам программирования со всей возможной скромно# стью» (Dijkstra, 1972). Из этого не следует, что вам нужно увеличить объем ваше# го черепа, чтобы иметь дело с невероятной сложностью. Это предполагает, что вы можете никогда не связываться с чрезмерной сложностью и всегда должны пред# принимать шаги для ее уменьшения. Сложность управляющей логики имеет большое значение, потому что она коррелирует с низкой надежностью и частыми ошибками (McCabe, 1976, Shen et al., 1985). Вильям Т. Уорд (William T. Ward) сообщал о значитель# ном выигрыше в надежности ПО, полученном в Hewlett#Packard в результате при# менения показателя сложности Маккейба (1989b). Этот показатель использовал# ся для идентификации проблемных участков в одной программе длиной 77 000 строк. Коэффициент ошибок после выпуска этой программы составил 0,31 ошибку на 1000 строк кода. Коэффициент ошибок в программе длиной 125 000 строк не превышал 0,02 ошибки на 1000 строк кода. Ворд отметил, что из#за своей мень# шей сложности обе программы имели значительно меньше дефектов, чем другие программы в Hewlett#Packard. Моя компания Construx Software в 2000 г. получила похожие результаты при использовании средств измерения сложности для поис# ка проблемных методов. Делайте вещи настолько про- стыми, насколько это возмож- но, но не проще. Альберт Эйнштейн Перекрестная ссылка О сложно- сти см. подраздел «Главный Тех- нический Императив ПО: управ- ление сложностью» раздела 5.2. 452 ЧАСТЬ IV Операторы Общие принципы уменьшения сложности Вы можете бороться со сложностью двумя способами. Во#первых, вы можете улуч# шить свои способности к умственному жонглированию, выполняя специальные упражнения. Но программирование само по себе — уже хорошее упражнение, а люди, похоже, сталкиваются с трудностями при жонглировании б ó льшим коли# чеством, чем от пяти до девяти воображаемых сущностей (Miller, 1956). Так что потенциал улучшения невелик. Во#вторых, вы можете уменьшить сложность ва# ших программ и количество усилий, прилагаемых для их понимания. Как измерить сложность У вас, возможно, есть интуитивное ощущение того, что де# лает метод более или менее сложным. Исследователи пыта# ются формализовать свои чувства и приводят несколько способов измерения сложности. Возможно, самый важный способ предложил Том Маккейб, Сложность в нем измеря# ется с помощью подсчета количества «точек принятия решения» в методе (табл. 19#2): Табл. 19-2. Способы подсчета точек принятия решения в методе 1. Начните считать с 1 на некотором участке кода. 2. Добавляйте 1 для каждого из следующих ключевых слов или их эквивалентов: if while repeat for and or. 3. Добавляйте 1 для каждого варианта в операторе case. Приведем пример: if ( ( (status = Success) and done ) or ( not done and ( numLines >= maxLines ) ) ) then ... В этом фрагменте вы начинаете считать с 1, получаете 2 для if, 3 — для and, 4 — для or и 5 — для and. Таким образом, этот фрагмент содержит всего пять точек принятия решения. Что делать с этим измерением сложности Посчитав количество точек принятия решения, вы можете использовать это чис# ло для анализа сложности вашего метода: 0–5 Этот метод, возможно, в порядке. 6–10 Начинайте думать о способах упрощения метода. 10+ Вынесите часть кода в отдельный метод и вызывайте его. Перенос части метода в другой метод не упрощает программу — он просто пере# мещает точки принятия решения. Но он уменьшает сложность, с которой вам при# ходится иметь дело в каждый момент времени. Поскольку одна из главных целей состоит в минимизации количества элементов, которыми приходится мысленно жонглировать, то уменьшение сложности отдельного метода дает свой результат. Дополнительные сведения Опи- санный здесь подход основан на важной статье Тома Маккейба «A Complexity Measure» (1976). |