【问题标题】:Java: how to handle a LOT of fields and their encapsulation cleanly?Java:如何干净地处理大量字段及其封装?
【发布时间】:2010-10-20 14:33:21
【问题描述】:

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

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

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

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

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

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

thisCharacter.intelligence.get()

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

thisCharacter.intelligence = somethingThatReallyIsNull;

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

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


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

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

【问题讨论】:

  • “使用公共字段是我放弃了对它的所有控制” - 当然,但是public final 字段,没有什么可以出错的。当你的字段是一个包装的 int 时,它应该是 final,否则你有两种设置方式:thisCharacter.getIntelligence().set(7)someIntelligence.set(7); thisCharacter.getIntelligence(someIntelligence)

标签: java encapsulation getter


【解决方案1】:

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

【讨论】:

  • 好点!超越对 D&D 手册的简单转录。
  • 虽然您确实提出了一个很好的观点,但恐怕将 Uri 重新考虑为线程的“答案”是公平的。 :)
【解决方案2】:

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

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

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

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

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

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

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

【讨论】:

【解决方案3】:

Steve Yegge 有一篇非常有趣(如果很长)的博客文章涵盖了这些问题:The Universal Design Pattern

【讨论】:

  • 好电话!这是一篇捍卫“属性”模式的好文章。
【解决方案4】:

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

【讨论】:

    【解决方案5】:

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

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

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

    【讨论】:

      【解决方案6】:

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

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

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

      【讨论】:

        【解决方案7】:

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

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

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

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

        【讨论】:

          【解决方案8】:

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

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

          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 可以抓取 GameObject。每个 GameCharacter 抓取同一个 GameObject 的效果可以(也应该)不同,但细节完全封装在每个 GameCharacter 实现中。

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

          【讨论】:

          • 这正是我想要避免的! getIntelligence() setIntelligence() applyIntelligenceBonus() resetIntelligenceBonus() getStrength() setStrength() applyStrengthBonus() resetStrengthBonus() getWisdom() setWisdom() giveWisdomBonus() resetWisdomBonus() getDexterity() setDexterity() applyDexterityBonus() zeroDexterityBonus() getHP( ) setHP() dealDamage() healDamage() ...你最终会有很多冗余代码在不同的字段上执行相同的操作。也许他们甚至没有一致地命名。恕我直言,这就是容易出错的定义。 :)
          【解决方案9】:

          除了 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
          

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

          【讨论】:

            【解决方案10】:

            考虑使用“建模”框架,例如 Eclipse 的 EMF。 这涉及使用诸如 Eclipse EMF 编辑器之类的东西来定义您的对象, 或者在纯 XML 中,或者在 Rational Rose 中。这并不像听起来那么困难。 您定义属性、约束等。然后框架生成 给你的代码。它将@generated 标签添加到它添加的部分,例如 getter 和 setter 方法。您可以自定义代码,然后再编辑 手动或通过一些 GUI,然后重新生成 java 文件。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2021-07-14
              • 2021-10-22
              • 2020-12-13
              • 2011-07-22
              • 2018-12-16
              • 1970-01-01
              相关资源
              最近更新 更多