通过对象本身的方法还是通过另一个类保存对象?


38

如果要保存和检索对象,是否应该创建另一个类来处理该对象,还是最好在该类本身中进行处理?还是混合两者?

根据OOD范式推荐哪个?

例如

Class Student
{
    public string Name {set; get;}
    ....
    public bool Save()
    {
        SqlConnection con = ...
        // Save the class in the db
    }
    public bool Retrieve()
    {
         // search the db for the student and fill the attributes
    }
    public List<Student> RetrieveAllStudents()
    {
         // this is such a method I have most problem with it 
         // that an object returns an array of objects of its own class!
    }
}

与。(我知道以下内容是推荐的,但是在我看来,这与Student阶级的凝聚力有些矛盾)

Class Student { /* */ }
Class DB {
  public bool AddStudent(Student s)
  {

  }
  public Student RetrieveStudent(Criteria)
  {
  } 
  public List<Student> RetrieveAllStudents()
  {
  }
}

如何混合它们?

   Class Student
    {
        public string Name {set; get;}
        ....
        public bool Save()
        {
            /// do some business logic!
            db.AddStudent(this);
        }
        public bool Retrieve()
        {
             // build the criteria 
             db.RetrieveStudent(criteria);
             // fill the attributes
        }
    }

3
您应该对Active Record模式进行一些研究,例如这个问题这个
Eric King

@gnat,我以为问哪个更好是基于意见,然后添加了“利弊”,并且没有问题就可以删除,但是实际上我的意思是关于OOD范式,这是值得推荐的
Ahmad 2014年

3
没有正确的答案。这取决于您的需求,喜好,类的使用方式以及项目的发展方向。唯一正确的面向对象是,您可以另存为对象。
Dunk 2014年

Answers:


34

单一责任原则关注点分离功能凝聚力。如果您阅读了这些概念,那么您得到的答案是:将它们分开

Student将“ DB”类与“ DB”类分开(或StudentRepository遵循更流行的约定)的简单原因是,您可以更改Student类中存在的“业务规则” ,而不会影响负责持久性的代码,反之亦然。反之亦然。

这种隔离非常重要,不仅在业务规则和持久性之间,而且在系统的许多方面之间,都可以使您以对不相关的模块产生最小影响的方式进行更改(这是最小的,因为有时是不可避免的)。它有助于构建更健壮的系统,在不断变化的情况下更易于维护且更可靠。

通过将业务规则和持久性混合在一起,或者像第一个示例中那样使用单个类,或者将其DB作为的依赖项Student,就可以将两个非常不同的关注点耦合在一起。看起来他们像是在一起的。它们似乎具有凝聚力,因为它们使用相同的数据。但这就是问题:内聚性不能仅由过程之间共享的数据来衡量,您还必须考虑它们所存在的抽象级别。实际上,理想的凝聚力类型描述为:

功能凝聚是指将模块的各个部分归为一组,因为它们都有助于完成模块的单个明确定义的任务。

显然,执行验证Student并保留一段时间不会形成“单个定义明确的任务”。同样,业务规则和持久性机制是系统的两个非常不同的方面,根据良好的面向对象设计的许多原则,应将它们分开。

我建议阅读有关Clean Architecture的文章,观看有关单一责任原则的讨论(使用了非常相似的示例),以及阅读有关Clean Architecture的讨论。这些概念概述了这种分离的原因。


11
嗯,但是Student该类应该正确封装,是吗?那么外部类如何管理私有状态的持久性?封装必须与SoC和SRP保持平衡,但是在不仔细权衡取舍的情况下简单地选择二者之一可能是错误的。解决此难题的可能方法是使用包专用访问器来使用持久性代码。
阿蒙2014年

1
@amon我同意这不是那么简单,但是我相信这只是耦合问题,而不是封装问题。例如,您可以使用用于业务需要的任何数据的抽象方法使Student类变得抽象,然后由存储库实现。这样,数据库的数据结构完全隐藏在存储库实现的后面,因此甚至可以将它封装在Student类中。但是,与此同时,存储库与Student类紧密耦合-如果更改了任何抽象方法,则实现也必须更改。这都是关于权衡的。:)
MichelHenrich 2014年

4
关于SRP使程序更易于维护的说法充其量是可疑的。SRP造成的问题是,它没有一个名称丰富的类的集合,而是在名称中告诉了该类可以做和不能做的所有事情(换句话说,易于理解,这使得易于维护)最终导致数百个/人们需要使用一个词库来选择一个唯一的名称的数千个类,许多名称是相似的,但并不完全相同,并且对所有谷壳进行分类是一件繁琐的事情。IOW,IMO根本无法维护。
Dunk 2014年

3
@Dunk,您好像在说类是唯一可用的模块化单元。在SRP之后,我已经看到并编写了许多项目,并且我同意您的示例确实会发生,但前提是您没有正确使用软件包和库。如果将职责分为多个包,则由于隐含了上下文,因此可以再次自由命名类。然后,要维护这样的系统,所需要做的就是在搜索需要更改的类之前将其钻入正确的包。:)
MichelHenrich 2014年

