【问题标题】:Object oriented programming - class design confusion面向对象编程——类设计混乱
【发布时间】:2009-11-16 07:52:28
【问题描述】:

我正在尝试围绕面向对象编程。

我的理解是我们拥有对象,因此我们可以设计程序来反映现实生活中的对象。

让我们看一个类层次结构:

class Fruit {
    void Eat() {

    }
}

class Apple extends Fruit {

}

显然,如果Eat() 是虚拟的,您可以多态地使用 Fruit。但这有意义吗?水果不能自己吃!

是否应该将水果对象传递给具有Eat() 函数的人类对象?

我正在尝试找出思考这个问题的正确方法。一般来说,编程对象应该与现实生活中的对象有多接近?

【问题讨论】:

    标签: oop class-design


    【解决方案1】:

    您遇到了设计问题——正如您正确指出的那样,Eat() 作为 Fruit 的成员没有明显意义。另一方面,“可食用的”属性会更有意义。就像“onEaten”事件等一样。您的水果/苹果类公开的内容(以及在您的模型中有意义的其他对象)取决于许多其他因素,包括您要尝试的内容在您的应用程序中使用这些构造来完成。

    一般来说,您希望您的类代表逻辑域级实体。有时它们对应于现实世界中的物理实体,但在许多情况下它们不对应。

    在我看来,OO 问题分解是程序员通常不擅长的事情。我不知道有多少次我看到了从方向盘衍生而来的汽车,我摇了摇头,而原始开发人员无法理解为什么他们的设计没有多大意义。

    【讨论】:

    • +1 好例子。另一个例子是 Fruit 属于 Edible 接口/类,而消费者对象(例如 Person 类的对象)具有 Eat(Edible e) 方法。这将导致 myPerson.Eat(myEdible) 就像 OP 推测的那样。
    • 受保护的虚拟 void OnEaten() { Barf(); }
    【解决方案2】:

    简单地镜像现实世界的对象很少是一个好主意。借用一个经典的例子——控制咖啡机的软件不是关于咖啡豆和热水——而是关于煮咖啡。

    您需要找到实际问题的底层抽象,而不仅仅是将名词复制到对象层次结构中。

    如果你的苹果来源于水果,它会增加什么有趣的行为吗?层次结构真的需要吗?继承给你的软件增加了一定程度的复杂性,任何增加复杂性的东西都是不好的。您的软件有点难以理解和理解,您的测试中还需要覆盖更多内容,并且出现错误的可能性稍大一些。

    我发现 OOP 更多的是关于空白 - 您遗漏的内容更重要。

    【讨论】:

      【解决方案3】:

      【讨论】:

        【解决方案4】:

        一些 Herbivore 类的东西会有一个 Eat 函数,就像 Carnivore 类的东西一样,但是每个人的 Eat 对可以传递给 Eat 函数的参数有一些不同的限制。水果是被吃掉的,因此它会作为参数传递给 Herbivore.Eat(),而您可能希望将 Hamburger 类型的对象传递给 Carnivore.Eat(),并引发如果将 Hamburger 传递给 Herbivore.Eat(),则会出现异常。

        但实际上,我不认为 OOP 可以让我们将软件对象建模为就像现实生活中的对象一样。我发现我的大部分 OOP 设计都适用于非常抽象的对象,并且仅针对它们所属的系统。如果我编写了一个图书馆签入/签出系统,我会根据其管理属性和功能对 Book 进行建模——我不会将其建模为 Page 对象的集合,我什至怀疑我是否会定义任何类似 Read() 方法的东西,尽管这首先是拥有一本书的主要目的。 Book 对象在系统中的作用决定了我的设计,而不是在现实世界中对书籍的作用。

        【讨论】:

        • 好答案。 OOP 中的命名类更多的是使用人类熟悉的概念。这样就更容易假设代码模块的行为。 Carnivore 可能有不同的行为,具体取决于所建模的系统,但无论如何它比说 ThingThatOnlyEatsMeat 更直观。
        【解决方案5】:

        假设您正在编写一个饥饿的人模拟器,那么我认为正如您所说,拥有一个Human::Eat(Fruit f) 函数会更有意义。你的 Fruit 可能没有方法,因为 Fruit 本身并没有做太多事情,但它可能有一个 calories 属性,等等。

        【讨论】:

          【解决方案6】:

          是否应该传递一个水果对象 到具有 Eat() 的人类对象 功能?

          是的。

          但是程序对象通常比这个幼稚的例子更抽象。在计算机编程的现实世界中,像水果和人类这样的对象通常会在数据库中表示为属性。此类数据的消费和操作将在编程对象中完成。

          【讨论】:

            【解决方案7】:

            你说得对,eat() 可能是人类或哺乳动物或果蝇的一种方法。水果本身可能没有行为,因此也没有方法。

            我不经常考虑 OO 在现实中对从真实事物到对象的映射的好处。这可能是因为我们处理的是不太具体的概念,例如发票和订单。

            我认为 OO 的主要优势在于它为我的代码带来的结构。在现实世界中,Invoices 和 Orders 实际上并没有任何事情,但在我的代码中它们会做。因此,程序化的订单可能更接近于表示订单的数据以及与订单相关的一些人工业务流程的组合。

            【讨论】:

              【解决方案8】:

              我倾向于考虑:

              是一个

              有一个

              所以,苹果是水果,所以继承是有意义的。

              但是,水果有(是)可食用的可能是有道理的,但这表明它是水果的属性,而不是动作(方法)。

              例如,您可能有未成熟的苹果,它不能食用(可食用),因此您可以设置此属性。

              现在,无论要吃什么,您都可以设置苹果是否是其饮食的一部分。

              现在Has a 将用于合成。所以,一个苹果有一个种子意味着种子不会扩展苹果,但一个苹果会有一个种子的集合。

              所以,你确实有一个设计问题,我希望这两个概念可以帮助澄清。

              【讨论】:

              • “Is-a”(继承)和“has-a”(组合)类比是思考事物的良好开端,但值得注意的是,这有一些局限性。最好选择“优先组合而不是继承”,而不是考虑关于“is-a and has-a”的一切,因为后者会导致偏好继承。很难维护大型继承结构。
              • @Spoike - 当一个人不熟悉 OOP 时,保持简单是最好的选择,我认为这两个概念有助于保持简单。一旦更好地理解了这些,那么您可以查看其他方法来理解,IMO。
              【解决方案9】:

              一般来说,编程对象应该与现实生活中的对象多接近。

              不多,只够。

              OOP 的主要特征之一是抽象。您无需拥有对象的所有属性/方法即可使用它。

              你只需要基本的使用它。

              关于对象的全部内容就是将数据和对这些数据执行某些操作的函数放在同一个地方。

              所以在你的水果课上,我最好有 Color 之类的东西,或者表明它是否会被吃掉。例如:

               Fruit
                   + color : Color
                   - isMature : Boolean
              
                   + canBeEaten() : Boolean
                        return isMature
              

              这样你可以创造出不同的水果

               apple = Fruit()
               appe.color = Color.red
               out.print( "Can this fruit be eaten? %s ", apple.canBeEaten() )
              
               orange = Fruit()
               orage.color = Color.orange
               out.print( "Can this fruit be eaten? %s ", orange.canBeEaten() )
              

              等等

              如果您看到属性( color 和 isMature )存储在对象内。这样您就不必从外部跟踪它们的状态。

              至于继承,只有当你需要为某个方法添加新的行为时才有意义,是的,这些方法与对象的属性或特性有关。正如您指出的那样,fruit.eat() 没有多大意义。

              但是考虑一种从水果中获取果汁的方法。

              Fruit
                  + getJuice(): Juice
              
              Apple -> Fruit
                   + getJuice(): Juice
                       // do what ever is needed internally to create the juice
              
              Orange -> Fruit
                  + getJuice(): Juice
                      // here, certainly the way to get the juice will be different
              

              【讨论】:

                【解决方案10】:

                我总是发现使用“动物”或“水果”的例子违反直觉。人是难以建模的对象,您不太可能遇到具有相同要求的应用程序。

                使用拟人化的概念确实有助于分配责任。基本上,假设您的对象是一个人(我通常在设计会议期间用脸和四肢勾勒它们)。

                现在您可以就您的对象提出以下问题:

                • “我的对象在系统中有什么职责?”
                • “这个对象是负责做xxx,还是应该xxx另一个对象负责?”
                • “这个对象做的比它应该做的多吗?” (例如,如果它是为了计算而设计的,它不应该负责加载值)

                David West 的“Object Thinking”是一本关于该主题的好书。

                【讨论】:

                  【解决方案11】:

                  我的理解是我们拥有对象,因此我们可以设计程序来反映现实生活中的对象。

                  可能更像是将它们与现实生活中的物体“联系起来”(如果适用)。

                  是否应该将水果对象传递给具有 Eat() 函数的人类对象?

                  是的,或者比人类更普遍的东西。

                  我正在尝试找出思考这个问题的正确方法。一般来说,编程对象应该与现实生活中的对象多接近。

                  只定义您需要定义的内容。然后实现(通常)变得非常明显。换句话说,你可能想得太用功了。

                  【讨论】:

                    【解决方案12】:

                    大多数人类语言遵循Subject Verb Object的句子结构(对于简单的陈述)

                    在像 c++ 这样的 OOP 语言中,并不完全具有讽刺意味的是,对象就是对象,它先出现,所以正常的顺序是相反的:

                    所以这是 c++ 中的“基本”模式:-

                    object.verb( subject );
                    

                    这实际上是垃圾。大多数类都设计有 subject.verb(object) 接口。然而,了解 SVO 确实可以测试一个类接口是否被设计为正确的方法(在这种情况下,它没有)。

                    也就是说,有大量人类语言天生就是 OVS,或者是其他一些典型的英语顺序颠倒的变体。在与国际开发的软件打交道时,其他开发人员可能对简单陈述中的主题和对象的正确和正常顺序有不同的想法。

                    【讨论】:

                    • 在这种情况下,您似乎倒退了,而且总的来说。 human.eat(apple) 是我们在这里看到的。主语.动词(宾语)。
                    • 实际上 OSV 和 OVS 语言似乎非常罕见。更多支持将 SVO 形式化为 OOP 设计中的正确顺序。
                    【解决方案13】:

                    我认为我们不应该尝试“镜像现实生活中的对象”。我认为更多的是找到与在系统(域)上下文中建模的行为非常相似的现实生活对象。一个游戏中的水果类,你切水果以获得分数,其行为和属性可能与游戏中的水果类截然不同,游戏中角色跑来跑去收集水果以获得分数;或模拟人们吃水果。将行为分配给以现实生活对象命名的类可以更容易地假设代码模块的行为并推测它们的交互。

                    【讨论】:

                      猜你喜欢
                      • 2012-10-29
                      • 2011-04-07
                      • 1970-01-01
                      • 2020-05-18
                      • 2019-02-25
                      • 1970-01-01
                      • 2010-11-04
                      • 1970-01-01
                      相关资源
                      最近更新 更多