Управление и организация массово увеличенного количества классов после перехода на SOLID?

50

За последние несколько лет мы постепенно переходили на все более и более качественно написанный код, по несколько шагов за раз. Мы наконец начинаем переключаться на что-то, что, по крайней мере, напоминает SOLID, но мы еще не совсем там. После внесения изменений одна из самых больших претензий разработчиков заключается в том, что они не выносят рецензирования и обхода десятков и десятков файлов, где ранее для выполнения каждой задачи требовалось, чтобы разработчик касался 5-10 файлов.

До начала переключения наша архитектура была организована примерно так: (предоставлено, на один или два порядка больше файлов):

Solution
- Business
-- AccountLogic
-- DocumentLogic
-- UsersLogic
- Entities (Database entities)
- Models (Domain Models)
- Repositories
-- AccountRepo
-- DocumentRepo
-- UserRepo
- ViewModels
-- AccountViewModel
-- DocumentViewModel
-- UserViewModel
- UI

В отношении файлов все было невероятно линейным и компактным. Было очевидно много дублирования кода, сильной связи и головной боли, однако каждый мог пройти через это и разобраться. Совершенные новички, люди, которые никогда не открывали Visual Studio, могли понять это всего за несколько недель. Отсутствие общей сложности файлов делает относительно простым для начинающих разработчиков и новых сотрудников, чтобы начать вносить свой вклад без слишком большого времени наращивания. Но это почти то, где все преимущества стиля кода выходят за рамки.

Я искренне одобряю каждую попытку, которую мы предпринимаем, чтобы улучшить нашу кодовую базу, но очень часто можно получить отпор от остальной команды в таких масштабных изменениях парадигмы. Пара самых больших точек преткновения в настоящее время:

  • Модульные тесты
  • Количество классов
  • Сложность экспертной оценки

Модульные тесты были невероятно трудными для продажи, поскольку все они считают, что это пустая трата времени, и что они в состоянии справиться с тестированием своего кода намного быстрее, чем каждый фрагмент в отдельности. Использование модульных тестов в качестве одобрения для SOLID в большинстве случаев было бесполезным и на данный момент стало шуткой.

Количество классов, вероятно, является самым большим препятствием для преодоления. Задачи, которые раньше занимали 5-10 файлов, теперь могут занимать 70-100! Хотя каждый из этих файлов служит определенной цели, объем файлов может быть огромным. Ответом команды в основном были стоны и царапины на голове. Ранее задача могла требовать одного или двух хранилищ, модели или двух, логического уровня и метода контроллера.

Теперь, чтобы создать простое приложение для сохранения файлов, у вас есть класс, чтобы проверить, существует ли файл, класс для записи метаданных, класс для абстрагирования, DateTime.Nowчтобы вы могли вводить время для модульного тестирования, интерфейсы для каждого файла, содержащего логику, файлы содержать модульные тесты для каждого класса, и один или несколько файлов, чтобы добавить все в ваш контейнер DI.

Для малых и средних приложений SOLID очень легко продать. Каждый видит выгоду и простоту обслуживания. Однако они просто не видят выгодного предложения для SOLID в очень крупномасштабных приложениях. Поэтому я пытаюсь найти способы улучшить организацию и управление, чтобы помочь нам справиться с растущими проблемами.


Я подумал, что приведу немного более сильный пример объема файла, основанного на недавно выполненной задаче. Мне было поручено реализовать некоторые функции в одном из наших новых микросервисов для получения запроса на синхронизацию файлов. Когда запрос получен, служба выполняет серию поисков и проверок и, наконец, сохраняет документ на сетевой диск, а также в 2 отдельные таблицы базы данных.

Чтобы сохранить документ на сетевой диск, мне понадобилось несколько конкретных классов:

- IBasePathProvider 
-- string GetBasePath() // returns the network path to store files
-- string GetPatientFolderName() // gets the name of the folder where patient files are stored
- BasePathProvider // provides an implementation of IBasePathProvider
- BasePathProviderTests // ensures we're getting what we expect

- IUniqueFilenameProvider
-- string GetFilename(string path, string fileType);
- UniqueFilenameProvider // performs some filesystem lookups to get a unique filename
- UniqueFilenameProviderTests

- INewGuidProvider // allows me to inject guids to simulate collisions during unit tests
-- Guid NewGuid()
- NewGuidProvider 
- NewGuidProviderTests

- IFileExtensionCombiner // requests may come in a variety of ways, need to ensure extensions are properly appended.
- FileExtensionCombiner
- FileExtensionCombinerTests

- IPatientFileWriter
-- Task SaveFileAsync(string path, byte[] file, string fileType)
-- Task SaveFileAsync(FilePushRequest request) 
- PatientFileWriter
- PatientFileWriterTests

