【问题标题】:Object Oriented Programming: Separation of Data and Behavior面向对象编程:数据和行为的分离
【发布时间】:2012-07-08 13:12:23
【问题描述】:

最近我们讨论了关于类中数据和行为分离的问题。数据和行为分离的概念是通过将领域模型及其行为放在单独的类中来实现的。
但是,我不相信这种方法的所谓好处。尽管它可能是由“伟大的”创造的(我认为是 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。但我认为为每个类引入接口并从接口继承似乎并不好。

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

【问题讨论】:

  • 我见过这种尝试至少一次 - 它很快就因自身的重量而分崩离析,尤其是因为它与大多数(可能是所有)ORM 不兼容。
  • 查看策略模式的理由en.wikipedia.org/wiki/Strategy_pattern

标签: c# oop design-patterns


【解决方案1】:

您描述的方法与strategy pattern 一致。它促进了以下设计原则:

开闭原则

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

组合优于继承

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

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

      【解决方案3】:

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

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

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

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

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

      对于 IWalkable,您可能有...

      CannotWalk : 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 行为。

      【讨论】:

      【解决方案4】:

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

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

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

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

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

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

      【讨论】:

        【解决方案5】:

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

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

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

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

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

        【讨论】:

          【解决方案6】:

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

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

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

          【讨论】:

            【解决方案7】:

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2013-01-08
              • 2020-05-18
              • 1970-01-01
              • 1970-01-01
              • 2010-09-18
              • 2010-12-30
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多