SOLID Принципы и структура кода

150

На недавнем собеседовании я не смог ответить на вопрос о SOLID - помимо предоставления основного значения различных принципов. Это действительно беспокоит меня. Я потратил пару дней на то, чтобы покопаться и еще не нашел удовлетворительного резюме.

Вопрос интервью был:

Если бы вы посмотрели на проект .Net, который, как я вам сказал, строго следовал принципам SOLID, что бы вы ожидали увидеть в плане проекта и структуры кода?

Я немного барахтался, на самом деле не ответил на вопрос, а потом разбомбил.

Как я мог лучше справиться с этим вопросом?

S-Unit
источник
3
Мне было интересно, что не ясно в вики-странице для SOLID
BЈовић
Расширяемые абстрактные строительные блоки.
Руонг
Следуя SOLID Принципам объектно-ориентированного проектирования, ваши классы, как правило, будут небольшими, хорошо продуманными и легко тестируемыми. Источник: docs.asp.net/en/latest/fundamentals/...
WhileTrueSleep

Ответы:

188

S = принцип единой ответственности

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

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

O = открытый / закрытый принцип

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

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

L = принцип подстановки Лискова

Это связано со способностью рассматривать подтипы как их родительский тип. Это происходит из коробки в C #, если вы реализуете правильную иерархию унаследованных объектов.

Я ожидаю увидеть код, обрабатывающий общие объекты как их базовый тип и вызывающий методы в базовых / абстрактных классах, а не создающий и работающий с самими подтипами.

I = принцип разделения интерфейса

Это похоже на SRP. По сути, вы определяете меньшие подмножества функциональных возможностей как интерфейсы и работаете с ними, чтобы поддерживать развязанность вашей системы (например, FileManagerможет иметь единственную ответственность при работе с Файловым вводом-выводом, но она может реализовывать IFileReaderи IFileWriterсодержать конкретные определения методов для чтения и написание файлов).

D = принцип обращения зависимостей.

Опять же, это относится к разделению системы. Возможно, вы будете в поисках использования библиотеки .NET Dependency Injection, используемой в решении, таком как Unityили, Ninjectили в системе ServiceLocator, такой как AutoFacServiceLocator.

Эойн Кэмпбелл
источник
36
Я видел множество нарушений LSP в C #, каждый раз, когда кто-то решает, что его конкретный подтип специализирован, и поэтому ему не нужно реализовывать часть интерфейса, а просто выдает исключение для этого элемента ... Это обычный младший подход к решению проблемы непонимания реализации интерфейса и дизайна
Джимми Хоффа
2
@JimmyHoffa Это одна из главных причин, по которой я настаиваю на использовании кодовых контрактов; Прохождение мыслительного процесса разработки контрактов помогает многим избавить людей от этой вредной привычки.
Энди
12
Мне не нравится «LSP выходит из коробки в C #» и приравнивает DIP к практике внедрения зависимостей.
Эйфорическое
3
+1, но инверсия зависимости <> инъекция зависимости. Они хорошо играют вместе, но инверсия зависимостей - это гораздо больше, чем просто внедрение зависимостей. Ссылка: DIP в дикой природе
Марьян Венема
3
@Andy: то, что также помогает, это юнит-тесты, определенные на интерфейсах, с которыми тестируются все реализаторы (любой класс, который может / создается).
Марьян Венема
17

Множество небольших классов и интерфейсов с внедрением зависимостей повсюду. Вероятно, в большом проекте вы также использовали бы IoC-инфраструктуру, чтобы помочь вам построить и управлять временем жизни всех этих небольших объектов. См. Https://stackoverflow.com/questions/21288/which-net-dependency-injection-frameworks-are-worth-looking-into

Обратите внимание, что большой проект .NET, который СТРОГО следует принципам SOLID, не обязательно означает хорошую кодовую базу для работы со всеми. В зависимости от того, кем был интервьюер, он / она, возможно, хотел, чтобы вы показали, что вы понимаете, что значит SOLID, и / или проверьте, насколько догматично вы следуете принципам дизайна.

Видите ли, чтобы быть твердым, вы должны следовать:

S Ingle принцип ответственности, так что вы будете иметь много небольших классов каждый из них делает одно только

O ручка-замкнутый принцип, который в .NET обычно реализуется с инъекцией зависимости, что также требует ввода и ниже D ...

L принцип замещения Исек вероятно inmpossible объяснить в C # с однострочником. К счастью, есть другие вопросы, касающиеся этого, например, https://stackoverflow.com/questions/4428725/can-you-explain-liskov-substitution-principle-with-a-good-c-sharp-example

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

