Есть естественный инстинкт предположить, что код всех остальных — неопрятный, недисциплинированный беспорядок. Но, если мы посмотрим объективно, некоторые люди действительно могут написать хорошо разработанный код. Недавно я столкнулся с другим подходом к чистому коду, который отличается от кода, с которым я большую часть своей карьеры работал (и писал).
Код корпоративного класса
Существует общий способ написания кода, возможно, особенно распространенный в Java, но также и в C #, который побуждает разработчика писать как можно больше классов. На крупных предприятиях такой способ построения кодекса является эндемичным. Перефразируя: каждая проблема в базе кода предприятия может быть решена путем добавления другого класса, за исключением слишком большого количества классов.
Почему происходит такой способ написания кода? Это реакция на слишком большую сложность? В написании небольшого класса есть определенная логика, которую можно легко протестировать отдельно. Но когда каждый выберет такой подход, вы получите миллионы уроков. Попытка выяснить, как разбить сложные системы , трудна, но если мы этого не сделаем и просто добавим больше классов, мы усугубим проблему, а не улучшим.
Тем не менее, это идет глубже, чем это. Многие люди (включая меня до недавнего времени) думают, что лучший способ написать хорошо разработанный, поддерживаемый код — это написать множество небольших классов. В конце концов, простой класс легко объяснить и, скорее всего, он будет нести единоличную ответственность . Но как вы объясните систему, состоящую из ста классов, новому разработчику? Бьюсь об заклад, вы в конечном итоге строчить на доске с коробки и линии везде. Я уверен, что ваш дизайн прост и элегантен, но когда мне нужно разобраться со 100 уроками, чтобы просто знать, как выглядит пейзаж — мне потребуется некоторое время, чтобы понять его. Масштабируйте это до предприятия с тысячами и тысячами классов, и это становится действительно сложным: ваш средний разработчик может никогда этого не понять.
Пример
Возможно, пример поможет? Представьте, что я работаю над некоторым торговым промежуточным программным обеспечением. Мы получаем сообщения, представляющие сделки, которые нам нужно сохранить и передать в системы дальше по линии. Прямо сейчас мы получаем эти сделки в ленте CSV. Я начинаю с создания класса TradeMessage.
TradeMessage
private long id; private Date timestamp; private BigDecimal amount; private TradeType type; private Asset asset;
Я хороший маленький функциональный разработчик, поэтому этот класс неизменен. Теперь у меня есть два варианта: i) я пишу большой конструктор, который принимает сто параметров, или ii) я создаю конструктор, чтобы придать упражнениям здравый смысл. Я иду на вариант II).
TradeMessageBuilder
public TradeMessageBuilder onDate(Date timestamp) public TradeMessageBuilder forAmount(BigDecimal amount) public TradeMessageBuilder ofType(TradeType type) public TradeMessageBuilder inAsset(Asset asset) public TradeMessage build()
Теперь у меня есть конструктор, из которого я могу создавать классы TradeMessage. Тем не менее, сборщик требует, чтобы строки были проанализированы по датам, десятичным числам и т. Д. Мне также нужно беспокоиться о поиске активов, поскольку TradeMessage использует класс Asset, но входящее сообщение имеет только имя актива.
Сейчас мы тестируем снаружи, как добрые маленькие разработчики GOOS . Мы начинаем с CSVTradeMessageParser (я игнорирую сеть или что-то еще, что передает наш парсер).
Нам нужно проанализировать одну строку CSV, разбить ее на составные части, из которых мы будем строить TradeMessage. Теперь у нас есть несколько вещей, которые мы должны сделать в первую очередь:
- Разобрать метку времени
- Разобрать сумму
- Разобрать тип сделки
- Поиск актива в базе данных
Теперь в самом экстремальном безумии предприятия мы могли бы написать один класс для каждой из этих «обязанностей». Однако в данном случае это просто смешно (хотя добавьте обработку ошибок или несколько попыток повторного использования кода, и вы будете удивлены, как быстро все эти дополнительные классы начинают выглядеть хорошей идеей).
Вместо этого, единственное дополнительное беспокойство, которое мы действительно имеем здесь, — это поиск активов. Синтаксический анализ даты, количества и типа, который я могу добавить к самому классу синтаксического анализатора, — это единственная обязанность синтаксического анализа сообщения, поэтому он имеет смысл.
CSVTradeMessageParser
public TradeMessage parse(String csvline) private Date parseTimestamp(String timestamp) private BigDecimal parseAmount(String amount) private TradeType parseType(String amount)
Теперь — есть проблема с вождением в этом классе — как мне проверить все эти частные методы? Я мог бы сделать их видимыми и поместить мои тесты в один пакет, но это неприятно. Или я вынужден протестировать открытый метод, смоделировать компоновщик и убедиться, что правильно проанализированные значения передаются компоновщику. Это не идеально, так как я не могу протестировать каждый метод разбора отдельно. Внезапно сделать их отдельными классами кажется лучшей идеей …
Наконец, мне нужно создать AssetRepository:
AssetRepository
public Asset lookupByName(String name)
Парсер использует это и передает полученный Актив в TradeMessageBuilder.
И мы сделали! Просто нет? Итак, если я проверил это с интерфейсами для моих смоделированных зависимостей, сколько классов мне пришлось написать?
- TradeMessage
- TradeType
- TradeMessageBuilder
- ITradeMessageBuilder
- CSVTradeMessageParser
- Актив
- AssetRepository
- IAssetRepository
- TradeMessageBuilderTest
- CSVTradeMessageParserTest
- AssetRepositoryTest
Да, и так как это только юнит-тесты, мне, вероятно, понадобятся комплексные тесты, чтобы проверить, работает ли весь матч по стрельбе:
- CSVTradeMessageParserIntegrationTest
12 классов! Ммм предприятие-у. Это просто игрушечный пример. В реальном мире у нас есть FactoryFactories и BuilderVisitors, которые действительно добавляют беспорядка.
Другой путь
Есть ли другой способ? Что ж, давайте рассмотрим TradeMessage — это API, который я хочу, чтобы люди использовали. Каковы важные вещи об этом API?
TradeMessage
public Date getTimestamp() public BigDecimal getAmount() public TradeType getType() public Asset getAsset() public void parse(String csvline)
Это действительно все, что беспокоит звонящих — получение значений и разбор из CSV. Этого мне достаточно, чтобы использовать в тестах и рабочем коде. Здесь мы создали красивый, чистый, простой API, который очень легко объяснить. Нет необходимости в доске, коробках, строках и длинных затяжных объяснениях.
Но как насчет нашего метода parse ()? Разве это не стало слишком сложным? В конце концов, он должен разложить строку, даты разбора, суммы и типы сделок. Это много обязанностей для одного метода. Но как плохо это на самом деле выглядит? Вот мой, в полном объеме:
public void parse(String csvline) throws ParseException { String[] parts = csvline.split(","); setTimestamp(fmt.parse(parts[0])); setTradeType(TradeType.valueOf(parts[1])); setAmount(new BigDecimal(parts[2])); setAsset(Asset.withName(parts[3])); }
Теперь, конечно, к тому времени, когда вы добавите некоторую сложность в реальный мир и улучшите обработку ошибок, это, вероятно, будет больше чем 20 строк.
Но позвольте мне спросить вас, что бы вы предпочли: 12 маленьких классов или 4 маленьких класса? Лучше ли, чтобы сложные операции были разложены на десятки классов или были аккуратно объединены в один метод?