面向对象
面向对象编程是关于向对象请求执行某些操作的概念:这是一个看似简单却很难正确应用的概念。
Goban
考虑一个二维游戏棋盘,比如下围棋时使用的棋盘(称为goban)。
首先考虑它需要完成任务的行为。这意味着列出对象的行为而不是决定行为所操作的数据。例如,一个基本的棋盘可能具有以下行为:
- 放置一颗围棋棋子。
- 移除一颗围棋棋子。
- 移除所有棋子。
对于计算机版的围棋,将注意力集中在特定区域是很方便的:
- 标记交叉点(例如三角形、数字、字母、圆、正方形)。
- 从标记的交叉点上移除标记。
- 移除所有标记。
注意,goban不需要提供一种方法来为客户端提供指向特定交叉点处棋子的引用。相反,它可以回答关于其状态的问题。例如,goban可能回答以下问题:
- 给定交叉点处是否有黑色棋子?
- 给定交叉点处是否有白色棋子?
- 给定交叉点处是否有标记?
这并非是围棋盘的责任去了解游戏状态:这应该由一个拥有规则的Game实例来处理。在现实生活中,围棋盘只是石头的舞台。
此时,我们可以编写一个接口用于goban,而不必知道底层实现如何工作。
public interface Goban {
public void place( Stone stone, Point point );
public void removeStone( Point point );
public void removeStones();
public void place( Mark mark, Point point );
public void removeMark( Point point );
public void removeMarks();
public boolean hasWhiteStone( Point point );
public boolean hasBlackStone( Point point );
public boolean hasMark( Point point );
}
注意看棋盘与规则和游戏的清晰分离。这使得围棋棋盘可重复使用于其他游戏(涉及石头和交叉点)。围棋棋盘可以继承自通用接口(例如,一个Board接口),但这应该足以解释一种对象思考方式。
封装
Goban接口的实现不会暴露其内部数据。此时,我可以要求您实现此接口、编写单元测试,并在完成后将编译后的类发送给我。
我不需要知道您使用了哪些数据结构。我可以使用您的实现来下棋(并描绘)在一个Goban上。这是许多项目常犯错误的关键点。许多项目编写以下代码:
public class Person {
private HairColour hairColour = new HairColour( Colour.BROWN );
public Person() {
}
public HairColour getHairColour() {
return hairColour;
}
public void setHairColour( HairColour hairColour ) {
this.hairColour = hairColour;
}
}
这是无效的封装。考虑一下Bob不喜欢把头发染成粉红色的情况。我们可以这样做:
public class HairTrickster {
public static void main( String args[] ) {
Person bob = new Person();
HairColour hc = bob.getHairColour();
hc.dye( Colour.PINK );
}
}
Bob现在把头发染成了粉色,没有什么能够阻止他。虽然有方法可以避免这种情况,但人们并不会去做。相反,封装被打破,导致系统变得僵硬、不灵活、充满错误和难以维护。
强制执行封装的一种可能的方式是返回HairColour
的克隆。修改后的Person类现在使得改变头发颜色为粉色变得困难。
public class Person {
private HairColour hairColour = new HairColour( Colour.BROWN );
public Person() {
}
public HairColour getHairColour() {
return hairColour.clone();
}
public void setHairColour( HairColour hairColour ) {
if( !hairColour.equals( Colour.PINK ) {
this.hairColour = hairColour;
}
}
}
Bob可以安心地睡觉,因为他不会醒来时发现自己被染成了粉色。