5
@Dunk OOD原则可以表示为:“原则上,这样做更好,因为X,Y和Z”。这并不意味着应该始终应用它;人们必须务实,并权衡取舍。在大型项目中,其好处通常超过了您所说的学习曲线-但是,当然,对于大多数人而言,这不是现实,因此不应将其广泛应用。但是,即使在较轻的应用程序SRP中,我认为我们都可以同意将持久性与业务规则分开始终会产生超出其成本的收益(可能会丢掉代码)。
MichelHenrich 2014年

10

两种方法都违反了单一责任原则。您的第一个版本将Student类赋予许多职责,并将其与特定的数据库访问技术联系在一起。第二个导致了一个庞大的DB类,它将不仅对学生负责,而且对程序中的任何其他类型的数据对象负责。编辑:您的第三种方法是最糟糕的,因为它在DB类和Student该类之间创建了循环依赖关系。

因此,如果您不打算编写玩具程序,请不要使用它们。相反,StudentRepository假设您将要自行实现CRUD代码,请使用不同的类(例如)来提供用于加载和保存的API。您可能还考虑使用ORM框架,该框架可以为您完成艰苦的工作(并且该框架通常会强制执行一些必须放置Load和Save操作的决策)。


谢谢,我修改了答案,第三种方法呢?无论如何,我认为这里的要点是要使各个层
Ahmad

还有一些建议给我使用StudentRepository而不是DB(不知道为什么!也许又是单项责任),但是我仍然不知道为什么每个班级使用不同的存储库吗?如果它只是一个数据访问层,那么会有更多的静态实用程序功能,可以并在一起,不是吗?只是那时可能会变得如此肮脏和拥挤(我的英语很抱歉)
艾哈迈德2014年

1
@Ahmad:有几个原因,还要考虑一些利弊。这可以填满整本书。这背后的原因归结为程序的可测试性,可维护性和长期可演化性,尤其是当它们具有持久的生命周期时。
布朗

是否有人在数据库技术发生重大变化的项目上工作?(无需大量重写?)我没有。如果是这样,按照此处建议的避免与该数据库绑定的建议进行SRP是否有很大帮助?
user949300

@ user949300:我想我没有清楚地表达自己。学生班级不会与特定的数据库技术联系在一起,而是与特定的数据库访问技术联系在一起,因此它期望像SQL DB这样的持久性。使Student类完全不了解持久性机制的确会使在不同上下文中轻松重用该类。例如,在测试上下文中,或者在对象存储在文件中的上下文中。这确实是我过去经常做的事情。无需更改系统的整个数据库堆栈即可从中受益。
Doc Brown

3

有很多模式可用于数据持久性。有工作单元模式,有存储库模式,还有一些可以使用的其他模式,例如远程外观,等等。

其中大多数都有他们的粉丝和批评家。通常,要选择最适合应用程序的内容并坚持使用(所有方面都有其优点和缺点,即不要同时使用这两种模式……除非您真的很确定)。

附带说明:在您的示例RetrieveStudent中,AddStudent应该是静态方法(因为它们与实例无关)。

具有类内保存/加载方法的另一种方法是:

class Animal{
    public string Name {get; set;}
    public void Save(){...}
    public static Animal Load(string name){....}
    public static string[] GetAllNames(){...} // if you just want to list them
    public static Animal[] GetAllAnimals(){...} // if you actually need to retrieve them all
}

就我个人而言,我只会在相当小的应用程序中使用这种方法,可能是用于个人使用的工具,或者可以可靠地预测到我不会有比保存或加载对象更复杂的用例的应用程序。

另外,个人而言,请参见工作单位模式。当您了解它时,无论大小,它都很好。它得到了许多框架/ API的支持,例如,命名为EntityFramework或RavenDB。


2
关于您的旁注:尽管从概念上讲,应用程序运行时只有一个数据库,但这并不意味着您应该将RetriveStudent和AddStudent方法设置为静态。通常使用接口来创建存储库,以允许开发人员在不影响应用程序其余部分的情况下切换实现。例如,这甚至可以使您在支持不同数据库的情况下分发应用程序。当然,这种逻辑同样适用于除持久性之外的许多其他领域,例如UI。
MichelHenrich 2014年

您绝对正确,我根据原始问题做出了很多假设。
Gerino 2014年

谢谢,我认为最好的方法是使用存储库模式中提到的图层
Ahmad

1

如果这是一个非常简单的应用程序,其中对象或多或少地与数据存储区绑定,反之亦然(即,可以将其视为数据存储区的属性),那么对该类使用.save()方法可能很有意义。

但是我认为那将是非常例外的。

相反,通常最好让类管理其数据和功能(如良好的OO公民),并将持久性机制外部化到另一个类或一组类。

另一个选择是使用一个持久性框架,该框架以声明的方式定义持久性(例如带有批注),但是仍然在外部化持久性。


-1
  • 在该类上定义一个对对象进行序列化的方法,并返回可以放入数据库或文件或任何其他内容的字节序列
  • 具有该对象的构造函数,该构造函数将字节序列作为输入

关于这种RetrieveAllStudents()方法,您的感觉是正确的,确实可能放错了位置,因为您可能有多个不同的学生列表。为什么不简单地将列表保留在Student课堂之外呢?


您知道,如果您知道,我已经在某些PHP框架(例如Laravel)中看到了这种趋势。该模型与数据库绑定,甚至可以返回一系列对象。
艾哈迈德2014年
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.