我是否在这种体系结构上打破了面向对象的实践?


23

我有一个Web应用程序。我不认为这项技术很重要。该结构是一个N层应用程序,如左图所示。共3层。

UI(MVC模式),业务逻辑层(BLL)和数据访问层(DAL)

我的问题是我的BLL非常庞大,因为它具有通过应用程序事件调用的逻辑和路径。

通过应用程序的典型流程可能是:

在UI中触发的事件遍历BLL中的方法,执行逻辑(可能在BLL的多个部分中),最终执行DAL,返回到BLL(可能还有更多逻辑),然后向UI返回一些值。

此示例中的BLL非常繁忙,我正在考虑如何将其拆分。我也有自己不喜欢的逻辑和对象。

在此处输入图片说明

右边的版本是我的努力。

逻辑仍然是应用程序的UI和DAL之间的流动,但也有可能没有属性...只有方法(在这一层中的大多数类可能可能是静态的,因为他们不存储任何状态)。Poco层是存在具有属性的类的地方(例如Person类,其中会有名称,年龄,身高等)。这些与应用程序的流程无关,它们仅存储状态。

流可以是:

甚至从UI触发,并将一些数据传递到UI层控制器(MVC)。这将转换原始数据并将其转换为poco模型。然后将poco模型传递到逻辑层(即BLL),最后传递到命令查询层,可能会在途中被操纵。Command查询层将POCO转换为数据库对象(几乎是同一件事,但是一个是为持久性而设计的,另一个是为前端设计的)。存储该项目,并将数据库对象返回到“命令查询”层。然后将其转换为POCO,在其中返回到逻辑层,可能进行进一步处理,然后最终返回到UI

共享逻辑和接口是我们可能拥有持久数据的地方,例如MaxNumberOf_X和TotalAllowed_X以及所有接口。

共享逻辑/接口和DAL都是体系结构的“基础”。这些人对外界一无所知。

除了共享的逻辑/接口和DAL,其他一切都与poco有关。

流程仍然与第一个示例非常相似,但是它使每一层都对一件事情负责(无论是状态,流程还是其他)……但是我是否用这种方法打破了OOP?

演示Logic和Poco的示例可能是:

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method1(PocoB pocoB) 
    { 
        return cmdQuery.Save(pocoB); 
    }

    /*This has no state objects, only ways to communicate with other 
    layers such as the cmdQuery. Everything else is just function 
    calls to allow flow via the program */
    public PocoA Method2(PocoB pocoB) 
    {         
        pocoB.UpdateState("world"); 
        return Method1(pocoB);
    }

}

public struct PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     public int DataC {get;set;}

    /*This simply returns something that is part of this class. 
     Everything is self-contained to this class. It doesn't call 
     trying to directly communicate with databases etc*/
     public int GetValue()
     {

         return DataB * DataC; 
     }

     /*This simply sets something that is part of this class. 
     Everything is self-contained to this class. 
     It doesn't call trying to directly communicate with databases etc*/
     public void UpdateState(string input)
     {        
         DataA += input;  
     }
}

正如您当前所描述的,我认为您的体系结构没有任何根本错误。
罗伯特·哈维

19
您的代码示例中没有足够的功能细节,无法提供任何进一步的见解。Foobar示例很少提供足够的说明。
罗伯特·哈维


4
我们可以找到这个问题的一个更好的标题,因此可以在网上更容易找到?
SonerGönül

1
只是为了学究:一层和一层不是同一件事。“层”表示部署,“层”表示逻辑。您的数据层将被部署到服务器端代码层和数据库层。您的UI层将同时部署到Web客户端和服务器端代码层。您显示的架构是3层架构。您的层是“ Web客户端”,“服务器端代码”和“数据库”。
洛朗LA RIZZA

Answers:


54

是的,您很可能会突破核心 OOP概念。但是,不要感到不好,人们一直在这样做,这并不意味着您的体系结构是“错误的”。我会说它可能比适当的OO设计更难以维护,但这是很主观的,无论如何都不是您的问题。(是我的文章,通常批评n层架构)。