Таким образом, всего 15 классов (исключая POCO и строительные леса), чтобы выполнить довольно простое сохранение. Это число значительно увеличилось, когда мне нужно было создать POCO для представления сущностей в нескольких системах, создать несколько репозиториев для взаимодействия со сторонними системами, несовместимыми с другими нашими ORM, и создать логические методы для обработки сложностей определенных операций.

JD Davis
источник
52
«Задачи, которые раньше занимали 5-10 файлов, теперь могут занимать 70-100!» Как, черт возьми? Это ни в коем случае не нормально. Какие изменения вы делаете, которые требуют изменения такого количества файлов?
Эйфорическая
43
Тот факт, что вам нужно изменить больше файлов для каждой задачи (значительно больше!), Означает, что вы делаете SOLID неправильно. Весь смысл в том, чтобы организовать ваш код (с течением времени) так, чтобы он отражал наблюдаемые шаблоны изменений, делая изменения проще. Каждый принцип в SOLID сопровождается определенными соображениями (когда и почему он должен применяться); похоже, что вы оказались в этой ситуации, применив их вслепую. То же самое с модульным тестированием (TDD); если вы делаете это, не понимая, как делать дизайн / архитектуру, вы будете копать себя в яме.
Филипп Милованович
60
Вы однозначно приняли SOLID как религию, а не прагматичный инструмент, помогающий выполнить работу. Если что-то в SOLID делает больше работы или усложняет задачу, не делайте этого.
Какое имя
25
@Euphoric: проблема может возникнуть в обоих случаях. Я подозреваю, что вы реагируете на вероятность того, что 70-100 классов являются излишними. Но не исключено, что это просто крупный проект, который был уложен в 5-10 файлов (я раньше работал с 20KLOC-файлами ...), а 70-100 - это правильное количество файлов.
Флатер
18
Существует расстройство мышления, которое я называю «объектной болезнью счастья», - это убеждение, что ОО-методы являются самоцелью, а не просто одним из многих возможных методов, позволяющих снизить затраты на работу в большой кодовой базе. У вас есть особенно продвинутая форма «Болезнь твёрдого счастья». ТВЕРДЫЙ не цель. Целью является снижение стоимости поддержки кодовой базы. Оценивайте свои предложения в этом контексте, а не в том, является ли это доктриной SOLID. (То, что ваши предложения также, вероятно, не являются доктриной SOLID, также является хорошим моментом для рассмотрения.)
Эрик Липперт,

Ответы:

104

Теперь, чтобы создать простое приложение для сохранения файлов, у вас есть класс, чтобы проверить, существует ли файл, класс для записи метаданных, класс для абстрагирования DateTime.Now, чтобы вы могли вводить время для модульного тестирования, интерфейсы для каждого файла, содержащего логика, файлы для хранения модульных тестов для каждого класса и один или несколько файлов для добавления всего в ваш DI-контейнер.

Я думаю, что вы неправильно поняли идею единой ответственности. Единственной обязанностью класса может быть «сохранить файл». Чтобы сделать это, он может затем разделить эту ответственность на метод, который проверяет, существует ли файл, метод, который записывает метаданные и т. Д. Каждый из этих методов несет единственную ответственность, которая является частью общей ответственности класса.

Класс абстрагироваться DateTime.Nowзвучит хорошо. Но вам нужен только один из них, и он может быть объединен с другими функциями среды в единый класс, отвечающий за абстрагирование функций среды. Опять одна ответственность с несколькими подответностями.

Вам не нужны «интерфейсы для каждого файла, содержащего логику», вам нужны интерфейсы для классов, которые имеют побочные эффекты, например, те классы, которые читают / записывают в файлы или базы данных; и даже тогда они нужны только для публичных частей этой функциональности. Так, например, в AccountRepo, возможно, вам не понадобятся какие-либо интерфейсы, вам может потребоваться только интерфейс для фактического доступа к базе данных, который вводится в это хранилище.

Модульные тесты были невероятно трудными для продажи, поскольку все они считают, что это пустая трата времени, и что они в состоянии справиться с тестированием своего кода намного быстрее, чем каждый фрагмент в отдельности. Использование модульных тестов в качестве одобрения для SOLID в большинстве случаев было бесполезным и на данный момент стало шуткой.

Это говорит о том, что вы неправильно поняли юнит-тесты. «Единица» модульного теста не является единицей кода. Что даже является единицей кода? Класс? Метод? Переменная? Отдельная машинная инструкция? Нет, «модуль» относится к изолированной единице, то есть коду, который может выполняться изолированно от других частей кода. Простой тест того, является ли автоматизированный тест модульным тестом, состоит в том, можете ли вы запускать его параллельно со всеми другими вашими модульными тестами, не влияя на его результат. Есть еще несколько практических правил в отношении юнит-тестов, но это ваша ключевая мера.

