Является ли это правильной реализацией n-уровневой архитектуры?

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

Текущий веб-сайт представляет собой старый ASP VBscript и имеет существующую базу данных, которая довольно уродлива (без внешних ключей и тому подобного), поэтому, по крайней мере, для первой версии в .NET я не хочу использовать и должен изучать какие-либо инструменты ORM в настоящее время.

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

productDTO.cs

public class ProductDTO
{
    public int ProductId { get; set; }
    public string Name { get; set; }

    public ProductDTO()
    {
        ProductId = 0;
        Name = String.Empty;
    }
}

productBLL.cs

public class ProductBLL
{

    public ProductDTO GetProductByProductId(int productId)
    {
        //validate the input            
        return ProductDAL.GetProductByProductId(productId);
    }

    public List<ProductDTO> GetAllProducts()
    {
        return ProductDAL.GetAllProducts();
    }

    public void Save(ProductDTO dto)
    {
        ProductDAL.Save(dto);
    }

    public bool IsValidProductId(int productId)
    {
        //domain validation stuff here
    }
}

productDAL.cs

public class ProductDAL
{
    //have some basic methods here to convert sqldatareaders to dtos


    public static ProductDTO GetProductByProductId(int productId)
    {
        ProductDTO dto = new ProductDTO();
        //db logic here using common functions 
        return dto;
    }

    public static List<ProductDTO> GetAllProducts()
    {
        List<ProductDTO> dtoList = new List<ProductDTO>();
        //db logic here using common functions 
        return dtoList;
    }

    public static void Save(ProductDTO dto)
    {
        //save stuff here
    }

}

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

ProductBLL productBll = new ProductBLL();
List<ProductDTO> productList = productBll.GetAllProducts();

для сохранения:

ProductDTO dto = new ProductDTO();
dto.ProductId = 5;
dto.Name = "New product name";
productBll.Save(dto);

Я совсем не в теме? Должен ли я также иметь те же свойства в моем BLL и не передавать DTO обратно в мой пользовательский интерфейс? Подскажите, пожалуйста, что не так, а что правильно. Имейте в виду, что я еще не эксперт.

Я хотел бы реализовать интерфейсы для своей архитектуры, но я все еще учусь, как это делать.


person jpshook    schedule 28.02.2011    source источник
comment
На моей работе мы поддерживаем проект с очень похожей архитектурой, и, зная то, что я знаю сейчас, я бы попытался смириться с этим и использовать ORM (мне нравится NHibernate). Я, вероятно, предвзят, но когда вы начинаете строить свою архитектуру таким образом, у вас появляются классы, которые просто передают информацию на следующий уровень. У NHibernate есть очень хорошие API-интерфейсы запросов, за которые вы много купите, и нет правила, говорящего, что вы должны иметь внешние ключи для использования NHibernate (хотя они, безусловно, полезны).   -  person dana    schedule 28.02.2011
comment
Попытка изучить NHibernate, когда я не очень хорошо разбираюсь в c#/ASP.net, — это немного больше, чем я хочу прямо сейчас. Кроме того, имена таблиц базы данных и имена полей встречаются повсеместно. Спасибо за ваш комментарий.   -  person jpshook    schedule 28.02.2011


Ответы (4)


Анемичный домен — это когда продукт или другой класс на самом деле не реализует ничего, кроме сеттеров и геттеров данных — никакого доменного поведения.

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

В противном случае версия BLL (объект домена) вряд ли лучше, чем DTO.

http://martinfowler.com/bliki/AnemicDomainModel.html

ProductBLL productBll = new ProductBLL();
List<ProductDTO> productList = productBll.GetAllProducts();

Проблема здесь в том, что вы предполагаете, что ваша модель анемична и предоставляет DTO потребителям бизнес-уровня (пользовательский интерфейс или что-то еще).

Код вашего приложения обычно хочет работать с <Product>s, а не с BLL или DTO или чем-то еще. Это классы реализации. Они мало что значат не только для уровня мышления прикладного программиста, но и для экспертов в предметной области, которые якобы разбираются в предметной области. Таким образом, они должны быть видны только тогда, когда вы работаете с сантехникой, а не при проектировании ванной комнаты, если вы понимаете, о чем я.

