不破坏开闭原则的数据构造函数

Posted

技术标签:

【中文标题】不破坏开闭原则的数据构造函数【英文标题】:Data constructors without breaking the open/closed principle 【发布时间】:2014-06-20 10:42:03 【问题描述】:

我有一个这样的数据构造函数

class FooClass a where
    foo :: a -> b

class BarClass a where
    bar :: a -> b

data FooBar = Foo :: FooClass a => a -> IO ()
            | Bar :: BarClass a => a -> IO ()

这样我就可以使用模式匹配了:

foobar :: FooBar -> a -> IO ()
foobar (Foo f) x = f (foo x)
foobar (Bar f) x = f (bar x)

但是,这打破了开/关原则。 我希望能够使用基于其他类的其他方法来扩展 FooBar

我将如何在 Haskell 中实现这一点?

【问题讨论】:

这段代码无法编译——你想做什么?我看不出如何使用这些类型实现 foobar “开闭”原则在非面向对象语言中如何应用?我不确定您的原则在 Haskell 类型系统的上下文中是否有意义。 这两个类只有一个“有意义的”实例:instance Foo Void where foo = absurd。对于任何其他实例,您只能设置foo = const undefined。我怀疑您实际上的意思是 foo :: a -> B 用于某些固定类型 B.... 但是为什么不将普通的 B 值存储在任何地方,在存储之前应用 foo 呢? @DonStewart OCP 并非特定于 OO 语言。它说扩展您的软件,以便旧代码可以使用新代码,不应该需要修改旧代码。在 OO 中,这是通过子类化来实现的,在 FP 中……对,通过使用高阶函数。 听起来与 FP/types 中的“The Expression Problem”非常相似。如果用户在静态类型设置中寻求表达问题的解决方案,那么这是一个有趣的问题。虽然也许“新旧”函数不如“旧类型和新类型”有趣 【参考方案1】:

正如其他人所指出的,此代码存在缺陷,会掩盖您的问题。尝试过分思考 OO 原则如何转化为 FP 也可能很危险。它们有一席之地,因为很多面向对象的内容都自然嵌入到了 FP 中,但最好先直接学习 FP,然后作为某些特殊情况观察规律。

特别是,我们可以讨论如何更好地细化类型是一种扩展形式。例如,比较诸如

之类的类型
(Num a)         => a -> IO ()
(Num a, Show a) => a -> IO ()

我们可以讨论第二个函数如何接收一组类型,这些类型是第一个函数输入的自然子类型。特别是,可以输入到第二个函数的一组可能类型是对第一个函数的输入的改进。作为这些功能的用户,使用第二个功能的有效方法较少。作为这些功能的实现者,有更多有效的方法来实现第二个功能。其实我们知道以下几点

    作为第二个函数的有效输入的所有值也是第一个函数的有效输入 第一个签名正确键入的所有函数也由第二个签名正确键入。

在Game semantics 的研究中探索了给予和索取之间的这种二元性。 “开放以供扩展”的想法很简单,因为我们总是可以决定要求更精致的类型,但这几乎完全无趣,因为这在如何使用精致类型时很明显。


那么直接使用 ADT(data 声明)呢?然后打开/关闭? Mu—ADT 不是对象,因此该规则不能直接应用。

【讨论】:

【参考方案2】:

在 Haskell 中做例子的诀窍是使用函数而不是类:

-- FooBar is like a base class
-- with methods foo and bar.
-- I've interpreted your example liberally
-- for purposes of illustration.
-- In particular, FooBar has two methods -
-- foo and bar - with different signatures.
data FooBar = FooBar 
  foo :: IO (),
  bar :: Int -> Int


-- Use functions for classes, like in javascript.
-- This doesn't mean Haskell is untyped, it just means classes are not types.
-- Classes are really functions that make objects.
fooClass :: Int -> FooBar
fooClass n = FooBar 
    foo = putStrLn ("Foo " ++ show n)
    bar = \n -> n+1


barClass :: FooBar
barClass = FooBar 
    foo = putStrLn "Bar ",
    bar = \n -> n * 2


-- Now we can define a function that uses FooBar and it doesn't matter
-- if the FooBar we pass in came from fooClass, barClass or something else,
-- bazClass, say.
foobar (FooBar foo bar) = do
    -- invoke foo
    foo
    -- use bar
    print (bar 7)

这里FooBar 是“对扩展开放”,因为我们可以使用不同的行为创建任意数量的FooBar 值。

要使用另一个字段baz“扩展”FooBar,而不更改FooBarfooClassbarClass,我们需要声明包含FooBarFooBarBaz 类型。我们仍然可以使用我们的foobar 函数,我们只需要先从FooBarBaz 中提取FooBar

到目前为止,我一直在接近 OOP。这是因为 Bertrand Meyer 将开放封闭原则表述为需要 OOP 或类似的东西:

软件实体(类、模块、函数等)应该是开放的 用于扩展,但关闭以进行修改

特别是,“扩展”一词传统上被解释为“子类化”。如果您准备将原理解释为仅仅是“具有扩展点”,那么任何以另一个函数作为参数的函数都是“开放扩展”。这在函数式编程中很常见,以至于它不被视为原则。 “参数化原则”听起来不一样。

【讨论】:

以上是关于不破坏开闭原则的数据构造函数的主要内容,如果未能解决你的问题,请参考以下文章

删除复制构造函数会破坏继承的构造函数

如何在不破坏移动和复制构造函数的情况下声明虚拟析构函数

WPF:在 UserControl 的构造函数中加载数据会破坏设计器

将对象的指针提供给构造函数会导致对象被破坏[重复]

开闭原则

你如何优先考虑全局构造函数?