16

我是一个以 OOP 术语思考的相对新手,还没有找到我的“直觉”来正确地做这件事。作为一个练习,我试图找出在不同类型的对象之间创建界限的位置,以我桌子上的饮料为例。

假设我创建了一个 object Drink,它具有像 and 这样的属性和像volumeandtemperature这样的方法pour()drink()我正在努力查看特定饮料“类型”的来源。

假设我有一种饮料类型TeaCoffee或者Juice,我的第一直觉是子类Drink,因为它们具有共同的属性和方法。

然后问题就变成了两者Tea和具有和之Coffee类的属性,但没有,而所有三个都具有(分别为伯爵茶、去咖啡因和橙色)。sugarsmilkJuicevariant

同样,Tea并且Coffee有一个方法,而这对一个对象addSugar()没有意义。Juice

那么这是否意味着超类应该具有这些属性和方法,即使所有子类都不需要它们,或者我是否在子类上定义它们,特别是对于像这样的属性variant,每个子类都有它自己的有效值列表?

但后来我得到了两种addSugar()方法,分别是子类TeaCoffee子类。

或者考虑到我最终将所有属性和方法放在超类上,因为大多数属性和方法至少在几种饮料类型之间共享,我想知道子类化的意义何在?

我担心我只是想抽象太多,但不想让自己陷入困境,如果我想添加一种新的类型,比如Water- with variantstill or sparkling - 在路上。

4

10 回答 10

21

部分问题在于现实世界的对象没有整齐地排列在层次结构中。每个对象都有许多不同的父母。一杯果汁当然是一种饮料,但它也是一种营养来源——但不是每一种饮料都是。一杯水没有多少营养。同样,有许多营养来源不是饮料。但是大多数语言只会让你从一个类继承。一杯茶在技术上是一种能源(它很热,它包含热能),但电池也是如此。他们有共同的基类吗?

当然,还有一个额外的问题,如果我愿意,有什么可以阻止我在果汁中加糖?从物理上讲,这是可能的。但它不是按惯例完成的。你想建模哪个?

最终,不要试图为“世界”建模。而是为您的问题建模。您希望您的应用程序如何处理一杯茶?一杯茶在您的应用程序中的作用是什么?在您的情况下,许多可能的父母中哪一个(如果有的话)是有意义的?

如果您的应用程序需要区分“您可以添加糖的饮料”和“您只能原封不动地饮用的饮料”,那么您可能会将它们设为两个不同的基类。你甚至需要普通的“饮料”课吗?也许,也许不是。酒吧可能不在乎它是一种饮料,它是可饮用的。它是一种可以出售的产品,在一种情况下,您必须询问客户是否要在其中加入糖,而在另一种情况下则不需要。但是“drink”基类可能不是必需的。

但是对于喝饮料的人,您可能希望有一个带有 Consume() 方法的“Drink”基类,因为这是它的重要方面。

最后,请记住,您的目标是编写一个正常运行的程序,而不是编写完美的 OOP 类图。问题不是“如何在 OOP 中表示不同类型的饮料”,而是“如何使我的程序能够处理不同类型的饮料”。答案可能是将它们排列在具有通用 Drink 基类的类层次结构中。但这也可能完全是另一回事。可能是“对所有饮料一视同仁,只需更换名称”,在这种情况下,一节课就足够了。或者可能只是将饮料视为另一种消耗品,或者只是另一种液体,而不实际模拟它们的“可饮用”属性。

如果您正在编写物理模拟器,那么也许电池和一杯茶应该来自同一个基类,因为与您相关的属性是两者都能够存储能量。现在果汁和茶突然之间没有共同之处了。它们可能都是另一个世界的饮料,但在你的应用程序中呢?它们是完全不同的。(请不要挑剔果汁是如何包含能量的。我会说一杯水,但是有人可能会提出聚变能或其他东西;))

永远不要忘记你的目标。为什么需要在程序中模拟饮料?

于 2009-07-03T11:45:34.343 回答
20

面向对象的设计不是孤立地完成的。您要问的问题是我的特定应用程序需要对 Drink 类做什么(如果有的话)?答案,因此设计,将因应用程序而异。在 OO 中失败的第 1 种方法是尝试构建完美的柏拉图类层次结构——这些东西只存在于完美的柏拉图世界中,在我们居住的现实世界中没有用。

于 2009-07-03T11:04:04.333 回答
14

糖问题有两种解决方案。首先是添加一个像 HotDrinks 这样的子类,其中包含 addSugar 和 addMilk,如下所示:

                 Drink
                  /  \
          HotDrink    Juice
           /   \
        Tea     Coffee

问题是,如果有很多这些,这会变得混乱。

另一种解决方案是添加接口。每个类可以实现零个或多个接口。所以你有 ISweetened 和 ICowPowerEnabled 接口。Tea 和 Coffee 都实现了 ISweetened 和 ICowPowerEnabled 接口。

于 2009-07-03T11:03:18.097 回答
7

为了增加其他答案,接口往往会在解耦代码方面为您提供更大的灵活性。

您必须注意不要将特性与基类相关联,因为它对所有派生类都是通用的。想一想,如果可以提取此功能并将其应用于其他不相关的对象 - 您会发现“真实世界”基类中的许多方法实际上可能属于不同的接口。

