学习面向对象思维

44

我目前正在用C++开发一个小型的2D游戏引擎,但是我现在面临一个难题——我不擅长设计一个真正可行的“类系统”。我的大脑中有一个障碍,阻止我看到应该在哪里使用类,哪里不应该使用。我正在阅读一篇关于引擎设计的文章,它建议使用“状态”类来管理不同游戏条目的状态(我之前使用了int)。 它还建议所有游戏对象(不包括io/视频/声音等)都派生自Renderable或NonRenderable类。这很聪明。我已经知道这是一种聪明的方式-我的意思是,Java中的每个对象都是基类Object对吧? 聪明,我知道! 为什么我没有这样做呢?我需要阅读什么才能真正进入这种思维方式?

另一个例子。我正在参加一个关于Ruby的暑期课程(非常简单),我们要设计一个露营地。很简单!所以,露营地是由每个都有电表的“地块”集合组成的。我的设计是三个类,一个是露营地-它使用了Guest和Plot类的数组。我的老师建议我使用更多的类。第一个想法是:WTF(!)。在哪里,什么类?在我看来,一切都是一个类-直到我意识到,也许电表应该是一个类?现在用整数表示它在Plot类中。

我想学习如何为我的问题提供面向对象的解决方案-不仅仅是如何将最明显的东西变成类!

有什么提示/书籍/文章/博客可以推荐吗?

我已经在攻读计算机科学专业的大学学位两年了,多年来一直以编程为爱好!我“只是”卡住了——这阻止了我创建任何更大的软件!


12
你的老师让你使用更多类,但没有给出充分的理由?是为了推广面向对象编程而强调面向对象编程吗? - Ionuț G. Stan
是的,我有类似的问题。有时候类很适合,但在其他情况下似乎没有办法停止使用纯C编码。 - MadH
2
是的,这个老师不太行。所以,在这种情况下,int 可能仍然是一个非常好的解决方案。然而,实验的主题是使用对象 - 所以在我对老师感到恼火之后,我试图看到更大的重点。 - Andreas
不错的答案种类。 - CiscoIPPhone
2
从我今天正在处理的一个真实案例中来看,一个仪表(或计量器)是一个对象,它所采取的读数或测量值是数字。尽管在我的情况下,读数也是一个对象,具有时间戳、数量和单位。 - Terry Wilcox
有时类别会半自然地从您尝试建模的内容中出现。但是有时 - 就像维护状态信息的情况一样 - 这并不那么明显。我不知道从您的描述中是否一个 int 足以满足状态的要求;但是请问自己 - 如果您添加一个新状态,需要触及多少其他类别(多少其他代码)?如果答案是“很多”,那么状态机实际上可能是一个好的解决方案。设计/分解的一个好原则是“封装变化”。 - David
16个回答

21

我的个人经验是通过Bertrand Meyer的面向对象软件构造第二版学习面向对象软件构造。

在那个时候,这本书对我来说非常宝贵,仍然是我在OO编程和软件构造方面学到最多的单一书籍。

以下是该书的一些优点:

  • 第A部分:问题中提供了一个非常好的关于软件质量的定义。
  • 第B部分:通往面向对象的道路中以逻辑、逐步寻找OO技术的方式,让读者感觉像亲身参与调查一样。你可能会从这一部分获得你所需要的思维方式。
  • 第C部分:面向对象技术是该书的技术核心,您将使自己的知识更加扎实,并学习到有关契约设计、继承、泛型等方面的非常有用的技术。
  • 第D部分:OO方法论:良好的应用方法是一个更实际的设计方法,我也认为非常有用。例如,请参见如何查找类(22),您可以在这里找到。

在这些部分之后,会涉及到更高级的主题,如并发(30)数据库(31)

由于该书使用作者设计的Eiffel语言,因此这将让您进入正确的思维方式,并教您如何思考。很容易将这些想法应用于其他多或少面向对象的编程语言。


谢谢你的建议,我会尽力找到这本书! - Andreas
2
+1:这是有史以来最具争议的书籍之一,然而我喜欢它。 - dfa
5
@dfa,你能解释一下为什么这是最具争议性的吗?我只是好奇。顺便说一句,我还没有读过这本书......打算买一本。 - Ajax3.14

13

面向对象

面向对象编程是关于向对象请求执行某些操作的概念:这是一个看似简单却很难正确应用的概念。

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可以安心地睡觉,因为他不会醒来时发现自己被染成了粉色。


1
掌握Go示例对于不熟悉该游戏的人可能有些棘手,但我确实明白了事先建立接口(行为)的重点。我发现这一系列文章鼓励同样的过程,特别是面向对象思考 - Piovezan

9

请记住:面向对象编程(OO)不是唯一的目标。使用面向对象编程的目的是为了使代码开发和维护更加容易。要避免“为了使用面向对象而使用面向对象”这种想法。


是的,谢谢你提醒我。不过目前我正处于完全相反的情况下 ;) - Andreas

