假设我的任务是编写某种RPG。这意味着,例如,我想跟踪 Character
GameCharacter
及其统计数据,如情报,伤害奖励或生命值。
我非常害怕在项目结束时我最终可能会处理非常多的字段 - 而且对于每一个我都必须确保他们遵循一组非常相似的约束和行为(对于例如,我希望它们在最小值和最大值之间有界限;我希望能够区分“基值”和“临时奖励”;我希望能够在不经过制定者的情况下增加和减少两者和吸气剂)。突然之间,对于每个领域,我都需要一个(两个?)吸气剂和四个安装者,也许还需要几个重置器!即使对于10个字段来说,这意味着很多方法都是相同的,eek。
对于DRYness,我已经开始封装了在Field
类中弄乱那些统计数据的逻辑,这样我就可以编写intelligence.applyBonus(10)
或hitpoints.get()
等代码(这样可以处理返回的值)在范围内等等我甚至已经花了这么长的时间来创建类来将这些字段组合在一起,但现在不是重点。
现在,我在将Field
“插入”GameCharacter
时遇到了这个问题:大多数Java教科书都说每个类都应该包含带有公共getter和setter的私有字段。这在理论上听起来不错,而且我已经围绕int
建立了一个完整的班级;然而,当你发现自己打电话给吸气剂时,这个想法听起来并不那么坚固......一个吸气剂:
thisCharacter.getIntelligence().get() //eeek
我更愿意直接访问该字段。也许这是我的Python / VB [1]“背景”,但对我来说它更清晰,更清晰,更直接:
thisCharacter.intelligence.get()
公共领域的(理论)问题是我放弃了对它的所有控制;例如,在代码库的某些其他方面,由于不幸,可能会发生以下情况:
thisCharacter.intelligence = somethingThatReallyIsNull;
听起来像一个微妙的错误......但是......我的意思是,我真的应该担心吗?我从来没有打算直接分配Field
[2],我在Javadoc中记录了这不是应该做的事情,但我仍然是新来的,所以我有点不知所措。
所以我想听听你对这个主题的看法。封装的优势是如此巨大,以至于我应该继续使用getter getter和setter getter等等......或者我应该采用健康措施封装并将Field
作为public
字段?
[1]是的,我知道。我一直试图忘记。但我们最近也看到了一些C#和男人,不是属性甜。哦,好吧。
[2]除了在构造函数中!一个吸气剂不会让我摆脱错误的构造函数。
答案 0 :(得分:18)
听起来你正在考虑if(player.dexterity > monster.dexterity) attacker = player
。你需要更像if(player.quickerThan(monster)) monster.suffersAttackFrom(player.getCurrentWeapon())
。不要乱用裸露的统计数据,表达你的实际意图,然后围绕他们应该做的事情设计你的课程。无论如何,统计数据是一个警察;你真正关心的是一个玩家是否可以做某些动作,或者他们的能力/能力与一些参考。考虑“玩家角色是否足够强大”(player.canOpen(trapDoor)
)而不是“角色是否具有至少50强度”。
答案 1 :(得分:8)
我的经验是,在您需要 lot 字段的情况下,字段的数量,性质,命名和类型非常灵活,并且可能会在项目的整个生命周期中发生变化可能需要某种地图而不是字段。
例如,有一个从键到值的属性映射。
提供获取和设置属性的公共调用,但不要让每个人都使用它们(或确保它们不使用它们)。相反,创建类来表示您感兴趣的每个属性,该类提供操作该属性的所有函数。例如,如果你有力量,你可以有一个“StrengthManipulation”类,它被初始化为一个特定的Player对象,然后提供getter,setter(所有有适当的验证和例外),也许还有像奖金计算力量等等。 。
这样做的一个优点是您可以从播放器类中分离属性的使用。因此,如果您现在添加一个Intelligence属性,则不必处理和重新编译仅操纵强度的所有内容。
至于直接访问字段,这是一个坏主意。当您访问VB中的字段(至少在旧VB中)时,通常会调用属性getter和setter,而VB只是为您隐藏了()调用。我的观点是你必须适应你正在使用的语言的约定。在C,C ++,Java等中,您有字段并且有方法。调用方法应始终使用()来表明它是一个调用,并且可能发生其他事情(例如,您可能会遇到异常)。无论哪种方式,Java的一个好处是它的语法和风格更加精确。
VB到Java或C ++就像发短信到研究生科学写作。 BTW:一些可用性研究表明,最好不要让构造函数有参数,而是在需要时构造和调用所有的setter。答案 2 :(得分:4)
史蒂夫·叶格(Steve Yegge)有一篇非常有趣(如果冗长)的博客文章,涵盖了这些问题:The Universal Design Pattern。
答案 3 :(得分:2)
对我来说,看起来'thisCharacter'可能有一个“情报”对象来处理幕后的情报,但我怀疑这是否应该是公开的。你应该公开thisCharacter.applyInt和thisCharacter.getInt而不是处理它的对象。不要暴露你的实现。
答案 4 :(得分:2)
保持您的字段私密!您永远不想暴露太多的API。在将来的版本中,你总是可以制作私人公开的东西,但不能反过来。
想想你是否会把它变成下一个MMORPG。你有很多错误,错误和不必要的恶魔的范围。确保不可变属性是最终的。
想象一下DVD播放器,它具有迷你主义界面(播放,停止,菜单),而且里面还有这样的技术性。你想要隐藏程序中非必要的一切。
答案 5 :(得分:1)
听起来你的主要抱怨不是使用setter / getter方法的抽象,而是使用它们的语言语法。即,你更喜欢像C#样式属性。
如果是这种情况,那么Java语言提供的功能相对较少。直接字段访问很好,直到你需要切换到getter或setter,然后你将进行一些重构(如果你控制整个代码库,可能没问题)。
当然,如果Java平台是一个要求,但语言不是那么还有其他选择。例如,Scala具有非常好的属性语法,以及许多可能对此类项目有用的其他功能。最重要的是,它运行在JVM上,因此您仍然可以通过用Java语言编写它来获得相同的可移植性。 :)
答案 6 :(得分:1)
您在这里看到的是单层复合模型。
您可能希望添加向模型添加抽象的方法,而不是仅将其作为一组较低级别的模型。
这些字段应该是最终字段,因此即使您确实将其公开,也不会意外地将null
分配给它们。
所有getter都有“get”前缀,因此它可能比新问题更像初始外观。
答案 7 :(得分:1)
封装的优势是否如此巨大以至于我应该继续使用吸气剂和吸气剂等等......或者我应该采用健康措施进行封装并将Field作为公共领域?
IMO,封装与在私有字段周围包装getter / setter无关。在小剂量或编写通用库时,权衡是可以接受的。但是当你在一个类似你所描述的系统中未选中时,它就是一个antipattern。
getter / setter的问题在于它们在具有这些方法的对象与系统的其余部分之间创建了过于紧密的耦合。
真正封装的一个优点是它减少了对getter和setter的需求,在此过程中将对象与系统的其余部分分离。
为什么不给GameCharacter提供一个更好地反映它在游戏系统中角色的界面,而不是用GameIntelligence公开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将这个想法带到logical conclusion,但这种方法似乎并不常见。
答案 8 :(得分:0)
除了Uri的答案(我完全支持),我建议你考虑在数据中定义属性映射。这将使您的程序非常灵活,并且会分解出许多您甚至没有意识到您不需要的代码。
例如,属性可以知道它在屏幕上绑定的字段,它在数据库中绑定的字段,属性更改时要执行的操作列表(重新计算的命中%可能适用于强度和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
虽然这个例子非常先进,但您应该能够直观地看到完全没有代码的方式。想象一下,如果你的“属性/技能”实际上是成员变量,那么在没有代码的情况下做这样的事情是多么困难。
答案 9 :(得分:0)
考虑使用“建模”框架,例如Eclipse的EMF。 这涉及使用Eclipse EMF编辑器定义对象, 或者在纯XML或Rational Rose中。它并不像听起来那么困难。 您定义属性,约束等。然后框架生成 代码给你。它将@generated标签添加到它添加的部件中,例如 getter和setter方法。您可以自定义代码,然后进行编辑 它可以通过手工或通过一些GUI来重新生成java文件。