推理:OOP的最基本概念是数据和逻辑形成一个单元(一个对象)。尽管这是一个非常简单和机械的陈述,但即使如此,您的设计也没有真正遵循它(如果我正确理解的话)。您已经很清楚地将大多数数据与大多数逻辑分离了。例如,具有无状态(类似于静态)的方法称为“过程”,并且通常与OOP相反。

当然总会有例外,但是这种设计通常会违反这些规定。

同样,我想强调“违反OOP”!=“错误”,因此这不一定是价值判断。这完全取决于您的体系结构约束,可维护性用例,需求等。


9
打个招呼,这是一个很好的答案,如果我自己编写,我会复制并粘贴,但还要补充一点,如果您发现自己不编写OOP代码,也许您应该考虑使用非OOP语言带有很多额外的开销,如果您不使用它,您可以不用做这些
TheCatWhisperer

2
@TheCatWhisperer:现代企业体系结构并没有完全放弃OOP,只是有选择地(例如对于DTO)。
罗伯特·哈维

@RobertHarvey同意,我的意思是,如果您在设计中几乎不使用OOP
TheCatWhisperer

@TheCatWhisperer许多的如C#的接力的优点并不一定是该语言的OOP部分,但可用诸如图书馆,Visual Studio中,存储器管理等的支持

@Orangesandlemons我确定那里还有很多其他受支持的语言...
TheCatWhisperer

31

函数式编程的核心原则之一是纯函数。

面向对象编程的核心原则之一是将函数与它们所作用的数据放在一起。

当您的应用程序必须与外界通信时,这两个核心原则都将消失。实际上,只有在系统中经过特别准备的空间中,您才能忠于这些理想。并非代码的每一行都必须满足这些理想。但是,如果您的代码中没有一行代码符合这些理想,那么您就不能真正宣称使用OOP或FP。

因此,只有数据随处可见的“对象”是可以的,因为您需要它们跨越一个边界,而您根本无法重构该边界来移动感兴趣的代码。只知道那不是面向对象的。那是现实。OOP是指,一旦进入该边界,您便会将对该数据进行操作的所有逻辑汇总到一个位置。

并非您也必须这样做。对所有人来说,OOP并不是万能的。就是这样。只是不要声称没有遵循OOP的东西,否则您将使试图维护您的代码的人感到困惑。

您的POCO似乎具有业务逻辑,因此我不必担心贫血。让我担心的是,它们似乎都非常不稳定。请记住,getter和setter并不提供真正的封装。如果您的POCO前往该边界,则可以。只是了解一下,这并不能为您带来真正封装的OOP对象的全部好处。有人称其为数据传输对象或DTO。

我成功使用的一个技巧是制作吃掉DTO的OOP对象。我将DTO用作参数对象。我的构造函数从中读取状态(作为防御性副本)并将其扔到一边。现在,我有了DTO的完全封装且不变的版本。与此数据有关的所有方法都可以移到此处,只要它们在该边界的这一侧即可。

我不提供获取器或设置器。我跟着说,不要问。您调用我的方法,它们就会去做需要做的事情。他们甚至可能不告诉您他们做了什么。他们只是这样做。

现在,最终某个地方的某个地方将进入另一个边界,而这一切又再次崩溃了。没关系。旋转另一个DTO并将其扔在墙上。

这就是端口和适配器体系结构的本质所在。我从功能的角度看过它。也许您也会感兴趣。


5
getter和setter不能提供真正的封装 ”-是的!
,蜘蛛侠鲍里斯(Boris)

3
@BoristheSpider-getter和setter绝对提供封装,它们根本不适合您狭义的封装定义。
达沃·扎德拉(DavorŽdralo)

4
@DavorŽdralo:它们有时作为解决方法很有用,但从本质上讲,getter和setter破坏了封装。提供一种获取和设置内部变量的方法与负责自己的状态并对其采取行动是相反的。
H

5
@cHao-你不明白什么是吸气剂。这并不意味着返回对象属性值的方法。这是一个常见的实现,但是它可以从数据库中返回一个值,通过http请求它,或者随时进行计算。就像我说的那样,仅当人们使用自己的狭窄(和错误)定义时,getter和setter才会破坏封装。
DavorŽdralo

