N-Tier架构 - VB.NET中包含多个项目的结构

7
我希望能就以下情况提供一些建议:我将拥有一个Windows应用程序和一个Web应用程序(表示层),它们都会访问一个共同的业务层。业务层将查看配置文件以找到dll(数据层)的名称,然后在运行时创建对其的引用(这是最佳方法吗?)。
之所以在运行时创建与数据访问层的引用,是因为应用程序将根据客户使用的第三方会计系统不同而进行接口。因此,我将有一个单独的数据访问层来支持每个会计系统。这些可以是单独的设置项目,每个客户端将使用其中一个,他们不需要在两者之间切换。
项目:
MyCompany.Common.dll - 包含接口,所有其他项目都引用此项目。 MyCompany.Windows.dll - Windows表单项目,引用MyCompany.Business.dll MyCompany.Web.dll - 网站项目,引用MyCompany.Business.dll MyCompany.Busniess.dll - 业务层,引用MyCompany.Data.*(在运行时) MyCompany.Data.AccountingSys1.dll - 会计系统1的数据层 MyCompany.Data.AccountingSys2.dll - 会计系统2的数据层
项目MyCompany.Common.dll将包含所有接口,每个其他项目都将引用此项目。
Public Interface ICompany
    ReadOnly Property Id() as Integer
    Property Name() as String
    Sub Save()
End Interface

Public Interface ICompanyFactory
    Function CreateCompany() as ICompany
End Interface

项目 MyCompany.Data.AccountingSys1.dllMyCompany.Data.AccountingSys2.dll 将包含以下类:

Public Class Company
    Implements ICompany

    Protected _id As Integer
    Protected _name As String

    Public ReadOnly Property Id As Integer Implements MyCompany.Common.ICompany.Id
        Get
            Return _id
        End Get
    End Property

    Public Property Name As String Implements MyCompany.Common.ICompany.Name
        Get
            Return _name
        End Get
        Set(ByVal value as String)
            _name = value
        End Set
    End Property

    Public Sub Save() Implements MyCompany.Common.ICompany.Save
        Throw New NotImplementedException()
    End Sub

End Class

Public Class CompanyFactory
    Implements ICompanyFactory

    Public Function CreateCompany() As ICompany Implements MyCompany.Common.ICompanyFactory.CreateCompany
        Return New Company()
    End Function

End Class

项目 MyCompany.Business.dll 负责提供业务规则并从数据层检索数据:

Public Class Companies

    Public Shared Function CreateCompany() As ICompany
        Dim factory as New MyCompany.Data.CompanyFactory
        Return factory.CreateCompany()
    End Function    

End Class

任何意见/建议将不胜感激。
3个回答

5

一些注释。

我建议避免使用MyCompany.Common.dll这样的程序集。这些通常会填满各种无关的东西,然后经常需要更改,导致重新构建所有程序集。

我建议将应用程序名称和公司名称命名为程序集名称。 MyCompany.MyApplication.Business.dllMyCompany.Business.dll更好。这样更容易将应用程序分成子部分,并从多个应用程序重用代码。

最好为您将要拥有的每种实现程序集单独创建一个契约程序集。在您的情况下,我建议按以下方式操作:

MyCompany.MyApplication.Windows-Contract.dll
MyCompany.MyApplication.Windows.dll

MyCompany.MyApplication.Web-Contract.dll
MyCompany.MyApplication.Web.dll

MyCompany.MyApplication.Business-Contract.dll
MyCompany.MyApplication.Business.dll

MyCompany.MyApplication.Data-Contract.dll
MyCompany.MyApplication.Data.AccountingSys1.dll
MyCompany.MyApplication.Data.AccountingSys2.dll

