为什么 Haskell 不需要工厂模式?模式在 OOP 中解决的需求如何在 Haskell 中解决?

Enr*_*lis 6 oop haskell design-patterns functional-programming factory-pattern

我读过这个关于抽象工厂模式的问题,但它的唯一答案试图在 Haskell 中模拟在 OOP 语言中的情况(尽管前言是沿着你在 Haskell 中不需要它的路线)。

另一方面,我的意图并不是在 Haskell 等函数式语言上强制使用特定于 OOP 的模式。恰恰相反,我想了解Haskell 如何解决 OOP 中通过工厂模式解决的需求

我有一种感觉,即使这些需求一开始在 Haskell 中也没有意义,但我无法更好地表述这个问题。

我对工厂模式结构的理解(基于这个看起来很清楚的视频)是

  1. 有很多不同的产品,都实现了一个通用接口
  2. 有一堆不同的创建者类,都实现了一个通用接口
  • 2 中的每个类都隐藏了一个逻辑来创建 1 中的产品
    • (如果我理解正确的话,对象和创建者之间没有必要存在一对一的映射,因为创建者可以隐藏不同的逻辑,根据用户输入/时间/条件/任何内容对创建哪个特定对象做出不同的选择.)
  • 客户端代码将拥有创建者,每次使用其中一个(创建者,而不是客户端代码)将知道如何创建产品以及哪个特定产品。

所有(或部分)如何适用于 Haskell?

有几个不同的产品类实现一个通用的产品接口是 Haskell 中的一个东西,接口是一个类型class,产品是类型(datas/ newtypes/现有类型)。例如,从链接的视频参考飞船和小行星的例子,我们可以有一个类型class定义Obstacles任何提供sizespeedposition

class Obstacle a where
  size :: a -> Double
  speed :: a -> Int
  position :: a -> (Int,Int)
Run Code Online (Sandbox Code Playgroud)

AsteroidPlanet可能以某种方式实现此接口的两个具体类型,

data Asteroid = Asteroid { eqside :: Double, pos :: (Int,Int) } deriving Show
instance Obstacle Asteroid where
  size a = eqside a ^ 3 -- yeah, cubic asteroids
  speed _ = 100
  position = pos

data Planet = Planet { radius :: Double, center :: (Int,Int) } deriving Show
instance Obstacle Planet where
  size a = k * radius a ^ 3
    where k = 4.0/3.0*3.14
  speed _ = 10
  position = center
Run Code Online (Sandbox Code Playgroud)

到目前为止,我没有看到我在 Haskell 或 OOP 语言中所做的事情之间有任何真正的区别。但它来了。

此时,按照链接视频中的示例,客户端代码可以是一个游戏,它遍历一些关卡并根据关卡数量生成不同的障碍物;它可能是这样的:

clientCode :: [Int] -> IO ()
clientCode levels = do
  mapM_ (print . makeObstacle) levels
Run Code Online (Sandbox Code Playgroud)

wheremakeObstacle应该是创建者函数,或几个函数之一,它给定类型的输入Int应用逻辑来选择它是否必须创建一个Asteroid或一个Planet

但是,我不明白我如何拥有一个返回不同类型输出的函数,Asteroidvs Planet(它们实现相同Obstacle接口的事实似乎没有帮助),基于所有相同类型的不同值[Int],更不用说理解了“工厂”功能及其通用接口应该是什么。

lef*_*out 7

有几个不同的产品类实现一个通用的产品接口是 Haskell 的一个东西,接口是一个类型类

不完全的。确实,类型类可以表达接口在 OO 语言中的作用,但这并不总是有意义的。具体来说,对于一个所有方法都具有 form 类型签名的类,实际上并没有任何意义a -> Fubar

为什么?好吧,你不需要一个类——只要让它成为一个具体的数据类型!

data Obstacle = Obstace
  { size :: Double
  , speed :: Int      -- BTW, why would speed be integral??
  , position :: (Int,Int) }
Run Code Online (Sandbox Code Playgroud)

记录字段也可以是函数、IO动作等——这足以模拟 OO 类的方法可以做什么。纯数据唯一不能表达的是继承——但即使在面向对象中,也有一些关于组合应该优先于继承的口头禅,所以就是这样。

或者,您可以创建Obstacle一个 sum 类型

data Obstacle = Asteroid ...
              | Planet ...
Run Code Online (Sandbox Code Playgroud)

这取决于应用程序哪个更好。无论哪种方式,它仍然是一个具体的数据类型,不需要类。

随着Obstacle作为一个简单的数据类型,没有什么需要被“抽象”它的创作者。相反,您可以简单地使用各种函数-> Obstacle来创建障碍,这些障碍恰好代表小行星、行星或其他任何东西。

