Например, у вас есть приложение с широко используемым классом, которое называется User
. Этот класс предоставляет всю информацию о пользователе, его идентификаторе, имени, уровнях доступа к каждому модулю, часовом поясе и т. Д.
Очевидно, что пользовательские данные широко используются во всей системе, но по какой-то причине система настроена таким образом, чтобы вместо передачи этого пользовательского объекта в классы, которые зависят от него, мы просто передавали отдельные свойства из него.
Класс, который требует идентификатор пользователя, просто потребует GUID userId
в качестве параметра, иногда нам также может понадобиться имя пользователя, чтобы оно передавалось как отдельный параметр. В некоторых случаях это передается отдельным методам, поэтому значения вообще не хранятся на уровне класса.
Каждый раз, когда мне нужен доступ к другой части информации из класса User, я должен вносить изменения, добавляя параметры, и там, где добавление новой перегрузки не подходит, я должен также изменять каждую ссылку на метод или конструктор класса.
Пользователь - только один пример. Это широко практикуется в нашем коде.
Правильно ли я считаю, что это нарушение принципа Open / Closed? Не просто акт изменения существующих классов, но в первую очередь их настройка, чтобы в будущем потребовались широкомасштабные изменения?
Если бы мы только передали User
объект, я мог бы внести небольшое изменение в класс, с которым я работаю. Если мне нужно добавить параметр, мне может потребоваться внести десятки изменений в ссылки на класс.
Какие-либо другие принципы нарушаются этой практикой? Возможно, инверсия зависимостей? Хотя мы не ссылаемся на абстракцию, существует только один вид пользователей, поэтому нет особой необходимости иметь пользовательский интерфейс.
Существуют ли другие, не твердые принципы, которые нарушаются, такие как базовые принципы защитного программирования?
Должен ли мой конструктор выглядеть так:
MyConstructor(GUID userid, String username)
Или это:
MyConstructor(User theUser)
Редактировать сообщение:
Было предложено ответить на этот вопрос в «Pass ID или Object?». Это не отвечает на вопрос о том, как решение пойти в ту или иную сторону влияет на попытку следовать принципам SOLID, что лежит в основе этого вопроса.
I
дюймаSOLID
?MyConstructor
в основном говорит сейчас "мне нужноGuid
иstring
". Так почему бы не иметь интерфейс, обеспечивающий aGuid
и astring
, позволитьUser
реализовать этот интерфейс и позволитьMyConstructor
зависеть от экземпляра, реализующего этот интерфейс? А если нужноMyConstructor
поменять, поменяйте интерфейс. - Это очень помогло мне придумать интерфейсы, которые «принадлежат» потребителю, а не поставщику . Поэтому подумайте: « Мне как потребителю нужно то, что делает то-то и то-то», а не как поставщику, я могу делать то-то и то-то ».Ответы:
Нет абсолютно ничего плохого в передаче целого
User
объекта в качестве параметра. Фактически, это может помочь прояснить ваш код и сделать более понятным для программистов, что метод берет, если сигнатура метода требует aUser
.Передавать простые типы данных приятно, пока они не означают что-то другое, чем они есть. Рассмотрим этот пример:
И пример использования:
Вы можете определить дефект? Компилятор не может. Передаваемый «идентификатор пользователя» является целым числом. Мы называем переменную,
user
но инициализируем ее значение изblogPostRepository
объекта, который, по-видимому, возвращаетBlogPost
объекты, а неUser
объекты - но код компилируется, и в результате вы получаете неожиданную ошибку времени выполнения.Теперь рассмотрим этот измененный пример:
Возможно,
Bar
метод использует только «идентификатор пользователя», но для подписи метода требуетсяUser
объект. Теперь давайте вернемся к тому же примеру использования, что и раньше, но изменим его, чтобы передать всего «пользователя»:Теперь у нас есть ошибка компилятора.
blogPostRepository.Find
Метод возвращаетBlogPost
объект, который мы называем хитро «пользователь». Затем мы передаем этого «пользователя» вBar
метод и быстро получаем ошибку компилятора, потому что мы не можем передать aBlogPost
в метод, который принимает aUser
.Система типов языка используется для быстрого написания правильного кода и выявления дефектов во время компиляции, а не во время выполнения.
Действительно, необходимость рефакторинга большого количества кода, потому что пользовательская информация изменяется, является просто симптомом других проблем. Передавая весь
User
объект, вы получаете преимущества, описанные выше, в дополнение к преимуществам отсутствия необходимости рефакторинга всех сигнатур методов, которые принимают пользовательскую информацию, когда что-то вUser
классе изменяется.источник
Нет, это не нарушение этого принципа. Этот принцип связан с тем, чтобы не изменяться таким
User
образом, чтобы это влияло на другие части кода, которые его используют. Ваши измененияUser
могут быть таким нарушением, но это не связано.Нет. То, что вы описываете - вводите только необходимые части пользовательского объекта в каждый метод - наоборот: это чистая инверсия зависимостей.
Нет. Этот подход является вполне допустимым способом кодирования. Это не нарушает такие принципы.
Но инверсия зависимости - это только принцип; это не нерушимый закон. И чистый DI может добавить сложности в систему. Если вы обнаружите, что только введение нужных пользовательских значений в методы, а не передача всего пользовательского объекта в метод или конструктор, создает проблемы, то не делайте так. Это все о достижении баланса между принципами и прагматизмом.
Чтобы ответить на ваш комментарий:
Отчасти проблема заключается в том, что вам явно не нравится этот подход, согласно комментарию «излишне [пройти] ...». И это достаточно справедливо; здесь нет правильного ответа. Если вы находите это обременительным, не делайте так.
Однако, что касается принципа открытого / закрытого, если вы строго следуете этому, то «... изменить все ссылки на все пять из этих существующих методов ...» означает, что эти методы были изменены, когда они должны быть закрыто для модификации. В действительности, хотя принцип open / closed имеет смысл для общедоступных API, но не имеет особого смысла для внутренних компонентов приложения.
Но тогда вы забредаете на территорию ЯГНИ, и это все равно будет ортогонально принципу. Если у вас есть метод,
Foo
который принимает имя пользователя, а затем вы также хотитеFoo
указать дату рождения, следуя этому принципу, вы добавляете новый метод;Foo
остается неизменной. Опять же, это хорошая практика для публичных API, но это нонсенс для внутреннего кода.Как упоминалось ранее, речь идет о балансе и здравом смысле для любой конкретной ситуации. Если эти параметры часто меняются, то да, используйте
User
напрямую. Это избавит вас от масштабных изменений, которые вы описываете. Но если они не часто меняются, то хороший подход - и передача только того, что нужно.источник
User
экземпляр и затем запрашиваете этот объект, чтобы получить параметр, то вы только частично инвертируете зависимости; все еще есть некоторые вопросы. Истинная инверсия зависимостей на 100% «говори, не спрашивай». Но это приходит по цене сложности.Да, изменение существующей функции является нарушением принципа Open / Closed. Вы модифицируете что-то, что должно быть закрыто для модификации из-за изменения требований. Лучшим дизайном (чтобы не измениться при изменении требований) было бы передать пользователю то, что должно работать на пользователей.
Но это может идти вразрез с интерфейсом сегрегация Принципа, так как вы могли бы проходить вдоль пути больше информации , чем потребности функции , чтобы сделать свою работу.
Так что, как и с большинством вещей - это зависит .
Используя только имя пользователя, мы сделаем функцию более гибкой, работая с именами пользователей независимо от того, откуда они берутся, и без необходимости создавать полностью функционирующий объект User. Это обеспечивает устойчивость к изменениям, если вы думаете, что источник данных изменится.
Использование всего Пользователя делает его более понятным об использовании и заключает более надежный договор со своими абонентами. Это обеспечивает устойчивость к изменениям, если вы считаете, что потребуется больше пользователей.
источник
User.find()
. На самом деле даже не должно бытьUser.find
. Поиск пользователя никогда не должен быть обязанностьюUser
.User
с функцией. Может быть, это имеет смысл. Но, возможно, функция должна заботиться только об имени пользователя - и передавать такие вещи, как дата присоединения пользователя или адрес, является неправильным.User
не должны иметь классы. Там может бытьUserRepository
или похожий, который имеет дело с такими вещами.Этот дизайн следует шаблону объекта параметров . Это решает проблемы, возникающие из-за наличия множества параметров в сигнатуре метода.
Нет. Применение этого шаблона активирует принцип открытия / закрытия (OCP). Например, производные классы
User
могут быть предоставлены в качестве параметра, который вызывает другое поведение в потребляющем классе.Это может случиться Позвольте мне объяснить на основе твердых принципов.
Принцип единой ответственности (SRP) может быть нарушен, если он имеет дизайн, как вы объяснили:
Проблема со всей информацией . Если у
User
класса много свойств, он становится огромным объектом передачи данных, который транспортирует несвязанную информацию с точки зрения потребляющих классов. Пример: с точки зрения класса-потребителяUserAuthentication
это свойствоUser.Id
иUser.Name
уместно, но нетUser.Timezone
.Принцип интерфейса сегрегации (ISP) также нарушается с рассуждениями , но похожими добавляет другую перспективу. Пример: предположим, что потребляющий класс
UserManagement
требует, чтобы свойствоUser.Name
было разделено до,User.LastName
иUser.FirstName
класс такжеUserAuthentication
должен быть изменен для этого.К счастью, интернет-провайдер также дает вам выход из проблемы: обычно такие объекты параметров или объекты транспорта данных начинаются с малого и растут со временем. Если это становится громоздким, рассмотрите следующий подход: представьте интерфейсы, адаптированные к потребностям потребляющих классов. Пример: представьте интерфейсы и позвольте
User
классу наследовать от него:Каждый интерфейс должен предоставлять подмножество связанных свойств
User
класса, необходимых для потребляющего класса, чтобы он полностью выполнял свою работу. Ищите кластеры свойств. Попробуйте повторно использовать интерфейсы. В случае использования классаUserAuthentication
потребленияIUserAuthenticationInfo
вместоUser
. Затем, если возможно, разбейтеUser
класс на несколько конкретных классов, используя интерфейсы как «трафарет».источник
Столкнувшись с этой проблемой в моем собственном коде, я пришел к выводу, что базовые классовые модели / объекты являются ответом.
Типичным примером может служить шаблон хранилища. Часто при запросах к базе данных через репозитории многие методы в репозитории принимают множество одинаковых параметров.
Мои эмпирические правила для репозиториев:
Если более чем один метод принимает одинаковые 2 или более параметров, параметры должны быть сгруппированы как объект модели.
Если метод принимает более 2 параметров, параметры должны быть сгруппированы как объект модели.
Модели могут наследовать от общей базы, но только тогда, когда это действительно имеет смысл (обычно лучше провести рефакторинг позже, чем начинать с учетом наследования).
Проблемы с использованием моделей из других слоев / областей не станут очевидными, пока проект не станет немного сложным. Только тогда вы обнаружите, что меньше кода создает больше работы или больше сложностей.
И да, совершенно нормально иметь 2 разные модели с одинаковыми свойствами, которые служат разным слоям / целям (т. Е. ViewModels против POCO).
источник
Давайте просто проверим отдельные аспекты SOLID:
Одна вещь, которая имеет тенденцию сбивать с толку инстинкты проектирования, заключается в том, что этот класс предназначен главным образом для глобальных объектов и, по существу, только для чтения. В такой ситуации нарушение абстракций не причиняет большого вреда: простое чтение данных, которые не были изменены, создает довольно слабую связь; только когда она становится огромной кучей, боль становится заметной.
Чтобы восстановить проектные инстинкты, просто предположите, что объект не очень глобален. Какой контекст понадобится функции, если
User
объект может быть изменен в любое время? Какие компоненты объекта могут быть видоизменены вместе? Они могут быть выделеныUser
как в виде подобъекта, на который ссылаются, или в качестве интерфейса, который предоставляет только «часть» связанных полей, не так уж важно.Другой принцип: посмотрите на функции, которые используют части
User
и посмотрите, какие поля (атрибуты) имеют тенденцию идти вместе. Это хороший предварительный список подобъектов - вам определенно нужно подумать, действительно ли они принадлежат друг другу.Это большая работа, и это несколько сложно сделать, и ваш код станет немного менее гибким, потому что станет немного труднее идентифицировать подобъект (подынтерфейс), который необходимо передать в функцию, особенно если подобъекты перекрываются.
Разделение
User
на самом деле станет уродливым, если субобъекты перекрываются, тогда люди будут не уверены, какой из них выбрать, если все обязательные поля находятся в перекрытии. Если вы разделитесь иерархически (например, у вас есть то,UserMarketSegment
что, помимо прочегоUserLocation
), люди не будут уверены, на каком уровне находится функция, которую они пишут: имеет ли дело пользовательские данные наLocation
уровне или наMarketSegment
уровне? Точно не помогает, что это может измениться со временем, то есть вы вернулись к изменению сигнатур функций, иногда по всей цепочке вызовов.Другими словами: если вы действительно не знаете свой домен и не имеете достаточно четкого представления о том, какой модуль имеет дело с какими аспектами
User
, на самом деле не стоит улучшать структуру программы.источник
Это действительно интересный вопрос. Это зависит.
Если вы думаете, что ваш метод может в будущем измениться внутренне, чтобы потребовать различных параметров объекта User, вы, безусловно, должны передать все целиком. Преимущество заключается в том, что внешний по отношению к методу код защищен от изменений внутри метода с точки зрения того, какие параметры он использует, что, как вы говорите, вызовет каскад изменений извне. Таким образом, передача всего пользователя увеличивает инкапсуляцию.
Если вы уверены, что вам никогда не понадобится использовать что-либо, кроме как сказать электронную почту Пользователя, вы должны передать это. Преимущество этого состоит в том, что вы можете использовать метод в более широком диапазоне контекстов: вы можете использовать его, например, с электронной почтой компании или с электронной почтой, которую кто-то только что набрал. Это увеличивает гибкость.
Это часть более широкого круга вопросов о создании классов, имеющих широкий или узкий охват, в том числе о том, внедрять ли зависимости и иметь ли глобально доступные объекты. В настоящее время существует печальная тенденция думать, что более узкие рамки всегда хороши. Однако всегда есть компромисс между инкапсуляцией и гибкостью, как в этом случае.
источник
Я считаю, что лучше всего передавать как можно меньше параметров и столько, сколько необходимо. Это облегчает тестирование и не требует создания целых объектов.
В вашем примере, если вы собираетесь использовать только идентификатор пользователя или имя пользователя, то это все, что вы должны передать. Если этот шаблон повторяется несколько раз и фактический пользовательский объект намного больше, тогда я советую создать меньший интерфейс (ы) для этого. Возможно
или
Это значительно облегчает тестирование с помощью имитации, и вы сразу узнаете, какие значения действительно используются. В противном случае вам часто нужно инициализировать сложные объекты со многими другими зависимостями, хотя в конце вам просто нужно одно или два свойства.
источник
Вот что я встречал время от времени:
User
(илиProduct
любого другого), который имеет множество свойств, хотя метод использует только несколько из них.User
объекта. Он создает экземпляр и инициализирует только те свойства, которые действительно нужны методу.User
аргумент, вы обнаруживаете, что вынуждены находить вызовы этого метода, чтобы найти источник,User
чтобы вы знали, какие свойства заполнены. Это «реальный» пользователь с адресом электронной почты, или он просто создан для передачи идентификатора пользователя и некоторых разрешений?Если вы создаете
User
и заполняете только несколько свойств, потому что это те, которые нужны методу, тогда вызывающая сторона действительно знает больше о внутренней работе метода, чем должна.Еще хуже, когда у вас есть экземпляр
User
, вы должны знать, откуда он взялся, чтобы вы знали, какие свойства заполняются. Вы не хотите знать это.Со временем, когда разработчики увидят, что они
User
используются в качестве контейнера для аргументов метода, они могут начать добавлять к нему свойства для одноразовых сценариев. Теперь это становится уродливым, потому что класс забивается свойствами, которые почти всегда будут нулевыми или по умолчанию.Такое повреждение не является неизбежным, но оно происходит снова и снова, когда мы передаем объект только потому, что нам нужен доступ к нескольким его свойствам. Опасная зона - это первый случай, когда вы видите, как кто-то создает экземпляр
User
и просто заполняет несколько свойств, чтобы они могли передать его методу. Положи свою ногу на нее, потому что это темный путь.По возможности, подайте правильный пример следующему разработчику, передавая только то, что вам нужно передать.
источник