Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 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). ГЛАВА 19 Общие вопросы управления 453 Максимум в 10 точек принятия решения не является абсолютным ограничением. Используйте количество этих точек как сигнал, предупреждающий о том, что метод, возможно, стоит перепроектировать. Не считайте его неколебимым правилом. Оператор case со многими вариантами может иметь более 10 элементов, но в зависимости от назначения case может быть глупо разбивать его на части. Другие виды сложности Измерение сложности, предложенное Маккейбом, — не единственный значимый показатель, но он наиболее широко обсуждался в компьютерной литературе и особенно поле- зен при рассмотрении управляющей логики. Другие пока- затели включают количество используемых данных, число уровней вложенности в управляющих конструкциях, число строк кода, число строк между успешными обращениями к переменной («диапа- зон»), число строк, в которых используется переменная («время жизни») и объем ввода и вывода. Некоторые исследователи разработали составные показатели слож- ности, основанные на сочетании перечисленных простых вариантов. Контрольный список: вопросы по управляющим структурам Используют ли выражения идентификаторы true и false, а не 1 и 0? Сравниваются ли логические значения с true и false неявно? Сравниваются ли числовые значения со своими тестовыми значениями явно? Выполнено ли упрощение выражений с помощью введения новых логиче- ских переменных, использования логических функций и таблиц решений? Составлены ли логические выражения позитивно? Сбалансированы ли пары скобок? Используются ли скобки везде, где они необходимы для большей ясности? Заключены ли логические выражения в скобки целиком? Написаны ли условия в соответствии с расположением чисел на числовой прямой? Используются ли в программах на Java выражения вида a.equals(b), а не a == b там, где это необходимо? Очевидно ли применение пустых операторов? Выполнено ли упрощение глубоко вложенных выражений с помощью повтор- ной проверки части условия, преобразования в операторы if-then-else или case, перемещения части кода в отдельные методы, преобразования с ис- пользованием более обеъктно-ориентированной модели или они были улуч- шены как-то иначе? Если метод содержит более 10 точек принятия решения, есть ли хорошая причина, чтобы не перепроектировать его? Дополнительные сведения Отлич- ное обсуждение показателей сложности см. в «Software En– gineering Metrics and Models» (Conte, Dunsmore and Shen, 1986). http://cc2e.com/1985 454 ЧАСТЬ IV Операторы Ключевые моменты 쐽 Упрощение и облегчение чтения логических выражений вносит существенный вклад в качество вашего кода. 쐽 Глубокая вложенность затрудняет понимание метода. К счастью, вы сравнитель- но легко можете ее избежать. 쐽 Структурное программирование — это простая, но все еще злободневная идея: вы можете построить любую программу с помощью комбинации последова- тельностей, выборов и итераций. 쐽 Уменьшение сложности — ключ к написанию высококачественного кода. |