您还可以将您的那种“OO 接口实例”包装成一个数据类型,一个存在的

{-# LANGUAGE GADTs #-}

class Obstacle a where ...

data AnObstacle where
  AnObstance :: Obstacle a => a -> AnObstacle
Run Code Online (Sandbox Code Playgroud)

但是不要这样做,除非你完全知道这是你想要的。


Jon*_*rdy 6

当我需要创建在运行时之前未知的类型值时,我通常会使用 OOP 中的工厂。

传统的例子是动态UI:我读在UI布局的说明,并创建的一些不同子类中UIComponent的基类据此─ LabelFieldButton,等。UIComponent将提供一些用于渲染、响应事件等的通用接口。

抽象工厂将只是一个间接级别:为不同平台(Windows/Mac/Linux)、渲染格式(GUI/文本/HTML)等提供不同类型的工厂。

所以看起来你是从关于如何建模的几个常见的以 OOP 为中心的假设开始的:

  • 每个 UI 组件都实现为单独的类型

  • 具有公共接口或基类的类型应该成为类型类的实例

遵循这一推理思路将引导您进入问题中描述的实现,这被称为“存在性反模式”:

-- There are some component types:

data Label = …
data TextField = …
data Button = …
data SubmitButton = …
…

-- There’s a common interface for components:

class Component c where
  render :: c -> IO ()
  handleEvent :: c -> Event -> IO ()
  getBounds :: c -> (Word, Word)
  …

-- Each component type implements that interface:

instance Component Label where
  render = renderLabel
  handleEvent _ _ = pure ()
  getBounds (Label text) = computeTextDimensions text
  …

instance Component TextField where …

…

-- Abstract components are created dynamically:

data SomeComponent where
  SomeComponent :: (Component c) => c -> SomeComponent

-- A UI is a collection of abstract components:

type UI = [SomeComponent]
Run Code Online (Sandbox Code Playgroud)

为了通过解析 UI 描述来处理动态类型,您需要包装器SomeComponent,它是一对抽象(“存在”)类型的值c及其实例Component。这类似于 OOP 虚表。但是因为你唯一能用这个值做的事情就是Component对它应用 的方法,它完全等同于一个函数的记录:

-- A component is described by just its operations:

data Component = Component
  { render :: IO ()
  , handleEvent :: Event -> IO ()
  , getBounds :: (Word, Word)
  …
  }

-- There are no separate component types, only functions
-- that construct different ‘Component’ values. Fields
-- are just captured variables.

label :: String -> Component
label text = Component
  { render = renderLabel text
  , handleEvent = \ _ _ -> pure ()
  , getBounds = computeTextDimensions text
  …
  }

textField :: … -> Component
button :: … -> Component
…

-- A UI is a collection of components of uniform type:

type UI = [Component]
Run Code Online (Sandbox Code Playgroud)

这是抽象工厂的直接类比:具体Component工厂只是一个动态构建 的函数Component,而抽象工厂是一个动态构建具体工厂的函数。

-- A common dynamic description of the constructor
-- argument values of a UI element.
data ComponentDescription
  = Label String
  | TextField …
  | Button …
  …

-- Parse such a description from JSON.
instance FromJSON ComponentDescription where …

-- Construct a component from its constructor values.
type ComponentFactory = ComponentDescription -> Component

-- A dynamic description of a platform.
data Platform = Windows | Mac | Linux

-- Construct a component factory for a platform theme.
type AbstractComponentFactory = Platform -> ComponentFactory
Run Code Online (Sandbox Code Playgroud)

设计模式几乎消失了。它仍然存在,但只是功能的不同用途。在某种程度上,这也让人联想到“实体组件系统”架构,其中对象被描述为表示行为组合的数据。(不同之处在于这里没有“系统”。)

当您希望组件集是开放的时,此公式主要有用;但更常见的是,默认情况下,我们在 Haskell 中使用带有 sum 类型的封闭数据建模。在这种情况下,我们将直接使用与ComponentDescription上述类型类似的 sum 类型,并带有合适的字段,作为组件的表示。

在组件上添加新操作很容易:只需对 进行模式匹配ComponentDescription。添加新类型的组件需要更新所有现有函数(除非它们具有通配符匹配),但实际上编译器通常需要告诉您需要更新的所有内容。

扩展在这两个业务和组件类型是可以实现的太多,但一个很好的说明是范围出来这个答案。要了解更多信息,请搜索“表达问题”;特别是,无标签最终样式在 Haskell 中被认为是一种很好的传统解决方案,它以不同的方式使用类型类。