6

Head First Object-Oriented Analysis and Design是一本有趣的书,因为它们让阅读变得有趣。这里有很多练习和难题可以让你挑战自己。我已经读过这本书,觉得非常不错。

这本书涵盖了以下内容:

  • 使用面向对象的原则(封装和委托)
  • 开闭原则(OCP)
  • 单一职责原则(SRP)
  • 设计模式、UML、用例等等。

2
当你推荐一本书时,不要只是发布一个链接 - 解释为什么这是一本好书并回答问题。 - anon
整个《Head First》系列都非常出色,无论你是多么有经验的程序员。它以非学术的方式简单地涵盖了所有“神秘的概念”。 - ohnoes
根据书上所述,如果你懂Java,你可以阅读它;如果不懂的话,最好先学习Java。 - Kamran Bigdely
@kami 我不确定,但我认为它也说如果你知道C++、C#或类似的语言也可以。 - Chandresh Pant
+1 这是一本很棒的书。我也建议阅读《Head First设计模式》。 - David

4
在我的头脑中存在一个障碍,使我无法看清应该在哪里使用类,以及不应该在哪里使用。
归根结底,类是将复杂系统分解为相互交互的简单部分的一种方式。尝试创建类,否则您将重复自己。
现在,仪表是Plot类中的整数。需要将仪表作为类吗?将其转换为类的优点是什么?这些都是您需要问自己的问题。
游戏引擎的设计很困难。这样一个模糊定义的要求的分离是一个复杂的过程,请阅读这里:有关游戏引擎的文章 设计是迭代的,您将进行多次重构,不要感到惊讶。

1
+1 “设计是迭代的,你会反复重构几次,不要对此感到惊讶。” - 我刚开始理解这一点!我正在重构我几个月前编写的一些代码,使用了一些设计模式,确实是一次丰富的经验,但你的陈述完全适用! :) - Tony The Lion

3
在Jeff Bay的书 "ThoughtWorks Anthology" 中有一篇文章:"Object Calisthenics",其中他给出了一组设计面向对象软件的规则:
  1. 每个方法只使用一级缩进
  2. 不要使用else关键字
  3. 将所有原始类型和字符串包装起来
  4. 每行只使用一个点
  5. 不要使用缩写
  6. 保持所有实体小
  7. 不要使用超过两个实例变量的类
  8. 使用一级集合
  9. 不要使用任何getter/setter/属性
乍一看可能会觉得这些规则太严格了。请记住,即使试图编写遵守这些规则的代码也会让您成为更好的面向对象设计者。

2

请记住,解决问题从来没有一种万能的方法。把所有东西都变成一个类也不是解决方案。特别是小的东西(比如仪表)可以很好地作为图形类中的 int 或 float 成员,就像你以前做过的那样。

我的建议是实践是良师。继续尝试,继续阅读。随着时间的推移,你会越来越流利。


2
我可能从Craig Larman的《应用UML和模式:面向对象分析与设计以及迭代开发简介》中学到了大部分关于面向对象软件开发的知识。
在他的方法中,类是从用例系统地派生出来的:
  • 用例中的名词被映射为类,
  • 动词被映射为方法,
  • 形容词被映射为成员变量。
当然,这对于问题域中的概念比如GUI小部件等并不适用。然而,从要编写的程序的描述/用例开始帮助我找到更好的抽象,比我省略这一步要好。

2
对我来说,直到我读了一本关于设计模式的书,OO才“恍然大悟”。如果你已经熟悉抽象类、接口等概念,那只是战斗的一半。
下一步是弄清楚为什么你应该优先选择组合而不是继承,如何编写接口和如何编写类,使它们解耦且封装良好。设计模式向你展示了常见的OO问题的解决方案,并帮助你按照上述准则构建代码结构。
我无法推荐任何关于C++的特定书籍,但GOF book是设计模式(Java)的标准书籍。我更喜欢讲解特定语言中的设计模式的书籍,这样你可以获得具体的代码示例。 Design Patterns In Ruby很不错,PHP: Objects, Patterns and Practice也不错。
我感觉你的教练并不特别懂他在说什么。仅仅给出“上更多课”的建议并没有太大用处。

2

其中有助于理解面向对象(OO)思想的方法,以及之前文章中提到的实践经验,就是使用OO原则改写/改进您已经编写的现有代码。

例如:

a. 在存在大量if/else结构的情况下,您可以考虑使用类层次结构来分配分支代码,并使用多态性。

b. 任何使用运算符(例如Java中的instanceof)都会指示针对具体类型进行编程,您可以考虑如何摆脱instanceof检查。

c. 使用“迪米特法则”作为指导方针,并查看类之间的耦合是否高

在某种程度上,“测试驱动开发”的实践也帮助了我,因为它迫使您以类要公开的接口/行为为思考重点,而不仅是集中精力于如何编写最佳解决方案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接