【问题标题】:Is this a proper implementation of n-layer architecture?这是 n 层架构的正确实现吗?
【发布时间】:2011-07-06 01:11:24
【问题描述】:

在过去一年左右的时间里,我一直在学习 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
    }

}

在我的 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 传递回我的 UI?请告诉我什么是错的,什么是对的。请记住,我还不是专家。

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

【问题讨论】:

  • 在我的工作中,我们维护了一个具有惊人相似架构的项目,并且知道我现在所知道的,我会尝试吸收它并使用 ORM(我喜欢 NHibernate)。我可能有偏见,但是当你开始像这样构建你的架构时,你开始拥有只将信息传递到下一层的类。 NHibernate 有非常好的查询 API,它们会给你带来很多好处,而且没有规定说你必须拥有外键才能使用 NHibernate(尽管它们显然很有帮助)。
  • 在我没有真正掌握 c#/ASP.net 的情况下尝试学习 NHibernate 比我现在想要的要多一点。另外,数据库表名和字段名都是全面的。感谢您的评论。

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


【解决方案1】:

贫血域是指产品或其他类除了数据设置器和获取器之外没有真正实现任何东西 - 没有域行为。

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

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

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

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

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

您的应用程序代码通常希望使用&lt;Product&gt;s,而不是任何 BLL 或 DTO 或其他任何东西。这些是实现类。它们不仅对应用程序程序员的思想水平意义不大,对表面上理解问题领域的领域专家也意义不大。因此,如果您明白我的意思,它们应该只在您处理管道时才可见,而不是在设计浴室时可见。

我将我的 BLL 对象命名为业务域实体的名称。 DTO 在业务实体和 DAL 之间是内部的。当域实体只做 DTO 之外的任何事情时 - 这就是它贫血的时候。

另外,我要补充一点,我经常忽略显式 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;
            }
        }
    }

稍后您可以继续使用成熟的框架。

【讨论】:

  • 是的,我已经知道如何使用 Google...寻找一些可以帮助我前进的答案。
  • @Developr 我不确定你的意思是什么——你只介绍了一个类并省略了业务逻辑,但表明有些存在——因此它可能不是真的贫血,具体取决于你遗漏了什么。
  • @Cade Roux - 保存、获取、更新、验证等
  • 我确实理解您所说的仅使用 Product 的意思,但是我需要与我的 DTO 为我的 BLL 重复的几乎相同的属性,对吗?实体/域对象如何从 DAL 返回自身?或者那是一个单独的服务类?我只需要一个可以重复和整合的好的具体方法。
  • @Developr 它业务逻辑,所以看起来 Product 不是被认为是贫血的好候选人。
【解决方案2】:

凯德有一个很好的解释。为了避免贫血域模型,您可以考虑做一些事情:

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

【讨论】:

  • 那么当我需要一个包含 5,000 多个项目的列表时,我真的想要承担 5000 多个 BLL 验证副本等的成本吗?另外,为什么我要将具有其他方法的对象传递给我的 DAL?我不应该只是传递数据吗?我的 BLL 是否应该成为产品,然后使用我的 DTO 在它和 DAL 之间传递数据?
  • @Developr 如果您需要 5000 多个项目的列表 - 那是另一个问题。在这种情况下,您需要查看将对 5000 个项目进行操作的进程,并确定它是否可以更接近数据库(即在 SQL 中)完成,如果不能,该操作是否能够对对象进行操作?轻量级版本(我称之为摘要),可能用于下拉或查找缓存(您只需要 ID 和描述)。域模型是包含整个问题域的类网络。所以一个班级真的不足以说设计糟糕/乏力。
  • 您担心运行哪些验证?它有比你的 DAL 需要更多的方法,但那又如何呢?如果你愿意,你可以为它制作一个仅限 DAL 的接口。 “我不应该只是传递数据吗?” => 不,你想要有行为的对象。
  • @Alex Lo - 所以,基本上,我需要结合我的 BLL 和 DTO?或者您是说我应该将我的 DTO 重命名为 Products 并为其添加一些方法?那么我怎么知道 BLL 中会使用哪些方法?
  • @Cade Roux - 我还有很多其他类,就像那些......大多数是通过数据库表,但有些是相互连接的。
【解决方案3】:

其他人对使用 ORM 的看法 - 随着您的模型扩展,如果没有 ORM,您将有很多代码重复。但我想评论你的“5,000 怎么样”的问题。

复制一个类不会创建其方法的 5,000 个副本。它只创建数据结构的副本。在域对象中拥有业务逻辑不会降低效率。如果某些业务逻辑不适用,那么您可以创建子类来装饰对象以用于特定目的,但这样做的目的是创建与您的预期用途相匹配的对象,而不是效率。贫乏的设计模型效率并不高。

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

【讨论】:

  • 我应该将 get all 列为 get many...在我的实际代码中,我使用 ROW_Number 和 CTE 来进行分页等。
  • 我读到的关于 nHibernate 的所有内容都说它非常慢,我真的很想学习最好的方法,而不仅仅是将它传递给工具。该网站非常大(想想拥有 1500 种产品的商店以及带有 cmets、论坛等的流式视频)和相对较高的流量(每月最多 50 万个独立用户)
  • OK - 但不返回列表,返回 IEnumerable。它更加灵活,否则每次返回列表时,您将遍历它两次:一次是在创建时,然后是在类外使用时。如果你真的需要一个列表,那么你可以说List&lt;T&gt; myList = new List&lt;T&gt;(someEntityThatReturnsIEnumerable&lt;T&gt;)——与List&lt;T&gt; mylist = someEntityThatReturnsList相比,这可能是相同的循环循环@
  • 我没有亲自使用过 nHibernate,所以我不能说。但请记住,与编写代码相比,增加 CPU 能力的成本更低。即使按照你所说的规模,我敢打赌你不会有太多麻烦。但是当你开始开发你的模型时,例如查看 Simon Mourier 的答案(这是一个很好的 模型),想想在每个属性中重复了多少代码,用于验证、数据库操作、异常处理。至少将属于映射一部分的通用功能封装到一个类中,这确实是一个非常基本的 ORM 实现。
【解决方案4】:

您要考虑添加的内容:验证、属性更改通知、数据绑定、 等等... 将每个类分为多个类(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);
}

【讨论】:

  • 所以产品可以自己退货吗?为什么要通过读者,即使它是一个 IDataReader,在我看来也是如此。不幸的是,我只能听懂你所说的大约 1/4...
  • @Developr - 是的,在这种情况下,产品返回自己。为什么不? .NET Framework 本身就有很多这样的例子。
  • 我不同意让对象处理自己的持久性。在我的建议中,我建议对象持久性由另一个类完成 - 如果您确实过渡到使用 ORM,这将有所帮助,因为原始对象本身仍然有用。
  • @Alex - 好吧,我相信有些人会不同意 :-) 我不需要任何 ORM。
  • @Simon,是的,我确信,对于小问题,我确信它运作良好。 OP 暗示在某些时候使用 ORM,所以我提到这是这种方法的一个弱点。您如何处理使用您的方案引用其他对象的对象?
猜你喜欢
  • 1970-01-01
  • 2012-02-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-11
相关资源
最近更新 更多