10

假设我的任务是编写某种 RPG。这意味着,例如,我要跟踪 a及其统计数据,例如智力、伤害加成或生命值。Character GameCharacter

我非常害怕在项目结束时我最终可能会处理大量字段 - 对于每个字段,我都必须确保它们遵循一组非常相似的约束和行为(例如,我希望它们限制在最小值和最大值之间;我希望能够区分“基本值”和“临时奖金”;我希望能够在不通过 setter 和 getter 的情况下增加和减少两者) . 突然之间,对于每个字段,我都需要一个(两个?)getter 和四个 setter,也许还需要几个重置器!即使对于 10 个字段,这也意味着很多方法都相似,eek。

对于 DRYness,我已经开始将在Field类中处理这些统计数据的逻辑封装起来,这样我就可以编写诸如intelligence.applyBonus(10)or之类的代码hitpoints.get()(它会注意返回的值在范围内)等。我什至花了这么长的时间来创建类将这些字段组合在一起,但这不是现在的重点。

现在,我在“插入”时遇到了这个问题FieldGameCharacter大多数 Java 教科书都说每个类都应该有带有公共 getter 和 setter 的私有字段。这在理论上听起来不错,而且我已经围绕int;建立了一个完整的课程。但是,当您发现自己调用 getter 来获取... getter 时,这个想法听起来并不可靠:

thisCharacter.getIntelligence().get() //eeek

我宁愿直接访问该字段。也许这是我的 Python/VB [1] “背景”,但对我来说它更干净、更清晰、更直接:

thisCharacter.intelligence.get()

公共领域的(理论上)问题是我放弃了对它的所有控制;例如,在代码库中的某个其他点,不幸的是,可能会发生以下情况:

thisCharacter.intelligence = somethingThatReallyIsNull;

听起来像是一个微妙的错误……但是……我的意思是,我真的应该担心吗?我从来没有打算Field直接分配 [2],我已经在 J​​avadoc 中记录了这不是应该做的事情,但我仍然是新来的,所以我有点被撕裂了。

所以我想听听你对这个话题的看法。封装的优势是否如此巨大,以至于我应该继续使用 getter getter 和 setter getter 等等……还是我应该采取健康措施进行封装并将其Field作为一个public领域?


[1] 是的,我知道。我一直在努力忘记。但是我们最近也看到了一些 C# 和 man,属性并不甜。那好吧。

[2] 构造函数除外!getter 不会把我从错误的构造函数中拯救出来。

4

10 回答 10

17

听起来你在考虑if(player.dexterity > monster.dexterity) attacker = player. 你需要多想if(player.quickerThan(monster)) monster.suffersAttackFrom(player.getCurrentWeapon())。不要乱用简单的统计数据,表达你的实际意图,然后围绕他们应该做的事情设计你的课程。无论如何,统计数据都是一种逃避。你真正关心的是一个玩家是否可以做一些动作,或者他们的能力/能力与一些参考。考虑“玩家角色是否足够强大”(player.canOpen(trapDoor))而不是“角色是否至少有 50 点力量”。

于 2009-04-20T17:17:17.040 回答
8

我的经验是,在您需要大量字段的情况下,字段的数量、性质、命名和类型非常灵活,并且可能在项目的整个生命周期中发生变化,您可能需要某种地图而不是字段.

例如,有一个从键到值的属性映射。

提供获取和设置属性的公开调用,但不要让每个人都使用它们(或确保它们不使用)。相反,创建类来表示您感兴趣的每个属性,并且该类提供用于操作该属性的所有功能。例如,如果你有力量,你可以有一个初始化为特定 Player 对象的“StrengthManipulation”类,然后提供 getter、setter(都带有适当的验证和例外),也许还有诸如用奖金计算力量等.

这样做的一个好处是您可以将属性的使用与玩家类分离。所以如果你现在添加一个智力属性,你就不必处理和重新编译所有只操纵力量的东西。

至于直接访问字段,这是一个坏主意。当您在 VB 中访问一个字段时(至少在旧 VB 中),您通常会调用属性 getter 和 setter,而 VB 只是为您隐藏 () 调用。我的观点是你必须适应你所使用的语言的约定。在 C、C++、Java 等语言中,您有字段,也有方法。调用方法应始终使用 () 来表明这是一个调用,并且可能会发生其他事情(例如,您可能会遇到异常)。无论哪种方式,Java 的好处之一是其更精确的语法和风格。

VB 到 Java 或 C++ 就像发短信到研究生院的科学写作。

顺便说一句:一些可用性研究表明,最好不要给构造函数提供参数,而是在需要时构造和调用所有设置器。

于 2009-04-20T15:32:21.897 回答
4

Steve Yegge 有一篇非常有趣(如果很长)的博客文章涵盖了这些问题:通用设计模式

于 2009-04-20T15:41:04.043 回答
2

在我看来,“thisCharacter”很可能有一个“情报”对象来处理幕后的情报,但我质疑这是否应该公开。您应该只公开 thisCharacter.applyInt 和 thisCharacter.getInt 而不是处理它的对象。不要像那样公开你的实现。

于 2009-04-20T15:33:12.237 回答
1

将您的字段保密!你永远不想暴露太多的 API。在未来的版本中,您始终可以将私有内容公开,但不能反过来。

想想好像你会把它变成下一个 MMORPG。你会有很多错误、错误和不必要的邪恶的余地。确保不可变属性是最终的。

想一想 DVD 播放器,它具有简约的界面(播放、停止、菜单),但内部却具有这样的技术性。你会想要隐藏程序中所有不重要的东西。

于 2009-04-20T15:34:45.180 回答
1

听起来您的主要抱怨不是 setter/getter 方法的抽象,而是使用它们的语言语法。即,您更喜欢 C# 样式属性之类的东西。