Поэтому, если части вашего кода действительно можно протестировать целиком, не затрагивая другие части, то сделайте это.

Всегда будьте прагматичны и помните, что все это компромисс. Чем больше вы придерживаетесь DRY, тем более тесно связанный код должен стать. Чем больше вы вводите абстракций, тем легче тестировать код, но тем сложнее его понять. Избегайте идеологии и найдите хороший баланс между идеалом и его простотой. В этом заключается сладкое пятно максимальной эффективности как для разработки, так и для обслуживания.

Дэвид Арно
источник
27
Я хотел бы добавить, что подобная головная боль возникает, когда люди пытаются придерживаться слишком часто повторяемой мантры «методы должны делать только одно» и заканчиваются тоннами однострочных методов только потому, что технически это можно превратить в метод ,
Логарр
8
Re «Всегда быть прагматиками и помнить все это компромисс» : ученики дяди Боба не известны для этого (не имеет значения , первоначальное намерение).
Питер Мортенсен
13
Подводя итог первой части, у вас обычно есть кофейный практикант, а не полный набор плагин-перколятор, флип-переключатель, проверка, если сахар нуждается в заправке, открытый холодильник, достать молоко, получить ложки, чашки для выпечки, налейте кофе, добавьте сахар, добавьте молоко, перемешайте чашку, и интерны чашки доставки. ; P
Джастин Тайм 2 Восстановить Монику
12
Основной причиной проблемы OP, по-видимому, является неправильное понимание различий между функциями, которые должны выполнять одну задачу, и классами, которые должны нести одну ответственность.
алефзеро
6
«Правила для руководства мудрецов и послушания дураков». - Дуглас Бадер
Каланус
29

Задачи, которые раньше занимали 5-10 файлов, теперь могут занимать 70-100!

Это противоположно принципу единственной ответственности (SRP). Чтобы добраться до этого момента, вы, должно быть, разделили свои функциональные возможности очень тонко, но это не то, о чем идет SRP - выполнение, которое игнорирует ключевую идею связности .

Согласно ПСП, программное обеспечение должно быть разделено на модули в соответствии с их возможными причинами изменения, чтобы одно изменение конструкции могло быть применено только в одном модуле, не требуя изменений в другом месте. Один «модуль» в этом смысле может соответствовать более чем одному классу, но если одно изменение требует от вас прикоснуться к десяткам файлов, то это либо несколько изменений, либо вы неправильно выполняете SRP.

Боб Мартин, который первоначально сформулировал SRP, написал сообщение в блоге несколько лет назад, чтобы попытаться прояснить ситуацию. В нем подробно обсуждается, что такое «причина для изменения» для целей ПСП. Это стоит прочитать целиком, но среди вещей, заслуживающих особого внимания, стоит упомянуть альтернативную формулировку ПСП:

Соберите вещи, которые меняются по тем же причинам . Отделите те вещи, которые меняются по разным причинам.

(акцент мой). SRP не о разделении вещей на мельчайшие кусочки. Это не хороший дизайн, и ваша команда имеет право сопротивляться. Это делает вашу кодовую базу труднее обновлять и поддерживать. Звучит так, будто вы пытаетесь продать свою команду, исходя из соображений юнит-тестирования, но это будет ставить телегу впереди лошади.

Точно так же принцип разделения интерфейса не следует воспринимать как абсолютный. Это больше не причина для того, чтобы разделить ваш код так хорошо, как SRP, и он, как правило, довольно хорошо согласуется с SRP. То, что интерфейс содержит некоторые методы, которые не используются некоторыми клиентами, не является причиной для его разрыва. Вы снова ищете сплоченность.

Кроме того, я призываю вас не принимать принцип открытого-закрытого или принципа подстановки Лискова в качестве причины для предпочтения иерархии глубокого наследования. Нет более тесной связи, чем подкласс с его суперклассами, и тесная связь является проблемой проектирования. Вместо этого отдавайте предпочтение композиции, а не наследованию, где это имеет смысл. Это уменьшит вашу связь, и, следовательно, число файлов, к которым может понадобиться конкретное изменение, и это будет хорошо согласовываться с инверсией зависимостей.

