装饰器模式的功能等价替代是什么?

42

函数式编程中与装饰器设计模式相对应的是什么?

例如,您将如何以函数式风格编写这个特定示例


我认为Haskell中的Monad转换器类似于装饰者模式。但我可能错了,因为我没有使用/编写Monad转换器的太多经验。 - Ionuț G. Stan
我必须承认,就单子而言,我还没有完全掌握,但我发现装饰器模式和单子之间有很多相似之处。 - David Grenier
组合库是您正在寻找的内容。尝试查看解析器组合器的经典示例。 - Eric
没有真正需要装饰器模式,因为行为自然地与对象解耦。您可以通过元组、记录、列表或任何包含数据类型来向您的类型添加数据。 - VoronoiPotato
10个回答

33
在函数式编程中,您会将给定的函数封装在一个新函数中。
为了举一个类似于您问题中引用的Clojure示例的人为例子:
我的原始绘图函数:
(defn draw [& args]
  ; do some stuff 
  )

我的函数包装器:

; Add horizontal scrollbar
(defn add-horizontal-scrollbar [draw-fn]
  (fn [& args]
    (draw-horizontal-scrollbar)
    (apply draw-fn args)))


; Add vertical scrollbar
(defn add-vertical-scrollbar [draw-fn]
  (fn [& args]
    (draw-vertical-scrollbar)
    (apply draw-fn args)))

; Add both scrollbars
(defn add-scrollbars [draw-fn]
  (add-vertical-scrollbar (add-horizontal-scrollbar draw-fn)))

这些函数返回一个新函数,可在任何使用原始绘图函数的地方使用,但也会绘制滚动条。


请参见 comp: (def add-scrollbars (comp add-vertical-scrollbar add-horizontal-scrollbar)) - user1804599

18

柯里化函数参数/组合是最接近的等效方法。然而,甚至提出这个问题都是错误的,因为模式存在于补偿宿主语言弱点的情况下。

如果C++/Java/C#或任何其他实际上相同的语言中内置装饰功能,你就不会将其视为一种模式。恰好“模式”是在早期绑定的命令式面向对象语言中用于结构化系统的模式,通常没有自动包装,并且根类的协议相对较少。

编辑:此外,在这些语言中,很多这样的东西被研究为模式,因为没有明显的内置高阶函数、高阶类型和类型系统也相对无用。显然,这不是这些语言的普遍问题,但当这些模式开始被编码时,这些问题存在。


8
是的,它们是习语,因为没有直接的语言支持,而且语言使得做这些事情变得非常困难。因此,使用习语来描述如何做好这些事情。 - Marcin
1
确切地说,就像我所说的,宿主语言中存在一些弱点。尽管在Haskell的单子支持方面,语言支持的水平是如此之高,以至于我不会称在该语言中使用单子为一种习惯用法,而只是一种特性。 - Marcin
2
@Marcin,确实,在引入do符号的语言支持之前,这几乎是一种模式(在某些情况下我仍然会称其为函数模式)。然而,Haskell有一些优点使得它看起来不像是一种模式(操作符支持、类型类和简洁的lambda符号)。 - Ionuț G. Stan
1
没错,没有人会认为函数式编程语言不需要模式。 - Marcin
6
在Clojure软件开发实用工作室中,Rich Hickey曾说过:“模式意味着'我已经用完了语言'”,这句话非常贴切地描述了这个讨论的主题。 - Michael Kohl
显示剩余3条评论

11
您可以通过将函数放入其他函数中进行“装饰”,通常使用某种形式的高阶函数来执行包装。
Clojure 的简单示例:
; define a collection with some missing (nil) values
(def nums [1 2 3 4 nil 6 7 nil 9])

; helper higher order function to "wrap" an existing function with an alternative implementation to be used when a certain predicate matches the value
(defn wrap-alternate-handler [pred alternate-f f]
  (fn [x] 
    (if (pred x) 
      (alternate-f x)
      (f x))))

; create a "decorated" increment function that handles nils differently
(def wrapped-inc 
  (wrap-alternate-handler nil? (constantly "Nil found!") inc))

(map wrapped-inc nums)
=> (2 3 4 5 "Nil found!" 7 8 "Nil found!" 10)

这种技术在函数库中被广泛应用。一个很好的例子是使用Ring中间件包装Web请求处理程序 - 所链接的示例将html请求的参数处理包装在任何现有处理程序周围。


6

类似这样的:

class Window w where
    draw :: w -> IO ()
    description :: w -> String

data VerticalScrollingWindow w = VerticalScrollingWindow w

instance Window w => Window (VerticalScrollingWindow w) where
    draw (VerticalScrollingWindow w)
       = draw w >> drawVerticalScrollBar w  -- `drawVerticalScrollBar` defined elsewhere
    description (VerticalScrollingWindow w)
       = description w ++ ", including vertical scrollbars"

注意:这些“装饰器”(即多态数据类型,其中定义了Window类型类的实例,如上所示)确实是可堆叠的。您可以编写类似于do { let { w = SimpleWindow; w1 = VerticalScrollingWindow w; w2 = BorderedWindow w1 }; draw w2; }的代码。 - rkhayrov
3
为什么要使用类型类,而不是简单地使用 data Window = Window { draw :: IO (), description :: String } - C. A. McCann
2
这是Haskell代码吗?也许需要为初学者注明一下? - mrsteve

6
在Haskell中,这个面向对象的模式可以直接翻译成一个字典。需要注意的是,直接翻译并不是一个好主意。试图将OO概念强制应用于Haskell有点反其道而行之,但既然您要求了,那么这就是它。
窗口界面
Haskell有类,具有接口的所有功能以及更多功能。因此,我们将使用以下Haskell类:
class Window w where
  draw :: w -> IO ()
  description :: w -> String

