面向对象设计问题

7
如果我正在编写一个游戏,其中有一个工人砍伐树木,那么“cutWood”方法应该放在工人类还是树类中?
编辑: 我读到的第一个OOD示例是关于一个圆(称为圆类)的,其中包含一个名为“calculate area”的方法。 现在,毫无疑问,圆不会计算自己的面积。 唯一的想法是计算面积是与圆相关的操作(在圆上执行的操作)。
因此,“cutWood”方法与工人和树都相关。

一个圆不会计算自己的面积?嗯,我想你可能误读了这个例子:对于不需要其他对象的操作,以及类具有最多知识(即半径)的操作,正是应该放在一个类中的。争议的地方在于像circle.draw(ScreenObject)这样的操作——一个自包含的类是否应该知道特定图形系统的细节? - Rodney Gitzel
你可能混淆了面向对象编程(OOP)和领域驱动设计(DDD)。OOP是过程式编程的基础上增加了继承、组合和多态等方面的特性(这些都非常强大,并且有许多最佳使用技术)。然而!DDD坚持要求您围绕真实世界的事物和行为对实体及其交互进行建模。 - STW
这种愚蠢的东西让面向对象编程变得荒谬可笑。难怪函数式编程正在复兴。 - duffymo
@Rodney Gitzel,如果没有更多实际问题的了解,这是个循环论证。图形引擎是执行element.Draw()还是drawer.draw(element)还是两者都有? - codelark
我建议你学习SOLID面向对象编程原则。这是Udemy提供的一门很棒的课程,我认为它可以帮助你提高设计技能:https://www.udemy.com/mastering-object-oriented-design-in-java/?dtcode=3TXJLpb2cYMQ - Horse Voice
12个回答

8
我认为在工人对象中没有任何必要加入砍木头的方法。砍树应该是树类的一部分。假设,砍伐木材也会涉及到改变木材类的一些内部状态。
工人应该在想砍哪棵树时调用 Cut 方法,而不是由树告诉工人他应该砍什么。如果您想像汉斯所暗示的那样进行抽象化,可以为 Cut 方法创建 ICuttable 接口,并让您的树实现它。
考虑一些您熟悉的东西,比如一个字符串。当您想要切割字符串(分割)时,您不会在每个将执行此操作的对象中定义 splitString 方法。无论哪个对象决定拆分字符串,都会发生相同的事情,并且通常需要了解目标对象(字符串)的内部情况才能执行。许多其他对象只需调用字符串的 split 方法即可。字符串对象具有高内聚性——因为它的方法有助于完成共同的任务——操作字符串。
我不认为砍伐木材对工人对象本身有多大贡献。

4

问问自己:是工人砍木头,还是树砍木头?

(这句话的意思是要我们思考问题的本质,而不是只关注表面现象。)

2
但如果要砍树,可以使用CutMe(工人whichWorker),它可以判断树木是否足够被砍倒。 - 是的,工人应该砍树。 - PostMan
1
如果树的内部状态需要知道是谁砍了它,以及它是否已经被砍倒,那么工人的cutTree方法将调用树公开的任何方法来更新其状态。 - codelark
工人类扩展木鸭类,还是相反? :p - Rodney Gitzel

4
你在问题中已经回答了它:“一个切割木材的工人”。你应该将它放到worker类中。

如果以后你想让工人去上班、烤面包、修车、吸大麻或者其他什么事情,那这个工人就会变成一个“全能对象”。 - Mark H
@Mark H:我不太明白你的意思。上帝对象是指具有过多职责的对象。这个木工永远不会修车或烤面包。该工人可能会有更多的方法,例如takePause(),但只要这些与工人的主要目标相关,就没有问题。 - Karel Petranek
你做出了一个假设,即工人永远不会修理汽车,但你并不知道这一点 - 或者你不知道它将来是否会改变。如果工人的责任发生变化,它将使他的对象膨胀。然而,无论工人的责任如何变化,砍树总是作用于树上 - 因此应该成为树的一部分。反正树的责任几乎没有什么机会改变。 - Mark H
一个对象不应该改变它的职责,这违反了良好的面向对象编程原则。对象设计应该是合乎逻辑和简单明了的,而一棵树砍掉自己听起来既不合乎逻辑也不简单明了。 - Karel Petranek
1
@Mark H:如果我们要考虑到那个层面,工作者最好只有一个doWork()方法,该方法接受一个IWorkable对象,该对象公开了一个单一的work()方法,然后调用所需的内部来完成这项工作。我的意思是,如果我们要关注这个层面的可扩展性,我们应该把它推向其逻辑结论。 - EricBoersma

