Аргументы создают еще больше проблем с точки зрения тестирования. Только представьте, как трудно составить все тестовые сценарии, проверяющие правильность работы кода со всеми комбинациями аргументов. Если аргументов нет — задача тривиальна. При одном аргументе все обходится без особых сложностей. С двумя аргументами ситуация усложняется. Если же аргументов больше двух, задача тестирования всех возможных комбинаций выглядит все более устрашающе.
Выходные аргументы запутывают ситуацию еще быстрее, чем входные. Читая код функции, мы обычно предполагаем, что функция получает информацию в аргументах, и выдает ее в возвращаемом значении. Как правило, никто не ожидает, что функция будет возвращать информацию в аргументах. Таким образом, выходные аргументы часто заставляют нас браться за чтение функции заново.
Если уж обойтись без аргументов никак не удается, постарайтесь хотя бы ограничиться одним входным аргументом. Смысл вызова SetupTeardownIncluder.render(pageData) вполне прозрачен — понятно, что мы собираемся сгенерировать данные для объекта pageData.
Стандартные унарные формы
Существует два очень распространенных случая вызова функции с одним аргументом. Первая — проверка некоторого условия, связанного с аргументом, как в вызове boolean fileExists("MyFile"). Вторая — обработка аргумента, его преобразование и возвращение. Например, вызов InputStream fileOpen("MyFile") преобразует имя файла в формате String в возвращаемое значение InputStream. Выбирайте имена, которые четко отражают различия, и всегда используйте две формы в логически непротиворечивом контексте. (См. далее «Разделение команд и запросов»).
Несколько менее распространенным, но все равно очень полезным частным случаем функции с одним аргументом является событие. В этой форме имеется входной аргумент, а выходного аргумента нет. Предполагается, что программа интерпретирует вызов функции как событие и использует аргумент для изменения состояния системы, например, void passwordAttemptFailedNtimes(int attempts). Будьте внимательны при использовании данной формы. Читателю должно быть предельно ясно, что перед ним именно событие. Тщательно выбирайте имена и контексты.
Старайтесь избегать унарных функций, не относящихся к этим формам, например void includeSetupPageInto(StringBuffer pageText). Преобразования, в которых вместо возвращаемого значения используется выходной аргумент, сбивают читателя с толку. Если функция преобразует свой входной аргумент, то результат должен передаваться в возвращаемом значении. В самом деле, вызов StringBuffer transform(StringBuffer in) лучше вызова void transform(StringBuffer out), даже если реализация в первом случае просто возвращает входной аргумент. По крайней мере она соответствует основной форме преобразования.
Аргументы-флаги
Аргументы-флаги уродливы. Передача логического значения функции — воистину ужасная привычка. Она немедленно усложняет сигнатуру метода, громко провозглашая, что функция выполняет более одной операции. При истинном значении флага выполняется одна операция, а при ложном — другая!
В листинге 3.7 у нас нет выбора, потому что вызывающая сторона уже передает этот флаг, а я хотел ограничить область переработки границами функции. Тем не менее вызов метода render(true) откровенно сбивает с толку бедного читателя. Если навести указатель мыши на вызов и увидеть render(boolean isSuite), ситуация слегка проясняется, но ненамного. Эту функцию следовало бы разбить на две: renderForSuite() и renderForSingleTest().
Бинарные функции
Функцию с двумя аргументами понять сложнее, чем унарную функцию. Например, вызов writeField(name) выглядит более доступно, чем writeField(outputStream, name)[17]. Хотя смысл обеих форм понятен, первая форма просто проскальзывает под нашим взглядом, моментально раскрывая свой смысл. Во второй форме приходится сделать непродолжительную паузу, пока вы не поймете, что первый параметр должен игнорироваться. И конечно, это в конечном итоге создает проблемы, потому что никакие части кода игнорироваться не должны. Именно в проигнорированных частях чаще всего скрываются ошибки.
Конечно, в некоторых ситуациях форма с двумя аргументами оказывается уместной. Например, вызов Point p = new Point(0,0); абсолютно разумен. Точка в декартовом пространстве естественным образом создается с двумя аргументами. В самом деле, вызов new Point(0) выглядел бы довольно странно. Однако два аргумента в нашем случае являются упорядоченными компонентами одного значения! Напротив, outputStream и name не имеют ни естественной связи, ни естественного порядка.
17
Я только что завершил переработку модуля, использовавшего бинарную форму. Мне удалось преобразовать outputStream в поле класса и привести все вызовы writeField к унарной форме. Результат получился гораздо более наглядным.