22

最近我们讨论了类中的数据和行为分离。数据和行为分离的概念是通过将领域模型及其行为放在单独的类中来实现的。
但是,我不相信这种方法的所谓好处。尽管它可能是由“伟大的”创造的(我认为是 Martin Fowler,尽管我不确定)。我在这里举一个简单的例子。假设我有一个 Person 类,其中包含 Person 及其方法(行为)的数据。

class Person
{
    string Name;
    DateTime BirthDate;

    //constructor
    Person(string Name, DateTime BirthDate)
    {
        this.Name = Name;
        this.BirthDate = BirthDate;
    }

    int GetAge()
    {
        return Today - BirthDate; //for illustration only
    }

}

现在,将行为和数据分离到单独的类中。

class Person
{
    string Name;
    DateTime BirthDate;

    //constructor
    Person(string Name, DateTime BirthDate)
    {
        this.Name = Name;
        this.BirthDate = BirthDate;
    }
}

class PersonService
{
    Person personObject;

    //constructor
    PersonService(string Name, DateTime BirthDate)
    {
        this.personObject = new Person(Name, BirthDate);
    }

    //overloaded constructor
    PersonService(Person personObject)
    {
        this.personObject = personObject;
    }

    int GetAge()
    {
        return personObject.Today - personObject.BirthDate; //for illustration only
    }
}

这应该是有益的,可以提高灵活性并提供松散耦合。我不明白怎么做。据我说,这引入了额外的编码和性能损失,每次我们必须初始化两个类对象。而且我在扩展此代码时看到了更多问题。考虑当我们在上述情况下引入继承时会发生什么。我们必须继承这两个类

class Employee: Person
{
    Double Salary;

    Employee(string Name, DateTime BirthDate, Double Salary): base(Name, BirthDate)
    {
        this.Salary = Salary;       
    }

}

class EmployeeService: PersonService
{
    Employee employeeObject;

    //constructor
    EmployeeService(string Name, DateTime BirthDate, Double Salary)
    {
        this.employeeObject = new Employee(Name, BirthDate, Salary);
    }

    //overloaded constructor
    EmployeeService(Employee employeeObject)
    {
        this.employeeObject = employeeObject;
    }
}

请注意,即使我们将行为隔离在一个单独的类中,我们仍然需要 Data 类的对象才能使用 Behavior 类方法。因此,最终我们的 Behavior 类包含数据和行为,尽管我们拥有模型对象形式的数据。
你可能会说你可以添加一些接口到 mix 中,所以我们可以有 IPersonService 和一个 IEmployeeService。但我认为为每个类引入接口并从接口继承似乎并不好。

那么,您能告诉我在上述情况下分离数据和行为所取得的成就是我无法通过将它们放在同一个班级中实现的吗?

4

7 回答 7

14

实际上,Martin Fowler 说,在领域模型中,数据和行为应该结合起来。看看AnemicDomainModel

于 2012-07-09T06:30:57.233 回答
12

我同意,您实施的分离很麻烦。但还有其他选择。具有 getAge(person p) 方法的 ageCalculator 对象呢?或 person.getAge(IAgeCalculator calc)。或者更好的是 calc.getAge(IAgeble a)

分离这些关注点有几个好处。假设您打算让您的实施返回几年,如果一个人/婴儿只有 3 个月大怎么办?你返回0吗?.25?抛出异常?如果我想要狗的年龄怎么办?几十年或几小时的年龄?如果我想要某个日期的年龄怎么办?人死了怎么办?如果我想使用火星轨道一年怎么办?还是希伯来历法?

这些都不应该影响使用人员界面但不使用生日或年龄的类。通过将年龄计算与其使用的数据分离,您可以获得更大的灵活性并增加重用的机会。(甚至可以用相同的代码计算奶酪和人的年龄!)

通常,最佳设计会随环境而有很大差异。然而,这种情况很少见,性能会影响我在这类问题上的决定。系统的其他部分可能是几个数量级的更大因素,例如浏览器和服务器或数据库检索或序列化之间的光速。与理论上的性能问题相比,时间/金钱更适合用于简化和可维护性的重构。为此,我发现分离域模型的数据和行为是有帮助的。毕竟,它们是不同的关注点,不是吗?

即使有这样的优先级,事情也很混乱。现在想要人年龄的类有另一个依赖项,即 calc 类。理想情况下,需要较少的类依赖关系。另外,谁负责实例化 calc?我们注射它吗?创建一个 calcFactory?还是应该是静态方法?该决定如何影响可测试性?追求简单的动力实际上增加了复杂性吗?

OO 的将行为与数据结合起来的实例与单一责任原则之间似乎存在脱节。当所有其他方法都失败时,两种方式都写,然后问同事,“哪个更简单?”

于 2013-02-19T04:16:29.123 回答
4

有趣的是,OOP 通常被描述为结合了数据和行为。

