2014-10-11 124 views
3

在我的申請,我有能夠呈現的HTML一堆組件:無形案例研究

class StandardComponent { 
    def render: Html 
} 

它們由ComponentBuilder在運行時實例從ComponentDefinition對象,它提供了訪問運行時數據:

class ComponentBuilder { 
    def makeComponent(componentDef: ComponentDefinition): StandardComponent 
} 

然後有幾個幫手促進組件內的子組件的渲染:

def fromComponent(componentDef: ComponentDefinition)(htmlFn: Html => Future[Html])(implicit componentBuilder: ComponentBuilder): Future[Html] 

def fromComponents(componentDefs: Seq[ComponentDefinition])(htmlFn: Seq[Html] => Future[Html])(implicit componentBuilder: ComponentBuilder): Future[Html] 

def fromOptionalComponent(componentDefOpt: Option[ComponentDefinition])(htmlFn: Option[Html] => Future[Html])(implicit componentBuilder: ComponentBuilder): Future[Html] 

def fromComponentMap[K](componentDefMap: Map[K, ComponentDefinition])(htmlFn: Map[K, Html] => Future[Html])(implicit componentBuilder: ComponentBuilder): Future[Html] 

問題是,組件通常需要使用這些調用中的幾個。雖然它們被設計成可嵌套,它可以變成一個有點亂:

implicit val componentBuilder: ComponentBuilder = ??? 

val subComponent: ComponentDefinition = ??? 
val subComponents: Seq[ComponentDefinition] = ??? 
val subComponentOpt: Option[ComponentDefinition] = ??? 

fromComponent(subComponent) { html => 
    fromComoponents(subComponents) { htmls => 
    fromOptionalComponent(subComponentOpt) { optHtml => 
     ??? 
    } 
    } 
} 

我希望能夠做的是一些大體是:

withSubComponents(
    subComponent, subComponents, subComponentOpt 
) { case (html, htmls, optHtml) => /* as Html, Seq[Html], and Option[Html] */ 
    ??? 
} 

所以,我想在其參數中創建withSubComponents可變參數,並且我想讓它的第二個參數列表中的閉包有一個參數列表,該參數列表取決於arity和type中的第一個參數列表。理想情況下,它也需要像個人助手那樣隱含的ComponentBuilder。這是理想的語法,但我願意接受替代方案。我和我可以提供我迄今爲止所擁有的一些例子,但我所擁有的只是迄今爲止的想法。感覺就像我需要成爲一個合作產品的HList,然後我需要將兩個參數結合在一起的方式。

回答

1

在提高DSL的第一步可以是方法移動到這樣的隱式轉換:我聲明fromComponent

implicit class SubComponentEnhancements[T](subComponent: T)(
    implicit cb: ComponentBuilder[T]) { 

    def fromComponent(f: cb.HtmlType => Future[Html]): Future[Html] = ??? 
} 

注是有效的用於具有定義的ComponentBuilder每種類型T。正如你所看到的,我也想象ComponentBuilder有一個HtmlType。在您的例子將是Seq[Html]Option[Html]ComponentBuilder現在看起來是這樣的:

trait ComponentBuilder[T] { 
    type HtmlType 
    def render(componentDef: T): HtmlType 
} 

我又想到了ComponentBuilder能夠爲組件呈現爲某種類型的Html。讓我們聲明一些組件構建器爲了能夠調用不同類型的fromComponent方法。

object ComponentBuilder { 

    implicit def single = 
    new ComponentBuilder[ComponentDefinition] { 
     type HtmlType = Html 
     def render(componentDef: ComponentDefinition) = { 
     // Create standard component from a component definition 
     val standardComponent = new StandardComponent 
     standardComponent.render 
     } 
    } 

    implicit def seq[T](
    implicit cb: ComponentBuilder[T]) = 
    new ComponentBuilder[Seq[T]] { 
     type HtmlType = Seq[cb.HtmlType] 
     def render(componentDef: Seq[T]) = 
     componentDef.map(c => cb.render(c)) 
    } 

    implicit def option[T](
    implicit cb: ComponentBuilder[T]) = 
    new ComponentBuilder[Option[T]] { 
     type HtmlType = Option[cb.HtmlType] 
     def render(componentDef: Option[T]) = 
     componentDef.map(c => cb.render(c)) 
    } 
} 

注意,每個組件助洗劑的指定了一個HtmlType即在同步與ComponentBuilder的類型。容器類型的構建器只需要爲其內容請求組件構建器。這使我們能夠嵌套不同的組合,而不需要額外的努力。我們可以進一步概括這個概念,但現在這沒問題。

至於single組件構建器,您可以更一般地定義,允許您擁有不同類型的組件定義。將它們轉換爲標準組件可以使用Converter,它可以位於多個不同的地方(伴侶對象X,伴侶對象Converter或用戶需要手動導入的單獨對象)。

trait Converter[X] { 
    def convert(c:X):StandardComponent 
} 

