Разрешение анти-паттерна цепочки вызовов
Я начал замечать что-то вроде анти-паттерна в моей разработке ASP.NET. Это беспокоит меня, потому что я чувствую, что это правильно, чтобы поддерживать хороший дизайн, но в то же время он пахнет неправильно.
Проблема в следующем: у нас многоуровневое приложение, нижний уровень - это класс, обрабатывающий вызовы службы, которая предоставляет нам данные. Над этим находится слой классов, которые могут преобразовывать, манипулировать и проверять данные. Над этим находятся страницы ASP.NET.
Во многих случаях методы из сервисного уровня не нуждаются в каких-либо изменениях перед переходом к представлению, поэтому модель представляет собой простой проход, например:
public List<IData> GetData(int id, string filter, bool check)
{
return DataService.GetData(id, filter, check);
}
Это не неправильно и не обязательно ужасно работать, но это создает странную зависимость от копирования / вставки. Я также работаю над основным сервисом, и он также много копирует этот паттер, и есть интерфейсы повсюду. Итак, что происходит, "Мне нужно добавить int someotherID
в GetData
"Поэтому я добавляю его к модели, вызывающей стороне службы, самой службе и интерфейсам. GetData
фактически представляет несколько методов, которые используют одну и ту же сигнатуру, но возвращают разную информацию. Интерфейсы немного помогают с этим повторением, но оно все еще появляется здесь и там.
Есть ли название для этого анти-паттерна? Есть ли исправление или серьезное изменение архитектуры - единственный реальный путь? Похоже, мне нужно сгладить мою объектную модель, но иногда слой данных выполняет преобразования, поэтому он имеет ценность. Мне также нравится разделять код между "вызовами внешней службы" и "поставкой данных страницы".
7 ответов
Я бы предложил вам использовать шаблон объекта запроса для решения этой проблемы. По сути, ваш сервис может иметь такую подпись:
IEnumerable<IData> GetData(IQuery<IData> query);
Внутри интерфейса IQuery у вас может быть метод, который принимает единицу работы в качестве входных данных, например контекст транзакции или что-то вроде ISession, если вы используете ORM, такой как NHibernate, и возвращает список объектов IData.
public interface IQuery<T>
{
IEnumerable<T> DoQuery(IUnitOfWork unitOfWork);
}
Таким образом, вы можете создавать строго типизированные объекты запросов, которые соответствуют вашим требованиям, и иметь чистый интерфейс для ваших служб. Эта статья от Ayende делает хорошее чтение на эту тему.
Похоже, вам нужен другой интерфейс, так что метод становится примерно таким:
public List<IData> GetData(IDataRequest request)
Важно определить, какая ответственность лежит на том или ином слое, и поместить такую логику только в тот слой, к которому она принадлежит.
Абсолютно нормально просто пройти, если вам не нужно добавлять какую-либо логику в конкретный метод. В какой-то момент вам может понадобиться сделать это, и уровень абстракции окупится в этот момент.
Еще лучше иметь параллельные иерархии, а не просто передавать объекты нижележащего слоя, поэтому каждый слой использует свою собственную иерархию классов, и вы можете использовать что-то вроде AutoMapper в случае, если вы чувствуете, что между иерархиями нет большой разницы. Это дает вам гибкость, и вы всегда можете заменить автоматическое отображение на собственный код отображения в определенных методах / классах, если иерархии больше не совпадают.
Если у вас много методов с практически одинаковой сигнатурой, вам следует подумать о шаблоне спецификации запроса.
IData GetData(IQuery<IData> query)
Затем на уровне представления вы можете реализовать связыватель данных для ваших объектов спецификации пользовательских запросов, где один обработчик aspnet может реализовать создание определенных объектов запроса и передать их в один метод сервиса, который передаст его в один метод репозитория, где он может быть отправлен в соответствии с определенным классом запросов, возможно, с шаблоном Visitor.
IQuery<IData> BindRequest(IHttpRequest request)
С помощью шаблона Automapping и Query Specification вы можете уменьшить дублирование до минимума.
Из того, что вы описали, это просто звучит так, как будто вы столкнулись с одним из "компромиссов" абстракции в вашем приложении.
Рассмотрим случай, когда эти "цепочки вызовов" больше не "пропускают" данные, а требуют некоторой трансформации. Это может быть не нужно сейчас, и, конечно, можно привести доводы в пользу YAGNI.
Тем не менее, в этом случае не кажется слишком большой технологической задолженностью, чтобы справиться с положительным побочным эффектом возможности легко вносить изменения в данные между уровнями.
На самом деле, путь, который вы выбрали, является причиной того, что у вас есть (я не говорю, что это плохо).
Во-первых, позвольте мне сказать, что ваш подход вполне нормальный.
Теперь позвольте мне подумать о ваших слоях:
- Ваш сервис - предоставляет довольно строгую модель доступа. Это означает, что он имеет несколько типов аргументов, использует их в некоторых специальных типах методов, которые снова возвращают некоторые особые типы результатов.
- Ваш уровень доступа к услугам - также предоставляет такую же модель. Таким образом, он принимает специальные виды аргументов для специальных видов методов, возвращая специальные виды результатов.
- так далее...
Чтобы не путать, вот что я называю особой разновидностью:
public UserEntity GetUserByID(int userEntityID);
В этом примере вам нужно передать именно In t, при этом вызывая именно GetUserByID, и он вернет точно UserEntity
объект.
Теперь другой подход:
Помни как SqlDataReader
работает? не очень сильно напечатано, верно? На мой взгляд, здесь вы вызываете то, что вам не хватает не сильно типизированного слоя.
Чтобы это произошло: вам нужно переключиться с строго типизированного на не строго типизированный где-то в ваших слоях.
Example
:
public Entity SelectByID(IEntityID id);
public Entity SelectAll();
Итак, если у вас есть что-то подобное вместо уровня доступа к сервису, вы можете вызывать его для любых аргументов, которые вы хотите.
Но это почти создание собственного ORM, поэтому я не думаю, что это лучший способ.
Я также использую этот шаблон. Однако я использовал его с целью отделения объектов моей доменной модели от объектов данных.
В моем случае вместо "прохождения" объекта, поступающего со слоя данных, как вы делаете в своем примере, я "сопоставляю" его с другим объектом, который живет в слое моего домена. Я использую AutoMapper, чтобы избавиться от боли ручного выполнения этого.
В большинстве случаев мой объект домена выглядит точно так же, как и мой объект данных, из которого он возник. Однако бывают случаи, когда мне нужно сгладить информацию, поступающую из моего объекта данных... или меня может не интересовать все, что находится в моем объекте данных и т. Д. Я сопоставляю объект данных с настроенным объектом домена, который содержит только поля Мой доменный слой заинтересован в.
Также у этого есть побочный эффект, что, когда я решаю переформулировать или изменить свой уровень данных для чего-то еще, это не должно затронуть мои доменные объекты, так как они разъединены, используя технику отображения.
Вот описание auto-mapper, которое, по-моему, пытается достичь этот шаблон проектирования:
AutoMapper предназначен для сценариев проецирования моделей, чтобы свести сложные объектные модели к DTO и другим простым объектам, дизайн которых лучше подходит для сериализации, связи, обмена сообщениями или просто для защиты от коррупции между доменом и уровнем приложений.
Вы делегируете на другой слой, и это совсем не обязательно плохо.
Вы можете добавить какую-то другую логику здесь или в другом методе, который принадлежит только этому слою, или поменять местами делегирование уровня другой реализации, так что это, безусловно, может быть очень хорошим использованием рассматриваемых уровней.
У вас может быть слишком много слоев, но я бы так не сказал, просто увидев это, больше не увидев ничего другого.