Джон Боллинджер
источник
1
Я думаю, я просто пытаюсь выяснить, где линия. В недавней задаче мне пришлось выполнить довольно простую операцию, но она была в кодовой базе без каких-либо существенных лесов или функциональности. Таким образом, все, что мне нужно было сделать, было очень простым, но все довольно уникальным и, казалось, не вписывалось в общие классы. В моем случае мне нужно было сохранить документ на сетевом диске и записать его в две отдельные таблицы базы данных. Правила, окружающие каждый шаг, были довольно специфическими. Даже генерация имени файла (простой гид) имела несколько классов, чтобы сделать тестирование более удобным.
Джей Ди Дэвис
3
Опять же, @JDDavis, выбирая несколько классов вместо одного исключительно для целей тестирования, ставит телегу перед лошадью, и это идет прямо против SRP, который требует объединения связанных функций. Я не могу посоветовать вам подробности, но проблема, что отдельные функциональные изменения требуют изменения многих файлов, - это проблема, которую вы должны решать (и пытаться избежать), а не та, которую вы должны пытаться оправдать.
Джон Боллинджер
Соглашаясь, я добавляю это. Цитируя Википедию, «Мартин определяет ответственность как причину для изменения и приходит к выводу, что у класса или модуля должна быть одна и только одна причина для изменения (т.е. переписанная)». и «он недавно заявил:« Этот принцип касается людей ».« На самом деле, я считаю, что это означает, что «ответственность» в ПСП относится к заинтересованным сторонам, а не к функциональности. Класс должен нести ответственность за изменения, которые требуются только одному заинтересованному лицу (человеку, требующему, чтобы вы изменили вашу программу), чтобы вы изменили как можно меньше вещей в ответ на различные заинтересованные стороны, требующие изменений.
Корродия
12

Задачи, которые раньше занимали 5-10 файлов, теперь могут занимать 70-100!

Это ложь. Задачи никогда не занимали всего 5-10 файлов.

Вы не решаете никаких задач с менее чем 10 файлами. Почему? Потому что вы используете C #. C # - язык высокого уровня. Вы используете более 10 файлов только для того, чтобы создать привет мир.

О, конечно, вы их не замечаете, потому что не написали их. Таким образом, вы не смотрите в них. Вы им доверяете.

Проблема не в количестве файлов. Это то, что у тебя сейчас так много всего, что ты не доверяешь.

Так что подумайте, как заставить эти тесты работать так, чтобы после их прохождения вы доверяли этим файлам так же, как вы доверяете файлам в .NET. Это и есть точка юнит-тестирования. Никто не заботится о количестве файлов. Их волнует количество вещей, которым они не могут доверять.

Для малых и средних приложений SOLID очень легко продать. Каждый видит выгоду и простоту обслуживания. Однако они просто не видят выгодного предложения для SOLID в очень крупномасштабных приложениях.

Изменение очень сложно в очень крупномасштабных приложениях, независимо от того, что вы делаете. Лучшая мудрость для применения здесь не от дяди Боба. Это происходит от Майкла Фезерса в его книге «Эффективная работа с устаревшим кодом».

Не начинайте фестиваль переписывания. Старый код представляет трудно завоеванные знания. Отказ от этого, потому что у него есть проблемы и он не выражен в новой и улучшенной парадигме X, просто требует нового набора проблем и никаких труднопреодолимых знаний.

Вместо этого найдите способы сделать тестируемый ваш старый не тестируемый код (устаревшим кодом в Feathers). В этой метафоре код похож на рубашку. Большие части соединяются в естественные швы, которые можно отменить, чтобы отделить код так, как вы бы удалили швы. Сделайте это, чтобы позволить вам присоединить тестовые «рукава», которые позволят вам изолировать остальную часть кода. Теперь, когда вы создаете тестовые рукава, вы уверены в рукавах, потому что вы сделали это с рабочей рубашкой. (Оу, эта метафора начинает болеть).

Эта идея вытекает из предположения о том, что, как и в большинстве магазинов, в рабочем коде есть только самые современные требования. Это позволяет заблокировать это в тестах, которые позволяют вносить изменения в проверенный рабочий код, не теряя при этом ни одного проверенного рабочего статуса. Теперь, когда началась первая волна тестов, вы можете начать вносить изменения, которые сделают «устаревший» (непроверяемый) код тестируемым. Вы можете быть смелыми, потому что тесты швов поддерживают вас, говоря, что это то, что он всегда делал, а новые тесты показывают, что ваш код действительно делает то, что вы думаете.

Какое это имеет отношение к:

Управление и организация массово увеличенного количества классов после перехода на SOLID?

Абстракция.

Ты можешь заставить меня ненавидеть любую кодовую базу плохими абстракциями. Плохая абстракция - это то, что заставляет меня заглядывать внутрь. Не удивляй меня, когда я загляну внутрь. Будь почти таким, как я ожидал.

Дайте мне хорошее имя, удобочитаемые тесты (примеры), которые показывают, как использовать интерфейс, и организуйте его так, чтобы я мог находить вещи, и мне будет все равно, если мы будем использовать 10, 100 или 1000 файлов.

Вы помогаете мне найти вещи с хорошими описательными именами. Положите вещи с хорошими именами в вещи с хорошими именами.