4
@cHao-封装意味着您隐藏了实现。那就是封装的东西。如果您在Square类上使用“ getSurfaceArea()” getter,则不知道表面积是不是一个字段,它是动态计算的(返回高度*宽度)还是某些第三种方法,因此可以更改内部实现任何时候都可以,因为它是封装的。
DavorŽdralo

1

如果我正确阅读了您的解释,则您的对象看起来像这样:(有点儿无上下文)

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method(PocoB pocoB) { ... }
}

public class PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     ... etc
}

因为您的Poco类仅包含数据,而Logic类包含对数据进行操作的方法。是的,您违反了“经典OOP”的原则

同样,很难从您的概括性描述中看出来,但我冒昧将您编写的内容归类为“贫血领域模型”。

我认为这不是特别糟糕的方法,如果您将Poco视为结构,也不会从更具体的意义上破坏OOP。因为您的对象现在是LogicClass。的确,如果您使Pocos不可变,那么该设计将被认为具有实用性。

但是,当您引用Shared Logic,Pocos时,它们几乎是一样的,而且都是静态的,我开始担心您的设计细节。


我已添加到我的帖子中,基本上是在复制您的示例。抱歉,我们一开始不清楚
MyDaftQuestions

1
我的意思是,如果您告诉我们应用程序将执行什么操作,则编写示例会更容易。除了LogicClass之外,您还可以使用PaymentProvider或其他任何方式
Ewan

1

我在您的设计中看到了一个潜在的问题(这是很常见的)-我遇到的一些绝对最糟糕的“ OO”代码是由将“数据”对象与“代码”对象分开的体系结构引起的。这是噩梦级的东西!问题在于,当您要访问数据对象时,在业务代码中的每个地方,您都倾向于将其直接内联编码(您不必这样做,您可以构建实用程序类或其他函数来处理它,但这就是我已经看到随着时间的推移反复发生)。

访问/更新代码通常不会被收集,因此您到处都会得到重复的功能。

另一方面,这些数据对象很有用,例如作为数据库持久性。我尝试了三种解决方案:

将值复制进出“真实”对象并丢弃数据对象是很麻烦的(但如果您想那样做,可能是有效的解决方案)。

向数据对象添加数据整理方法虽然可以,但是它会使大型的数据对象杂乱无章。由于许多持久性机制都需要公共访问器,因此它也使封装变得更加困难。当我这样做时,我并不喜欢它,但这是一个有效的解决方案

最适合我的解决方案是“包装器”类的概念,该类封装“数据”类并包含所有数据整理功能-然后,我根本不会公开数据类(甚至不包括setter和getters)除非绝对必要)。这消除了直接操作对象的诱惑,并迫使您改为向包装程序添加共享功能。

另一个优点是,您可以确保数据类始终处于有效状态。这是一个快速的伪代码示例:

// Data Class
Class User {
    String name;
    Date birthday;
}

Class UserHolder {
    final private User myUser // Cannot be null or invalid

    // Quickly wrap an object after getting it from the DB
    public UserHolder(User me)
    {
        if(me == null ||me.name == null || me.age < 0)
            throw Exception
        myUser=me
    }

    // Create a new instance in code
    public UserHolder(String name, Date birthday) {
        User me=new User()
        me.name=name
        me.birthday=birthday        
        this(me)
    }
    // Methods access attributes, they try not to return them directly.
    public boolean canDrink(State state) {
        return myUser.birthday.year < Date.yearsAgo(state.drinkingAge) 
    }
}

请注意,您没有在所有代码中的不同区域进行年龄检查,并且您也不会尝试使用它,因为您甚至无法弄清楚生日是什么(除非您在其他方面需要它,否则在这种情况下,您可以添加它)。

我倾向于不只是扩展数据对象,因为您失去了这种封装和安全性的保证,这时您最好将方法添加到数据类中。

这样,您的业务逻辑就不会在其中散布一堆数据访问垃圾/迭代器,它变得更具可读性,并且冗余性降低。我还建议出于相同的原因养成总是包装集合的习惯-使循环/搜索构造脱离业务逻辑,并确保它们始终处于良好状态。


1

