最近我们讨论了类中的数据和行为分离。通过将域模型及其行为放入单独的类中来实现数据和行为分离的概念 但是,我不相信这种方法的假定好处。即使它可能是由一个伟大的"创造出来的。 (我认为这是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类既包含数据又包含行为,尽管我们以模型对象的形式存在数据
您可能会说可以添加一些接口,因此我们可以使用IPersonService和IEmployeeService。但我认为为每个类引入接口和从接口引入接口似乎不太好。
那么你能告诉我通过分离上述案例中的数据和行为我取得了哪些成就,而这些数据和行为是我在同一个班级中无法实现的吗?
答案 0 :(得分:10)
实际上,Martin Fowler说在域模型中,数据和行为应该结合起来。看看AnemicDomainModel。
答案 1 :(得分:9)
我同意,你实施的分离很麻烦。但还有其他选择。那个有方法getAge(person p)的ageCalculator对象怎么样?或者person.getAge(IAgeCalculator calc)。或者更好的是calc.getAge(IAgeble a)
分离这些问题可以带来一些好处。假设你打算让你的实施回归多年,如果一个人/宝宝只有3个月大了怎么办?你回0吗? 0.25?抛出异常?如果我想要一只狗的年龄怎么办?几十年或几小时的年龄?如果我想要某个日期的年龄怎么办?如果这个人死了怎么办?如果我想在一年中使用火星轨道怎么办?还是希伯来语的calander?
这些都不会影响使用person界面但不使用birthdate或age的类。通过将年龄计算与其消耗的数据分离,您可以获得更高的灵活性并增加重用的可能性。 (甚至可以计算奶酪的年龄和相同代码的人!)
通常,最佳设计会因环境而异。然而,这种情况很少见,表现会影响我在这类问题上的决定。系统的其他部分可能会有几个数量级的因素,例如浏览器和服务器之间的光速或数据库检索或序列化。与理论性能问题相比,时间/美元更倾向于重构简单性和可维护性。为此,我发现分离数据和域模型的行为是有帮助的。毕竟,它们是分开的问题,不是吗?
即使有这样的优先事项,事情也会混乱。现在,想要人年龄的类有另一个依赖,即calc类。理想情况下,需要较少的类依赖性。另外,谁负责实例化calc?我们注射吗?创建一个calcFactory?或者它应该是静态方法?该决定如何影响可测试性?简化的驱动力实际上增加了复杂性吗?
OO关于将行为与数据相结合的实例与单一责任原则之间似乎存在脱节。当所有其他方法都失败时,请双向写下,然后问同事,“哪一个更简单?”
答案 2 :(得分:2)
有趣的是,OOP通常被描述为结合数据和行为。
你在这里展示的是我认为反模式的东西:“贫血领域模型”。它确实遭受了你提到的所有问题,应该避免。
应用程序的不同级别可能具有更多程序性的弯曲,这有助于像您所示的服务模型,但通常只能处于系统的最边缘。即便如此,内部也可以通过传统的对象设计(数据+行为)来实现。通常,这只是一件令人头疼的事。
答案 3 :(得分:2)
我意识到我迟到了大约一年回复,但无论如何......哈哈
我之前将行为分开了,但没有按照你展示的方式分开。
当你有行为时,应该有一个共同的接口,但允许对不同的对象进行不同的(唯一的)实现,分离行为是有意义的。
例如,如果我正在制作游戏,某些可用于对象的行为可能是行走,飞行,跳跃等等。
通过定义IWalkable,IFlyable和IJumpable等接口,然后根据这些接口制作具体的类,它为您提供了极大的灵活性和代码重用。
对于IWalkable,你可能会......
CannotWalk:IWalkableBehavior
LimitedWalking:IWalkableBehavior
UnlimitedWalking:IWalkableBehavior
IFlyableBehavior和IJumpableBehavior的类似模式。
这些具体的类将实现CannotWalk,LimitedWalking和UnlimitedWalking的行为。
在对象(例如敌人)的具体类中,您将拥有这些行为的本地实例。例如:
IWalkableBehavior _walking = new CannotWalk();
其他人可能会使用新的LimitedWalking()或新的UnlimitedWalking();
当需要处理敌人的行为时,比如AI发现玩家在敌人的某个范围内(这可能是一种行为,也就是说IReactsToPlayerProximity)它可能会自然地尝试移动敌人更接近“搞”敌人。
所需要的只是调用_walking.Walk(int xdist)方法,它将自动整理出来。如果对象正在使用CannotWalk,则不会发生任何事情,因为Walk()方法将被定义为简单地返回并且什么都不做。如果使用LimitedWalking,敌人可能会向玩家移动很短的距离,如果UnlimitedWalking敌人可能会向右移动到玩家。
我可能不会非常清楚地解释这一点,但基本上我的意思是以相反的方式来看待它。而不是将您的对象(您在此处调用Data)封装到Behavior类中,使用Interfaces将Behavior封装到对象中,这为您提供了“松散耦合”,允许您优化行为并轻松扩展每个“行为基础” (步行,飞行,跳跃等)新的实现,但你的对象本身没有区别。即使该行为被定义为CannotWalk,它们也只有行为行为。
答案 4 :(得分:1)
对一个人而言具有内在性的年龄(任何人)。因此它应该是Person对象的一部分。
hasExperienceWithThe40mmRocketLauncher()不是一个人固有的,但可能是可以扩展或聚合Person对象的MilitaryService接口。因此它不应该是Person对象的一部分。
通常,目标是避免向基础对象(“Person”)添加方法,因为它是最简单的方法,因为您向正常Person行为引入了异常。
基本上,如果你看到自己在你的基础对象中添加了诸如“hasServedInMilitary”之类的东西,你就会遇到麻烦。接下来,您将执行大量语句,例如if(p.hasServedInMilitary())blablabla。这在逻辑上与执行instanceOf()检查一样,并且表明Person和“看过兵役的人”实际上是两个不同的东西,并且应该以某种方式断开连接。
退一步,OOP是关于减少if和switch语句的数量,而是让各种对象按照抽象方法/接口的具体实现来处理事物。分离数据和行为促进了这一点,但是没有理由将其置于极端并将所有行为中的所有数据分开。
答案 5 :(得分:0)
答案是,在正确的情况下确实很好。作为开发人员,您的工作是为出现的问题确定最佳的解决方案,并尝试定位该解决方案以使其能够适应将来的需求。
我通常不遵循这种模式,但是如果编译器或环境是专门为支持数据和行为分离而设计的,则平台在处理和组织脚本方面可以实现许多优化。
尽可能熟悉尽可能多的设计模式,而不是每次都自定义构建整个解决方案,这并不是您的最大利益,因为这种模式没有立即意义,所以不要过于判断。您通常可以使用现有的设计模式在整个代码中实现灵活而强大的解决方案。请记住,它们只是作为起点,因此您应该始终准备好进行自定义,以适应遇到的各种情况。
答案 6 :(得分:0)
您描述的方法与strategy pattern一致。它促进了以下设计原则:
应该为扩展而打开类,但为修改而关闭
行为定义为单独的接口和实现这些接口的特定类。这样可以在行为与使用行为的类之间实现更好的解耦。可以在不破坏使用行为的类的情况下更改行为,并且这些类可以通过更改使用的特定实现在行为之间进行切换,而无需进行任何重大的代码更改。