大数跨境

卑微纪事:无法逃避的事物

卑微纪事:无法逃避的事物 索引目录
2024-12-16
1

HumbleUI中,有一个功能齐全的 OOP 系统,为低级组件实例提供支持。我知道,在 Clojure 中我们不应该谈论它,这是亵渎神明的。但是...

看。组件(我们在 Humble UI 中称它们为节点,因为它们的作用与 DOM 节点相同)具有状态。简单明了。没有办法绕过它。所以我们需要一些有状态的东西来存储它们。

它们也有行为。同样,这是不可避免的。状态和行为协同工作。

仍然不是 OOP 的案例:可能是映射和函数。

(def node []  {:state   (volatile! state)   :measure (fn [...] ...)   :draw    (fn [...] ...)})

但还有更多需要考虑的。



代码重用

许多节点共享相同的模式:例如,包装器是“包装”另一个节点的节点。padding包装器:

[ui/padding {:padding 10} [ui/button "Click me"]]

还有center

[ui/center [ui/button "Click me"]]

所以也是如此rect(它在子项后面绘制一个矩形):

[ui/rect {:paint ...} [ui/button "Click me"]]

前两个在子元素的定位方式上有所不同,但在绘制和事件处理上相同。第三个具有不同的绘制函数,但布局和事件处理相同。

我想写AWrapperNode一次并让其余节点重用它。

现在 — 你可能会想 — 这仍然不是 OOP 的情况。只需提取一堆函数,然后挑选即可!

;; shared library code(defn wrapper-measure [...] ...)
(defn wrapper-draw [...] ...)
;; a node(defn padding [...] {:measure (fn [...] <custom measure fn>) :draw wrapper-draw}) ;; reused

这具有自由选择的额外好处:您可以混合和匹配来自不同父级的实现,例如从包装器测量并从容器中提取。


部分代码替换

有些函数会调用其他函数!真是令人惊讶。

一个方向很简单。例如,Rect 节点可以先绘制自身,然后调用父节点。我们通过将一个函数包装到另一个函数中来解决这个问题:

(defn rect [opts child]  {:draw (fn [...]           (canvas/draw-rect ...)           ;; reuse by wrapping           (wrapper-draw ...))})

但现在我想用另一种方式来实现:父级定义包装行为而子级仅替换其中的一部分。

例如,对于 Wrapper 节点,我们总是希望保存和恢复绘图周围的画布状态,但绘图本身可以由子节点重新定义:

(defn wrapper-draw [callback]  (fn [...]    (let [layer (canvas/save canvas)]      (callback ...)      (canvas/restore canvas layer))))
(defn rect [opts child] {:draw (wrapper-draw ;; reuse by inverse wrapping (fn [...] (canvas/draw-rect ...) ((:draw child) child ...)}))})

我不确定你是怎么想的,但是对我来说,它开始感觉有点太高调了。

另一个选择是传递“this”并使共享函数在其中查找实现:

(defn wrapper-draw [this ...]  (let [layer (canvas/save canvas)]    ((:draw-impl this) ...) ;; lookup in a child    (canvas/restore canvas layer))))
(defn rect [opts child] {:draw wrapper-draw ;; reused :draw-impl (fn [this ...] ;; except for this part (canvas/draw-rect ...) ((:draw child) child ...)}))

开始感觉像 OOP 了,不是吗?


面向未来

最后的问题:我希望 Humble UI 用户能够编写自己的节点。请注意,这不是默认界面,但如果有人想要/需要使用低级界面,为什么不呢?我希望他们拥有我拥有的所有工具。

问题是,如果将来我添加了另一种方法怎么办?例如,当一切开始时,我只有:

  • -measure

  • -draw

  • -event

最后,我添加了-context、、-iterate-*-impl这些版本。没人能保证我将来不再需要另一个。

现在,使用 map 方法,问题在于没有节点。节点写为:{:draw    ... :measure ... :event   ...}

当我添加一个方法时不会突然有一个context方法。

这就是 OOP 解决的问题!如果我控制根实现并向其中添加更多内容,那么无论何时编写节点,每个人都会得到它。


看起来怎么样

我们仍然有正常的协议:

(defprotocol IComponent  (-context              [_ ctx])  (-measure      ^IPoint [_ ctx ^IPoint cs])  (-measure-impl ^IPoint [_ ctx ^IPoint cs])  (-draw                 [_ ctx ^IRect rect canvas])  (-draw-impl            [_ ctx ^IRect rect canvas])  (-event                [_ ctx event])  (-event-impl           [_ ctx event])  (-iterate              [_ ctx cb])  (-child-elements       [_ ctx new-el])  (-reconcile            [_ ctx new-el])  (-reconcile-impl       [_ ctx new-el])  (-should-reconcile?    [_ ctx new-el])  (-unmount              [_])  (-unmount-impl         [_]))

然后我们有基类(抽象类):

(core/defparent ANode  [^:mut element   ^:mut mounted?   ^:mut rect   ^:mut key   ^:mut dirty?]    protocols/IComponent  (-context [_ ctx]    ctx)
(-measure [this ctx cs] (binding [ui/*node* this ui/*ctx* ctx] (ui/maybe-render this ctx) (protocols/-measure-impl this ctx cs)))
...)

请注意,父母也可以有字段!承认吧:我们都使用 Clojure 来编写更好的 Java。

然后我们有中间抽象类,一方面,重用父类行为,但也在需要时重新定义它。例如

(core/defparent AWrapperNode [^:mut child] :extends ANode  protocols/IComponent  (-measure-impl [this ctx cs]    (when-some [ctx' (protocols/-context this ctx)]      (measure (:child this) ctx' cs)))
(-draw-impl [this ctx rect canvas] (when-some [ctx' (protocols/-context this ctx)] (draw-child (:child this) ctx' rect canvas))) (-event-impl [this ctx event] (event-child (:child this) ctx event)) ...)

最后,叶子节点几乎是正常的deftype,但是它们从父母节点中提取基本的实现。

(core/deftype+ Padding [] :extends AWrapperNode  protocols/IComponent  (-measure-impl [_ ctx cs] ...)  (-draw-impl [_ ctx rect canvas] ...))

底层几乎没有什么魔法。父类实现只是复制到子类中,字段与子类的字段连接起来,等等。

再次强调,这不是最终用户将使用的界面。最终用户将编写如下组件:

(ui/defcomp button [opts child]  [clickable opts   [clip-rrect {:radii [4]}    [rect {:paint button-bg)}     [padding {:padding 10}      [center       [label child]]]]]])

但所有这些rect/padding/center/label最终都会被实例化为节点。哎呀,甚至你的button意志也会变成FnNode。但你不需要知道这一点。

另外,提醒一下:所有这些解决方案,就像 Humble UI 本身一样,目前都在进行中。不保证它会一直保持这种状态。


结论

我听说 OOP 最初是专门为 UI 发明的。可变对象具有大部分共享但有时不同的行为,与对象范式完美匹配。

好吧,现在我知道了:即使在今天,无论你如何开始,最终你都会得出相同的结论。

希望您觉得这篇文章有趣。如果您有更好的想法,请告诉我。


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读12
粉丝0
内容444