根据您的描述,似乎 AccountingSys1AccountingSys2 组件共享一个常见契约,因此这两个实现组件只需要一个契约组件。
契约组件应该代表您的设计而不是您的实现,并且只有在设计更改时才会发生更改。您应该避免使用任何“重要”的代码(以避免错误),并将代码限制为接口、枚举、异常、属性、事件参数和结构体 - 所有这些都没有“重要”的代码。
在设置程序集引用时,您应该确保程序集只引用契约程序集,如下所示:
Data.AccountingSys1
    Data-Contract

Data.AccountingSys2
    Data-Contract

Business
    Business-Contract
    Data-Contract

Windows
    Windows-Contract
    Business-Contract
    Data-Contract (maybe)

Web
    Web-Contract
    Business-Contract
    Data-Contract (maybe)

作为结果,实现程序集永远不会依赖于其他实现程序集。当一个实现发生变化时,您只需要重新构建一个程序集。
这个规则的例外是创建继承层次结构。例如,您可以创建一个 *.Data.AccountingSys.dll 来定义两个特定会计系统程序集的基类。
如果您能遵循以上所有要求,那么您将需要实现某种依赖注入方法,以能够从合同程序集中的接口创建对象实例。您可以使用现有的 DI 框架或创建第三组包含工厂方法的 *-Factory.dll 程序集。
这种结构的另一个好处是单元测试更简单,并且可以基于合同而不是实现来进行,帮助您编写干净、可测试的代码。
这可能看起来像很多程序集,但是保持代码不创建恶意依赖所获得的好处将显著降低项目变得过于复杂的机会,并在您前进时帮助推动良好的质量。现在稍微付出一些代价,以后就会减少很多痛苦。

+1 你忘了提到,一段时间后,Common库的一半也会变得无用,而且没有人会解开它。你可能还会对我的想法感兴趣(主要在我的评论中),链接为https://dev59.com/ukfRa4cB1Zd3GeqP_8je#2430056。 - Ruben Bartelink
+1 谢谢!我们刚刚实现了类似的东西,很高兴看到其他人也经过深思熟虑并采用了与我们相同的结构。 - Ralph Willgoss
+1,谢谢!我正在使用相同的解决方案结构,但是我的命名方式例如Business-Contract是Business.Abstract。此外,在我的经验中,命名为...Common的项目或程序集随着时间的推移变得充满垃圾。 - Regfor

1

你的一般方法是正确的 :)

你可以考虑将所有接口放在一个单独的程序集(dll)中(严格来说,接口位于业务逻辑和数据访问实现之间 - 它们应该是唯一能够访问接口的东西),但从大局上看,这可能并不是那么重要。

个人而言,我会有一个共享的工厂方法,返回一个对象,并在使用时适当地进行转换。


如果我在数据层和业务层之间有接口的单独程序集,那么表示层如何访问从业务层返回的公司对象的属性。例如(来自表示层):Dim company As ICompany = MyCompany.Business.Companies.CreateCompany()为了使此代码在表示层中工作,需要引用位于接口程序集中的ICompany接口类。 还有其他方法吗? - Craig F
请看一下我的回答,因为我认为我在那里解释了。https://dev59.com/tEzSa4cB1Zd3GeqPoKLz#2467026 - Enigmativity
@focus.nz - 接口的定义不必与其使用的实体/类型的定义位于同一位置,您可以将它们放在不同的(中央)位置(因此可见性更好)。这还取决于您是否想在 DAL 和 BL 之间以及 BL 和 UI 之间使用相同的类型(例如)。 - Adrian K
1
“Adrian K” - 我建议避免使用一个中央位置来定义接口,因为这会对所有其他程序集产生重大依赖。如果您必须更改UI层接口,则还需要重新编译BL和DAL。一对一的方法是最好的选择。 - Enigmativity

1

这是一个非常棒的方法!我在工作中使用过它,在我们的系统中,它已经被证明是可靠的、易于维护的,并且允许我们在需要时快速添加额外的接口(例如当我们需要与另一个我们收购的公司的会计系统进行接口时)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接