Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 7 Высококачественные методы 169 ботчик класса Supervisor сделал id значением, возвращаемым по умолчанию. Класс Candidate предоставлял доступ к объекту id, который по умолчанию возвращал значение идентификатора. К середине проекта никто из нас уже не мог вспом# нить, какой из методов предполагалось использовать для того или иного объек# та, но мы уже написали слишком много кода, чтобы возвращаться назад и все согласовывать. Поэтому каждому члену группы пришлось тратить лишние усилия на запоминание несогласованных подробностей синтаксиса получения id из каж# дого класса. Конвенция именования, определяющая получение id, сделала бы та# кую неприятность невозможной. 7.4. Насколько объемным может быть метод? На пути в Америку пилигримы 1 спорили о лучшей максимальной длине метода. И вот они прибыли к Плимутскому камню и начали составлять Мейфлауэрское соглашение. О максимальной длине методов пилигримы так и не договорились, а так как до подписания соглашения они не могли высадиться на берег, то сда# лись и не включили этот пункт в соглашение. Результатом стали нескончаемые дебаты о допустимой длине методов. В теоретических работах длину метода часто советуют ограничивать числом строк, помещающихся на экране монитора, или же одной#двумя страницами, что соот# ветствует примерно 50–150 строкам. Следуя этому правилу, в IBM однажды огра# ничили методы 50 строками, а в TRW — двумя страницами (McCabe, 1976). Со# временные программы обычно включают массу очень коротких методов, вызы# ваемых из нескольких более крупных методов. Однако длинные методы далеки от вымирания. Незадолго до завершения работы над этой книгой я в течение месяца посетил двух клиентов. В одном случае программисты боролись с методом, вклю# чавшим примерно 4000 строк, а во втором пытались укротить метод, содержав# ший более 12 000 строк! Длина методов уже давно стала предметом исследований. Некоторые из них устарели, а другие актуальны и по сей день. Базили и Перриконе обнаружили обратную корреляцию между размером метода и уровнем ошибок: при росте размера методов (вплоть до 200 строк) число ошибок в расчете на одну строку снижалось (Basili and Perricone, 1984). Другое исследование показало, что с числом ошибок коррелировали структурная сложность и объем используемых данных, но не размер метода (Shen et al., 1985). В исследовании 1986 г. было обнаружено, что небольшой размер методов (32 строки или менее) не коррелировал с меньшими затратами на их разработку или меньшим числом дефектов (Card, Church, and Agresti, 1986; Card and Glass, 1 Пилигримы (pilgrims) — пассажиры английского судна «Мейфлауэр» («Mayflower»), основатели Плимутской колонии в Северной Америке, заключившие Мейфлауэрское соглашение (Mayflower Compact) о создании «гражданской политической организации» для поддержания порядка и безопасности, «принятия справедливых и обеспечивающих равноправие законов». Плимутский камень (Plymouth Rock) — по преданию гранитная глыба, на которую ступил первый сошед# ший с корабля пилигрим в декабре 1620 г. Почитается в США как национальная святыня. — Прим. перев. 170 ЧАСТЬ II Высококачественный код 1990). Разработка крупных методов (65 строк или более) в расчете на одну строку кода была дешевле. Опытное изучение 450 методов показало, что небольшие методы (включавшие менее 143 команд исходного кода с учетом комментариев) содержали на 23% больше ошибок в расчете на строку кода, чем более крупные методы, но ис# правление меньших методов было в 2,4 раза менее дорогим (Selby and Basili, 1991). Исследования позволили обнаружить, что код требовал минимальных измене# ний, если методы состояли в среднем из 100–150 строк (Lind and Vairavan, 1989). Исследование, проведенное в IBM, показало, что максимальный уровень оши# бок был характерен для методов, размер которых превышал 500 строк кода. При дальнейшем увеличении методов уровень ошибок возрастал пропорцио# нально числу строк (Jones, 1986a). Так какую же длину методов считать приемлемой в объектно#ориентированных программах? Многие методы в объектно#ориентированных программах будут ме# тодами доступа, обычно очень короткими. Время от времени реализация сложно# го алгоритма будет требовать создания более длинного метода, и тогда методу можно будет позволить вырасти до 100–200 строк (строкой считается непустая строка исходного кода, не являющаяся комментарием). Десятилетия исследований гово# рят о том, что методы такой длины не более подвержены ошибкам, чем методы меньших размеров. Пусть длину метода определяют не искусственные ограниче# ния, а такие факторы, как связность метода, глубина вложенности, число перемен# ных, число точек принятия решений, число комментариев, необходимых для объяс# нения метода, и другие соображения, связанные со сложностью кода. Что касается методов, включающих более 200 строк, то к ним следует относиться настороженно. Ни в одном из исследований, в которых было обнаружено, что более крупным методам соответствует меньшая стоимость разработки, меньший уровень ошибок или оба фактора, эта тенденция не усиливалась при увеличении размера свыше 200 строк, а при превышении этого предела методы неизбежно становят# ся менее понятными. 7.5. Советы по использованию параметров методов Интерфейсы между методами — один из основных источников ошибок. В одном часто цитируемом исследовании, проведенном Базили и Пер# риконе (Basili and Perricone, 1984), было обнаружено, что 39% всех оши# бок были ошибками внутренних интерфейсов — ошибками коммуникации меж# ду методами. Вот несколько советов по предотвращению подобных проблем. Передавайте параметры в порядке «входные значения — изменяемые значения — выходные значения» Вме# сто упорядочения параметров случайным образом или по алфавиту указывайте в списке сначала исключительно вход# ные параметры, затем входные#и#выходные параметры и на# конец — исключительно выходные параметры. Такой по# Перекрестная ссылка О доку- ментировании параметров мето- дов см. подраздел «Комменти- рование методов» раздела 32.5, а о форматировании парамет- ров — раздел 31.7. ГЛАВА 7 Высококачественные методы 171 рядок соответствует последовательности выполняемых в методе операций: ввод данных, их изменение и возврат результата. Вот примеры списков параметров, на# писанные на языке Ada: Примеры размещения параметров в порядке «входные значения — изменяемые значения — выходные значения» (Ada) procedure InvertMatrix( В языке Ada ключевые слова in и out поясняют суть входных и выходных параметров. originalMatrix: in Matrix; resultMatrix: out Matrix ); procedure ChangeSentenceCase( desiredCase: in StringCase; sentence: in out Sentence ); procedure PrintPageNumber( pageNumber: in Integer; status: out StatusType ); Такая конвенция упорядочения параметров противоречит конвенции библиотек C, предполагающей указание изменяемого параметра в первую очередь. Конвен# ция «входные значения — изменяемые значения — выходные значения» кажется мне более разумной, но, даже если вы будете согласованно упорядочивать пара# метры любым иначе, вы окажете услугу программистам, которым придется читать ваш код. Подумайте о создании собственных ключевых слов in и out В отличие от Ada другие языки не поддерживают ключевые слова in и out. Однако даже в этом случае вы скорее всего сможете создать их с помощью препроцессора: Пример определения собственных ключевых слов In и Out (C++) #define IN #define OUT void InvertMatrix( IN Matrix originalMatrix, OUT Matrix *resultMatrix ); void ChangeSentenceCase( IN StringCase desiredCase, IN OUT Sentence *sentenceToEdit ); > 172 ЧАСТЬ II Высококачественный код void PrintPageNumber( IN int pageNumber, OUT StatusType &status ); В данном примере ключевые слова#макросы IN и OUT используются для докумен# тирования. Чтобы значение параметра можно было изменить в вызванном мето# де, параметр все же нужно передавать по указателю или по ссылке. Прежде чем принять этот подход, обдумайте два его важных недостатка. Собствен# ные ключевые слова IN и OUT окажутся незнакомыми большинству программис# тов, которые будут читать ваш код. Расширяя язык таким образом, делайте это согласованно, лучше всего в масштабе всего проекта. Второй недостаток в том, что компилятор не будет проверять соответствие параметров ключевым словам IN и OUT, из#за чего вы сможете отметить параметр как IN и все же изменить его внутри метода. Так вы только введете программиста, читающего ваш код, в за# блуждение. Обычно для определения исключительно входных параметров лучше применять ключевое слово const языка C++. Если несколько методов используют похожие параметры, передавайте их в согласованном порядке Порядок параметров может как облегчить, так и затруднить их запоминание. Например, в C прототипы методов fprintf() и printf() различаются только тем, что fprintf() принимает файл в качестве дополнительно# го первого аргумента. Похожее отношение наблюдается и между методами fputs() и puts(), но в fputs() файл передается последним. Это досадное различие только затрудняет запоминание параметров названных методов. С другой стороны, методы strncpy() и memcpy() в том же C принимают аргументы в одинаковом порядке: строка#приемник, строка#источник и максимальное число копируемых байт. Такое сходство помогает запомнить параметры обоих методов. Используйте все параметры Если вы передаете параметр в метод, ис# пользуйте его, в противном случае удалите параметр из интерфейса ме# тода. Наличие неиспользуемых параметров соответствует более высоко# му уровню ошибок. Исследования показали, что ошибки отсутствовали в 46% ме# тодов, не включавших неиспользуемых переменных, и только в 17–29% методов, содержавших более одной неиспользуемой переменной (Card, Church, and Agresti, 1986). Это правило допускает одно исключение. При условной компиляции кода из ком# пиляции могут быть исключены части метода, использующие некоторый параметр. Опасайтесь этого подхода, но, если вы убеждены, что все правильно, он вполне допустим. В общем, если у вас есть серьезная причина не использовать параметр, оставьте его в списке. Если таковой нет, очистите интерфейс метода от примесей. Передавайте переменные статуса или кода ошибки последними Перемен# ные статуса и переменные, указывающие на ошибку, следует располагать в спис# ке параметров последними. Они второстепенны по отношению к главной цели метода и являются исключительно выходными параметрами, поэтому такая кон# венция вполне разумна. ГЛАВА 7 Высококачественные методы 173 Не используйте параметры метода в качестве рабочих переменных Ис# пользовать передаваемые в метод параметры как рабочие переменные опасно. Создайте для этой цели локальные переменные. Так, в следующем фрагменте Java# кода переменная inputVal некорректно служит для хранения промежуточных ре# зультатов вычислений: Пример некорректного использования входного параметра (Java) int Sample( int inputVal ) { inputVal = inputVal * CurrentMultiplier( inputVal ); inputVal = inputVal + CurrentAdder( inputVal ); Переменная inputVal уже не содержит входного значения. return inputVal; } В этом фрагменте переменная inputVal вводит в заблуждение, потому что при за# вершении метода она больше не содержит входного значения; она содержит резуль# тат вычисления, частично основанного на входном значении, и поэтому ее имя неудачно. Если позднее вам придется задействовать первоначальное входное зна# чение в другом месте метода, вы, вероятно, задействуйте переменную inputVal, пред# полагая, что она содержит первоначальное значение, но это предположение бу# дет ошибочным. Можно ли решить эту проблему путем переименования inputVal? Наверное, нет. Переменной можно было бы присвоить имя вроде workingVal, но такое решение было бы неполным, так как это имя не говорит о том, что первоначальное значение пе# ременной передается в метод извне. Вы могли бы присвоить ей нелепое имя input% ValThatBecomesWorkingVal (входное значение, которое становится рабочим значе# нием) или сдаться и просто назвать ее x или val, но все эти подходы неудачны. Лучше избегать настоящих и будущих проблем, используя рабочие переменные явно, например: Пример корректного использования входного параметра (Java) int Sample( int inputVal ) { int workingVal = inputVal; workingVal = workingVal * CurrentMultiplier( workingVal ); workingVal = workingVal + CurrentAdder( workingVal ); Если первоначальное значение inputVal понадобится здесь или где#то еще, оно все еще доступно. return workingVal; } > 174 ЧАСТЬ II Высококачественный код Создание новой переменной workingVal поясняет роль inputVal и исключает воз# можность ошибочного использования inputVal в неподходящий момент. (Не рас# сматривайте это рассуждение как оправдание присвоения переменным имен inputVal или workingVal. Имена inputVal и workingVal просто ужасны и служат в данном примере только для пояснения ролей переменных.) Присвоение входного значения рабочей переменной подчеркивает тот факт, что значение поступает в метод извне. Кроме того, это исключает возможность слу# чайного изменения параметров. В C++ ответственность за это можно возложить на компилятор при помощи ключевого слова const. Отметив параметр как const, вы не сможете изменить его значение внутри метода. Документируйте выраженные в интерфейсе предпо' ложения о параметрах Если вы предполагаете, что пе# редаваемые в метод данные должны иметь определенные характеристики, сразу же документируйте эти предположе# ния. Документирование предположений и в самом методе, и в местах его вызова нельзя назвать пустой тратой времени. Пишите коммента# рии, не дожидаясь завершения работы над методом: к тому времени вы многое забудете. Еще лучше применить утверждения (assertions), позволяющие встроить предположения в код. Какие типы предположений о параметрах следует документировать? Вот какие: вид параметров: являются ли они исключительно входными, изменяемыми или исключительно выходными; единицы измерения (дюймы, футы, метры и т. д.); смысл кодов статуса и ошибок, если для их представления не используются перечисления; диапазоны допустимых значений; специфические значения, которые никогда не должны передаваться в метод. Ограничивайте число параметров метода примерно семью 7 — магическое число. Психологические исследования показали, что люди, как правило, не могут следить более чем за семью элементами инфор# мации сразу (Miller, 1956). Это открытие используется в огромном числе дисцип# лин, поэтому резонно предположить, что большинство людей не может удержи# вать в уме более семи параметров метода одновременно. На практике возможность ограничения числа параметров зависит от того, как в выбранном вами языке реализована поддержка сложных типов данных. Программируя на совре# менном языке, поддерживающем структурированные дан# ные, вы можете передать в метод составной тип данных, содержащий 13 полей, и рассматривать его как один «элемент» данных. При использовании более прими# тивного языка вам, возможно, придется передать все 13 полей по отдельности. Если вам постоянно приходится передавать в методы слишком большое число аргументов, ваши методы имеют слишком сильное сопряжение. Проектируйте ме# тоды или группы методов так, чтобы сопряжение было слабым. Если вы передае# те одни и те же данные во многие разные методы, сгруппируйте эти методы и дан# ные в класс. Перекрестная ссылка О связан- ных с интерфейсами предполо- жениях см. также главу 8, о до- кументировании кода — главу 32. Перекрестная ссылка Анализ ин- терфейсов см. в подразделе «Хо- рошая абстракция» раздела 6.2. ГЛАВА 7 Высококачественные методы 175 Подумайте об определении конвенции именования входных, изменяемых и выходных параметров Если нужно провести различие между входными, из# меняемыми и выходными параметрами, сформулируйте соответствующую конвен# цию их именования. Например, вы можете дополнить их префиксами i_, m_ и o_. Программисты пословоохотливее могут использовать префиксы Input_, Modify_ и Output_. Передавайте в метод те переменные или объекты, которые нужны ему для поддержания абстракции интерфейса Есть два конкурирующих под# хода к передаче членов объекта в методы. Допустим, у вас есть объект, предостав# ляющий доступ к данным посредством 10 методов доступа, но вызываемому ме# тоду нужны лишь три элемента данных объекта. Сторонники первого подхода утверждают, что в метод следует передать только три нужных ему элемента. Они считают, что это позволяет поддерживать мини# мальное сопряжение между методами, способствует пониманию методов, облег# чает их повторное использование и т. д. Они говорят, что передача всего объекта в метод нарушает принцип инкапсуляции, позволяя вызванному методу исполь# зовать все 10 методов доступа. Сторонники второго подхода утверждают, что следует передать весь объект. Они говорят, что если вызываемый метод получит доступ к дополнительным членам объекта, это позволит сохранить стабильность интерфейса метода. Им кажется, что именно передача трех конкретных элементов нарушает инкапсуляцию, потому что это указывает на конкретные элементы данных, используемые методом. Я думаю, что оба этих правила слишком упрощены и не учитывают самого важ# ного: какую абстракцию формирует интерфейс метода? Если абстракция под# разумевает, что метод ожидает три конкретных элемента данных, которые по чистой случайности принадлежат одному объекту, передайте три элемента по отдельности. Если же абстракция состоит в том, что элементы данных всегда при# надлежат конкретному объекту, над которым метод должен выполнять ту или иную операцию, тогда, раскрывая три этих специфических элемента, вы на самом деле нарушаете абстракцию. Если при передаче всего объекта вы создаете объект, заполняете его тремя эле# ментами, нужными методу, а после вызова извлекаете эти элементы из объекта, значит, вам следует передать в метод только три конкретных элемента, а не весь объект. (Обычно наличие кода, «подготавливающего» данные перед вызовом ме# тода или «разбирающего» объект после вызова, — признак неудачного проекти# рования метода.) Если же вам часто приходится изменять список параметров метода, при этом каждый раз параметры относятся к одному и тому же объекту, в метод следует передавать весь объект, а не конкретные элементы. Используйте именованные параметры Некоторые языки позволяют явно со# поставить формальные параметры с фактическими. Это делает применение па# раметров более ясным и помогает избегать ошибок, обусловленных неправиль# ным сопоставлением параметров, например: 176 ЧАСТЬ II Высококачественный код Пример явной идентификации параметров (Visual Basic) Private Function Distance3d( _ Объявления формальных параметров. ByVal xDistance As Coordinate, _ ByVal yDistance As Coordinate, _ ByVal zDistance As Coordinate _ ) End Function Private Function Velocity( _ ByVal latitude as Coordinate, _ ByVal longitude as Coordinate, _ ByVal elevation as Coordinate _ ) Сопоставление фактических параметров с формальными. Distance = Distance3d( xDistance := latitude, yDistance := longitude, _ zDistance := elevation ) End Function Данный подход особенно полезен при использовании длинных списков параметров одинакового типа, потому что в этом случае вероятность неправильного сопо# ставления параметров более высока, а компилятор эту ошибку определить не мо# жет. Во многих средах явное сопоставление параметров может оказаться пальбой из пушки по воробьям, но в средах, от которых зависит безопасность людей, или других средах с повышенными требованиями к надежности дополнительный спо# соб гарантии правильного сопоставления параметров не помешает. Убедитесь, что фактические параметры соответствуют формаль' ным Формальные параметры, известные также как «фиктивные параметры» (dum# my parameters), — это переменные, объявленные в определении метода. Факти# ческими параметрами называют переменные, константы или выражения, на са# мом деле передаваемые в метод. По невнимательности довольно часто передают в метод переменную неверного типа — например, целое число вместо числа с плавающей запятой. (Эта пробле# ма характерна только для слабо типизированных языков, таких как C, при исполь# зовании неполного набора предупреждений компилятора. Строго типизирован# ные языки, такие как C++ и Java, не имеют этого недостатка.) Если аргументы яв# ляются исключительно входными, это редко становится проблемой: обычно ком# пилятор при вызове метода преобразует фактический тип в формальный. Если это приводит к проблеме, компилятор обычно генерирует предупреждение. Но иногда, особенно если аргумент является и входным, и выходным, передача аргумента неверного типа может привести к серьезным последствиям. > > |