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

  • Отделение конструирования системы от ее использования

  • Рис . 11 .2 .

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


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница21 из 49
    1   ...   17   18   19   20   21   22   23   24   ...   49
    Структурирование с учетом изменений
    Большинство систем находится в процессе непрерывных изменений . Каждое из- менение создает риск того, что остальные части системы будут работать не так, как мы ожидаем . В чистой системе классы организованы таким образом, чтобы риск от изменений был сведен к минимуму .
    Класс
    Sql в листинге 10 .9 используется для построения правильно сформиро- ванных строк SQL по соответствующим метаданным . Работа еще не завершена, поэтому класс не поддерживает многие функции SQL (например, команды update
    ) . Когда придет время включения в класс
    Sql поддержки update
    , придется
    «открыть» этот класс для внесения изменений . Но как уже говорилось, открытие класса создает риск . Любые изменения в этом классе создают потенциальную возможность для нарушения работы остального кода класса, поэтому весь код приходится полностью тестировать заново .
    листинг 10 .9 . Класс, который необходимо открыть для внесения изменений public class Sql {
    public Sql(String table, Column[] columns)
    public String create()
    public String insert(Object[] fields)
    public String selectAll()
    public String findByKey(String keyColumn, String keyValue)
    public String select(Column column, String pattern)
    176

    Структурирование с учетом изменений
    177
    public String select(Criteria criteria)
    public String preparedInsert()
    private String columnList(Column[] columns)
    private String valuesList(Object[] fields, final Column[] columns)
    private String selectWithCriteria(String criteria)
    private String placeholderList(Column[] columns)
    }
    Класс
    Sql изменяется при добавлении нового типа команды . Кроме того, он будет изменяться при изменении подробностей реализации уже существующего типа команды — скажем, если нам понадобится изменить функциональность select для поддержки подчиненной выборки . Две причины для изменения означают, что класс
    Sql нарушает принцип единой ответственности .
    Нарушение принципа единой ответственности проявляется и в структуре кода .
    Из набора методов
    Sql видно, что класс содержит приватные методы (например, selectWithCriteria
    ), относящиеся только к командам select
    Приватные методы, действие которых распространяется только на небольшое подмножество класса, — хороший признак для поиска потенциальных усовер- шенствований . Тем не менее основные усилия следует направить на изменение самой системы . Если бы класс
    Sql выглядел логически завершенным, то нам не пришлось бы беспокоиться о разделении ответственности . Если бы в обозримом будущем функциональность update не понадобилась,
    Sql можно было бы оставить в покое . Но как только выясняется, что класс необходимо открыть, нужно рас- смотреть возможность усовершенствования его структуры .
    Почему бы не воспользоваться решением, представленным в листинге 10 .10? Для каждого метода открытого интерфейса, определенного в предыдущей версии
    Sql из листинга 10 .9, создается соответствующий класс, производный от
    Sql
    . При этом приватные методы (такие, как valuesList
    ) перемещаются непосредственно туда, где они понадобятся . Общее приватное поведение изолируется в паре вспо- могательных классов,
    Where и
    ColumnList
    листинг 10 .10 . Набор закрытых классов abstract public class Sql {
    public Sql(String table, Column[] columns)
    abstract public String generate();
    }
    public class CreateSql extends Sql {
    public CreateSql(String table, Column[] columns)
    @Override public String generate()
    }
    public class SelectSql extends Sql {
    public SelectSql(String table, Column[] columns)
    @Override public String generate()
    }
    продолжение
    177

    178
    Глава 10 . Классы
    листинг 10 .10 (продолжение)
    public class InsertSql extends Sql {
    public InsertSql(String table, Column[] columns, Object[] fields)
    @Override public String generate()
    private String valuesList(Object[] fields, final Column[] columns)
    }
    public class SelectWithCriteriaSql extends Sql {
    public SelectWithCriteriaSql(
    String table, Column[] columns, Criteria criteria)
    @Override public String generate()
    }
    public class SelectWithMatchSql extends Sql {
    public SelectWithMatchSql(
    String table, Column[] columns, Column column, String pattern)
    @Override public String generate()
    }
    public class FindByKeySql extends Sql public FindByKeySql(
    String table, Column[] columns, String keyColumn, String keyValue)
    @Override public String generate()
    }
    public class PreparedInsertSql extends Sql {
    public PreparedInsertSql(String table, Column[] columns)
    @Override public String generate() {
    private String placeholderList(Column[] columns)
    }
    public class Where {
    public Where(String criteria)
    public String generate()
    }
    Код каждого класса становится до смешного простым . Время, необходимое для понимания класса, падает почти до нуля . Вероятность того, что одна из функций нарушит работу другой, ничтожно мала . С точки зрения тестирования проверка всех фрагментов логики в этом решении упрощается, поскольку все классы изо- лированы друг от друга .
    Что не менее важно, когда придет время добавления update
    , вам не придется из- менять ни один из существующих классов! Логика построения команды update реализуется в новом субклассе
    Sql с именем
    UpdateSql
    . Это изменение не нару- шит работу другого кода в системе .
    Переработанная логика
    Sql положительна во всех отношениях . Она поддерживает принцип единой ответственности . Она также поддерживает другой ключевой принцип проектирования классов в ООП, называемый принципом открытости/
    закрытости [PPP]: классы должны быть открыты для расширений, но закрыты
    178

    Структурирование с учетом изменений
    179
    для модификации . Наш переработанный класс
    Sql открыт для добавления новой функциональности посредством создания производных классов, но при внесе- нии этого изменения все остальные классы остаются закрытыми . Новый класс
    UpdateSql просто размещается в положенном месте .
    Структура системы должна быть такой, чтобы обновление системы (с добав- лением новых или изменением существующих аспектов) создавало как можно меньше проблем . В идеале новая функциональность должна реализовываться расширением системы, а не внесением изменений в существующий код .
    Изоляция изменений
    Потребности меняются со временем; следовательно, меняется и код . В начальном курсе объектно-ориентированного программирования мы узнали, что классы делятся на конкретные, содержащие подробности реализации (код), и абстракт- ные, представляющие только концепции . Если клиентский класс зависит от конкретных подробностей, то изменение этих подробностей может нарушить его работоспособность . Чтобы изолировать воздействие этих подробностей на класс, в систему вводятся интерфейсы и абстрактные классы .
    Зависимости от конкретики создает проблемы при тестировании системы . Если мы строим класс
    Portfolio
    , зависящий от внешнего API
    TokyoStockExchange для вычисления текущей стоимости портфеля ценных бумаг, наши тестовые сценарии начинают зависеть от ненадежного внешнего фактора . Трудно написать тест, если вы получаете разные ответы каждые пять минут!
    Вместо того чтобы проектировать
    Portfolio с прямой зависимостью от
    Tokyo-
    StockExchange
    , мы создаем интерфейс
    StockExchange
    , в котором объявляется один метод:
    public interface StockExchange {
    Money currentPrice(String symbol);
    }
    Класс
    TokyoStockExchange проектируется с расчетом на реализацию этого ин- терфейса . При ссылке на
    StockExchange передается в аргументе конструктора
    Portfolio
    :
    public Portfolio {
    private StockExchange exchange;
    public Portfolio(StockExchange exchange) {
    this.exchange = exchange;
    }
    // ...
    }
    Теперь наш тест может создать пригодную для тестирования реализацию интер- фейса
    StockExchange
    , эмулирующую реальный API
    TokyoStockExchange
    . Тестовая реализация задает текущую стоимость каждого вида акций, используемых при тестировании . Если тест демонстрирует приобретение пяти акций Microsoft, мы
    179

    180
    Глава 10 . Классы кодируем тестовую реализацию так, чтобы для Microsoft всегда возвращалась стоимость $100 за акцию . Тестовая реализация интерфейса
    StockExchange сводится к простому поиску по таблице . После этого пишется тест, который должен вер- нуть общую стоимость портфеля в $500:
    public class PortfolioTest {
    private FixedStockExchangeStub exchange;
    private Portfolio portfolio;
    @Before protected void setUp() throws Exception {
    exchange = new FixedStockExchangeStub();
    exchange.fix("MSFT", 100);
    portfolio = new Portfolio(exchange);
    }
    @Test public void GivenFiveMSFTTotalShouldBe500() throws Exception {
    portfolio.add(5, "MSFT");
    Assert.assertEquals(500, portfolio.value());
    }
    }
    Если система обладает достаточной логической изоляцией для подобного тести- рования, она также становится более гибкой и более подходящей для повторного использования . Отсутствие жестких привязок означает, что элементы системы лучше изолируются друг от друга и от изменений . Изоляция упрощает понима- ние каждого элемента системы .
    Сведение к минимуму логических привязок соответствует другому принципу проектирования классов, известному как принцип обращения зависимостей (DIP,
    Dependency Inversion Principle) . По сути DIP гласит, что классы системы должны зависеть от абстракций, а не от конкретных подробностей .
    Вместо того чтобы зависеть от подробностей реализации класса
    Tokyo Stock-
    Exchange
    , наш класс
    Portfolio теперь зависит от интерфейса
    StockExchange
    . Ин- терфейс
    StockExchange представляет абстрактную концепцию запроса текущей стоимости акций . Эта абстракция изолирует класс от конкретных подробностей получения такой цены — в том числе и от источника, из которого берется реаль- ная информация .
    литература
    [RDD]: Object Design: Roles, Responsibilities, and Collaborations, Rebecca Wirfs-
    Brock et al ., Addison-Wesley, 2002 .
    [PPP]: Agile Software Development: Principles, Patterns, and Practices, Robert
    C . Martin, Prentice Hall, 2002 .
    [Knuth92]: Literate Programming, Donald E . Knuth, Center for the Study of language and Information, Leland Stanford Junior University, 1992 .
    180

    Системы
    Кевин Дин Уомплер
    Сложность убивает . Она вытягивает жизненные силы из разработчиков, затрудняя планирование, построение и тестирование продуктов .
    Рэй Оззи, технический директор
    Microsoft Corporation
    11
    181

    182
    Глава 11 . Системы
    Как бы вы строили город?
    Смогли бы вы лично разработать план до последней мелочи? Вероятно, нет . Даже управление существующим городом не под силу одному человеку . Да, города ра- ботают (в основном) . Они работают, потому что в городах есть группы людей, управляющие определенными аспектами городской жизни: водопроводом, элек- тричеством, транспортом, соблюдением законности, правилами застройки и т . д .
    Одни отвечают за общую картину, другие занимаются мелочами .
    Города работают еще и потому, что в них развились правильные уровни абстрак- ции и модульности, которые обеспечивают эффективную работу людей и «ком- понентов», находящихся под их управлением, — даже без понимания полной картины .
    Группы разработки программного обеспечения тоже организуются по анало- гичным принципам, но системы, над которыми они работают, часто не имеют ана логичного разделения обязанностей и уровней абстракции . Чистый код помогает достичь этой цели на нижних уровнях абстракции . В этой главе мы поговорим о том, как сохранить чистоту на более высоких уровнях, то есть на уровне системы .
    Отделение конструирования системы
    от ее использования
    Прежде всего необходимо понять, что конструирование и использование систе- мы — два совершенно разных процесса . Когда я пишу эти строки, из моего окна в Чикаго виден новый строящийся отель . Сейчас это голая бетонная коробка со строительным краном и лифтом, закрепленным на наружной стене . Все рабочие носят каски и спецовки . Через год-другой строительство будет завершено . Кран и служебный лифт исчезнут . Здание очистится, заблестит стеклянными окнами и новой краской . Люди, работающие и останавливающиеся в нем, тоже будут выглядеть совершенно иначе .
    В программных системах фаза инициализации, в которой конструируются объек- ты приложения и «склеиваются» основные зависимости, тоже должна отделяться от логики времени выполнения, получающей управление после ее завершения .
    Фаза инициализации присутствует в каждом приложении . Это первая из областей
    ответственности (concerns), которую мы рассмотрим в этой главе, а сама кон- цепция разделения ответственности относится к числу самых старых и важных приемов нашего ремесла .
    К сожалению, во многих приложениях такое разделение отсутствует . Код ини- циализации пишется бессистемно и смешивается с логикой времени выполнения .
    182

    Отделение конструирования системы от ее использования
    183
    Типичный пример:
    public Service getService() {
    if (service == null)
    service = new MyServiceImpl(...); // Инициализация по умолчанию,
    // подходящая для большинства случаев?
    return service;
    }
    Идиома ОТЛОЖЕННОЙ ИНИЦИАЛИЗАЦИИ обладает определенными достоинствами . Приложение не тратит времени на конструирование объекта до момента его фактического использования, а это может ускорить процесс инициализации . Кроме того, мы следим за тем, чтобы функция никогда не воз- вращала null
    Однако в программе появляется жестко закодированная зависимость от класса
    MyServiceImpl и всего, что необходимо для его конструктора (который я не при- вел) . Программа не компилируется без разрешения этих зависимостей, даже если объект этого типа ни разу не используется во время выполнения!
    Проблемы могут возникнуть и при тестировании . Если
    MyServiceImpl представ- ляет собой тяжеловесный объект, нам придется позаботиться о том, чтобы перед вызовом метода в ходе модульного тестирования в поле service был сохранен соответствующий ТЕСТОВЫЙ ДУБЛЕР [Mezzaros07] или ФИКТИВНЫЙ
    ОБЪЕКТ . А поскольку логика конструирования смешана с логикой нормальной обработки, мы должны протестировать все пути выполнения (в частности, про- верку null и ее блок) . Наличие обеих обязанностей означает, что метод выполняет более одной операции, а это указывает на некоторое нарушение принципа единой ответственности .
    Но хуже всего другое — мы не знаем, является ли
    MyServiceImpl правильным объ- ектом во всех случаях . Я намекнул на это в комментарии . Почему класс с этим методом должен знать глобальный контекст? Можем ли мы вообще определить, какой объект должен здесь использоваться? И вообще, может ли один тип быть подходящим для всех возможных контекстов?
    Конечно, одно вхождение ОТЛОЖЕННОЙ ИНИЦИАЛИЗАЦИИ не создает серьезных проблем . Однако в приложениях идиомы инициализации обычно встречаются во множество экземпляров . Таким образом, глобальная стратегия инициализации (если она здесь вообще присутствует) распределяется по всему приложению, с минимальной модульностью и значительным дублированием кода .
    Если вы действительно стремитесь к созданию хорошо структурированных, надежных систем, никогда не допускайте, чтобы удобные идиомы вели к нару- шению модульности . Процесс конструирования объектов и установления связей не является исключением . Этот процесс должен быть отделен от нормальной логики времени выполнения, а вы должны позаботиться о выработке глобальной, последовательной стратегии разрешения основных зависимостей .
    183

    184
    Глава 11 . Системы
    Отделение main
    Один из способов отделения конструирования от использования заключается в простом перемещении всех аспектов конструирования в main
    (или модули, вызываемые из main
    ) . Далее весь остальной код системы пишется в предположе- нии, что все объекты были успешно сконструированы и правильно связаны друг с другом (рис . 11 .1) .
    Рис . 11 .1 . Изоляция конструирования в main
    На рисунке хорошо видна последовательность передачи управления . Функция main строит объекты, необходимые для системы, а затем передает их приложе- нию, которое их просто использует . Обратите внимание на направление стрелок зависимостей, пересекающих границу между main и приложением . Все стрелки указывают в одном направлении — от main
    . Это означает, что приложение ниче- го не знает о main или о процессе конструирования . Оно просто ожидает, что все объекты были построены правильно .
    Фабрики
    Конечно, в некоторых ситуациях момент создания объекта должен определять- ся приложением . Например, в системе обработки заказов приложение должно создать экземпляры товаров
    LineItem для включения их в объект заказа
    Order
    В этом случае можно воспользоваться паттерном АБСТРАКТНАЯ ФАБРИКА
    [GOF], чтобы приложение могло само выбрать момент для создания
    LineItem
    , но при этом подробности конструирования были отделены от кода приложения
    (рис . 11 .2) .
    И снова обратите внимание на то, что все стрелки зависимостей ведут от main к приложению
    OrderProcessing
    . Это означает, что приложение изолировано от подробностей построения
    LineItem
    . Вся информация хранится в реализации
    184

    Отделение main
    185
    LineItemFactoryImplementation
    , находящейся на стороне main
    . Тем не менее при- ложение полностью управляет моментом создания экземпляров
    LineItem и даже может передать аргументы конструктора, специфические для конкретного при- ложения .
    Рис . 11 .2 . Отделение конструирования с применением фабрики
    Внедрение зависимостей
    Внедрение зависимостей (DI, Dependency Injection) — мощный механизм отде- ления конструирования от использования, практическое применение обращения контроля (IoC, Inversion of Control) в области управления зависимостями
    1
    . Об- ращение контроля перемещает вторичные обязанности объекта в другие объекты, созданные специально для этой цели, тем самым способствуя соблюдению прин- ципа единой ответственности . В контексте управления зависимостями объект не должен брать на себя ответственность за создание экземпляров зависимостей .
    Вместо этого он передает эту обязанность другому «уполномоченному» меха- низму . Так как инициализация является глобальной областью ответственности, этим уполномоченным механизмом обычно является либо функция main
    , либо специализированный контейнер .
    Примером «частичной» реализации внедрения зависимостей является запрос
    JNDI, когда объект обращается к серверу каталоговой информации с запросом на предоставление «сервиса» с заданным именем:
    MyService myService = (MyService)(jndiContext.lookup("NameOfMyService"));
    Вызывающий объект не управляет тем, какой именно объект будет возвращен
    (конечно, при условии, что этот объект реализует положенный интерфейс), но при этом происходит активное разрешение зависимости .
    1
    Например, см . [Fowler] .
    185

    1   ...   17   18   19   20   21   22   23   24   ...   49


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