Если вы все сделаете правильно, вы будете абстрагировать файлы туда, где вы можете завершить задачу, в зависимости от 3–5 других файлов. 70-100 файлов все еще там. Но они прячутся за 3 до 5. Это работает, только если вы доверяете 3 до 5, чтобы сделать это правильно.

Так что вам действительно нужен словарь, чтобы найти хорошие имена для всех этих вещей и тестов, которым люди доверяют, чтобы они перестали пробираться через все. Без этого ты бы тоже свела меня с ума.

@Delioth хорошо говорит о росте болей. Когда вы привыкли к посуде, находящейся в шкафу над посудомоечной машиной, нужно привыкнуть к тому, что она находится над барной стойкой. Делает некоторые вещи сложнее. Делает некоторые вещи проще. Но это вызывает всевозможные ночные кошмары, если люди не соглашаются, куда идут блюда. В большой базе кода проблема заключается в том, что вы можете перемещать только некоторые блюда одновременно. Так что теперь у вас есть блюда в двух местах. Это сбивает с толку. Трудно поверить, что блюда находятся там, где они должны быть. Если вы хотите преодолеть это, единственное, что нужно сделать, это продолжать двигать посуду.

Проблема в том, что вы действительно хотели бы знать, стоит ли заказывать еду за барной стойкой, прежде чем пройти через всю эту ерунду. Ну, для этого все, что я могу порекомендовать, это пойти в поход.

Когда вы впервые пробуете новую парадигму, последнее место, где вы должны ее применять, находится в большой базе кода. Это касается каждого члена команды. Никто не должен полагать, что SOLID работает, что ООП работает, или что функциональное программирование работает. У каждого члена команды должен быть шанс поиграть с новой идеей, какой бы она ни была, в игрушечном проекте. Это позволяет им хотя бы увидеть, как это работает. Это позволяет им увидеть, что это не так хорошо. Это позволяет им научиться делать это правильно, прежде чем они устроят большой беспорядок.

Предоставление людям безопасного места для игр поможет им принять новые идеи и даст им уверенность в том, что блюда действительно могут работать в их новом доме.

candied_orange
источник
3
Возможно, стоит упомянуть, что некоторые из проблем этого вопроса, скорее всего, только усиливаются - хотя, да, им может понадобиться сделать 15 файлов для этой цели ... теперь им никогда не придется снова писать GUIDProvider или BasePathProvider или ExtensionProvider и т. д. Это то же самое препятствие, с которым вы сталкиваетесь, когда начинаете новый проект с нуля - куча вспомогательных функций, которые в основном тривиальны, глупы для записи и все же должны быть написаны. Отстой, чтобы построить их, но как только они там, вам не нужно думать о них .... никогда.
Делиот
@Delioth Я невероятно склонен верить, что это так. Ранее, если нам требовалось некоторое подмножество функций (скажем, мы просто хотели, чтобы URL размещался в AppSettings), у нас просто был один массивный класс, который был передан и использован. С новым подходом, нет никакой причины, чтобы AppSettingsпросто обойти URL или путь к файлу.
Джей Ди Дэвис
1
Не начинайте фестиваль переписывания. Старый код представляет собой с трудом завоеванные знания. Отказ от этого, потому что у него есть проблемы и он не выражен в новой и улучшенной парадигме X, просто требует нового набора проблем и никаких труднопреодолимых знаний. Этот. Абсолютно.
Flot2011
10

Похоже, ваш код не очень хорошо отделен и / или размеры ваших задач слишком велики.

Изменения кода должны составлять 5-10 файлов, если вы не выполняете кодмод или масштабный рефакторинг. Если одно изменение затрагивает много файлов, это, вероятно, означает, что ваши изменения каскадируются. Должны помочь некоторые улучшенные абстракции (более простая ответственность, разделение интерфейса, инверсия зависимостей). Также возможно, что вы взяли на себя слишком большую ответственность и могли бы использовать немного больше прагматизма - более короткие и более тонкие иерархии типов. Это также должно облегчить понимание кода, поскольку вам не нужно разбираться в десятках файлов, чтобы знать, что делает код.

Это также может быть признаком того, что ваша работа слишком большая. Вместо «эй, добавьте эту функцию» (которая требует изменений пользовательского интерфейса и API, изменений доступа к данным, изменений безопасности и тестовых изменений и ...) разбить его на более удобные куски. Это становится легче анализировать и легче понять, потому что это требует от вас создания приличных контрактов между битами.

И конечно же, модульные тесты помогают во всем этом. Они заставляют вас делать приличные интерфейсы. Они вынуждают вас сделать свой код достаточно гибким, чтобы вводить биты, необходимые для тестирования (если тестирование сложно, его будет сложно использовать повторно). И они отталкивают людей от чрезмерного проектирования, потому что чем больше вы разрабатываете, тем больше вам нужно тестировать.