Я называю свои объекты BLL именем объекта бизнес-домена. И DTO является внутренним между бизнес-объектом и DAL. Когда объект домена не делает ничего, кроме DTO, — вот когда он анемичен.

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

Я бы придерживался самой простой вещи, которая может работать:

public class Product {
    // no one can "make" Products
    private Product(IDataRecord dr) {
        // Make this product from the contents of the IDataRecord
    }

    static private List<Product> GetList(string sp, Action<DbCommand> addParameters) {
        List<Product> lp = new List<Product>();
        // DAL.Retrieve yields an iEnumerable<IDataRecord> (optional addParameters callback)
        // public static IEnumerable<IDataRecord> Retrieve(string StoredProcName, Action<DbCommand> addParameters)
        foreach (var dr in DAL.Retrieve(sp, addParameters) ) {
            lp.Add(new Product(dr));
        }
        return lp;
    }

    static public List<Product> AllProducts() {
        return GetList("sp_AllProducts", null) ;
    }

    static public List<Product> AllProductsStartingWith(string str) {
        return GetList("sp_AllProductsStartingWith", cm => cm.Parameters.Add("StartsWith", str)) ;
    }

    static public List<Product> AllProductsOnOrder(Order o) {
        return GetList("sp_AllProductsOnOrder", cm => cm.Parameters.Add("OrderId", o.OrderId)) ;
    }
}

Затем вы можете переместить очевидные части в DAL. DataRecords служат вашим DTO, но они очень недолговечны — их коллекция никогда не существует.

Вот DAL.Retrieve для SqlServer, который является статическим (вы можете видеть, что его достаточно просто изменить для использования CommandText); У меня есть версия этого, которая инкапсулирует строку подключения (поэтому это не статический метод):

    public static IEnumerable<IDataRecord> SqlRetrieve(string ConnectionString, string StoredProcName,
                                                       Action<SqlCommand> addParameters)
    {
        using (var cn = new SqlConnection(ConnectionString))
        using (var cmd = new SqlCommand(StoredProcName, cn))
        {
            cn.Open();
            cmd.CommandType = CommandType.StoredProcedure;

            if (addParameters != null)
            {
                addParameters(cmd);
            }

            using (var rdr = cmd.ExecuteReader())
            {
                while (rdr.Read())
                    yield return rdr;
            }
        }
    }

Позже вы можете перейти к полноценным фреймворкам.

