Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
167 public void setAçowDragging(boolean allowDragging) public boolean allowDragging() public boolean isCustomizing() public void setTitle(String title) public IdeMenuBar getIdeMenuBar() public void showHelper(MetaObject metaObject, String propertyName) // ... и еще много других, не-открытых методов... } А если бы класс SuperDashboard содержал только методы, приведенные в листин- ге 10 .2? листинг 10 .2 . Достаточно компактно? public class SuperDashboard extends JFrame implements MetaDataUser public Component getLastFocusedComponent() public void setLastFocused(Component lastFocused) public int getMajorVersionNumber() public int getMinorVersionNumber() public int getBuildNumber() } Пять методов — не слишком много, не так ли? В нашем случае слишком, потому что несмотря на малое количество методов, класс SuperDashboard по-прежнему имеет слишком много ответственностей . Имя класса должно описывать его ответственности . В сущности, имя должно стать первым фактором, способствующим определению размера класса . Если для класса не удается подобрать четкое, короткое имя, вероятно, он слишком велик . Чем туманнее имя класса, тем больше вероятность, что он имеет слиш- ком много ответственностей . В частности, присутствие в именах классов слов- проныр «Processor», «Manager» и «Super» часто свидетельствует о нежелательном объединении ответственностей . Краткое описание класса должно укладываться примерно в 25 слов, без выраже- ний «если», «и», «или» и «но» . Как бы вы описали класс SuperDashboard ? «Класс предоставляет доступ к компоненту, который последним имел фокус ввода, и по- зволяет отслеживать номера версии и сборки» . Первое «и» указывает на то, что SuperDashboard имеет слишком много ответственностей . Принцип единой ответственности (SrP) Принцип единой ответственности (SRP 1 ) утверждает, что класс или модуль должен иметь одну — и только одну — причину для изменения . Этот принцип дает нам как определение ответственности, так и критерий для оценки размера класса . Классы должны иметь одну ответственность, то есть одну причину для изменений . 1 За более подробной информацией об этом принципе обращайтесь к [PPP] . 167 168 Глава 10 . Классы Небольшой, казалось бы, класс SuperDashboard в листинге 10 .2 имеет две причины для изменений . Во-первых, он отслеживает версию, которая, вероятно, будет изменяться при каждом обновлении продукта . Во-вторых, он управляет компо- нентами Java Swing (потомки класса JFrame , представляющего графическое окно верхнего уровня в Swing) . Несомненно, номер версии должен обновляться при любых изменениях кода Swing, но обратное не всегда верно: номер версии также может изменяться вследствие изменений в другом коде системы . Попытки идентификации ответственностей (причин для изменения) часто по- могают выявить и создать более качественные абстракции для нашего кода . Все три метода SuperDashboard , относящиеся к версии, легко выделяются в отдельный класс с именем Version (листинг 10 .3) . Класс Version обладает хорошим потенциа- лом для повторного использования в других приложениях! листинг 10 .3 . Класс с единой ответственностью public class Version { public int getMajorVersionNumber() public int getMinorVersionNumber() public int getBuildNumber() } Принцип единой ответственности — одна из самых важных концепций в объектно-ориентированном проектировании . Кроме того, его относительно несложно понять и соблюдать . Но как ни странно, принцип единой ответствен- ности часто оказывается самым нарушаемым принципом проектирования клас- сов . Мы постоянно встречаем классы, которые делают слишком много всего . Почему? Заставить программу работать и написать чистый код — совершенно разные вещи . Обычно мы думаем прежде всего о том, чтобы наш код заработал, а не о его структуре и чистоте . И это абсолютно законно . Разделение ответственности в работе программиста играет не менее важную роль, чем в наших программах . К сожалению, слишком многие из нас полагают, что после того, как программа заработает, их работа закончена . Мы не переключаемся на усовершенствование ее структуры и чистоты . Мы переходим к следующей задаче вместо того, чтобы сделать шаг назад и разделить разбухшие классы на отдельные блоки с единой ответственностью . В то же время многие разработчики опасаются, что множество небольших узко- специализированных классов затруднит понимание общей картины . Их беспоко- ит то, что им придется переходить от класса к классу, чтобы разобраться в том, как решается более крупная задача . Однако система с множеством малых классов имеет не больше «подвижных частей», чем система с несколькими большими классами . В последней тоже придется разбираться, и это будет ничуть не проще . Так что вопрос заключает- ся в следующем: хотите ли вы, чтобы ваши инструменты были разложены по ящикам с множеством небольших отделений, содержащих четко определенные 168 Строение класса 169 и подписанные компоненты? Или вы предпочитаете несколько больших ящиков, в которые можно сваливать все подряд? Каждая крупная система содержит большой объем рабочей логики и обладает высокой сложностью . Первоочередной целью управления этой сложностью яв- ляется формирование структуры, при которой разработчик знает, где искать то, что ему требуется, и в любой момент времени может досконально знать только ту часть системы, которая непосредственно относится к его работе . Напротив, в системе с большими, многоцелевыми классами нам неизбежно приходится раз- бираться с множеством аспектов, которые в данный момент нас не интересуют . Еще раз выделю основные моменты: система должна состоять из множества мел- ких классов, а не из небольшого числа больших . Каждый класс инкапсулирует одну ответственность, имеет одну причину для изменения и взаимодействует с другими классами для реализации желаемого поведения системы . Связность Классы должны иметь небольшое количество переменных экземпляров . Каждый метод класса должен оперировать с одной или несколькими из этих переменных . В общем случае, чем с большим количеством переменных работает метод, тем выше связность этого метода со своим классом . Класс, в котором каждая пере- менная используется каждым методом, обладает максимальной связностью . Как правило, создавать классы с максимальной связностью не рекомендуется… а скорее всего, это нереально . С другой стороны, связность класса должна быть высокой . Высокая связность означает, что методы и переменные класса взаимо- зависимы и существуют как единое целое . Рассмотрим реализацию стека из листинга 10 .4 . Этот класс обладает очень высо- кой связностью . Из трех его методов только size() не использует обе переменные . листинг 10 .4 . Stack.java — класс с высокой связностью public class Stack { private int topOfStack = 0; List public int size() { return topOfStack; } public void push(int element) { topOfStack++; elements.add(element); } public int pop() throws PoppedWhenEmpty { if (topOfStack == 0) throw new PoppedWhenEmpty(); продолжение 169 170 Глава 10 . Классы листинг 10 .4 (продолжение) int element = elements.get(--topOfStack); elements.remove(topOfStack); return element; } } Стратегия компактных функций и коротких списков параметров иногда приво- дит к росту переменных экземпляров, используемых подмножеством методов . Это почти всегда свидетельствует о том, что по крайней мере один класс пыта- ется выделиться из более крупного класса . Постарайтесь разделить переменные и методы на два и более класса, чтобы новые классы обладали более высокой связностью . Поддержание связности приводит к уменьшению классов Сам акт разбиения больших функций на меньшие приводит к росту количества классов . Допустим, имеется большая функция, в которой объявлено много переменных . Вы хотите выделить один небольшой фрагмент этой функции в от- дельную функцию . Однако выделяемый код использует четыре переменные, объ- явленные в исходной функции . Может, передать все четыре переменные новой функции в виде аргументов? Ни в коем случае! Преобразовав эти четыре переменные в переменные экземпля- ров класса, мы сможем выделить код без передачи переменных . Таким образом, разбиение функции на меньшие фрагменты упрощается . К сожалению, это также означает, что наши классы теряют связность, потому что в них накапливается все больше переменных экземпляров, созданных ис- ключительно для того, чтобы они могли совместно использоваться небольшим подмножеством функций . Но постойте! Если группа функций должна работать с некоторыми переменными, не образуют ли они класс сами по себе? Конечно, образуют . Если классы утрачивают связность, разбейте их! Таким образом, разбиение большой функции на много мелких функций также часто открывает возможность для выделения нескольких меньших классов . В результате строение программы улучшается, а ее структура становится более прозрачной . Для демонстрации мы воспользуемся проверенным временем примером из за- мечательной книги Кнута «Literate Programming» [Knuth92] . В листинге 10 .5 представлена программа Кнута PrintPrimes , переведенная на Java . Справедливо- сти ради стоит отметить, что это не та программа, которую написал Кнут, а та, которую выводит его утилита WEB . Я воспользуюсь ей, потому что она являет- ся отличной отправной точкой для разбиения большой функции на несколько меньших функций и классов . 170 Строение класса 171 листинг 10 .5 . PrintPrimes.java package literatePrimes; public class PrintPrimes { public static void main(String[] args) { final int M = 1000; final int RR = 50; final int CC = 4; final int WW = 10; final int ORDMAX = 30; int P[] = new int[M + 1]; int PAGENUMBER; int PAGEOFFSET; int ROWOFFSET; int C; int J; int K; boolean JPRIME; int ORD; int SQUARE; int N; int MULT[] = new int[ORDMAX + 1]; J = 1; K = 1; P[1] = 2; ORD = 2; SQUARE = 9; while (K < M) { do { J = J + 2; if (J == SQUARE) { ORD = ORD + 1; SQUARE = P[ORD] * P[ORD]; MULT[ORD - 1] = J; } N = 2; JPRIME = true; while (N < ORD && JPRIME) { while (MULT[N] < J) MULT[N] = MULT[N] + P[N] + P[N]; if (MULT[N] == J) JPRIME = false; N = N + 1; } } while (!JPRIME); K = K + 1; P[K] = J; } { продолжение 171 172 Глава 10 . Классы листинг 10 .5 (продолжение) PAGENUMBER = 1; PAGEOFFSET = 1; while (PAGEOFFSET <= M) { System.out.println("The First " + M + " Prime Numbers --- Page " + PAGENUMBER); System.out.println(""); for (ROWOFFSET = PAGEOFFSET; ROWOFFSET < PAGEOFFSET + RR; ROWOFFSET++){ for (C = 0; C < CC;C++) if (ROWOFFSET + C * RR <= M) System.out.format("%10d", P[ROWOFFSET + C * RR]); System.out.println(""); } System.out.println("\f"); PAGENUMBER = PAGENUMBER + 1; PAGEOFFSET = PAGEOFFSET + RR * CC; } } } } Записанная в виде одной функции, эта программа представляет собой полную неразбериху . Многоуровневая вложенность, множество странных переменных, структура с жесткой привязкой… По крайней мере, одну большую функцию следует разбить на несколько меньших функций . В листингах 10 .6–10 .8 показано, что получается после разбиения кода из листин- га 10 .5 на меньшие классы и функции, с выбором осмысленных имен для классов, функций и переменных . листинг 10 .6 . PrimePrinter.java (переработанная версия) package literatePrimes; public class PrimePrinter { public static void main(String[] args) { final int NUMBER_OF_PRIMES = 1000; int[] primes = PrimeGenerator.generate(NUMBER_OF_PRIMES); final int ROWS_PER_PAGE = 50; final int COLUMNS_PER_PAGE = 4; RowColumnPagePrinter tablePrinter = new RowColumnPagePrinter(ROWS_PER_PAGE, COLUMNS_PER_PAGE, "The First " + NUMBER_OF_PRIMES + " Prime Numbers"); tablePrinter.print(primes); } } 172 Строение класса 173 листинг 10 .7 . RowColumnPagePrinter.java package literatePrimes; import java.io.PrintStream; public class RowColumnPagePrinter { private int rowsPerPage; private int columnsPerPage; private int numbersPerPage; private String pageHeader; private PrintStream printStream; public RowColumnPagePrinter(int rowsPerPage, int columnsPerPage, String pageHeader) { this.rowsPerPage = rowsPerPage; this.columnsPerPage = columnsPerPage; this.pageHeader = pageHeader; numbersPerPage = rowsPerPage * columnsPerPage; printStream = System.out; } public void print(int data[]) { int pageNumber = 1; for (int firstIndexOnPage = 0; firstIndexOnPage < data.length; firstIndexOnPage += numbersPerPage) { int lastIndexOnPage = Math.min(firstIndexOnPage + numbersPerPage - 1, data.length - 1); printPageHeader(pageHeader, pageNumber); printPage(firstIndexOnPage, lastIndexOnPage, data); printStream.println("\f"); pageNumber++; } } private void printPage(int firstIndexOnPage, int lastIndexOnPage, int[] data) { int firstIndexOfLastRowOnPage = firstIndexOnPage + rowsPerPage - 1; for (int firstIndexInRow = firstIndexOnPage; firstIndexInRow <= firstIndexOfLastRowOnPage; firstIndexInRow++) { printRow(firstIndexInRow, lastIndexOnPage, data); printStream.println(""); } } продолжение 173 174 Глава 10 . Классы листинг 10 .7 (продолжение) private void printRow(int firstIndexInRow, int lastIndexOnPage, int[] data) { for (int column = 0; column < columnsPerPage; column++) { int index = firstIndexInRow + column * rowsPerPage; if (index <= lastIndexOnPage) printStream.format("%10d", data[index]); } } private void printPageHeader(String pageHeader, int pageNumber) { printStream.println(pageHeader + " --- Page " + pageNumber); printStream.println(""); } public void setOutput(PrintStream printStream) { this.printStream = printStream; } } листинг 10 .8 . PrimeGenerator.java package literatePrimes; import java.util.ArrayList; public class PrimeGenerator { private static int[] primes; private static ArrayList protected static int[] generate(int n) { primes = new int[n]; multiplesOfPrimeFactors = new ArrayList set2AsFirstPrime(); checkOddNumbersForSubsequentPrimes(); return primes; } private static void set2AsFirstPrime() { primes[0] = 2; multiplesOfPrimeFactors.add(2); } private static void checkOddNumbersForSubsequentPrimes() { int primeIndex = 1; for (int candidate = 3; primeIndex < primes.length; candidate += 2) { if (isPrime(candidate)) primes[primeIndex++] = candidate; } } 174 Строение класса 175 private static boolean isPrime(int candidate) { if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) { multiplesOfPrimeFactors.add(candidate); return false; } return isNotMultipleOfAnyPreviousPrimeFactor(candidate); } private static boolean isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) { int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()]; int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor; return candidate == leastRelevantMultiple; } private static boolean isNotMultipleOfAnyPreviousPrimeFactor(int candidate) { for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) { if (isMultipleOfNthPrimeFactor(candidate, n)) return false; } return true; } private static boolean isMultipleOfNthPrimeFactor(int candidate, int n) { return candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n); } private static int smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) { int multiple = multiplesOfPrimeFactors.get(n); while (multiple < candidate) multiple += 2 * primes[n]; multiplesOfPrimeFactors.set(n, multiple); return multiple; } } Прежде всего бросается в глаза, что программа стала значительно длиннее . От одной с небольшим страницы она разрослась почти до трех страниц . Это объ- ясняется несколькими причинами . Во-первых, в переработанной программе ис- пользуются более длинные, более содержательные имена переменных . Во-вторых, объявления функций и классов в переработанной версии используются для комментирования кода . В третьих, пробелы и дополнительное форматирование обеспечивают удобочитаемость программы . Обратите внимание на логическое разбиение программы в соответствии с тре- мя основными видами ответственности . Основной код программы содержится в классе PrimePrinter ; он отвечает за управлении средой выполнения . Именно этот код изменится в случае смены механизма вызова . Например, если в будущем 175 176 Глава 10 . Классы программа будет преобразована в службу SOAP, то изменения будут внесены в код PrimePrinter Класс RowColumnPagePrinter специализируется на форматировании списка чисел в страницы с определенным количеством строк и столбцов . Если потребуется изменить формат вывода, то изменения затронут только этот класс . Класс PrimeGenerator специализируется на построении списка простых чисел . Со- здание экземпляров этого класса не предполагается . Класс всего лишь определяет удобную область видимости, в которой можно объявлять и скрывать переменные . Он изменится при изменении алгоритма вычисления простых чисел . При этом программа не была переписана! Мы не начинали работу «с нуля» и не писали код заново . В самом деле, внимательно присмотревшись к двум про- граммам, вы увидите, что они используют одинаковые алгоритмы и одинаковую механику для решения своих задач . Модификация началась с написания тестового пакета, досконально проверявше- го поведение первой программы . Далее в код последовательно вносились много- численные мелкие изменения . После каждого изменения проводились тесты, ко- торые подтверждали, что поведение программы не изменилось . Так, шаг за ша- гом, первая программа очищалась и трансформировалась во вторую . |