您在这里展示的是我认为是反模式的东西:“贫血域模型”。它确实存在您提到的所有问题,应该避免。

应用程序的不同级别可能具有更多的程序倾向,这适合于您所展示的服务模型,但这通常只位于系统的最边缘。即便如此,这仍将通过传统的对象设计(数据+行为)在内部实现。通常,这只是一个令人头疼的问题。

于 2012-07-09T06:31:21.290 回答
4

我意识到我迟到了大约一年才回复这个问题,但无论如何......哈哈

我之前已经将行为分开,但不是以您展示的方式。

当您的行为应该具有公共接口但允许不同对象的不同(唯一)实现时,分离行为才有意义。

例如,如果我在制作游戏,对象的一些可用行为可能是行走、飞行、跳跃等的能力。

通过定义 IWalkable、IFlyable 和 IJumpable 等接口,然后基于这些接口创建具体类,它为您提供了极大的灵活性和代码重用。

对于 IWalkable,您可能有...

不能行走:IWalkableBehavior

LimitedWalking : IWalkableBehavior

UnlimitedWalking : IWalkableBehavior

IFlyableBehavior 和 IJumpableBehavior 的模式类似。

这些具体的类将实现CannotWalk、LimitedWalking 和UnlimitedWalking 的行为。

在对象(例如敌人)的具体类中,您将拥有这些行为的本地实例。例如:

IWalkableBehavior _walking = new CannotWalk();

其他人可能会使用 new LimitedWalking() 或 new UnlimitedWalking();

当需要处理敌人的行为时,假设 AI 发现玩家在敌人的某个范围内(这可能是一种行为,也可以说是 IReactsToPlayerProximity),它可能会自然而然地尝试将敌人移近“与“敌人交战。

所需要的只是调用 _walking.Walk(int xdist) 方法,它会自动被整理出来。如果对象正在使用CannotWalk,那么什么都不会发生,因为Walk() 方法将被定义为简单地返回并且什么都不做。如果使用LimitedWalking,敌人可能会向玩家移动很短的距离,如果UnlimitedWalking,敌人可能会向上移动到玩家身边。

我可能没有很清楚地解释这一点,但基本上我的意思是用相反的方式看待它。不是将您的对象(您在此处称为数据)封装到 Behavior 类中,而是使用接口将 Behavior 封装到对象中,这为您提供了“松散耦合”,允许您改进行为并轻松扩展每个“行为基础” (行走、飞行、跳跃等)具有新的实现,但您的对象本身并没有区别。即使该行为被定义为CannotWalk,它们也只是具有Walking 行为。

于 2013-08-03T05:58:35.590 回答
3

一个人(任何人)的内在年龄。因此它应该是 Person 对象的一部分。

hasExperienceWithThe40mmRocketLauncher() 不是人固有的,但可能是接口 MilitaryService 可以扩展或聚合 Person 对象。因此它不应该是 Person 对象的一部分。

一般来说,我们的目标是避免仅仅因为这是最简单的方法就向基础对象(“Person”)添加方法,因为您会在正常的 Person 行为中引入异常。

基本上,如果您看到自己在基础对象中添加了诸如“hasServedInMilitary”之类的内容,那么您就有麻烦了。接下来,您将执行大量语句,例如 if (p.hasServedInMilitary()) blablabla。这在逻辑上与一直进行 instanceOf() 检查在逻辑上是相同的,并且表明 Person 和“看过兵役的人”实际上是两个不同的东西,应该以某种方式断开连接。

退后一步,OOP 是关于减少 if 和 switch 语句的数量,而是让各种对象根据其抽象方法/接口的特定实现来处理事物。分离数据和行为促进了这一点,但没有理由把它走极端并将所有数据与所有行为分开。

于 2012-07-10T12:57:19.197 回答
1

答案确实是在正确的情况下很好。作为开发人员,您的工作的一部分是确定解决问题的最佳解决方案,并尝试定位解决方案以适应未来的需求。

我不这样做经常遵循这种模式,但如果编译器或环境是专门为支持数据和行为的分离而设计​​的,那么在平台如何处理和组织你的脚本方面可以实现许多优化。

尽可能多地熟悉设计模式对您最有利,而不是每次都自定义构建整个解决方案,并且不要过于判断,因为模式不会立即有意义。您通常可以使用现有的设计模式在整个代码中实现灵活且健壮的解决方案。请记住,它们都是一个起点,因此您应该始终准备好进行定制以适应您遇到的各个场景。

于 2018-11-14T18:20:03.050 回答
0

您描述的方法与策略模式一致。它促进了以下设计原则:

开闭原则

类应该对扩展开放但对修改关闭

组合优于继承

行为被定义为单独的接口和实现这些接口的特定类。这允许在行为和使用该行为的类之间更好地解耦。可以在不破坏使用它的类的情况下更改行为,并且类可以通过更改使用的特定实现在行为之间切换,而无需任何重大的代码更改。

于 2020-07-13T21:23:21.383 回答