D ependency принцип инверсии классы высокого уровня не должны зависеть от классов низкого уровня, и должен зависеть от абстракций.

Паоло Фалабелла
источник
SRP не означает «делать только одну вещь».
Роберт Харви
13

Некоторые основные вещи, которые я ожидал бы увидеть в кодовой базе магазина, который поддерживал SOLID в своей повседневной работе:

  • Множество небольших файлов кода - с одним классом на файл в качестве наилучшей практики в .NET и принципом единой ответственности, поощряющим небольшие модульные структуры классов, я ожидаю увидеть множество файлов, каждый из которых содержит один маленький, сфокусированный класс.
  • Множество паттернов Adapter и Composite - я ожидаю использовать множество паттернов Adapter (класс, реализующий один интерфейс путем «перехода» к функциональности другого интерфейса), чтобы упростить включение зависимости, разработанной для одной цели, в слегка различные места, где его функциональность также необходима. Обновление, столь же простое, как замена логгера консоли файловым логгером, нарушит LSP / ISP / DIP, если интерфейс обновляется, чтобы предоставить средство для указания имени файла для использования; вместо этого класс файлового регистратора будет предоставлять дополнительных членов, а затем Adapter сделает файловый регистратор похожим на консольный регистратор, скрывая новый материал, поэтому только объект, соединяющий все это вместе, должен знать разницу.

    Точно так же, когда классу необходимо добавить зависимость аналогичного интерфейса как существующего, чтобы избежать изменения объекта (OCP), обычный ответ - реализовать шаблон Composite / Strategy (класс, реализующий интерфейс зависимостей и потребляющий несколько других реализации этого интерфейса, с различной степенью логики, позволяющей классу передавать вызов одной, нескольким или всем реализациям).

  • Множество интерфейсов и ABC - DIP обязательно требует наличия абстракций, и ISP рекомендует, чтобы они были узко ограничены. Следовательно, интерфейсы и абстрактные базовые классы являются правилом, и вам понадобится много их, чтобы покрыть функциональность разделяемой зависимости вашей кодовой базы. В то время как строгий SOLID потребовал бы внедрения всего , очевидно, что вам нужно где-то создавать, и поэтому, если форма GUI когда-либо создается только как дочерняя форма одной родительской формы, выполняя какое-то действие с указанным родителем, у меня нет никаких сомнений в обновлении дочерней формы. из кода непосредственно в родительском. Я просто обычно делаю этот код своим собственным методом, поэтому, если два действия одной формы когда-либо открывают окно, я просто вызываю метод.
  • Многие проекты. Смысл всего этого заключается в ограничении масштабов изменений. Изменения включают в себя необходимость перекомпиляции (это более тривиальное занятие, но оно все еще важно во многих операциях, критичных к процессору и пропускной способности, таких как развертывание обновлений в мобильной среде). Если один файл в проекте должен быть перестроен, все файлы делают. Это означает, что если вы размещаете интерфейсы в тех же библиотеках, что и их реализации, вы упускаете суть; вам придется перекомпилировать все использования, если вы измените реализацию интерфейса, потому что вы также будете перекомпилировать само определение интерфейса, требуя, чтобы использования указывали на новое местоположение в полученном двоичном файле. Поэтому, сохраняя интерфейсы отдельно от использования и реализации, в то же время дополнительно разделяя их по общей области использования, является типичной наилучшей практикой.
  • Большое внимание уделяется терминологии «Банды четырех». Шаблоны проектирования, определенные в книге « Шаблоны проектирования» 1994 года, подчеркивают модульный дизайн кода размером с кусочек, который стремится создать SOLID. Например, Принцип инверсии зависимостей и Открытый / Закрытый принципы лежат в основе большинства выявленных закономерностей в этой книге. Таким образом, я ожидаю, что магазин, который строго придерживается принципов SOLID, также охватит терминологию в книге «Банды четырех» и будет называть классы в соответствии с их функциями в соответствии с этими функциями, такими как «AbcFactory», «XyzRepository», «DefToXyzAdapter». "," A1Command "и т. Д.
  • Универсальный репозиторий - в соответствии с общепринятыми ISP, DIP и SRP, репозиторий почти повсеместно используется в дизайне SOLID, так как он позволяет потреблять код для запроса классов данных абстрагированным образом, не требуя специальных знаний о механизме поиска / сохранения, и он помещает код, который делает это, в одно место, в отличие от шаблона DAO (в котором, если бы у вас был, например, класс данных Invoice, у вас также был бы InvoiceDAO, который создавал гидратированные объекты этого типа, и так далее для все объекты данных / таблицы в кодовой базе / схеме).
  • Контейнер IoC - я не решаюсь добавить этот, так как на самом деле я не использую инфраструктуру IoC для выполнения большей части моего внедрения зависимостей. Он быстро становится анти-паттерном Божественного Объекта - бросать все в контейнер, встряхивать его и выливать нужную вам вертикально-гидратированную зависимость с помощью инъекционного фабричного метода. Звучит великолепно, пока вы не поймете, что структура становится довольно монолитной, и проект с регистрационной информацией, если он «беглый», теперь должен знать все обо всем в вашем решении. Это много причин для изменения. Если он не беглый (регистрация с поздним связыванием с использованием конфигурационных файлов), то ключевой элемент вашей программы опирается на «волшебные строки», что может быть совершенно иным червем.
