3

我想编写具有多个用户界面后端(例如文本和图形)的代码,因此它们很容易切换。我的方法是使用 CLOS:

(defgeneric draw-user-interface (argument ui)
  (:documentation "Present the user interface")
  (:method (argument (ui (eql :tui)))
    (format t "Textual user interface! (~A)" argument))
  (:method (argument (ui (eql :gui)))
    (format t "Graphical user interface! (~A)" argument)))

这种方法乍一看似乎没问题,但它有一些缺点。为了简化调用,我定义了参数ui-type将在每个函数调用中使用,以简化切换后端,但是在使用高阶函数时会出现问题:

(defparameter *ui-type* :tui
  "Preferred user interface type")

(draw-user-interface 3 *ui-type*)

;;; I can't use the following due to the `ui' argument:
;(mapcar #'draw-user-interface '(1 2 3))

;;; Instead I have to write this
(mapcar #'(lambda (arg)
            (draw-user-interface arg *ui-type*))
        '(1 2 3))

;; or this
(mapcar #'draw-user-interface
        '(1 2 3)
        (make-list 3 :initial-element *ui-type*))

;; The another approach would be defining a function
(defun draw-user-interface* (argument)
  (draw-user-interface argument *ui-type*))

;; and calling mapcar
(mapcar #'draw-user-interface* '(1 2 3))

如果采用这种方法,我们可以将通用函数命名为 %draw-user-interface,而将包装函数命名为 draw-user-interface。

这是有效的方法还是有更直接的方法?问题是为相同的功能提供不同的后端,不一定是用户界面。

另一个用例可能是这样一种情况,当我有许多相同算法的实现(针对速度、内存消耗等进行了优化)并且我想以一种干净的方式切换它们,保留接口和参数类型。

4

4 回答 4

5

我会将后端实现为单独的类,而不是传递一个关键字,因为这将允许我将各种状态挂钩到一个对象中并保留它。

我可能(否则)会使用您一直在暗示的通用功能设计。

于 2016-01-15T16:58:51.167 回答
5

Common Lisp 接口管理器和多个后端

CLOS 中支持多个后端的 UI 层的一个示例是 CLIM,即 Common Lisp 接口管理器。你可以研究它的软件设计。请参阅下面的链接。例如,请参阅围绕类的协议,如端口(与显示服务的连接)、介质(绘图发生的地方,与某种表格的输出状态相对应的协议类)、表格(用于绘画和输入的表面,大致类似于分层窗口),嫁接(代表主机窗口的表格),...在应用程序中打开一个端口(例如到特定的窗口系统,如 X11/Motif),并且应用程序的其余部分应该基本不变地运行。CLIM 的架构将其所有服务映射到特定的 CLIM 后端,该后端提供到 X11/Motif(或您将使用的任何端口)的接口。

例如,该函数draw-line将绘制到图纸介质。然后,通用函数 medium-draw-line*将针对一个或多个中等子类实现各种版本的绘制线。

一般来说,这不是很成功,因为可移植的用户界面层带来了复杂性,并且需要大量的工作来开发和维护。在 90 年代中期,Lisp 应用程序的市场很小(参见 AI Winter),CLIM 还不够好,并且实现是闭源和/或专有的。后来开发了一个名为 McCLIM 的开源/免费实现,它创建了工作软件 - 但最终开发人员/用户失去了兴趣。

一点历史

以前,Symbolics 开发了一种称为“动态窗口”的用户界面系统。它于 1986 年发布。它在 Symbolics 操作系统中运行,可以利用其原生操作系统/硬件组合和 X11。从 1988 年左右开始,开发了基于 CLOS 的便携式版本。第一个可用版本(尤其是 1991 年的 1.0 版)可在多个平台上使用:Genera、X11、Mac 和 Windows。后来开发了一个新版本(2.0 版),它再次在各种系统上运行,但包括一个复杂的面向对象层,它提供了一个更明确的后端层,称为 Silica。这个后端层不仅支持诸如可移植绘图之类的东西,而且还支持抽象窗口系统的一部分。更雄心勃勃的部分,例如支持外观和感觉的适应(滑块、窗口样式、滚动条、菜单、对话框元素,......

指针

CLIM 导览,Common Lisp 接口管理器(PDF)

Silica:Silica中的实现反射(PDF)

规范(包括 Silica):Common Lisp Interface Manager 2.0 规范

于 2016-01-15T21:08:51.913 回答
3

为了补充其他答案,此用例有两个库。两者都受到Magritte 元模型的启发,您应该检查一下。

一种是描述,它允许您定义对象的不同“视图”。它不使用 CLOS,而是使用Sheeple,一个基于原型的 CL 对象系统。较早的方法是MAO,它是基于 CLOS 的。它向标准插槽对象添加了 3 个附加插槽。属性标签、属性功能和属性值。属性函数 a 中的函数将槽值转换为最终表示,如果函数为 nil,则按原样使用属性值中的值。而 label 是对值的描述,类似于 html5 表单中的标签。

于 2016-01-15T23:14:55.867 回答
1

“后端”是指前端,对吗?例如,用户与之交互的部分,而不是处理应用程序逻辑的部分?

最干净的选择是将您的程序分成一个库(它提供程序的所有逻辑和功能,无需任何 UI 代码)和两个完全独立的 UI 程序,它们本身不实现任何功能,而只是使用该库。如果需要,您当然可以有一个包装器来选择要运行的接口。您应该将每个组件保存在它们自己的系统中。

编辑:当您想在不同的算法之间切换时,最好的选择可能只是将接口定义为一个类,并将所有不同的算法定义为子类。

(defclass backend () ())
(defgeneric do-something (backend x y))

(defclass fast-backend (backend) ())
(defmethod do-something ((backend fast-backend) x y)
  (format t "Using fast backend with arguments ~a, ~a.~%" x y))

(defclass low-mem-backend (backend) ())
(defmethod do-something ((backend low-mem-backend) x y)
  (format t "Using memory efficient backend with arguments ~a, ~a.~%" x y))

(defun main (x y)
  (let ((backends (list (make-instance 'fast-backend)
                        (make-instance 'low-mem-backend))))
    (dolist (b backends)
      (do-something b x y))))

另一个编辑:如果您需要能够使用类似的函数mapcar,您可能需要一个包含当前后端的全局变量。然后定义一个使用全局的包装函数。

(defparameter *backend* (make-instance 'fast-backend))
(defun foobar (x y)
  (do-something *backend* x y))

(defun main (x y)
  (foobar x y)
  (let ((*backend* (make-instance 'low-mem-backend)))
    (foobar x y))
  (foobar x y))
于 2016-01-15T16:03:43.457 回答