Разрешение антишаблона цепочки вызовов

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

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

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

public List<IData> GetData(int id, string filter, bool check)
{
    return DataService.GetData(id, filter, check);
}

Это не неправильно и не обязательно ужасно работать, но это создает странную зависимость копирования/вставки. Я также работаю над базовым сервисом, и он также во многом повторяет этот шаблон, и повсюду есть интерфейсы. Итак, что происходит: «Мне нужно добавить int someotherID к GetData». Поэтому я добавляю его к модели, вызывающей стороне службы, самой службе и интерфейсам. Не помогает и то, что GetData на самом деле представляет несколько методов, которые используют одну и ту же сигнатуру, но возвращают разную информацию. Интерфейсы немного помогают с этим повторением, но оно все еще возникает то тут, то там.

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


person CodexArcanum    schedule 22.03.2011    source источник


Ответы (7)


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

IEnumerable<IData> GetData(IQuery<IData> query);

Внутри интерфейса IQuery у вас может быть метод, который принимает единицу работы в качестве входных данных, например, контекст транзакции или что-то вроде ISession, если вы используете ORM, например NHibernate, и возвращает список объектов IData.

public interface IQuery<T> 
{
 IEnumerable<T> DoQuery(IUnitOfWork unitOfWork);
}

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

person Can Gencer    schedule 22.03.2011
comment
Я думаю, что ваш ответ лучше всего подходит для решения моей проблемы, не требуя значительных изменений. Некоторые другие предлагали подобное, но вы на самом деле назвали его. Я был настолько сосредоточен на абстрагировании самой функции, что мне и в голову не пришло, что я могу объединить список параметров, чтобы абстрагироваться только от этого элемента. - person CodexArcanum; 23.03.2011

Мне кажется, вам нужен другой интерфейс, чтобы метод стал примерно таким:

public List<IData> GetData(IDataRequest request)
person Robert Rossney    schedule 22.03.2011

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

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

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

person Jon Hanna    schedule 22.03.2011

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

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

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

person MattC    schedule 22.03.2011

Я тоже использую этот узор. Однако я использовал его для отделения объектов модели предметной области от объектов данных.

В моем случае вместо того, чтобы «пропускать» объект, поступающий из уровня данных, как вы делаете в своем примере, я «сопоставляю» его с другим объектом, который находится в моем доменном слое. Я использую AutoMapper, чтобы избавиться от необходимости делать это вручную.

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

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

Вот описание автоматического сопоставления, которое, как я думаю, является тем, чего пытается достичь этот шаблон проектирования:

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

person 7wp    schedule 22.03.2011

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

Во-первых, позвольте мне сказать, что ваш подход вполне нормальный.

Теперь позвольте мне подумать о ваших слоях:

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

Чтобы не путать, вот что я называю особым видом:

public UserEntity GetUserByID(int userEntityID);

В этом примере вам нужно передать именно Int, вызывая точно GetUserByID, и он вернет именно объект UserEntity.

Теперь другой подход:

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

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

Example:

public Entity SelectByID(IEntityID id);
public Entity SelectAll();

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

Но это почти создание собственного ORM, поэтому я бы не подумал, что это лучший способ.

person Tengiz    schedule 22.03.2011

Очень важно определить, какая ответственность лежит на каком слое, и размещать такую ​​логику только на том слое, к которому она принадлежит.

Это абсолютно нормально просто пройти, если вам не нужно добавлять какую-либо логику в конкретный метод. В какой-то момент вам может понадобиться это сделать, и в этот момент уровень абстракции окупится.

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

Если у вас много методов с почти одинаковой сигнатурой, вам следует подумать о шаблоне спецификации запроса.

IData GetData(IQuery<IData> query)

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

IQuery<IData> BindRequest(IHttpRequest request)

С помощью шаблона Automapping and Query Specification вы можете свести дублирование к минимуму.

person George Polevoy    schedule 22.03.2011