Keiths
источник
1
почему негативы?
KeithS
Я думаю, что это хороший ответ. Вместо того, чтобы быть похожими на многие сообщения в блоге об этих терминах, вы перечислили примеры и объяснения, которые показывают их использование и ценность
Crowie
10

Отвлеките их обсуждением Джона Скита о том, что «О» в SOLID «бесполезно и плохо понято», и заставьте их поговорить об «защищенном варианте» Алистера Кокберна и «замысле Джоша Блоха о наследовании или запретите его».

Краткое резюме статьи Скита (хотя я бы не рекомендовал оставлять его имя без чтения оригинального сообщения в блоге!):

  • Большинство людей не знают, что означают «открытые» и «закрытые» в «открытом-закрытом принципе», даже если они думают, что знают.
  • Общие интерпретации включают в себя:
    • что модули всегда должны расширяться за счет наследования реализации, или
    • что исходный код исходного модуля никогда не может быть изменен.
  • Основное намерение OCP, и оригинальная формулировка его Бертраном Мейером, прекрасно:
    • что модули должны иметь четко определенные интерфейсы (не обязательно в техническом смысле «интерфейс»), от которых могут зависеть их клиенты, но
    • должна быть возможность расширить то, что они могут делать, не нарушая эти интерфейсы.
  • Но слова «открытый» и «закрытый» только запутывают проблему, даже если они действительно дают хорошую произносимую аббревиатуру.

ФП спросил: «Как я мог лучше справиться с этим вопросом?» Будучи старшим инженером, проводящим собеседование, я был бы неизмеримо больше заинтересован в кандидате, который может разумно говорить о плюсах и минусах различных стилей разработки кода, чем о ком-то, кто может вычеркнуть список ключевых моментов.

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

Дэвид Моулз
источник
6

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

Итак, вы ожидаете увидеть следующее:

  • Классы имеют одну ответственность (например, класс доступа к данным для клиентов будет получать данные о клиентах только из базы данных клиентов).
  • Классы легко расширяются, не влияя на существующее поведение. Мне не нужно изменять свойства или другие методы, чтобы добавить дополнительную функциональность.
  • Производные классы могут быть заменены базовыми классами, и функциям, которые используют эти базовые классы, не нужно разворачивать базовый класс для более конкретного типа, чтобы обрабатывать их.
  • Интерфейсы маленькие и понятные. Если класс использует интерфейс, он не должен зависеть от нескольких методов для выполнения задачи.
  • Код достаточно абстрагирован, чтобы реализация высокого уровня не зависела от конкретной реализации низкого уровня. Я должен иметь возможность переключать низкоуровневую реализацию без воздействия на высокоуровневый код. Например, я могу переключить свой уровень доступа к данным SQL на уровень, основанный на Web-сервисе, не влияя на остальную часть моего приложения.
villecoder
источник
4

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

Принципы SOLID действительно управляют классами и интерфейсами и тем, как они связаны друг с другом.

Этот вопрос действительно имеет отношение к файлам, а не к классам.

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

Роберт Мартин обсуждает эту тему в области C ++ в разделе «Проектирование объектно-ориентированных приложений C ++ с использованием метода Booch» (см. Разделы «Сплоченность, замыкание и повторное использование») и в « Чистом коде» .

Дж. Полфер
источник
.NET-кодеры IME обычно следуют правилу «1 класс на файл», а также отражают структуры папок / пространств имен; Visual Studio IDE поддерживает обе практики, и различные плагины, такие как ReSharper, могут применять их. Итак, я ожидаю увидеть структуру проекта / файла, отражающую структуру класса / интерфейса.
KeithS