Это правильная реализация n-слойной архитектуры? - PullRequest
3 голосов
/ 28 февраля 2011

Я изучал 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
    }

}

В моемUI, я бы сделал что-то вроде этого:

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 в мой интерфейс?Пожалуйста, скажите мне, что не так и что правильно.Имейте в виду, что я еще не эксперт.

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

Ответы [ 4 ]

2 голосов
/ 01 марта 2011

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

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

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);
}
2 голосов
/ 28 февраля 2011

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

  • сделать объект DTO объектом вашего домена (просто назовите его «Product»), тогда
  • IsValidProductId может быть включенПродукт, и когда вызывается сеттер, вы можете проверить, что он действителен, и выбросить, если он не
  • , реализовать какие-то правила об Имени
  • , если есть какие-либо другие объекты, которыевзаимодействуя с продуктом, мы могли бы поговорить о более интересных вещах
1 голос
/ 01 марта 2011

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

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

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

1 голос
/ 28 февраля 2011

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

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

В противном случае версия 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, - это когда она анемична.

Кроме того, я добавлю, что я часто просто пропускаю классы DTO explcit, и заставляю объект домена переходить к универсальному 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;
            }
        }
    }

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

...