person Cade Roux    schedule 28.02.2011
comment
Да, я уже умею пользоваться Google... Ищу ответы, которые помогут мне двигаться дальше. - person jpshook; 28.02.2011
comment
@Developr Я не уверен, в чем ваша точка зрения - вы представили только один класс и не включили бизнес-логику, но указали, что некоторые из них существуют - поэтому на самом деле это может быть не анемично, в зависимости от того, что вы упустили. - person Cade Roux; 28.02.2011
comment
@Cade Roux — сохранить, получить, обновить, проверить и т. д. — Разве это не бизнес-логика? - person jpshook; 01.03.2011
comment
Я понимаю, что вы говорите об использовании продукта, но тогда мне понадобятся в основном те же свойства, что и мой DTO, повторенный для моего BLL, верно? Как объект сущности/домена вернет себя из DAL? или это отдельный класс обслуживания? Мне просто нужен хороший конкретный способ сделать это, который я могу повторить и интегрировать. - person jpshook; 01.03.2011
comment
@Developr Это это бизнес-логика, поэтому кажется, что Продукт не является хорошим кандидатом на то, чтобы считаться анемичным. - person Cade Roux; 01.03.2011
comment
Итак, вы говорите, что я должен объединить DTO с BLL и рассматривать это как свою сущность? Или мне по-прежнему понадобится DTO для переноса данных между моим доменом и DAL? - person jpshook; 01.03.2011
comment
@Developr Я обновил свой ответ, чтобы показать, что IDataRecord из DataReader используется в качестве DTO. - person Cade Roux; 01.03.2011
comment
Спасибо Кейд за помощь.. Это имеет смысл. Поэтому, если я хочу по-прежнему использовать DTO и DAL для получения/передачи данных между DAL и BLL/entity, это нормально, если я перестану полагаться на DTO в своем пользовательском интерфейсе и сосредоточусь на домене. Так что мне нужно будет повторить большинство моих свойств DTO в моем объекте BLL/entity/Domain, верно? - person jpshook; 01.03.2011
comment
@Developr Все, что исходит от DTO, которое является частью общедоступного интерфейса объекта домена, должно быть представлено соответствующим образом, а все, что является внутренним, может быть частным для объекта домена. Поскольку DTO обычно является общедоступным, я обнаружил, что DataRecord достаточно для этой роли, как я привел в своем примере. Поэтому обычно все, что я реализую, — это хранимые процедуры в серверной части по мере изменения интерфейса базы данных, бизнес-объектов и кода пользовательского интерфейса — все остальное является общим или генерируется кодом. - person Cade Roux; 01.03.2011
comment
@Cade Roux - нужно ли мне настраивать слой сопоставления для сопоставления DTO с POCO, или мой уровень данных может знать о POCO и напрямую сопоставлять его? например, когда у вас есть: статический общедоступный список‹Product›AllProducts() {}, сопоставляется ли слой данных с объектом домена или каким-либо переходным слоем? Я не большой поклонник хранимых процедур, поэтому предпочел бы использовать параметризованные запросы. Может, мне лучше просто перейти на Entity Framework, Nhibernate или Linq2SQL для доступа к данным? - person jpshook; 23.03.2011
comment
@Developr Продукт может знать DAL, но не наоборот. В моем примере DAL очень тонкий, он просто обеспечивает это извлечение. Он может так же легко выполнять параметризованный запрос, как CommandText, вместо хранимой процедуры. DAL.Retrieve создаст IEnumerable‹IDataRecord›, а Product знает, как создать себя из IDataRecord. Я отредактирую, чтобы показать пример, если DAL.Retrieve. - person Cade Roux; 23.03.2011
comment
@Developr У меня проблема с людьми, использующими фреймворки, не понимающими причин проблем, которые они решают (и тех, которые они не решают). Для вас будет лучше, по крайней мере, увидеть некоторые из проблем, лежащих в основе, чтобы, когда вы достигнете болевых точек в чужой абстракции, вы могли распознать это и, возможно, немного легче выйти из себя. - person Cade Roux; 23.03.2011
comment
@Cade Roux - Спасибо за вашу постоянную поддержку. Просто чтобы уточнить, когда вы возвращаете IDataReader/Records обратно в свой объект BLL/Domain, не остается ли соединение db открытым во время всего этого? Есть ли способ, которым вы его отключаете? Обычно в моем текущем коде DAL сопоставляет читателя с DTO и возвращает его в BLL. В ваших примерах BLL/DO фактически отправляет запрос в DAL напрямую, верно? - person jpshook; 23.03.2011
comment
@Developr DAL возвращает IEnumerable IDataRecord, который обрабатывается как DTO. Вы правы в том, что в этой реализации полная копия набора данных НЕ создается, а затем передается целиком - фактически между слоями передается только одна строка (через выход). Это уменьшит рабочий набор, но может привести к тому, что соединение будет оставаться открытым дольше, если конструкции объектов длинные. Здесь есть компромисс, но мы больше не создаем DataSet или другой DTO, а ЗАТЕМ создаем BO, мы просто передаем IDataRecord. Меньше кода, меньше оперативной памяти. - person Cade Roux; 23.03.2011
comment
@Developr Вы всегда можете поместить больше слоев и больше копий / сериализаций вещей в любую структуру, но я думаю, что накладные расходы на копирование всего набора данных только для того, чтобы вы могли быстро закрыть соединение перед созданием объектов, имеет смысл только в том случае, если ваши объекты чрезвычайно сложный и дорогой в строительстве. В этот момент кажется, что вы все равно не захотите создавать их много, поэтому время подключения не должно быть проблемой. Если соединение для одной строки извлекает много данных, это, вероятно, BLOB, поэтому проблема заключается в простом вводе-выводе. - person Cade Roux; 23.03.2011
comment
@Developr Таким образом, это зависит от ваших конкретных моделей использования. Очевидно, что в более физически обособленной многоуровневой среде, где данные обслуживаются через службу или портал, все это должно быть сериализовано для потребителя, поскольку постоянное соединение отсутствует. В этот момент гидратация полных бизнес-объектов может быть или не быть разумной. - person Cade Roux; 23.03.2011
comment
Кроме того, в этом решении объекты домена тесно связаны с базой данных. Наша БД довольно устаревшая и не имеет соглашений об именах. Мне, вероятно, понадобится слой отображения, чтобы отделить таблицы БД от BO. - person jpshook; 23.03.2011
comment
@Developr - все сопоставления будут выполняться в частном продукте (IDaRecord dr) - как вы это сделаете, зависит от вас. Я склонен делать это в коде. BO — это бизнес-объект, он не будет соответствовать каждому столбцу в таблице, особенно для отложенных дочерних коллекций или даже простых вещей, таких как флаги или состояния. (т. е. DateInvoiced IS NULL сопоставляется со свойством bool IsInvoiced = False, а свойство datetime InvoiceDate может вызывать исключение) - person Cade Roux; 23.03.2011