Telastyn
источник
2
От 5-10 файлов до 70-100 файлов - это больше, чем гипотетически. Моей последней задачей было создать некоторые функциональные возможности в одном из наших новых микросервисов. Новый сервис должен был получить запрос и сохранить документ. При этом мне нужны были классы для представления пользовательских сущностей в 2 отдельных базах данных и репозиториях для каждой. Repos для представления других таблиц мне нужно было написать. Выделенные классы для обработки файловых данных и генерации имен. И список продолжается. Не говоря уже о том, что каждый класс, содержащий логику, был представлен интерфейсом, чтобы его можно было смоделировать для модульных тестов.
ДжейДи Дэвис
1
Что касается наших старых кодовых баз, они все тесно связаны и невероятно монолитны. При использовании подхода SOLID единственная связь между классами была в случае POCO, все остальное передается через DI и интерфейсы.
ДжейДи Дэвис
3
@JDDavis - подождите, почему один микросервис работает напрямую с несколькими базами данных?
Теластин
1
Это был компромисс с нашим менеджером по разработке. Он массово предпочитает монолитное и процедурное программное обеспечение. Таким образом, наши микроуслуги намного больше макро, чем они должны быть. По мере того, как наша инфраструктура улучшается, постепенно все будет переходить на собственные микросервисы. На данный момент мы несколько придерживаемся подхода удушения, чтобы перенести определенные функции в микросервисы. Поскольку нескольким службам необходим доступ к определенному ресурсу, мы перемещаем их и в их собственные микросервисы.
ДжейДи Дэвис
4

Я хотел бы остановиться на некоторых вещах, уже упомянутых здесь, но больше с точки зрения того, где нарисованы границы объекта. Если вы следуете чему-то похожему на доменно-управляемый дизайн, то ваши объекты, вероятно, будут представлять аспекты вашего бизнеса. Customerи Order, например, будут объекты. Теперь, если бы я должен был сделать предположение на основе имен классов, которые вы указали в качестве отправной точки, у вашего AccountLogicкласса был бы код, который будет работать для любой учетной записи. В ОО, однако, каждый класс должен иметь контекст и идентичность. Вы не должны получать Accountобъект, а затем передавать его в AccountLogicкласс и заставлять этот класс вносить изменения в Accountобъект. Это то, что называется анемичной моделью и не очень хорошо представляет ОО. Вместо этого вашAccountкласс должен иметь поведение, такое как Account.Close()или Account.UpdateEmail(), и такое поведение будет влиять только на этот экземпляр учетной записи.

Теперь, КАК эти поведения обрабатываются, можно (и во многих случаях следует) выгружать их в зависимости, представленные абстракциями (то есть интерфейсами). Account.UpdateEmailНапример, может потребоваться обновить базу данных или файл, или отправить сообщение на служебную шину и т. д. И это может измениться в будущем. Таким образом, ваш Accountкласс может зависеть, например, IEmailUpdateот AccountRepositoryобъекта , который может быть одним из многих интерфейсов, реализованных объектом. Вы не захотите передавать объекту целый IAccountRepositoryинтерфейс, Accountпотому что он, вероятно, будет делать слишком много, например, искать и находить другие (любые) учетные записи, к которым вы, возможно, не хотите, чтобы Accountобъект имел доступ, но даже при том, что AccountRepositoryмогут реализовать оба IAccountRepositoryи IEmailUpdateинтерфейсы,AccountОбъект будет иметь доступ только к тем маленьким частям, которые ему нужны. Это помогает вам поддерживать принцип разделения интерфейса .

Реально, как уже упоминали другие люди, если вы имеете дело со взрывом классов, есть вероятность, что вы используете принцип SOLID (и, как следствие, OO) неправильно. SOLID должен помочь вам упростить ваш код, а не усложнить его. Но требуется время, чтобы действительно понять, что означают такие вещи, как SRP. Однако более важная вещь заключается в том, что работа SOLID будет сильно зависеть от вашего домена и ограниченного контекста (еще один термин DDD). Там нет серебряной пули или один размер подходит всем.

Еще одна вещь, на которую я хотел бы обратить внимание людям, с которыми я работаю: опять же, объект ООП должен иметь поведение и фактически определяется его поведением, а не данными. Если ваш объект не имеет ничего, кроме свойств и полей, он все еще имеет поведение, хотя, вероятно, не такое, как вы предполагали. Публично доступное для записи / устанавливаемое свойство без какой-либо другой логики набора подразумевает, что поведение его содержащего класса заключается в том, что кому угодно в любом месте по любой причине и в любое время разрешено изменять значение этого свойства без какой-либо необходимой бизнес-логики или проверки между ними. Это обычно не то поведение, которое люди намереваются, но если у вас анемичная модель, это, как правило, поведение, которое ваши классы объявляют всем, кто их использует.