3
为工人创建一个cut(Material m)方法,以增加面向对象性。

5
除非工人将来会割其他东西,否则这将是过度抽象的一个好例子;-) - codelark
1
//TODO: 插入EmoWorker笑话 - STW
如果描述中说的是伐木工,我同意,但是工人可以指任何事情 /讽刺 - Hans

2

在砍树的过程中,您是否需要修改树?

在工人砍树时,是否需要修改工人?

我想您最终会得到一个 WoodCutting 服务,该服务处理事件(可能需要修改树和工人),或者依次调用 worker.cutWood()tree.woodCut()

哈哈,好问题;)


2
听起来对我来说是一个策略模式的候选者。您可能需要一个WorkerAction抽象类,其中包含performAction方法。然后,WorkerAction类的子类将实现诸如砍树、挖金等工作细节。因此,子类知道树的细节,并且可以在砍伐树木时调用树木上必要的方法以影响树木。
然后,工人类只需要引用一个具体的WorkerAction实例,然后调用其performAction()方法。工人不知道树木或黄金等细节。这样,工人可以执行操作,但您不会限制工人仅执行一项操作。事实上,如果您希望您的工人在未来执行更多操作,则无需修改工人类。

我同意这种方法是可行的,但是对于WorkerAction子类的职责,我的观点可能会有所不同。例如,如果有一个TreeCuttingAction类,我只需要让它的performAction方法调用树的cut方法(根据我上面的答案给出的原因),TreeCuttingAction不必知道它正在砍伐的树的详细信息,只需知道它正在砍伐树木即可。 - Mark H
@Mark H 嗯,听起来我只是将决策推到了不同的抽象层次,而没有真正回答问题。不过我认为我同意你的答案,即砍树的细节应该驻留在 Tree 中。所以是的,performAction 应该只调用 Tree 上的 cut() 方法。 - Vincent Ramdhanie

1
在工人类中,您可以有一个名为cutWood的方法,在树类中有一个名为cutted的方法。显然,worker.cutWood调用tree.cutted,这可能会返回收获的木材量。 tree.cutted将执行所有必要的操作,让树死亡。
如果您将方法调用视为“从一个对象发送到另一个对象的消息”,那么说“玩家向工人发送'砍伐木材'消息,工人再向树发送'砍伐'消息”就更有意义了。

1

对象设计的核心在于为类分配职责。这意味着对于这个问题,其实并没有一个确定的答案。因为它完全取决于你如何在你的设计中建模职责(参见:Larman)。

因此,需要决定哪个对象/类负责什么。这将帮助你正确回答关于放置何种类型方法的问题。

因此,请问自己以下问题:工人是否自行决定要砍伐木材?如果是,他可能不需要被命令这样做,因此他不需要一个 cut(tree) 方法。但如果用户可以命令他这样做,那么他将需要一个 cut(tree) 方法。另一个例子:树必须知道自己被砍了吗?如果一棵被砍倒的树只会导致它被从森林中移除,那么它可能不需要这样一个 tree.cut() 方法。但是,如果您将重新绘制树的责任分配给树在砍伐期间(重新)绘制自己,则可能需要一个 tree.cut() 方法。甚至可以将该树建模为具有宽度,并且它可以计算在垮台之前需要多少次砍伐。

0
这个问题只有在你解释了cutWood的作用后才能真正回答。
如果cutWood只影响Tree的状态,那么它应该属于Tree,例如:
public void cutWood() {
    this.height = 0;
}

calculateArea的情况下,你只需要圆的半径(假设π是常数)来进行计算 - 没有其他东西。这就是为什么它属于Circle的原因。
停止试图过于紧密地模拟现实世界,看看你的方法实际上需要做什么。

0

由于一个工人可能会砍多棵树,因此更有可能将cutWood()方法放在工人类中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接