我想用多用户界面后端(例如文本和图形)编写代码,因此它们很容易切换 . 我的方法是使用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 回答
我将后端实现为单独的类,而不是传递一个关键字,因为这将允许我将各种状态挂钩到一个对象并保持它 .
我可能(否则)使用你一直暗指的通用功能设计 .
Common Lisp Interface Manager and multiple backends
支持多个后端的CLOS中的UI层的示例是CLIM,Common Lisp Interface Manager . You could study its software design. 请参阅以下链接 . 例如,参见类似端口的协议(与显示服务的连接),介质(绘图发生的地方,对应于某种工作表的输出状态的协议类),工作表(用于绘制和输入的表面,大致类似于分层窗口),移植(一个代表主机窗口的工作表),...在一个应用程序中打开一个端口(例如到特定的窗口系统,如X11 / Motif),应用程序的其余部分应该是运行基本不变 . CLIM的体系结构将其所有服务映射到特定的CLIM后端,后端为X11 / Motif(或您将使用的任何端口)提供接口 .
例如,函数
draw-line
将绘制到工作表,流和媒体 . 然后,通用函数medium-draw-line*
将实现各种版本的绘制线到一个或多个中等子类 .总的来说,这不是很成功,因为便携式用户界面层带来了复杂性,需要大量的工作来开发和维护 . 在90年代中期,Lisp应用程序的市场规模很小(参见AI Winter),CLIM不够好,而且实现是封闭源代码或专有代码 . 后来开发了一个名为McCLIM的开源/免费实现,它创建了工作软件 - 但最终开发者/用户失去了兴趣 .
A bit of history
在过去,Symbolics开发了一个名为“Dynamic Windows”的用户界面系统 . 它于1986年发布 . 它运行在Symbolics操作系统中,可以绘制其原生OS /硬件组合和X11 . 从1988年左右开始,开发了基于便携式CLOS的版本 . 第一个可用的版本(特别是1991年的1.0版本)可以在几个平台上使用:Genera,X11,Mac和Windows . 后来开发了一个新版本(版本2.0),它再次运行在各种系统上,但包含一个复杂的面向对象层,它提供了一个更明确的后端层,称为Silica . 这个后端层不仅支持便携式绘图之类的东西,还支持抽象窗口系统的一部分 . 更加雄心勃勃的部分,比如支持外观和感觉的适应(滑块,窗口样式,滚动条,菜单,对话框元素......)都没有完全解决,但至少可以作为第一代版本使用 .
Pointers
A Guided Tour of CLIM, Common Lisp Interface Manager(PDF)
二氧化硅:Implementation Reflection in Silica(PDF)
Spec(包括Silica):Common Lisp Interface Manager 2.0 Specification
为了补充其他答案,这个用例有两个库 . 两者都受到启发Magritte Meta Model,你应该检查一下 .
一个是descriptions,它允许您定义一个对象的不同'views' . 它不使用CLOS而是Sheeple,这是一个基于原型的CL对象系统 . 早期的方法是MAO,这是基于CLOS的 . 它为标准插槽对象添加了3个额外的插槽 . attribute-label,attribute-function和attribute-value . 属性函数a中的函数将slot-value转换为最终表示,如果function为nil,则attribute-value中的值按原样使用 . 而label是值的描述,类似于html5表单中的标签 .
“后端”是指前端,对吗?在用户与之交互的部分,而不是处理应用程序逻辑的部分?
最干净的选择是将程序划分为一个库(它提供程序的所有逻辑和功能,没有任何UI代码)和两个完全独立的UI程序,这些程序本身不实现任何功能,而只是使用库 . 你当然可以有一个包装器,如果需要,可以选择运行哪个接口 . 您应该将每个组件保留在自己的系统中 .
Edit: 当你想在不同的算法之间切换时,最好的选择可能只是将接口定义为一个类,将所有不同的算法定义为子类 .
Another edit: 如果您需要能够使用
mapcar
之类的函数,您可能希望拥有一个包含当前后端的全局变量 . 然后定义一个使用全局的包装函数 .