面向对象方法的美妙之处在于,有无数种方法可以抽象和设计相同的概念。问题是你想选择哪个。
似乎在这个特定的例子中,您对所描述的抽象的关注是 player1 需要使用 player2 的方法来实现目标。但是,我想改变你对方法的看法。
一般来说,方法首先应该被认为是我们与世界的公共接口。另一方面,属性是我们保密并锁定在我们的对象/头部/身体/等中的那些私人事物。
确实,许多编程语言都有私有和受保护的方法,仅供内部使用,但这实际上只是为了清理代码,因此我们的公共方法不会有数百行长。
对于您的纸牌游戏,任何其他对象都可以为玩家提供一张卡片,因此我将定义公共方法 Player.giveCard(Card)。这是简单的部分,其他答案已经触及。
但是问题变成了,这个方法内部会发生什么?另外,这张牌是如何从原来的手牌中取出的?
我们可以在这里做几件事,这个列表绝不是完整的:
- 玩家可以只与其他玩家互动。
在这种情况下,玩家 1 选择将 card_589 给玩家 2。因此,player1 调用方法 player2.giveCard(card_589)。在现实世界中,这可以通过玩家 1 将卡片拿出来让玩家 2 拿走来证明。如果玩家 2 接受了这张牌,那么玩家 1 将不再拥有它,必须将其从手中移除。如果玩家 2 不接受,则玩家 1 不会松手,而是将其放回手中。
为了对此进行建模,我们将制定一个简单的规则:giveCard 方法返回一个布尔值结果来指示 player2 是否拿走这张牌……在 player1 调用 player2.giveCard() 之后,他对 player2 是否拿走这张牌没有发言权,因为在这种情况下,这取决于 player2。
我们的代码在 player1 的函数中可能看起来像这样:
//begin pseudocode to give card to player identified by player2
//let self refer to player1's own methods
Player{
public giveCardToPlayer( player2, card_id ){
card = self.removeCardFromHand( card_id );
cardTaken = player2.giveCard( card );
if( cardTaken === false ){
self.addCardToHand( card );
}
}
//in the game management system, or elsewhere in player1's code, you then write
player1.giveCardToPlayer( player2, card_587 );
//or, if in another method "player1.play()", for example:
//self.giveCardToPlayer( player2, card_587 )
//
//end pseudocode
这是最简单的解决方案。在这里,玩家 1 在玩家 2 的决策中看不到任何关于卡片 1 是否被拿走的决定。玩家 1 选择在交出之前从自己手中取出卡片,这样卡片就不会同时出现在两个地方。如果玩家 2 没有拿走这张牌,玩家 1 就会把它放回他的牌库,否则什么都不做,因为这张牌现在和玩家 2 在一起。
就个人而言,这是抽象模型的最简单方法,也是我的最爱。
- 玩家可以通过一些中介进行交互
这是我最喜欢的场景,当我们对具有某种延迟的游戏进行建模时,例如在计算机网络模拟或国际象棋模拟中。玩家 1 将卡片邮寄给玩家 2,但玩家 2 可能会也可能不会收到卡片。在这个游戏中,假设你有一张桌子,就像扑克桌一样,任何玩家都可以在自己和另一个玩家之间放一张牌,这样只有其他玩家才能拿到这张牌。
对于这种情况,我们将创建一个名为 Table 的新对象,虽然我们可以选择多种方法来抽象卡片在桌子上的放置,但我将选择此方法作为该操作的公开可用接口:
Table.placeCardForUser( card, userId, myId, securityToken ) : bool
Table.countCardsOnTableToUserFromUser( userId, myId, securityToken ) : int
Table.pickUpCardToUser( userId, myId, securityToken ) : Card[0..*]
Table.pickUpCardsToMe( myId, securityToken ) : Card[0..*]
这就引入了安全问题,因为我是告诉 Table 只有 userId 可以取卡,只有 myId 可以验证和取卡,所以 Table 对象需要某种方式来验证我(“当前对象”)是否拥有有权访问由“userId”和“myId”标识的表上的位置,但也有很多解决方案。
//begin psuedocode for current example
//we are in player1's function body
card = self.removeCardFromHand( card_587 );
player2_id = self.identifyPlayerToReceive( card );
table.placeCardForUser( card, player2_id, myId, securityToken );
//end current action
//at next opportunity to act, check to see
//if card was taken
cardCount = table.countCardsOnTableToUserFromUser( userId, myId, securityToken );
if( cardCount > 1 ){
//player2 has not taken card or other cards that have accumulated
pickUpCards = self.decideToPickUpCardsToPlayer( player2_id );
if( pickUpCards === true ){
cards = table.pickUpCardToUser( player2_id, myId, securityToken );
foreach( cards as card ){
self.addToHand( card );
}
}
}
//now check to see if anyone has given me cards between last round and this round
cards = table.pickUpCardsToMe( myId, securityToken );
foreach( cards as card ){
//let us assume that player1 takes all cards given to him
self.addToHand( card );
}
可以对此进行变体。您可以想象 player1 和 player2 之间的隧道。玩家 1 通过识别他当前没有办法给玩家 2 卡片来建立隧道,因此他创建了一个隧道。他将隧道的副本交给玩家 2,拿着“另一端”,然后玩家 2 也保留隧道的副本。就像桌子的情况一样,这个隧道现在是一个可以存放来回传递给玩家 2 的物品的地方,但是因为只有玩家 1 和玩家 2 有指向隧道的链接或指针,所以只有这两个玩家可以将物品放入隧道或将它们取出,因此,我们有一个不需要太多安全性的中介。我们可以创建隧道将所有玩家与所有其他玩家联系起来,这仍然是中介设计的一种变体。
- 自我意识卡
有时,我们想要更容易编码且不太像现实的设计。如果 Player 对象的代码忘记从他的手中取出卡片对象会发生什么?现在,因为对象一般是通过引用传递的,所以player2和player1各有一张卡片的引用,游戏模拟认为同一张卡片有两份!
在这种情况下,我们可以将卡片设计为具有自我意识,并让卡片进入玩家的手牌。
对于这个抽象,我将这样对卡片进行建模:
//begin pseudocode
Card{
private owner;
//this is a private link to the object in which the card lies
//we will allow any object to be the owner of the card, as long
//as the object implements the "CardOwner" interface.
public putInto( newOwner ){
//whoever takes the card must specify a newOwner, which will
//implement the "CardHolder" interface.
success = newOwner.addCard( self );
if( success ){
self.owner.removeCard( self );
self.owner = newOwner;
}
}
}
然后我们可以定义接口如下:
//begin psuedocode
iCardHolder{
public removeCard( card ) : bool
public addCard( card ) : bool
}
在这种情况下,我们通过赋予卡片本身执行动作的能力,使自己脱离了“现实”。但这在大型项目中很有用,您不能相信其他程序员会记住有关如何正确处理卡片的详细信息。
通过让卡控制谁拥有指向它的指针,我们可以确保在任何时候都只存在一张卡的副本,无论谁在使用它。
现在,player1 的代码可能如下所示:
//give card to player2
card = self.chooseCard();
player2.giveCard( card );
//put card on the floor
card = self.chooseCard();
floor.giveCard( card );
//put card on the table
card = self.chooseCard();
table.giveCard( card );
在这些对象中的每一个中,我们都可以自由更改接收卡片的方式和存放位置。
//player2 - is a simple CardHolder
public function giveCard( card ){
myHand = self;
card.putInto( myHand );
}
//the dealer is a cheat and does not implement CardHolder,
//but he has three locations that can act as CardHoldes
//they are:
// dealer.sleave, dealer.hand, and dealer.pocket
public function giveCard( card ){
location = self.chooseCardOwner( [ sleeve, hand, pocket ] );
card.putInto( location );
}
//the floor has random piles that are accumulating
public function giveCard( card ){
pile = self.chooseRandomPile();
card.putInto( pile );
}
这个选项很奇怪,但它给了我们很大的灵活性。在上面的示例中,Dealer 和 Floor 甚至都不是 iCardHolder 接口的实现者,但它们持有对实现该接口的对象的引用,因此它们仍然可以拿到卡片。
在每个使用 iCardHolder 的实现中,这与其他的完全不同,代码非常简单,因为我们已经卸载了对卡片位置的操作并将该责任放在卡片本身上,卡片所关心的只是与之交互的对象,同意某种契约并承诺实现一个 removeCard 方法和一个 addCard 方法。作为安全措施,卡片会在自己的内存中保存当前所有者的副本,因此如果其中一位持卡人出现错误,卡片本身会将答案保存给当前所有者。
长话短说
没有一种正确的方法来为您的游戏建模。这实际上完全取决于个人偏好以及您希望系统如何运行。这就是成为程序员的美妙之处。作为进行代码设计的人,您可以设置程序如何运行的规则,以及什么是好的对象交互,什么是坏的对象交互。