У Кейда есть хорошее объяснение. Чтобы избежать анемичной модели предметной области, вы можете сделать следующее:

  • сделайте объект DTO своим объектом домена (просто назовите его «Продукт»)
  • Затем IsValidProductId может быть в Product, и когда вызывается установщик, вы можете проверить, что он действителен, и выбросить, если это не так.
  • реализовать какие-то правила об имени
  • если есть какие-либо другие объекты, которые взаимодействуют с продуктом, у нас может быть более интересные темы для разговора.
person Alex Lo    schedule 28.02.2011
comment
Итак, что насчет того, когда мне нужен список из 5000+ элементов, действительно ли я хочу нести затраты на 5000+ копий всех проверок BLL и тому подобное? Кроме того, зачем мне передавать объект с другими методами в мой DAL? Разве я не должен просто передавать данные? Должен ли мой BLL стать просто продуктом, а затем использовать мои DTO для передачи данных между ним и DAL? - person jpshook; 28.02.2011
comment
@Developr Если вам нужен список из 5000+ элементов - это другая проблема. В этом случае вам нужно посмотреть на процесс, который будет работать с 5000 элементов, и решить, можно ли это сделать ближе к базе данных (т. е. в SQL), и если нет, может ли операция работать с объектами, которые более легкие версии (то, что я называю дайджестами), возможно, для выпадающего списка или кеша поиска (где вам нужен только идентификатор и описание). Модель предметной области — это сеть классов, охватывающая всю предметную область. Так что одного класса действительно недостаточно, чтобы сказать, что дизайн плохой/анемичный. - person Cade Roux; 28.02.2011
comment
о каких проверках вы беспокоитесь? у него больше методов, чем нужно вашему DAL, ну и что? вы можете сделать к нему интерфейс только для DAL, если хотите. Разве я не должен просто передавать данные? =› нет, вам нужны объекты с поведением. - person Alex Lo; 28.02.2011
comment
@Alex Lo - Итак, мне нужно объединить BLL и DTO? Или вы говорите, что я должен просто переименовать свой DTO в Products и добавить к нему несколько методов? Откуда мне тогда знать, какие методы будут в BLL? - person jpshook; 01.03.2011
comment
@Cade Roux - у меня есть много других подобных классов ... большинство из них основаны на таблице базы данных, но некоторые связаны между собой. - person jpshook; 01.03.2011
comment
Я бы начал с продукта, а затем с ProductMapper или ProductPersistanceService. Продукт имеет данные и бизнес-поведение, и если вы хотите отдельно, отдельный интерфейс BBL и интерфейс данных. ProductPersistanceService получает продукты из базы данных и сохраняет их. - person Alex Lo; 01.03.2011

Вещи, которые вы хотите добавить: проверка, уведомление об изменении свойств, привязка данных и т. д. Одна из распространенных проблем при разделении каждого класса на несколько классов (DAL, BLL и т. д.) часто заключается в том, что вы получаете a много кода нужно продублировать. Другая проблема заключается в том, что если вам нужна некоторая близость между этими классами, вам придется создавать внутренние члены (интерфейсы, поля и т. д.)

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