Лао
источник
2

Таким образом, всего 15 классов (исключая POCO и строительные леса), чтобы выполнить довольно простое сохранение.

Это безумие ... но эти занятия звучат как то, что я бы написал сам. Итак, давайте посмотрим на них. Давайте пока проигнорируем интерфейсы и тесты.

  • BasePathProvider- ИМХО любой нетривиальный проект, работающий с файлами, нуждается в этом. Итак, я предполагаю, что такая вещь уже есть, и вы можете использовать ее как есть.
  • UniqueFilenameProvider - Конечно, у вас уже есть, не так ли?
  • NewGuidProvider - Тот же случай, если только вы не начинаете использовать GUID.
  • FileExtensionCombiner - тот же случай.
  • PatientFileWriter - Думаю, это основной класс для текущей задачи.

Для меня это выглядит хорошо: вам нужно написать один новый класс, который нуждается в четырех вспомогательных классах. Все четыре вспомогательных класса звучат довольно многократно, поэтому я готов поспорить, что они уже где-то в вашей кодовой базе. В противном случае это либо неудача (вы действительно человек в вашей команде, который пишет файлы и использует GUID ???), либо какая-то другая проблема.


Что касается тестовых классов, обязательно, когда вы создаете новый класс или обновляете его, он должен быть протестирован. Таким образом, написание пяти классов также означает написание пяти тестовых классов. Но это не делает дизайн более сложным:

  • Вы никогда не будете использовать тестовые классы в другом месте, так как они будут выполняться автоматически, и все.
  • Вы хотите когда-нибудь взглянуть на них снова, если только вы не обновите тестируемые классы или если вы не используете их в качестве документации (в идеале тесты ясно показывают, как должен использоваться класс).

Что касается интерфейсов, они нужны только тогда, когда ваша инфраструктура DI или среда тестирования не могут работать с классами. Вы можете увидеть их как пошлину за несовершенные инструменты. Или вы можете рассматривать их как полезную абстракцию, позволяющую вам забыть, что есть более сложные вещи - чтение источника интерфейса занимает гораздо меньше времени, чем чтение источника его реализации.

maaartinus
источник
Я благодарен за эту точку зрения. В этом конкретном случае я писал функциональность в довольно новый микросервис. К сожалению, даже в нашей основной кодовой базе, хотя у нас есть некоторые из вышеперечисленных в использовании, ни один из них на самом деле не может быть использован повторно. Все, что должно быть повторно использовано, попало в какой-то статический класс или просто скопировано и вставлено в код. Я думаю, что я все еще немного далеко, но я согласен, что не все должно быть полностью рассечено и отделено.
Джей Ди Дэвис
@JDDavis Я пытался написать что-то отличное от других ответов (с которыми я в основном согласен). Всякий раз, когда вы копируете и вставляете что-то, вы предотвращаете повторное использование, поскольку вместо того, чтобы обобщать что-либо, вы создаете другой фрагмент кода, который нельзя использовать повторно, что заставит вас копировать и вставлять больше за один день. ИМХО это второй по величине грех, только после слепого следования правилам. Вы должны найти свое место, где следование правилам делает вас более продуктивными (особенно в отношении будущих изменений), и иногда их нарушение помогает в тех случаях, когда усилия не будут неуместными. Это все относительно.
Маартин
@JDDavis И все зависит от качества ваших инструментов. Пример: есть люди, утверждающие, что DI предприимчив и сложен, в то время как я утверждаю, что это в основном бесплатно . +++Что касается нарушения правил: есть четыре класса, которые мне нужны в местах, где я могу внедрить их только после серьезного рефакторинга, делающего код более уродливым (по крайней мере, для моих глаз), поэтому я решил превратить их в синглтоны (лучший программист) может найти лучший способ, но я доволен этим: количество этих синглетонов не менялось с возрастов).
Маартин
Этот ответ в значительной степени отражает то, о чем я думал, когда ФП добавил пример к вопросу. @JDDavis Позвольте мне добавить, что вы можете сохранить некоторые стандартные коды / классы, используя функциональные инструменты для простых случаев. Например, провайдер графического интерфейса - вместо того, чтобы вводить в новый интерфейс новый класс для этого, почему бы просто не использовать Func<Guid>для этого и не внедрить анонимный метод, как ()=>Guid.NewGuid()в конструктор? И нет необходимости тестировать эту функцию .Net Framework, это то, что Microsoft сделала для вас. В общей сложности это сэкономит вам 4 класса.
Док Браун
... и вам следует проверить, можно ли упростить другие представленные вами случаи таким же образом (вероятно, не все из них).
Док Браун
2

