Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
Структурирование с учетом изменений Большинство систем находится в процессе непрерывных изменений . Каждое из- менение создает риск того, что остальные части системы будут работать не так, как мы ожидаем . В чистой системе классы организованы таким образом, чтобы риск от изменений был сведен к минимуму . Класс 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 |