Enr*_*lis 6 oop haskell design-patterns functional-programming factory-pattern
我读过这个关于抽象工厂模式的问题,但它的唯一答案试图在 Haskell 中模拟在 OOP 语言中的情况(尽管前言是沿着你在 Haskell 中不需要它的路线)。
另一方面,我的意图并不是在 Haskell 等函数式语言上强制使用特定于 OOP 的模式。恰恰相反,我想了解Haskell 如何解决 OOP 中通过工厂模式解决的需求。
我有一种感觉,即使这些需求一开始在 Haskell 中也没有意义,但我无法更好地表述这个问题。
所有(或部分)如何适用于 Haskell?
有几个不同的产品类实现一个通用的产品接口是 Haskell 中的一个东西,接口是一个类型class
,产品是类型(data
s/ newtype
s/现有类型)。例如,从链接的视频参考飞船和小行星的例子,我们可以有一个类型class
定义Obstacles
任何提供size
,speed
和position
,
class Obstacle a where
size :: a -> Double
speed :: a -> Int
position :: a -> (Int,Int)
Run Code Online (Sandbox Code Playgroud)
并Asteroid
与Planet
可能以某种方式实现此接口的两个具体类型,
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
。
但是,我不明白我如何拥有一个返回不同类型输出的函数,Asteroid
vs Planet
(它们实现相同Obstacle
接口的事实似乎没有帮助),基于所有相同类型的不同值[Int]
,更不用说理解了“工厂”功能及其通用接口应该是什么。
有几个不同的产品类实现一个通用的产品接口是 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)
但是不要这样做,除非你完全知道这是你想要的。
当我需要创建在运行时之前未知的类型值时,我通常会使用 OOP 中的工厂。
传统的例子是动态UI:我读在UI布局的说明,并创建的一些不同子类中UIComponent
的基类据此─ Label
,Field
,Button
,等。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 中被认为是一种很好的传统解决方案,它以不同的方式使用类型类。
归档时间: |
|
查看次数: |
243 次 |
最近记录: |