这是n层架构的正确实现吗?

时间:2011-02-28 20:21:05

标签: c# asp.net architecture anemic-domain-model

我在过去一年左右的时间里一直在学习C#,并试图在整个过程中融入最佳实践。在StackOverflow和其他网络资源之间,我认为我正在正确地分离我的问题,但现在我有一些疑虑,并希望确保我走在正确的道路上,然后我将整个网站转换为这个新的架构。

当前的网站是旧的ASP VBscript并且有一个非常难看的现有数据库(没有外键等)所以至少对于.NET中的第一个版本我不想使用并且必须学习任何ORM工具这一次。

我有以下项目在单独的命名空间和设置中,以便UI层只能看到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传回我的UI?请告诉我什么是错的,什么是对的。请记住,我还不是专家。

我想实现我的架构的接口,但我仍然在学习如何做到这一点。

4 个答案:

答案 0 :(得分:2)

凯德有很好的探索。为了避免Anemic域模型,你可以考虑做一些事情:

  • 将DTO对象设为您的域对象(只需将其称为“产品”)
  • IsValidProductId可以在Product上,当调用setter时你可以验证它是否有效,如果不是则抛出
  • 实施有关名称的某些规则
  • 如果有任何其他与Product交互的对象,我们可以讨论更多有趣的事情

答案 1 :(得分:2)

您要考虑添加的内容:验证,属性更改通知,数据绑定, 等等...分离多个类(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 :(得分:1)

贫血域是指产品或其他类实际上没有实现除数据设置器和getter之外的任何东西 - 没有域行为。

例如,产品域对象应该公开一些方法,一些数据验证,一些真正的业务逻辑。

否则,BLL版本(域对象)几乎不比DTO好。

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

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

这里的问题是你预先假设你的模型贫乏,并将DTO暴露给业务层消费者(UI或其他)。

您的应用程序代码通常希望使用<Product>,而不是任何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,但它们非常短暂 - 它们的集合从未真正存在。

这是SqlServer的DAL.Retrieve,它是静态的(你可以看到它很简单,可以将它改为使用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;
            }
        }
    }

稍后您可以继续使用完整的框架。

答案 3 :(得分:1)

其他人对使用ORM所说的内容 - 随着模型的扩展,你将会有很多代码重复而没有。但我想评论你的“5000左右”问题。

复制类不会创建5,000个方法副本。它只创建数据结构的副本。在域对象中拥有业务逻辑没有效率损失。如果某些业务逻辑不适用,那么您可以创建为特定目的装饰对象的子类,但这样做的目的是创建符合您的预期用途的对象,而不是效率。贫血设计模型效率不高。

另外,请考虑如何在应用程序中使用数据。我想不出一次我曾经使用像“GetAllOfSomething()”这样的方法,除了可能是参考列表。检索数据库中的所有内容的目的是什么?如果要进行某些过程,数据操作,报告,您应该公开执行该过程的方法。如果需要为某些外部用途公开列表,例如填充网格,则公开IEnumerable并提供用于子集化数据的方法。如果您开始考虑使用内存中的完整数据列表,那么随着数据的增长,您将面临严重的性能问题。