抽象窗口装饰器类

这个有点棘手,因为Haskell没有继承的概念。通常我们根本不会提供这种类型,而是直接让装饰器实现Window,但让我们完全遵循示例。在这个示例中,一个WindowDecorator是一个带有窗口参数的构造函数的窗口,让我们增加一个函数来获取装饰后的窗口。

class WindowDecorator w where
   decorate :: (Window a) => a -> w a
   unDecorate :: (Window a) => w a -> a
   drawDecorated :: w a -> IO ()
   drawDecorated = draw . unDecorate
   decoratedDescription :: w a -> String
   decoratedDescription = description . unDecorate

instance (WindowDecorator w) => Window w where
   draw = drawDecorated
   description = decoratedDescription

注意,我们提供了Window的默认实现,它可以被替换,所有WindowDecorator的实例都会是一个Window

装饰器

制作装饰器可以按照以下步骤完成:

data VerticalScrollWindow w = VerticalScrollWindow w

instance WindowDecorator VerticalScrollWindow where
  decorate = VerticalScrollWindow
  unDecorate (VerticalScrollWindow w ) = w
  drawDecorated (VerticalScrollWindow w )  = verticalScrollDraw >> draw w

data HorizontalScrollWindow w = HorizontalScrollWindow w

instance WindowDecorator HorizontalScrollWindow where
  decorate = HorizontalScrollWindow
  unDecorate (HorizontalScrollWindow w .. ) = w
  drawDecorated (HorizontalScrollWindow w ..)  = horizontalScrollDraw >> draw w

结束

最后,我们可以定义一些窗口:

data SimpleWindow = SimpleWindow ...

instance Window SimpleWindow where
   draw = simpleDraw
   description = simpleDescription

makeSimpleWindow :: SimpleWindow
makeSimpleWindow = ...

makeSimpleVertical = VerticalScrollWindow . makeSimpleWindow
makeSimpleHorizontal = HorizontalScrollWindow . makeSimpleWindow
makeSimpleBoth = VerticalScrollWindow . HorizontalScrollWindow . makeSimpleWindow

2

首先,让我们尝试从面向对象编程的角度找到装饰器模式的所有主要组件。该模式基本上用于装饰,即为现有对象添加额外功能。这是该模式的最简单定义。现在,如果我们试图在FP世界中找到与此定义相同的组件,我们可以说额外功能=新函数,而对象在FP中不存在,而是FP拥有您所谓的数据或以各种形式表示的数据结构。因此,在FP术语中,该模式变为为FP数据结构添加附加函数或使用一些附加功能增强现有函数。


2
“Joy of Clojure”在第13.3章“缺乏设计模式”中讨论了这个问题。根据这本书,`- >` 和 `->>` 宏有点类似于装饰者模式。其中,`Joy of Clojure`是一个链接,“- >”和“->>”也都被标记成了链接。

1

我不是100%确定,但我认为C9高级函数式编程系列讲座非常好地解释了这个问题。

除此之外,你可以在F#中使用完全相同的技术(它支持完全相同的面向对象机制),在这种特殊情况下,我会这样做。

我想这是品味和你试图解决的问题的问题。


0
这是一个使用JSGI的示例,JSGI是JavaScript的Web服务器API:
function Log(app) {
    return function(request) {
         var response = app(request);
         console.log(request.headers.host, request.path, response.status);
         return response;
     };
 }

 var app = Logger(function(request) {
     return {
         status: 200,
         headers: { "Content-Type": "text/plain" },
         body: ["hello world"]
     };
  }

当然,符合规范的中间件可以堆叠起来使用(例如:Lint(Logger(ContentLength(app)))


看起来像是高阶函数。 - Alexander Mung-Zit Chiu

0
type Window = {Description: string}
type HorizontalScroll = {HPosition: int}
type VerticalScroll = {VPosition: int}
type Feature = 
    | HorizontalScroll of HorizontalScroll
    | VerticalScroll of VerticalScroll

let drawWithFeatures w (f: Feature) = 
    match f with
    | HorizontalScroll h ->  {Description= w.Description + "Horizontal"}
    | VerticalScroll v -> {Description= w.Description + "Vertical"} 

type FeatureTwo = Red of int| Blue of int | Green of int | Feature of Feature
let rec drawWithMoreFeatures (w: Window) (ft:FeatureTwo)=
        match ft with 
        | Red x     ->  {Description = w.Description + "Red"} 
        | Green x   ->  {Description = w.Description + "Green"} 
        | Blue x    ->  {Description = w.Description + "Blue"}
        | Feature f ->  drawWithFeatures w f

let window = {Description = "Window title"}
let horizontalBar =   HorizontalScroll {HPosition = 10}
let verticalBar =  VerticalScroll {VPosition = 20}

[Red 7; Green 3; Blue 2; Feature horizontalBar; Feature verticalBar] |> List.fold drawWithMoreFeatures window

F# 尝试

这是我尝试在 F# 中创建一些有意义的东西,因为你要求了很多例子。我有点生疏,希望没有人会羞辱我 :P。装饰器基本上需要两个部分,新行为和新数据。在函数式语言中,新行为非常容易实现,因为它们只是“另一个函数”,因为函数与对象本质上是解耦的。新数据实际上也同样容易实现,有几种方法可以实现,最简单的方法是使用元组。你可以看到我创建了一个新的数据类型,它是先前数据类型的超集,并且我已经为该现有行为调用了现有函数。因此,我们仍然尊重旧数据和旧行为,但我们也拥有了新行为和新数据。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接