Главная страница
Навигация по странице:

  • Принцип единой ответственности (SrP)

  • Поддержание связности приводит к уменьшению классов

  • Создание, анализ ирефакторинг


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница20 из 49
    1   ...   16   17   18   19   20   21   22   23   ...   49
    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 elements = new LinkedList();
    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 multiplesOfPrimeFactors;
    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 специализируется на построении списка простых чисел . Со- здание экземпляров этого класса не предполагается . Класс всего лишь определяет удобную область видимости, в которой можно объявлять и скрывать переменные .
    Он изменится при изменении алгоритма вычисления простых чисел .
    При этом программа не была переписана! Мы не начинали работу «с нуля» и не писали код заново . В самом деле, внимательно присмотревшись к двум про- граммам, вы увидите, что они используют одинаковые алгоритмы и одинаковую механику для решения своих задач .
    Модификация началась с написания тестового пакета, досконально проверявше- го поведение первой программы . Далее в код последовательно вносились много- численные мелкие изменения . После каждого изменения проводились тесты, ко- торые подтверждали, что поведение программы не изменилось . Так, шаг за ша- гом, первая программа очищалась и трансформировалась во вторую .
    1   ...   16   17   18   19   20   21   22   23   ...   49


    написать администратору сайта