10

与静态类型环境(例如 Ruby 与 C#)相比,哪些 OOP 原则(如果有)在动态类型环境中不适用或应用方式不同?这不是要求进行静态与动态辩论,而是我想看看在该分歧的任何一方是否有公认的原则适用于一方而不适用于另一方,或者适用不同的原则。在静态类型的 OOP 文献中,诸如“更喜欢组合而不是继承”之类的短语是众所周知的。它们是否同样适用于动态方面?

例如,在动态类型环境中,耦合的粒度似乎不超过方法的级别。换句话说,任何给定的函数调用只会将调用者耦合到任何类都可能满足的特定接口 - 或者换句话说,任何像特定鸭子一样嘎嘎叫的东西。

另一方面,在 Java 中,耦合的粒度可以与包一样高。一个特定的方法调用不仅与另一个类/接口建立契约,而且还将它耦合到该类/接口的包/jar/程序集中。

这样的差异会导致不同的原则和模式吗?如果是这样,这些差异是否已经阐明?Ruby Pickaxe书中有一个部分朝这个方向发展(Duck Typing/Classes Aren't Types),但我想知道是否还有其他内容。我知道Ruby 中的设计模式,但还没有读过。

编辑——有人认为Liskov在动态环境中的应用与在静态环境中的应用不同,但我不禁认为它确实如此。一方面,整个班级没有高级别的合同。但是,不是所有对任何给定类的调用都构成了一个隐式契约,需要按照 Liskov 规定的方式由子类来满足吗?考虑以下。“做一些酒吧的事情”中的调用创建了一个需要由子类参与的合同。这不是“将专用对象视为基类”的情况吗?

class Bartender
    def initialize(bar)
       @bar = bar
    end

    def do_some_bar_stuff
        @bar.open
        @bar.tend
        @bar.close
    end
end

class Bar
    def open
        # open the doors, turn on the lights
    end
    def tend
        # tend the bar
    end
    def close
        #clean the bathrooms
    end
end

class BoringSportsBar < Bar
    def open
        # turn on Golden Tee, fire up the plasma screen
    end

    def tend
        # serve lots of Bud Light
    end
end

class NotQuiteAsBoringSportsBar < BoringSportsBar
    def open
        # turn on vintage arcade games
    end
end

class SnootyBeerSnobBar < Bar
    def open
        # replace empty kegs of expensive Belgians
    end

    def tend
        # serve lots of obscure ales, porters and IPAs from 124 different taps
    end
end

# monday night
bartender = Bartender.new(BoringSportsBar.new)
bartender.do_some_bar_stuff

# wednesday night
bartender = Bartender.new(SnootyBeerSnobBar.new)
bartender.do_some_bar_stuff

# friday night
bartender = Bartender.new(NotQuiteAsBoringSportsBar.new)
bartender.do_some_bar_stuff
4

4 回答 4

5

我认为你所触及的本质区别是:

  • 语言组 1. 调用 object.method1、object.method2、object.method3 时调用的实际方法可以在对象的生命周期内更改。

  • 语言组 2。当调用 object.method1、object.method2、object.method3 等实际方法时,在对象的生命周期内不能更改。

组 1 中的语言倾向于具有动态类型并且不支持编译时检查接口,组 2 中的语言倾向于具有静态类型并且支持编译时检查接口。

我想说所有面向对象的原则都适用于两者,但是

  • 在第 1 组中可能需要一些额外的(显式)编码来实现(运行时而不是编译时)检查,以断言新对象是使用所有适当的方法创建的,以满足接口契约,因为没有编译时接口协议检查,(如果你想让第 1 组代码更像第 2 组)

  • 在第 2 组中可能需要一些额外的编码,以通过使用额外的状态标志来调用子方法来模拟为方法调用调用的实际方法的更改,或者将方法或一组方法包含在对附加的几个对象之一的引用中到主要对象,其中几个对象中的每一个都有不同的方法实现,(如果你想让第 2 组代码更像第 1 组代码)

  • 第 2 组语言对设计的限制使得它们更适合于易于沟通(而不是理解)变得更重要的大型项目

  • 第 1 组语言对设计没有限制,因此更适合较小的项目,程序员可以更轻松地检查是否满足各种设计管道约束,仅仅因为代码更小

  • 从一组语言编写代码就像另一种语言一样有趣且值得研究,但语言差异的重点实际上与它们对不同规模团队的帮助程度有关(-我相信!:))

  • 还有其他各种差异

  • 根据所涉及的确切原则,可能需要或多或少的工作来以一种或另一种语言实现 OO 设计。


编辑

所以为了回答你原来的问题,我检查了

http://c2.com/cgi/wiki?PrinciplesOfObjectOrientedDesign

http://www.dofactory.com/patterns/Patterns.aspx

在实践中,出于各种好的原因(当然还有一些不好的原因),系统中没有遵循 OO 原则。很好的理由包括性能问题超过纯粹的设计质量问题,替代结构/命名的文化利益超过纯粹的设计质量问题,以及在特定语言中以非标准方式实现功能的额外工作成本超过了纯粹的设计。

像抽象工厂、构建器、工厂方法、原型、适配器、策略、命令链、桥接器、代理、观察者、访问者甚至 MVC/MMVM 等粗粒度模式在小型系统中的使用较少,因为关于代码更少,因此创建此类结构的好处并不大。

更细粒度的模式,如状态、命令、工厂方法、复合、装饰器、外观、享元、备忘录、模板方法在第一组代码中可能更常见,但通常有几种设计模式并不适用于对象本身,而是适用于不同部分一个对象,而在第 2 组中,代码模式往往以每个对象一个模式存在。

恕我直言,在大多数第 1 组语言中,将所有全局数据和函数视为一种单例“应用程序”对象是很有意义的。我知道我们正在模糊过程和面向对象编程之间的界限,但是这种代码在很多情况下肯定像“应用程序”对象!:)