public class Product: IRecord, IDataErrorInfo, INotifyPropertyChanged
{
    // events
    public event PropertyChangedEventHandler PropertyChanged;

    // properties
    private int _id;
    public virtual int Id
    {
        get
        {
            return _id;
        }
        set
        {
            if (value != _id)
            {
                _id = value;
                OnPropertyChanged("Id");
            }
        }
    }

    private string _name;
    public virtual string Name
    {
        get
        {
            return _name;
        }
        set
        {
            if (value != _name)
            {
                _name = value;
                OnPropertyChanged("Name");
            }
        }
    }

    // parameterless constructor (always useful for serialization, winforms databinding, etc.)
    public Product()
    {
        ProductId = 0;
        Name = String.Empty;
    }

    // update methods
    public virtual void Save()
    {
       ValidateThrow();
       ... do save (insert or update) ...
    }

    public virtual void Delete()
    {
       ... do delete ...
    }    

    // validation methods
    public string Validate()
    {
       return Validate(null);
    }

    private void ValidateThrow()
    {
      List<Exception> exceptions = new List<Exception>();
      SummaryValidate(exceptions,memberName);
      if (exceptions.Count != 0)
         throw new CompositeException(exceptions);
    }

    public string Validate(string memberName)
    {
      List<Exception> exceptions = new List<Exception>();
      SummaryValidate(exceptions,memberName);
      if (exceptions.Count == 0)
        return null;

      return ConcatenateAsString...(exceptions);
    }

    string IDataErrorInfo.Error
    {
      get
      {
         return Validate();
      }
    }

    string IDataErrorInfo.this[string columnName]
    {
      get
      {
        return validate(columnName);
      }
    }

    public virtual void SummaryValidate(IList<Exception> exceptions, string memberName)
    {
       if ((memberName == null) || (memberName == "Name"))
       {
         if (!... validate name ...)
            exceptions.Add(new ValidationException("Name is invalid");
       }
    }

    protected void OnPropertyChanged(string name)
    {
       OnPropertyChanged(new PropertyChangedEventArgs(name));
    }

    // property change notification
    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if ((PropertyChanged != null)
            PropertyChanged(this, e);
    }

    // read from database methods
    protected virtual Read(IDataReader reader)
    {
      Id = reader.GetInt32(reader.GetOrdinal("Id"));
      Name = = reader.GetString(reader.GetOrdinal("Id"));
      ...
    }

    void IRecord.Read(IDataReader reader)
    {
      Read(reader);
    }

    // instance creation methods
    public static Product GetById(int id)
    {
        // possibly use some cache (optional)
        Product product = new Product();
        using (IDataReader reader = GetSomeReaderForGetById...(id))
        {
            if (!reader.Read())
              return null;

            ((IRecord)product).Read(reader);
            return product;
        }
    }

    public static List<Product> GetAll()
    {
        // possibly use some cache (optional)
        List<Product> products = new List<Product>(); // if you use WPF, an ObservableCollection would be more appropriate?
        using (IDataReader reader = GetSomeReaderForGetAll...(id))
        {
            while (reader.Read())
            {
              Product product = new Product();
              ((IRecord)product).Read(reader);
              products.Add(product);
            }
        }
        return products;
    }
}

// an interface to read from a data record (possibly platform independent)
public interface IRecord
{
  void Read(IDataReader reader);
}
person Simon Mourier    schedule 28.02.2011
comment
То есть товар может вернуться сам? Зачем пропускать читалку, хоть это и IDataReader, мне кажется оффлайн. К сожалению, я могу понять только около 1/4 того, что вы там сказали... - person jpshook; 01.03.2011
comment
@Developr - да, в этом случае Product возвращает сам себя. Почему бы нет? В самой .NET Framework есть много примеров этого. - person Simon Mourier; 01.03.2011
comment
я не согласен с тем, чтобы заставить объект обрабатывать свое собственное постоянство. в моем предложении я предлагаю, чтобы постоянство объекта выполнялось другим классом - это поможет, если вы когда-нибудь перейдете на использование ORM, поскольку исходный объект все еще полезен сам по себе. - person Alex Lo; 01.03.2011
comment
@ Алекс - ну, я уверен, что некоторые люди не согласятся :-) Мне не нужен никакой ORM. - person Simon Mourier; 01.03.2011
comment
@ Саймон, да, я уверен, и для небольших проблем я уверен, что это работает хорошо. ОП в какой-то момент намекает на использование ORM, поэтому я упомянул это как слабость этого подхода. как вы обрабатываете объекты, которые ссылаются на другие объекты, используя вашу схему? - person Alex Lo; 01.03.2011
comment
@Alex - Ну, мой опыт показывает, что это действительно может работать для любого размера проекта. Что касается ссылочных объектов, я бы использовал идентификатор этих других объектов (так, например, 1 свойство для самого объекта ref'd и свойство N для ключей объекта N ref'd), здесь ничего особенного. - person Simon Mourier; 01.03.2011

