【问题标题】:Business objects and multiple data sources业务对象和多个数据源
【发布时间】:2016-08-23 19:08:30
【问题描述】:

在我的 WPF 应用程序中,我创建了两个独立的项目:UI(带有 XAML 和 ViewModel)和“核心”(它具有人们所谓的“域对象”或“业务对象”——代表离散概念的对象在我的用例中)。我知道这是一个很好的做法。

但我的许多业务对象都与多个数据源交互。例如,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) - 我的现实生活中有超过六个这样的参数)。

在类似的问题上,答案涉及 SOLID、YAGNI、用于 CRUD 的存储库等。我重视这些原则,但我无法从中获得实用的设计。例如,Web 服务并不是真正的 CRUD-y。我是否有一个“存储库”,以便我可以在单元测试期间将其切换出来?文件系统呢?

TL;DR - 该代码有什么问题?

感谢您的指导。谢谢!

【问题讨论】:

    标签: c# wpf oop design-patterns


    【解决方案1】:

    如果不使用一些高级单元测试工具(例如 Stubs:https://msdn.microsoft.com/en-us/library/ff798446.aspx),静态方法和 I/O 函数自然很难测试。您面临的问题是您有一个调用两个 I/O 函数的静态方法。目标是将它们解耦。

    我要做的第一件事是将您的 GetById 方法重构为工厂类。您可以通过将数据库和文件系统 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")
    
        }
    }
    

    【讨论】:

      【解决方案2】:

      如上一个答案所述,仅适用于测试您的业务类并仅在客户端代码上捕获来自基础架构、数据库或 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");
          }
      }
      

      使用一些 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);
          }    
      }
      

      结果如下:

      亲切的问候!

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-04-12
        • 2010-10-31
        • 1970-01-01
        • 2010-11-18
        • 1970-01-01
        • 2010-11-02
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多