Интерфейсы на разных логических слоях

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

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

Но вот сложная часть, когда вы начинаете писать уровень доступа к данным, интерфейс к бизнес-логике не соответствует вашим потребностям. У вас должно быть больше доступа и геттеров для установки полей, которые/будут скрыты. Теперь вы вынуждены разрушать интерфейс вашей бизнес-логики; теперь возможно установить поля из уровня пользовательского интерфейса, который на уровне пользовательского интерфейса не имеет деловых настроек.

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

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

Ответ 1

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

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

например

public class User
{
    private string name;
    private AccountStatus status;

    private User()
    {
    }

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public AccountStatus Status
    {
        get { return status; }
    }

    public void Activate()
    {
        status = AccountStatus.Active;
    }

    public void Suspend()
    {
        status = AccountStatus.Suspended;
    }

    public static User GetById(int id)
    {
        User fetchedUser = new User();

        // Lots of database and error-checking code
        // omitted for clarity
        // ...

        fetchedUser.name = (string) reader["Name"];
        fetchedUser.status = (int)reader["statusCode"] == 0 ? AccountStatus.Suspended : AccountStatus.Active;

        return fetchedUser;
    }

    public static void Save(User user)
    {
        // Code to save User internal structure to database
        // ...
    }
}

В этом примере у нас есть объект, представляющий пользователя с именем и AccountStatus. Мы не хотим, чтобы статус был задан напрямую, возможно, потому, что мы хотим проверить, является ли изменение действительным переходом статуса, поэтому у нас нет сеттера. К счастью, код сопоставления в методах GetById и Save static имеет полный доступ к именам объектов и полям состояния.

Второй вариант - иметь второй класс, который отвечает за отображение. Это имеет то преимущество, что разделяет различные проблемы бизнес-логики и настойчивости, которые могут позволить вашему дизайну быть более проверяемым и гибким. Проблема с этим методом заключается в том, как открыть поля имени и статуса внешнему классу. Некоторые параметры: 1. Используйте рефлексию (которая не испытывает никаких сомнений в том, что вы глубоко погружаетесь в частные объекты вашего объекта) 2. Предоставьте специально названные публичные сеттеры (например, прикрепите их к слову 'Private') и надейтесь, что никто не использует их случайно 3. Если ваш язык поддерживает это, сделайте сеттеры внутренними, но предоставите доступ к модулю модуля данных. Например. используйте InternalsVisibleToAttribute в .NET 2.0 или функции друга в С++

За дополнительной информацией я бы рекомендовал классическую книгу Мартина Фаулера "Шаблоны архитектуры предприятия"

Однако, как слово предупреждения, прежде чем идти по пути написания собственных карточек, я настоятельно рекомендую использовать сторонний инструмент реляционного сопоставления объектов (ORM), такой как nHibernate или Microsoft Entity Framework. Я работал над четырьмя различными проектами, где по разным причинам мы написали собственный картограф, и очень легко тратить много времени на сохранение и расширение карты, вместо того, чтобы писать код, который обеспечивает конечное значение пользователя. Я использовал nHibernate в одном проекте до сих пор и, хотя изначально он имеет довольно крутую кривую обучения, инвестиции, которые вы вкладываете в начале, значительно окупаются.

Ответ 2

Это классическая проблема - разделение вашей модели домена от вашей модели базы данных. Есть несколько способов атаковать его, это действительно зависит от размера вашего проекта, на мой взгляд. Вы можете использовать шаблон репозитория, как говорили другие. Если вы используете .net или java, вы можете использовать NHibernate или Hibernate.

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

ps название вопроса немного неверно.

Ответ 3

