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

我在过去一年左右的时间里一直在学习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 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 GetAllProducts() { List dtoList = new List(); //db logic here using common functions return dtoList; } public static void Save(ProductDTO dto) { //save stuff here } } 

在我的UI中,我会做这样的事情:

 ProductBLL productBll = new ProductBLL(); List productList = productBll.GetAllProducts(); 

保存:

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

我完全不在基地吗? 我的BLL中是否也应该具有相同的属性,而不是将DTO传回我的UI? 请告诉我什么是错的,什么是对的。 请记住,我还不是专家。

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

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

例如,产品域对象应该暴露一些方法,一些数据validation,一些真实的业务逻辑。

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

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

 ProductBLL productBll = new ProductBLL(); List productList = productBll.GetAllProducts(); 

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

您的应用程序代码通常希望使用 ,而不是任何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 GetList(string sp, Action addParameters) { List lp = new List(); // DAL.Retrieve yields an iEnumerable (optional addParameters callback) // public static IEnumerable Retrieve(string StoredProcName, Action addParameters) foreach (var dr in DAL.Retrieve(sp, addParameters) ) { lp.Add(new Product(dr)); } return lp; } static public List AllProducts() { return GetList("sp_AllProducts", null) ; } static public List AllProductsStartingWith(string str) { return GetList("sp_AllProductsStartingWith", cm => cm.Parameters.Add("StartsWith", str)) ; } static public List AllProductsOnOrder(Order o) { return GetList("sp_AllProductsOnOrder", cm => cm.Parameters.Add("OrderId", o.OrderId)) ; } } 

然后,您可以将明显的部分移动到DAL中。 DataRecords充当您的DTO,但它们非常短暂 – 它们的集合从未真正存在过。

这是一个静态的SqlServer的DAL.Retrieve(你可以看到它很简单,可以将它改为使用CommandText); 我有一个版本,它封装了连接字符串(所以它不是一个静态方法):

  public static IEnumerable SqlRetrieve(string ConnectionString, string StoredProcName, Action 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; } } } 

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

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

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

你想要考虑添加的东西:validation,属性更改通知,数据绑定等…在多个类(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 exceptions = new List(); SummaryValidate(exceptions,memberName); if (exceptions.Count != 0) throw new CompositeException(exceptions); } public string Validate(string memberName) { List exceptions = new List(); 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 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 GetAll() { // possibly use some cache (optional) List products = new List(); // 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); } 

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

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

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