В зависимости от абстракций создание классов с одной ответственностью и написание модульных тестов не являются точными науками. Во время обучения совершенно нормально отклоняться слишком далеко в одном направлении, идти к экстриму, а затем находить норму, которая имеет смысл. Звучит так, будто ваш маятник качнулся слишком далеко и даже может застрять.

Вот где я подозреваю, что это сходит с рельсов:

Модульные тесты были невероятно трудными для продажи, поскольку все они считают, что это пустая трата времени, и что они в состоянии справиться с тестированием своего кода намного быстрее, чем каждый фрагмент в отдельности. Использование модульных тестов в качестве одобрения для SOLID в большинстве случаев было бесполезным и на данный момент стало шуткой.

Одно из преимуществ большинства принципов SOLID (безусловно, не единственное) заключается в том, что оно облегчает написание модульных тестов для нашего кода. Если класс зависит от абстракций, мы можем высмеивать абстракции. Абстракции, которые разделены, легче издеваться. Если класс делает что-то одно, он, вероятно, будет иметь меньшую сложность, что означает, что его легче узнать и проверить все возможные пути.

Если ваша команда не пишет модульные тесты, происходят две взаимосвязанные вещи:

Во-первых, они проделывают большую дополнительную работу по созданию всех этих интерфейсов и классов, не осознавая всех преимуществ. Требуется немного времени и практики, чтобы понять, как написание модульных тестов облегчает нашу жизнь. Есть причины, по которым люди, которые учатся писать модульные тесты, придерживаются его, но вы должны упорствовать достаточно долго, чтобы открыть их для себя. Если ваша команда не пытается этого сделать, то они почувствуют, что остальная часть дополнительной работы, которую они выполняют, бесполезна.

Например, что происходит, когда им нужно провести рефакторинг? Если у них есть сто маленьких классов, но нет тестов, чтобы сказать им, будут ли их изменения работать, эти дополнительные классы и интерфейсы будут казаться бременем, а не улучшением.

Во-вторых, написание модульных тестов может помочь вам понять, какая абстракция действительно нужна вашему коду. Как я уже сказал, это не наука. Мы начинаем плохо, поворачиваемся повсюду и поправляемся. Модульные тесты имеют своеобразный способ дополнения SOLID. Как вы знаете, когда вам нужно добавить абстракцию или разбить что-то на части? Другими словами, как вы узнаете, когда вы «достаточно твердый»? Часто ответ - когда ты не можешь что-то проверить.

Возможно, ваш код будет тестируемым без создания как можно большего количества крошечных абстракций и классов. Но если вы не пишете тесты, как вы можете сказать? Как далеко мы идем? Мы можем стать одержимыми разрушением вещей все меньше и меньше. Это кроличья нора. Возможность писать тесты для нашего кода помогает нам увидеть, когда мы достигли своей цели, чтобы мы могли перестать одерживаться, двигаться дальше и получать удовольствие от написания большего количества кода.

Модульные тесты - это не серебряная пуля, которая решает все проблемы, но они действительно потрясающая пуля, которая делает жизнь разработчиков лучше. Мы не идеальны, как и наши тесты. Но тесты дают нам уверенность. Мы ожидаем, что наш код будет правильным, и мы удивляемся, когда он ошибается, а не наоборот. Мы не идеальны, как и наши тесты. Но когда наш код протестирован, у нас появляется уверенность. Мы реже ломаем ногти при развертывании нашего кода и задаемся вопросом, что сломается в этот раз и будет ли это нашей ошибкой.

Вдобавок ко всему, как только мы это освоим, написание модульных тестов делает разработку кода быстрее, а не медленнее. Мы тратим меньше времени на пересмотр старого кода или отладку, чтобы найти проблемы, похожие на иголки в стоге сена.

Ошибки уменьшаются, мы делаем больше, и мы заменяем беспокойство уверенностью. Это не прихоть или змеиное масло. Это реально. Многие разработчики подтвердят это. Если ваша команда не испытала этого, ей нужно пройти через эту кривую обучения и преодолеть горб. Дайте ему шанс, понимая, что они не получат результаты мгновенно. Но когда это случится, они будут рады, что сделали, и никогда не оглядываются назад. (Или они станут изолированными париями и напишут гневные посты в блоге о том, что модульные тесты и большинство других накопленных знаний в области программирования - пустая трата времени.)

После внесения изменений одна из самых больших претензий разработчиков заключается в том, что они не выносят рецензирования и обхода десятков и десятков файлов, где ранее для выполнения каждой задачи требовалось, чтобы разработчик касался 5-10 файлов.

Рецензирование становится намного проще, когда все модульные тесты пройдены, и большая часть этого обзора просто удостоверяется, что тесты значимы.

Скотт Ханнен
источник