Что другие говорили об использовании ORM - по мере расширения вашей модели у вас будет много повторений кода без него. Но я хотел прокомментировать ваш вопрос «а как насчет 5000».

Копирование класса не создает 5000 копий его методов. Он только создает копию структур данных. Наличие бизнес-логики в объекте предметной области не снижает эффективности. Если какая-то часть бизнес-логики неприменима, вы можете создать подклассы, которые украшают объект для определенных целей, но цель этого — создать объекты, соответствующие вашему предполагаемому использованию, а не эффективности. Анемичная модель дизайна не более эффективна.

Также подумайте о том, как вы будете использовать данные в своем приложении. Я не могу вспомнить ни одного раза, когда я когда-либо использовал такой метод, как «GetAllOfSomething()», за исключением, может быть, списка ссылок. Какова цель извлечения всего из вашей базы данных? Если вам нужно выполнить какой-то процесс, манипулировать данными, составить отчет, вы должны предоставить метод, который выполняет этот процесс. Если вам нужно предоставить список для какого-то внешнего использования, например, для заполнения сетки, тогда откройте IEnumerable и предоставьте методы для подмножества данных. Если вы начнете с идеи, что вы работаете с полными списками данных в памяти, у вас возникнут серьезные проблемы с производительностью по мере роста данных.

person Jamie Treworgy    schedule 28.02.2011
comment
Я должен был перечислить get all как get many... В моем реальном коде я использую ROW_Number и CTE для подкачки страниц и тому подобного. - person jpshook; 01.03.2011
comment
Все, что я читал о nHibernate, говорит о том, что он очень медленный, и я действительно хотел научиться лучшему способу, не просто передавая его инструменту. Сайт очень большой (например, магазин с 1500 товарами плюс потоковое видео с комментариями, форумы и т. д.) и относительно высокий трафик (до 500 тысяч уникальных пользователей в месяц). - person jpshook; 01.03.2011
comment
ОК, но не возвращайте список, возвращайте IEnumerable‹T›. Это гораздо более гибко, и в противном случае каждый раз, когда вы возвращаете список, вы будете перебирать его дважды: один раз, когда он был создан, а затем еще раз, когда он используется вне класса. Если вам действительно нужен список, вы можете просто сказать List<T> myList = new List<T>(someEntityThatReturnsIEnumerable<T>), что, вероятно, идентично цикл за циклом по сравнению с List<T> mylist = someEntityThatReturnsList - person Jamie Treworgy; 01.03.2011
comment
Я лично не использовал nHibernate, поэтому не могу говорить об этом. Но имейте в виду, что добавление мощности процессора обходится дешевле, чем написание кода. Даже в том масштабе, о котором вы говорите, держу пари, у вас не будет особых проблем. Но когда вы начинаете разрабатывать свою модель, например. глядя на ответ Саймона Мурье (который является прекрасной моделью), подумайте о том, сколько кода повторяется в каждом отдельном свойстве для проверки, операций с базой данных, обработки исключений. По крайней мере, инкапсулируйте общую функциональность, которая является частью отображения, в класс, что на самом деле является очень простой реализацией ORM. - person Jamie Treworgy; 01.03.2011