如果是这种情况,那么 Java 语言可以为您提供的东西相对较少。直接字段访问很好,直到您需要切换到 getter 或 setter,然后您将进行一些重构(如果您控制整个代码库,可能还可以)。

当然,如果 Java 平台是必需的,但语言不是,那么还有其他选择。例如,Scala 具有非常好的属性语法,以及许多其他可能对此类项目有用的特性。最重要的是,它在 JVM 上运行,因此您仍然可以获得与使用 Java 语言编写它相同的可移植性。:)

于 2009-04-20T15:38:31.910 回答
1

您在这里似乎拥有的是复合模型的单层。

您可能希望添加向模型添加抽象的方法,而不仅仅是将其作为一组较低级别的模型。

这些字段应该是最终的,因此即使您将它们公开,也不会意外分配null给它们。

所有 getter 都存在“get”前缀,因此它可能更像是最初的外观而不是新问题。

于 2009-04-20T15:45:52.720 回答
1

封装的优势是如此巨大,以至于我应该继续使用 getter getter 和 setter getter 等等......还是我应该采取健康措施进行封装并将 Field 作为公共领域?

IMO,封装与在私有字段周围包装 getter/setter 无关。在小剂量或编写通用库时,权衡是可以接受的。但是,如果在您所描述的系统中不加以检查,则它是一种反模式

getter/setter 的问题在于它们在具有这些方法的对象与系统的其余部分之间创建了过于紧密的耦合。

真正封装的优点之一是它减少了对 getter 和 setter 的需求,将您的对象与过程中的系统其余部分解耦。

与其用 setIntelligence 暴露 GameCharacter 的实现,为什么不给 GameCharacter 一个更好地反映它在游戏系统中的角色的接口呢?

例如,而不是:

// pseudo-encapsulation anti-pattern
public class GameCharacter
{
  private Intelligence intelligence;

  public Intelligence getIntelligence()
  {
    return intelligence
  }

  public void setIntelligence(Intelligence intelligence)
  {
    this.intelligence = intelligence;
  }
}

为什么不试试这个?:

// better encapsulation
public class GameCharacter
{
  public void grabObject(GameObject object)
  {
    // TODO update intelligence, etc.
  }

  public int getIntelligence()
  {
    // TODO
  }
}

甚至更好:

// still better
public interface GameCharacter
{
  public void grabObject(GameObject object); // might update intelligence
  public int getIntelligence();
}

public class Ogre implements GameCharacter
{
  // TODO: never increases intelligence after grabbing objects
}

换句话说,一个 GameCharacter 可以抓取 GameObjects。每个 GameCharacter 抓取相同 GameObject 的效果可以(并且应该)有所不同,但细节完全封装在每个 GameCharacter 实现中。

请注意 GameCharacter 现在如何负责处理自己的智能更新(范围检查等),例如,当它抓取 GameObjects 时会发生这种情况。二传手(以及你注意到它的并发症)已经消失了。根据具体情况,您也许可以完全放弃 getIntelligence 方法。Allen Holub 将这个想法推向了合乎逻辑的结论,但这种方法似乎并不常见。

于 2009-04-20T17:22:28.340 回答
0

除了 Uri 的回答(我完全支持),我想建议您考虑在 data.xml 中定义属性映射。这将使您的程序非常灵活,并且会排除很多您甚至没有意识到您不需要的代码。

例如,一个属性可以知道它绑定到屏幕上的哪个字段,它绑定到数据库中的哪个字段,当属性更改时要执行的操作列表(重新计算命中百分比可能适用于强度和 dex... )

这样做,您没有每个属性的代码来将类写入数据库或将其显示在屏幕上。您只需遍历属性并使用其中存储的信息。

同样的事情也适用于技能——事实上,属性和技能可能来自同一个基类。

走上这条路之后,你可能会发现自己有一个非常严肃的文本文件来定义属性和技能,但添加新技能就像这样简单:

Skill: Weaving
  DBName: BasketWeavingSkill
  DisplayLocation: 102, 20  #using coordinates probably isn't the best idea.
  Requires: Int=8
  Requires: Dex=12
  MaxLevel=50

在某些时候,添加这样的技能不需要任何代码更改,这一切都可以很容易地在数据中完成,并且所有数据都存储在附加到类的单个技能对象中。当然,您可以以相同的方式定义操作:

Action: Weave Basket
  text: You attempt to weave a basket from straw
  materials: Straw
  case Weaving < 1
    test: You don't have the skill!
  case Weaving < 10
    text: You make a lame basket
    subtract 10 straw
    create basket value 8
    improve skill weaving 1%
  case Weaving < 40
    text: You make a decent basket
    subtract 10 straw
    create basket value 30
    improve skill weaving 0.1%
  case Weaving < 50
    text: You make an awesome basket!
    subtract 10 straw
    create basket value 100
    improve skill weaving 0.01%
  case Weaving = 50
    text: OMG, you made the basket of the gods!
    subtract 10 straw
    create basket value 1000

虽然这个例子非常先进,但你应该能够想象它是如何在完全没有代码的情况下完成的。想象一下,如果你的“属性/技能”实际上是成员变量,那么在没有代码的情况下做这样的事情会有多困难。

于 2009-04-20T16:56:56.467 回答
0

Consider using a "modeling" framework, such as Eclipse's EMF. This involves defining your objects using something like Eclipse EMF editor, or in plain XML, or in Rational Rose. It's not as difficult as it sounds. You define attributes, constraints etc. Then the framework generates the code for you. It adds @generated tags to parts it added such as getter and setter methods. You can customize the code, and later edit it either by hand or via some GUI, and regenerate the java files.

于 2009-04-22T17:11:32.793 回答