切勿更改代码,因为您认为或有人告诉您这不是或不是。如果它给您带来了问题,请更改代码,并且您想出了一种避免这些问题而不创建其他问题的方法。

因此,除了您不喜欢事物之外,您还希望投入大量时间进行更改。写下您现在遇到的问题。写下您的新设计如何解决问题。找出改进的价值和进行更改的成本。然后-这是最重要的-确保您有时间完成这些更改,否则您将以这种状态结束一半,以该状态结束一半,这是最糟糕的情况。(我曾经在一个项目中使用13种不同类型的琴弦,并且进行了三项可识别的半精打细工作来标准化一种类型)


0

“ OOP”类别比您所描述的更大,更抽象。它并不关心所有这些。它关心明确的责任,凝聚力,耦合。因此,在您要询问的层面上,询问“ OOPS练习”没有多大意义。

也就是说,以您的示例为例:

在我看来,MVC的含义存在误解。您正在将UI称为“ MVC”,与业务逻辑和“后端”控件分开。但是对我来说,MVC包含整个Web应用程序:

  • 模型-包含业务数据+逻辑
    • 数据层作为模型的实现细节
  • 视图-UI代码,HTML模板,CSS等
    • 包括客户端方面(例如JavaScript)或“一页” Web应用程序的库等。
  • 控制-所有其他部分之间的服务器端粘合
  • (这里有一些扩展名,例如ViewModel,Batch等。

这里有一些非常重要的基本假设:

  • 一个模型类/对象从来没有任何在所有的任何其他部分的知识(查看,控制,...)。它从不调用它们,它不假定被它们调用,它没有会话属性/参数或沿此方向的任何其他内容。它是完全孤独的。在支持此功能的语言(例如Ruby)中,您可以启动手动命令行,实例化Model类,使用它们来满足您的内心需求,并可以执行所有操作事情而无需任何Control或View实例或任何其他类别。最重要的是,它不了解会话,用户等。
  • 除了通过模型,没有任何东西可以触及数据层。
  • 该视图仅在模型上轻轻触摸(显示东西等),而没有其他任何东西。(请注意,“ ViewModel”是一个很好的扩展,它是特殊类,可以以复杂的方式对数据进行更为实质性的处理,因此不适用于“模型”或“视图”-这是消除/避免膨胀的好方法纯模型)。
  • 控件尽可能轻巧,但是它负责将所有其他参与者聚集在一起,并在它们之间进行转移(即,从表单中提取用户条目并将其转发到模型,将异常从业务逻辑转发到有用的控件)用户的错误消息等)。对于Web / HTTP / REST API等,所有授权,安全性,会话管理,用户管理等均在此处(仅在此处)进行。

重要的是:UI是MVC 的一部分。并非相反(如您的图中所示)。如果您接受这一点,那么胖模型实际上是非常好的-前提是它们确实不包含不应包含的内容。

请注意,“胖模型”意味着所有业务逻辑都在“模型”类别中(包,模块,无论您选择的语言是什么名称)。显然,根据您提供的任何编码指南,各个类都应以一种很好的方式进行OOP结构化(即,每个类或方法的最大代码行数等)。

还应注意,如何实现数据层会产生非常重要的后果。尤其是模型层是否可以在没有数据层的情况下运行(例如,用于单元测试,或用于开发人员笔记本电脑上的廉价内存数据库,而不是昂贵的Oracle数据库或您拥有的任何数据库)。但这确实是我们目前正在研究的架构级别的实现细节。显然,在这里您仍然需要分开,即我不想看到直接将纯域逻辑与数据访问交织在一起的代码,将它们紧密地耦合在一起。另一个问题的话题。

让我们回到您的问题:在我看来,您的新架构与我描述的MVC方案之间存在很大的重叠,因此您并没有采取完全错误的方式,但是您似乎正在重塑一些东西,或使用它,因为您当前的编程环境/库建议这样做。很难告诉我。因此,对于您打算做的是好是坏,我无法给您确切的答案。您可以通过检查每个“事物”是否都有一个完全负责的类来进行查找。一切是否具有高度凝聚力和低耦合性。这为您提供了一个很好的指示,并且在我看来,这足以构成一个好的OOP设计(或者,如果可以的话,它是一个很好的基准)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.