我在过去一年左右的时间里一直在学习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?请告诉我什么是错的,什么是对的。请记住,我还不是专家。
我想实现我的架构的接口,但我仍然在学习如何做到这一点。
答案 0 :(得分:2)
答案 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
并提供用于子集化数据的方法。如果您开始考虑使用内存中的完整数据列表,那么随着数据的增长,您将面临严重的性能问题。