一些非常细粒度的设计模式(如迭代器)倾向于内置到第 1 组语言中。

于 2009-12-17T00:32:00.613 回答
3

让我首先说,就个人而言,不适用于动态和静态类型语言的 OOP 原则不是原则。

也就是说,这里有一个例子:

接口隔离原则 ( http://objectmentor.com/resources/articles/isp.pdf ) 指出客户端应依赖于满足其需求的最具体的接口。如果客户端代码需要使用 C 类的两个方法,那么 C 应该实现接口 I,只包含这两个方法,客户端将使用 I 而不是 C。这个原则与不需要接口的动态类型语言无关(因为接口定义的类型,并且在变量是无类型的语言中不需要类型)

[编辑]

第二个例子——依赖倒置原则(http://objectmentor.com/resources/articles/dip.pdf)。该原则认为是“依赖接口或抽象函数和类,而不是具体函数和类的策略”。同样,在动态类型语言中,客户端代码不依赖任何东西——它只是指定方法签名——从而避免了这个原则。

第三个示例 - Liskov 替换原则 ( http://objectmentor.com/resources/articles/lsp.pdf )。该原理的教科书示例是 Square 类,它是 Rectangle 类的子类。然后,当高度也发生变化时,调用 Rectangle 变量的 setWidth() 方法的客户端代码会感到惊讶,因为实际对象是 Square。同样,在动态类型语言中,变量是无类型的,在客户端代码中不会提及 Rectangle 类,因此不会出现这样的意外。

于 2009-12-16T22:50:13.023 回答
1

我对这一切有一个“激进”的看法:在我看来,在数学的支持下,OOP 不能在静态类型的环境中解决任何有趣的问题。我将有趣定义为涉及抽象关系的意思。这可以很容易地证明(参见“协方差问题”)。

这个问题的核心是,OOP 的概念承诺它是一种建模抽象的方式,并结合静态类型传递的契约编程,如果不破坏封装,关系就无法实现。只需尝试任何协变二元运算符即可查看:尝试在 C++ 中实现“小于”或“添加”。您可以轻松地编写基本抽象代码,但无法实现它。

在动态系统中,没有高级的形式化类型和封装,因此 OO 实际上可以工作,特别是,像原始 Smalltalk 这样的基于原型的系统实际上提供的工作模型根本无法使用静态类型约束进行编码。

以另一种方式回答这个问题:这个问题的基本假设在本质上是有缺陷的。OO 没有任何连贯的原则,因为它不是一个一致的理论,因为不存在任何具有足够能力的模型来处理除了简单的编程任务之外的任何事情。不同之处在于您放弃了什么:在动态系统中您放弃封装,在静态系统中您只需切换到可以工作的模型(函数式编程、模板等),因为所有静态类型的系统都支持这些东西。

于 2010-12-30T09:21:35.157 回答
0

接口可能会增加一定程度的开销,尤其是当您直接依赖其他人的 API 时。简单的解决方案 - 不要依赖其他人的 API。

让每个对象与它希望在理想世界中存在的接口对话。如果你这样做,你最终会得到具有小范围的小接口。通过这样做,您将在接口更改时获得编译时失败。

您的界面越小越具体,当界面更改时您必须做的“簿记”就越少。

静态类型的真正好处之一不是静态地知道您可以调用哪些方法,而是保证值对象已经过验证...如果您需要一个名称,并且名称必须小于 10 个字符,请创建一个 Name 类封装了该验证(尽管不一定是任何 I/O 方面 - 将其保留为纯值类型),编译器可以帮助您在编译时捕获错误,而不必在运行时进行验证。

如果您要使用静态语言,请充分利用它。

于 2009-12-17T06:11:55.803 回答