Я всегда создаю отдельный сборник, содержащий:

  • Множество небольших интерфейсов (думаю, ICreateRepository, IReadRepository, IReadListRepsitory.. список продолжается, и большинство из них в значительной степени зависит от дженериков)
  • Много конкретных интерфейсов, таких как IPersonRepository, которые наследуются от IReadRepository, вы понимаете.
    Все, что вы не можете описать с помощью меньших интерфейсов, вы вносите в конкретный интерфейс.
    До тех пор, пока вы используете IPersonRepository для объявления своего объекта, вы получаете чистый, согласованный интерфейс для работы. Но кикер, вы также можете сделать класс, который принимает f.x. ICreateRepository в своем конструкторе, поэтому код будет очень легко сделать некоторые действительно фанковые вещи. Здесь также есть интерфейсы для Сервисов бизнес-уровня.
  • Наконец, я вставляю все объекты домена в дополнительную сборку, просто чтобы сделать базу кода более чистым и более слабо связанным. Эти объекты не имеют никакой логики, они всего лишь общий способ описания данных для всех слоев 3+.

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

Ответ 4

@Ice ^^ тепла:

Что вы подразумеваете под этим, что уровень данных не должен знать уровень бизнес-логики? Как бы вы заполнили бизнес-объект данными?

Пользовательский интерфейс запрашивает ServiceClass в бизнес-уровне для службы, а именно, получение списка объектов, отфильтрованных объектом с необходимыми данными параметров.
Затем ServiceClass создает экземпляр одного из классов репозитория в уровне данных и вызывает GetList (фильтры ParameterType).
Затем уровень данных обращается к базе данных, извлекает данные и сопоставляет их с общим форматом, определенным в сборке "домен".
BL не имеет больше работы с этими данными, поэтому он выводит его в пользовательский интерфейс.

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

Пользовательский интерфейс знает сервис в бизнес-уровне, который снова знает об уровне данных.

Пользовательский интерфейс отвечает за сопоставление входных данных пользователей с объектами и из них, а уровень данных отвечает за сопоставление данных в db с объектами и из них. Бизнес-уровень остается чисто деловым.:)

Ответ 5

Это может быть решение, так как оно не разрушит интерфейс. Я думаю, у вас может быть класс вроде этого:

public class BusinessObjectRecord : BusinessObject
{
}

Ответ 6

Что вы подразумеваете под этим, что уровень данных не должен знать уровень бизнес-логики? Как бы вы заполнили бизнес-объект данными?

Я часто делаю это:

namespace Data
{
    public class BusinessObjectDataManager
    {
         public void SaveObject(BusinessObject object)
         {
                // Exec stored procedure
         {
    }
}

Ответ 7

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

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

public class BusinessLayer : ISimpleBusiness
{}

public class Some3LayerObject : ISimpleSome3LayerObject
{}

Ответ 8

Вы можете разделить свои интерфейсы на два типа, а именно:

  • Просмотр интерфейсов - интерфейсы, которые определяют ваши взаимодействия с вашим пользовательским интерфейсом, и
  • Интерфейсы данных - это интерфейсы, которые позволят вам указать взаимодействие с вашими данными.

Можно наследовать и реализовать оба набора интерфейсов, чтобы:

public class BusinessObject : IView, IData

Таким образом, на вашем уровне данных вам нужно будет увидеть реализацию интерфейса IData, тогда как в пользовательском интерфейсе вам нужно будет увидеть реализацию интерфейса IView.

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

public class BusinessObject : DomainObject

public class ViewManager<T> where T : DomainObject

public class DataManager<T> where T : DomainObject

Это, в свою очередь, позволяет вашему бизнес-объекту оставаться не осведомленным о слое UI/View и уровне данных.

Ответ 9

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

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

Напишите хранимые процедуры для инкапсуляции ваших данных. Используйте набор результатов, DataSet, DataTable, SqlCommand (или java/php/любой эквивалент) по мере необходимости из кода для взаимодействия с базой данных. Эти объекты вам не нужны. Отличным примером является внедрение SqlDataSource в страницу .ASPX.

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

Объектно-реляционные карты - дьявол. Прекратите использовать их.

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