object ComponentDefinition { 
    implicit val defaultConverter = 
    new Converter[ComponentDefinition] { 
     def convert(c: ComponentDefinition):StandardComponent = ??? 
    } 
} 

implicit def single[X](implicit converter: Converter[X]) = 
    new ComponentBuilder[X] { 
    type HtmlType = Html 
    def render(componentDef: X) = 
     converter.convert(componentDef).render 
    } 

不管怎麼說,現在該代碼如下所示:

subComponent fromComponent { html => 
    subComponents fromComponent { htmls => 
    subComponentOpt fromComponent { optHtml => 
     ??? 
    } 
    } 
} 

這看起來像一個熟悉的模式,讓我們重命名方法:

subComponent flatMap { html => 
    subComponents flatMap { htmls => 
    subComponentOpt map { optHtml => 
     ??? 
    } 
    } 
} 

請注意,我們是在一廂情願思維空間,上面的代碼不會編譯。如果我們做的一些方式,但是編譯我們可以寫一些像下面這樣:

for { 
    html <- subComponent 
    htmls <- subComponents 
    optHtml <- subComponentOpt 
} yield ??? 

這看起來非常令人驚異的是我,可惜OptionSeqflatMap功能本身,所以我們需要隱藏的。以下代碼看起來很乾淨,給我們提供了隱藏flatMapmap方法的機會。

trait Wrapper[+A] { 
    def map[B](f:A => B):Wrapper[B] 
    def flatMap[B](f:A => Wrapper[B]):Wrapper[B] 
} 

implicit class HtmlEnhancement[T](subComponent:T) { 
    def html:Wrapper[T] = ??? 
} 

for { 
    html <- subComponent.html 
    htmls <- subComponents.html 
    optHtml <- subComponentOpt.html 
} yield ??? 

正如你所看到的,我們仍然處於一廂情願的空間,讓我們看看我們是否可以填補空白。

case class Wrapper[+A](value: A) { 
    def map[B](f: A => B) = Wrapper(f(value)) 
    def flatMap[B](f: A => Wrapper[B]) = f(value) 
} 

implicit class HtmlEnhancement[T](subComponent: T)(
    implicit val cb: ComponentBuilder[T]) { 

    def html: Wrapper[cb.HtmlType] = Wrapper(cb.render(subComponent)) 
} 

實現並不複雜,因爲我們可以使用我們之前創建的工具。請注意,在一廂情願的想法中,我返回了一個Wrapper[T],而我們實際上需要html,所以我現在使用組件構建器中的HtmlType

爲了改進類型推斷,我們將稍微更改ComponentBuilder。我們將將HtmlType類型的成員更改爲類型參數。

trait ComponentBuilder[T, R] { 
    def render(componentDef: T): R 
} 

implicit class HtmlEnhancement[T, R](subComponent: T)(
    implicit val cb: ComponentBuilder[T, R]) { 

    def html:Wrapper[R] = Wrapper(cb.render(subComponent)) 
} 

不同的建設者需要的改變以及

object ComponentBuilder { 

    implicit def single[X](implicit converter: Converter[X]) = 
    new ComponentBuilder[X, Html] { 
     def render(componentDef: X) = 
     converter.convert(componentDef).render 
    } 

    implicit def seq[T, R](
    implicit cb: ComponentBuilder[T, R]) = 
    new ComponentBuilder[Seq[T], Seq[R]] { 
     def render(componentDef: Seq[T]) = 
     componentDef.map(c => cb.render(c)) 
    } 

    implicit def option[T, R](
    implicit cb: ComponentBuilder[T, R]) = 
    new ComponentBuilder[Option[T], Option[R]] { 
     def render(componentDef: Option[T]) = 
     componentDef.map(c => cb.render(c)) 
    } 
} 

最終的結果現在看起來是這樣的:

val wrappedHtml = 
    for { 
    html <- subComponent.html 
    htmls <- subComponents.html 
    optHtml <- subComponentOpt.html 
    } yield { 
    // Do some interesting stuff with the html 
    htmls ++ optHtml.toSeq :+ html 
    } 

// type of `result` is `Seq[Html]` 
val result = wrappedHtml.value 
// or 
val Wrapper(result) = wrappedHtml 

正如你可能已經注意到,我跳過了Future,你可以隨你自己補充一點。

我不確定您是如何設想您的DSL的,但它至少會爲您提供一些工具來創建非常酷的工具。

+0

哇,我不確定這與我們實際工作的方式是一致的(我在我的問題中大大簡化以試圖強調問題的本質),但這是驚人的,我相信甚至如果我們不能使用整個解決方案,我們可能會使用件。特別是,我同意利用'for ... yield'語法會有所幫助。我希望我可以給你更多的讚揚這樣一個深思熟慮的職位。 – acjay 2014-10-12 23:47:25

+0

那麼,'for ... yield'語法(在我看來)最適合於進程(或步驟)。在你的情況下,它似乎更多的是結構。我還沒有找到理想的Scala結構解決方案。 – EECOLOR 2014-10-13 00:45:01