[英]Business objects and multiple data sources
在WPF应用程序上,我创建了两个单独的项目:UI(带有XAML和ViewModels)和“ Core”(人们称之为“域对象”或“业务对象”的对象),这些对象代表了我所使用的离散概念案例)。 我知道这是一个好习惯。
但是我的许多业务对象都与多个数据源进行交互。 例如,一个Document
对象可能包含来自数据库以及来自文件的数据。
然后,您可以使用Document
“执行”的某些操作(我作为Document
方法实现)涉及其他资源。 例如, Document.SubmitForProcessing()
调用Web服务。
所以我这样写:
public class Document
{
public string Name { get; set; }
public string FilePath { get; set; }
public string FileData { get; set; }
private Document() { }
public static Document GetByID(int documentID, string databaseConnectionString, string baseFilePath)
{
// Using Dapper
Document newDoc = db.Query<Document>("SELECT Name, FilePath FROM Documents WHERE ID = @pID", new { pID = documentID });
newDoc.FileData = File.ReadAllText(Path.Combine(basePath, newDoc.FilePath));
return newDoc;
}
public void SubmitForProcessing(IWebService webService)
{
webService.ExecuteFoo(this.Name, this.FileData);
}
public void DoBusinessStuff()
{
this.FileData = this.FileData.Replace("foo", "bar");
}
}
毋庸置疑,这真的很快就令人讨厌,并且很难针对它编写测试。
因此,我阅读了有关依赖项注入和存储库模式的信息。 但是我不确定在这种情况下如何正确执行。 我是否为每个数据源都有单独的存储库类,然后是否有某种DocumentFactory
或某种访问单独存储库并将Document对象缝合在一起的东西? 还是有更简单的方法?
我主要关心的是使代码易于测试,这样我就可以编写一些单元测试,而无需模拟整个数据库和文件系统,而且还可以停止向我拥有的每个工厂方法(例如, GetByID(int documentID, string databaseConnectionString, string baseFilePath)
-我的现实生活中有超过6个这样的参数)。
在类似的问题上,答案涉及诸如SOLID,YAGNI,用于CRUD的存储库之类的东西。我重视这些原则,但是我很难从中获得实用的设计。 例如,Web服务并不是真正的CRUD-y。 我是否有一个“存储库”,以便可以在单元测试期间将其切换出来? 那文件系统呢?
TL; DR-该代码有什么问题?
指导表示赞赏。 谢谢!
如果您不使用某些高级单元测试工具(例如,存根: https : //msdn.microsoft.com/zh-cn/library/ff798446.aspx ),则静态方法和I / O函数自然很难进行测试。 您面临的问题是您有一个调用两个I / O函数的静态方法。 目的是使它们解耦。
我要做的第一件事是将GetById方法重构为Factory类。 您可以通过将数据库和文件系统I / O实现作为接口传递来创建工厂类。 使用接口的优点是它允许您模拟I / O行为。 对于像下面这样的简单接口,我什至可以简单地在测试代码中实现它们,而无需任何模拟。 这样,您可以将GetById方法的业务逻辑隔离到Factory类,并且不必费心测试I / O本身,因为它是由数据库提供程序和win32 api完成的。 这就是您的全部需求。
class Document
{
public string FileData { get; set; }
public string FileRelativePath { get; set; }
}
interface IDocumentRepository
{
Document Get(int id);
}
abstract class DocumentFactory
{
public abstract Document Create(int docId);
}
interface IFileStore
{
string Read(string fileName);
}
class ConcreteDocumentFactory : DocumentFactory
{
private IDocumentRepository _db;
private IFileStore _fileStore;
public ConcreteDocumentFactory(IDocumentRepository db, IFileStore fileStore)
{
_db = db;
_fileStore = fileStore;
}
public override Document Create(int docId)
{
Document newDoc = _db.Get(docId);
newDoc.FileData = _fileStore.Read(newDoc.FileRelativePath);
return newDoc;
}
}
/////// Test Code Below
[TestFixture]
class TestClass
{
class TestFriendlyFileStore : IFileStore
{
public string Read(string fileName)
{
if (fileName == "sample.txt")
return "Some File Content";
throw new Exception("Not good file name.");
}
}
class TestFriendlyDocRepo : IDocumentRepository
{
public Document Get(int id)
{
if (id != 999)
return new Document() {FileRelativePath = "sample.txt"};
throw new Exception("Not good id.");
}
}
[Test]
public void Test()
{
var concreteDocFactory = new ConcreteDocumentFactory(new TestFriendlyDocRepo(), new TestFriendlyFileStore());
var doc = concreteDocFactory.Create(999);
Assert.AreEqual(doc.FileData == "Some File Content")
}
}
如上一个答案所述,它仅适用于测试您的业务类,并且仅捕获来自基础结构,数据库或Web服务的预期异常(仅在客户端代码上调用),因此,我建议您设计与基础结构无关的软件以使其具备坚固且可测试:
public class Document : IAcceptDocumentVisitor
{
public int Id { get; private set; }
public string Name { get; private set; }
public string FilePath { get; private set; }
public string FileData { get; private set; }
public Document(int id, string name, string filePath, string fileData)
{
Id = id;
Name = name;
FilePath = filePath;
FileData = fileData;
}
/// <summary>
/// This method replace SubmitForProcessing
/// </summary>
/// <param name="visitor"></param>
public void Accept(IDocumentVisitor visitor)
{
if (visitor == null) throw new ArgumentNullException(nameof(visitor));
visitor.Visit(Name, FileData);
}
public void ReplaceFileData(string fileData, Action onSuccess)
{
//Business valdation
var validate = true;
//Business valdation
if (!validate) return;
FileData = fileData;
onSuccess();
}
}
public interface IAcceptDocumentVisitor
{
void Accept(IDocumentVisitor visitor);
}
public interface IDocumentVisitor
{
void Visit(string name, string fileData);
}
public class FakeWebServiceVisitor : IDocumentVisitor
{
public void Visit(string name, string fileData)
{
Name = name;
FileData = fileData;
}
public string FileData { get; set; }
public string Name { get; set; }
}
public class WebServiceVisitor : IDocumentVisitor
{
public void Visit(string name, string fileData)
{
//Call web service
//webService.ExecuteFoo(this.Name, this.FileData);
}
}
public interface IDocumentReader
{
Document GetById(int id);
}
public class DocumentDbReader : IDocumentReader
{
public Document GetById(int id)
{
//Get from database
//Document newDoc = db.Query<Document>("SELECT Name, FilePath FROM Documents WHERE ID = @pID", new { pID = documentID });
return new Document(id, "Name", "Path", "Data");
}
}
Usign的一些OOP技术和方法,例如存储库,访问者,CQRS和solid,您可以享受诸如SOLID代码之类的好处,并且还将开始使您的代码可测试:
[TestClass]
public class DocumentSpecs
{
public string Name = "Name";
public string FilePath = "Path";
public string FileData = "Data";
[TestMethod]
public void AcceptVisitorCorrectly()
{
//Arrange
var document = new DocumentDbReader().GetById(0);
var visitor = new FakeWebServiceVisitor();
//Act
document.Accept(visitor);
//Assert
Assert.AreEqual(FileData, visitor.FileData);
Assert.AreEqual(Name, visitor.Name);
}
[TestMethod]
public void ReplaceFileDataCorrectly()
{
//Arrange
var successActionCalled = false;
var expectedFileData = Guid.NewGuid().ToString();
var document = new DocumentDbReader().GetById(0);
//Act
var documentInitialData = document.FileData;
document.ReplaceFileData(expectedFileData, () => successActionCalled = true);
//Assert
Assert.IsTrue(successActionCalled);
Assert.AreEqual(expectedFileData, document.FileData);
Assert.IsFalse(documentInitialData == document.FileData);
}
}
结果如下:
亲切的问候!
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.