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

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

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

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

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

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

Ответов (9)

Решение

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

Сделав шаг назад, у вас, по сути, есть два варианта размещения кода сопоставления данных - внутри самого класса домена или во внешнем классе сопоставления. Первый вариант часто называется шаблоном 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's internal structure to database
        // ...
    }
}

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

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

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

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

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

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

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

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

public class BusinessObjectRecord : BusinessObject
{
}

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

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

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

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

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

ps название вопроса немного вводит в заблуждение

@ Ice ^^ Heat:

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

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

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

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

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

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

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

public class BusinessLayer : ISimpleBusiness
{}

public class Some3LayerObject : ISimpleSome3LayerObject
{}

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

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

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

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

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

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

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

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

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

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

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