函数式编程在IT领域获得了巨大的发展。 事情来来往往,但FP并不是其中之一。 比OOP更具表现力
几年前,我在博洛尼亚举行的LambaConf会议上开始研究它,我获得的见识越多,我就越喜欢FP。 在六月,我去了这个工作坊,老师深入研究了代数数据类型和模式匹配 。 我最终也了解了Monad是什么,但这是另一个故事。 我曾经将模式匹配视为一种解构列表的有趣方式。 现在,我知道这是FP软件设计的基础,并且是解决将域对象数据和行为分离的有效方法。
让我们来看一个例子!
假设我们要编写一个ERP软件,以竞争两个不同国家的市场。 意大利和德国。 我们将在许多域对象上编写许多CRUD操作。 这里没有挑战。
但是,如何在相同的功能流内在不同的数据结构上针对特定国家/地区的业务规则建模呢?
例如,这两个国家/地区的数据库中都有文章,并且您肯定要在它们上进行搜索。 但是搜索规则和获取的数据可能完全不同。
由于数据结构不同,我们不能仅仅拥有域实体“ Article”:我们至少需要一个“ ItalianArticle”和“ GermanArticle”。 考虑到数据存储的结构也可能不同。
让我们看一下Scala的实现:写一个sum类型的Article
。 然后我们将其专门化为ItalianArticle
和GermanArticle
的产品类型。 另外,我们希望当我们搜索某些内容时,有时什么也找不到。 因此,我们还将考虑ArticleNotFound
。
sealed trait Article
case class ItaArticle ( id: Int , itaData1: String , itaData2: Int ) extends Article
case class DeuArticle ( id: Int , deuData1: Int , deuData2: Int ) extends Article
case class ArticleNotFound ( ) extends Article
每次我们从某个地方收到文章时,我们都会在其上进行图案匹配。 在比赛中,我们可以访问其专门数据
def doSomething (article: Article ) : Unit = {
article match {
case ita: ItaArticle => print(ita.itaData1)
case deu: DeuArticle => print(deu.deuData2)
case nf: ArticleNotFound => print( "None" )
}
}
乍一看,它看起来像是经典的程序“开关”,然后是向下转换。 有一个重要的区别:编译器知道我们是否匹配每种类型的Article。 例如,如果我们忘记在ArticleNotFound
上进行匹配
def doSomething (article: Article ) : Unit = {
article match {
case ita: ItaArticle => print(ita.itaData1)
case deu: DeuArticle => print(deu.deuData2)
}
}
那么默认情况下, sbt编译器将发出警告。
编译器知道有问题时,我们可以使它引发错误而不是警告。
此时,完全出乎意料的事情发生了! 业务对我们提出了新要求:我们还需要在西班牙进行分销。
其实我们已经准备好了,所以让我们添加SpaArticle
我们的Article
和类型。
sealed trait Article
case class ItaArticle ( id: Int , itaData1: String , itaData2: Int ) extends Article
case class DeuArticle ( id: Int , deuData1: Int , deuData2: Int ) extends Article
case class SpaArticle ( id: Int , spaData1: Float , spaData2: String ) extends Article
case class ArticleNotFound ( ) extends Article
现在,我们需要添加西班牙语业务逻辑。
如果正常切换,搜索在每个实施了国家特定业务规则的地方将很痛苦。
相反,使用模式匹配,编译器会告诉我们必须进行干预。
是的,当然可以使用“ 访客”模式 !
如果您不了解访问者模式 ,请在此处查看
如果您不知道四个帮派的话,请在这里看看。
访客模式作为反模式已经很久了。 当您添加新的项目类型时,您还将向访问者界面添加新的方法。 在这种情况下,编译器将中断每个具体的访问者,直到您在每个访问者中实现新方法为止。 这是成为反模式的原因之一。
实际上,对于我们的担忧,这个“问题”看起来与我们正在寻找的完全一样!
在此遵循我们的总和类型的Kotlin实现:
interface Article {
fun applyTo (consumer: ArticleConsumer )
}
class ItaArticle ( val itaData1: String, val itaData2: Int ) : Article {
override fun applyTo (consumer: ArticleConsumer ) {
consumer.use( this )
}
}
class DeuArticle ( val deuData1: String) : Article {
override fun applyTo (consumer: ArticleConsumer ) {
consumer.use( this )
}
}
您会注意到,我更喜欢考虑“应用于数据使用者的数据”,而不是使用“ accept”和“ Visitor”命名。 (我正在寻找更好的命名方式,因此,如果您有任何建议,请发表评论!谢谢 )
在这里,我们可以实施特定国家/地区的业务规则
interface ArticleConsumer {
fun use (article: ItaArticle )
fun use (article: DeuArticle )
fun use (article: ArticleNotFound )
}
fun useAnArticle (article: Article ) : String {
var x = ""
article.applyTo( object : ArticleConsumer {
override fun use (article: ItaArticle ) {
x = article.itaData1
}
override fun use (article: DeuArticle ) {
x = article.deuData1
}
override fun use (article: ArticleNotFound ) {
x = "not found"
}
})
return x
}
此示例的语义与FP模式匹配完全相同。 当我们要添加新的国家,那么编译器是要打破一切,直到我们不执行新的fun use(article: SpaArticle)
在每一个ArticleConsumer
。
访客模式的最初目的是对异构对象的集合进行迭代操作,该异构对象不共享相同的接口和数据类型。
在本文中,我建议将其用作路由点。 当您最终枚举域实体并需要设置本地化的域上下文时,此功能很有用。
在方法中枚举域实体被认为是一个问题。 在这种用例中,它是模式的关键特征。
我还证明了这种用法在语义上等同于FP模式匹配 。
请在评论中留下您的意见和反馈。
感谢您的阅读!
From: https://hackernoon.com/welcome-to-the-oop-pattern-matching-visitor-pattern-1q7n031xc