3

去年左右我一直在学习 C#,并尝试在此过程中整合最佳实践。在 StackOverflow 和其他网络资源之间,我认为我在正确分离我的关注点方面处于正确的轨道上,但现在我有一些疑问,并想确保在我将整个网站转换到这个新网站之前我走的是正确的道路建筑学。

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

我有以下项目,它们位于单独的命名空间和设置中,因此 UI 层只能看到 DTO 和业务层,而数据层只能从业务层看到。这是一个简单的例子:

产品DTO.cs

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

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

产品BLL.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
    }
}

产品DAL.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?请告诉我什么是错的,什么是对的。请记住,我还不是专家。

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

4

4 回答 4

2

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

  • 使 DTO 对象成为您的域对象(只需将其称为“产品”)
  • IsValidProductId 然后可以在产品上,当调用 setter 时,您可以验证它是否有效,如果不是则抛出
  • 实施一些关于名称的规则
  • 如果还有其他与 Product 交互的对象,我们可以讨论更多有趣的事情
于 2011-02-28T20:35:26.317 回答
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);
}
于 2011-02-28T21:17:15.703 回答
1

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

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

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

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

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

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

您的应用程序代码通常希望使用<Product>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;
            }
        }
    }

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

于 2011-02-28T20:27:00.850 回答
1

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

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

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

于 2011-02-28T21:01:16.410 回答