OOP。选择对象

时间:2009-07-03 10:56:46

标签: oop object subclassing

我是以OOP术语思考的相对新手,并且还没有找到关于正确方法的“直觉”。作为一个练习,我试图找出你在不同类型的对象之间创建线的位置,以我桌上的饮料为例。

假设我创建了一个对象Drink,其中包含volumetemperature等属性,以及pour()drink()等方法,我正在努力看看特定饮料'类型'的来源。

假设我有TeaCoffeeJuice类型的饮品,我的第一直觉是子类Drink,因为它们有共同的属性和方法。

然后问题变为TeaCoffee都有sugarsmilk等属性,但Juice没有,而所有三个都有{ {1}}(Earl Gray,decaff和orange)。

同样,variantTea有一个Coffee方法,而这对于addSugar()对象毫无意义。

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

但最后我在Juicevariant子类上使用了两种addSugar()方法。

或者考虑到我最后将所有属性和方法放在超类上,因为大多数属性和方法至少在几种饮料类型之间共享,我想知道在分类中有什么意义呢? / p>

我担心我只是想抽象太多,但如果我想添加一个新类型,例如Tea - 仍然Coffee,我不想回到自己的角落或闪闪发光的道路。

10 个答案:

答案 0 :(得分:18)

面向对象的设计不是孤立完成的。您需要问的问题是我的特定应用程序需要做什么(如果有的话)与Drink类?答案以及设计因应用而异。在OO失败的第一种方法是尝试构建完美的柏拉图式的等级 - 这种事物只存在于完美的柏拉图式世界中,并且在我们所居住的现实世界中没有用。

答案 1 :(得分:16)

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

当然,奖金问题是,如果我愿意,有什么可以阻止我将糖加入果汁中?在物理上,这是可能的。但这不是传统的做法。你想建模哪个?

最终,不要试图模仿“世界”。改为模拟您的问题。 您希望您的应用程序如何与一杯茶进行交易?一杯茶在您的应用中的作用是什么?在你的情况下,许多可能的父母中哪一个有意义?

如果你的应用需要区分“你可以添加糖的饮料”和“你只能消费不饮的饮料”,那么你可能会有两种不同的基类。你甚至需要普通的“饮料”课吗?也许,也许不是。酒吧可能不在乎它是一种饮料,它是可以饮用的。它是一种可以出售的产品,在一种情况下,您必须询问客户是否需要糖,而在另一种情况下则不需要。但可能没有必要使用“饮料”基类。

但是对于喝酒的人来说,你可能想要一个带有Consume()方法的“Drink”基类,因为这是它的重要方面。

最后,请记住,您的目标是编写一个正常运行的程序,而来编写完美的OOP类图。问题不是“我怎样才能在OOP中代表不同类型的饮料”,而是“如何使我的程序能够处理不同类型的饮料”。答案可能是将它们安排在具有公共Drink基类的类层次结构中。但它也可能完全不同。它可能是“对所有饮料都一样,只需更换名称”,在这种情况下,一个课程就足够了。或者它可能只是将饮料当作另一种消费品,或者只是另一种液体,而不是实际模拟他们的“可饮用”财产。

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

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

答案 2 :(得分:13)

sugart问题有两种解决方案。第一个是添加一个子类,如HotDrinks,包含addSugar和addMilk,如:

                 Drink
                  /  \
          HotDrink    Juice
           /   \
        Tea     Coffee

问题是如果有很多这样的话会变得混乱。

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

答案 3 :(得分:6)

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

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

例如,如果您今天在“强烈饮酒”<添加糖>,您可能会决定稍后将其添加到煎饼。然后你最终会得到你的代码,要求在许多地方喝一杯,使用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()。

但是如果你有一天意识到这是你方法的最佳位置,那么你就不必改变代码的其他部分,如果他们只是通过界面使用这个方法。 / p>

道德是:只要您的界面保持不变,您的等级可能会发生变化。

答案 4 :(得分:4)

这个pdf可能对你有很大的帮助,它完全符合你的例子:

The Decorator Pattern

答案 5 :(得分:2)

可能相关的两件事:

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

2)Favour composition over inheritance。饮料具有体积和温度的特性(在您的建模环境中可能与人类感知相关,即特冷,冷,温,热)并且由成分(水,茶,咖啡,牛奶,糖,调味剂)组成。请注意,它不会从成分中继承,而是由它们组成。至于将成分组成饮料的好方法,请试着查看decorator pattern

答案 6 :(得分:1)

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

至于你的例子,我可能会为茶和咖啡等饮料创建一个抽象类(因为它们具有相似的属性)。并尝试使饮料类具有很少的属性和方法。水和果汁可以从抽象饮料类中继承。

答案 7 :(得分:1)

使用装饰器模式

答案 8 :(得分:1)

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

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

如果您在为4种不同类型的饮品创建课程时遇到问题,请制作4种不具有继承性的不同类型。如果你有一个问题,说“好的,现在我们想把这些饮料给人并打印出他们喝的东西”,这是多态性的完美用法。使用函数drink()添加接口或抽象基类。推送编译,注意所有5个类都说该函数没有实现,实现所有5个函数,推送编译就完成了。

要回答你的问题关于添加牛奶或糖的问题,我假设你正在运行的问题是你想要将对象Drinkable传递给Person但是{{1没有DrinkableaddMilk。这个问题可以通过两种或多种方式解决,一种是非常糟糕的方法。

糟糕的方法:将addSugar传递给Drinkable并进行类型检查并对该对象进行投射以取回PersonTea

Juice

一种方法:一种方法是通过具体课程将饮料传递给void prepare (Drinkable drink) { if (drink is Juice) { Juice juice_drink = (Juick) drink; juice_drink.thing(); } } ,然后将其传递给饮料。

Person

在你要求这个人准备饮料之后,你要求他们喝下去。

答案 9 :(得分:0)

这完全取决于具体情况 - 这真的意味着 - 从最简单的设计开始,并在您看到需要时重构它。也许它不会看起来非常闪亮的OOP - 但我会从一个类和一个属性'CanAddSugar'和'SugarAmount'开始(后来你可以将它推广到谓词CanAddIngredient(IngredientName),以便人们可以在他们的饮料中添加威士忌: )

所有这一切都有一个深层次的问题 - 两个对象之间的差异是否足以要求为它们分别设置一个类?我没有看到任何好的答案。