例如,如果您今天要在饮料中“添加糖” ,您可能会决定稍后将其添加到煎饼中。然后你的代码可能会在很多地方使用 AddSugar 和其他各种不相关的方法来请求饮料——它可能很难重构。

如果你的类知道如何做某事,那么它就可以实现接口,而不管类实际上是什么。例如:

interface IAcceptSugar
{
    void AddSugar();
}

public class Tea : IAcceptSugar { ... }
public class Coffee : IAcceptSugar { ... }
public class Pancake : IAcceptSugar { ... }
public class SugarAddict : IAcceptSugar { ... }

使用接口从实际实现它的对象请求特定功能有助于您编写高度独立的代码。

public void Sweeten(List<IAcceptSugar> items)
{
   if (this.MustGetRidOfAllThisSugar)
   {
       // I want to get rid of my sugar, and
       // frankly, I don't care what you guys
       // do with it

       foreach(IAcceptSugar item in items)
          item.AddSugar();
   }
}

您的代码也变得更容易测试。接口越简单,您就越容易测试该接口的使用者。

[编辑]

也许我不是很清楚,所以我要澄清一下:如果 AddSugar() 对一组派生对象做同样的事情,你当然会在它们的基类中实现它。但是在这个例子中,我们说 Drink 并不总是必须实现 IAcceptSugar。

所以我们最终可能会得到一个通用类:

 public class DrinkWithSugar : Drink, IAcceptSugar { ... }

这将以相同的方式为一堆饮料实现 AddSugar() 。

但是如果有一天你意识到这不是你的方法的最佳位置,你将不必更改其他代码部分,如果它们只是通过接口使用方法。

寓意是:只要您的界面保持不变,您的层次结构可能会发生变化。

于 2009-07-03T12:01:13.463 回答
5

此 pdf 可能会对您有很大帮助,并且非常适合您的示例:

装饰器模式

于 2009-07-03T11:05:00.900 回答
3

可能相关的两件事:

1) Neil Butterworth 在某些情况下提出了真实世界模型与真实世界本身之间的非常重要的区别。出于某种原因,面向对象设计有时也称为面向对象建模。模型只关注现实世界的“有趣”方面有趣的是取决于您的应用程序的上下文。

2)支持组合而不是继承。饮料具有体积和温度属性(在您的建模上下文中,这可能与人类感知有关,即超冷、冷、暖、热)并由成分(水、茶、咖啡、牛奶、糖、调味剂)组成。请注意,它不是从成分继承的,而是由它们制成的。至于将成分组合成饮料的好方法,请尝试查找装饰器模式

于 2009-07-03T11:31:10.493 回答
2

使用装饰器模式

于 2009-07-03T11:54:26.040 回答
2

新 OO 程序员的一个常见错误是没有认识到继承用于两个不同的目的,即多态性和重用。在单继承语言中,通常您将不得不牺牲重用部分而只做多态性。

另一个错误是过度使用继承。虽然开始布置漂亮的继承层次结构似乎是个好主意,但最好等到多态问题出现后再创建继承。

如果您在为 4 种不同类型的饮料创建类时遇到问题,请创建 4 种不同类型且没有继承。如果你遇到的问题是“好的,现在我们想把这些饮料中的任何一种给一个人并打印出他们喝了什么”,这就是多态性的完美使用。添加带有函数的接口或抽象基类drink()。push compile,注意所有 5 个类都说函数没有实现,实现所有 5 个函数,push compile 就完成了。

要回答您关于添加牛奶或糖的问题,我假设您遇到的问题是您想将对象传递Drinkable给 aPersonDrinkable没有addMilkor addSugar。这个问题可以通过两种或多种方式解决,一种是非常糟糕的方式。

不好的方法:将 a 传递Drinkable给 aPerson并进行类型检查并强制转换对象以返回 a Teaor Juice

void prepare (Drinkable drink)
{
  if (drink is Juice)
  {
    Juice juice_drink = (Juick) drink;
    juice_drink.thing();
  }
}

一种方法:一种方法是将饮料传递给Person具体的班级,然后再将其传递给饮料。

class Person
{
  void prepare_juice (Juice drink)
  {
    drink.thing1();
  }
  void prepare_coffee (Coffee drink)
  {
    drink.addSugar ();
    drink.addCream ();
  }
}

在您要求该人准备饮料之后,您然后请他们喝它。

于 2009-07-03T16:13:29.343 回答
1

如果您是新手,我不会担心您的设计模式不完美。事实上,许多人会争辩说没有完美的设计模式:P

至于您的示例,我可能会为茶和咖啡等饮料创建一个抽象类(因为它们具有相似的属性)。并且尽量让drink类的属性和方法很少。Water and Juice 可以从抽象饮料类中继承。

于 2009-07-03T11:07:01.660 回答
1

这一切都取决于环境——这实际上意味着——从最简单的设计开始,并在你看到需要时对其进行重构。也许它看起来不会很闪亮 OOP - 但我会从一个类和一个属性“CanAddSugar”和“SugarAmount”开始(稍后你可以将它推广到谓词 CanAddIngredient(IngredientName) 以便人们可以在他们的饮料中添加威士忌: )

所有这一切都有一个深刻的问题 - 两个对象之间的什么差异足以要求为它们提供单独的类?我还没有看到任何好的答案。

于 2010-01-01T10:10:54.407 回答