电脑故障

位置:IT落伍者 >> 电脑故障 >> 浏览文章

架构模式-界面组装器模式


发布日期:2020/9/20
 

本文提出了一种界面设计中的架构模式界面组装器模式它致力于分解界面将界面和组装行为解耦将界面逻辑处理与领域逻辑处理解耦这样我们在开发GUI胖客户端界面应用时可以从众多的界面控制管理中解脱出来而专注于我们的后台业务逻辑的开发通过该模式我们可以动态地组装我们的界面我们甚至还可以在我们的界面中轻松地插入 transaction 事务或 session 会话管理

本文将通过分析设计一个架构的过程来讲解该模式从一个简单的设计模型开始一步步走向一个完整的架构借此也向大家展示一个架构设计的思维历程另外本文给出了 Eclipse SWT(Standard Widget Toolkit) 的示例

问题引出

界面设计常常是模式产生的根源无论是架构模式还是设计模式比如 MVC 模式ObserverFacade 等也是整个软件行业向前发展的动力遗憾的是即使在软件技术发达的今天界面设计仍是软件设计中的难以突破的瓶颈之一我们用过 Java swing 或 Eclipse SWT 作过项目的都知道要将界面进行分解是很困难的它不像我们的业务逻辑可以方便地按职责分解到不同的类中去实现因为各个业务逻辑之间耦合度很低但界面逻辑不一样你不可能将一个文本框的读取操作委任到另一个类中去而且各个界面元素之间相互依赖无法去除耦合一般的做法只能是在界面元素的事件触发(比如按钮点击事件)时将输入数据封装成一个数据对象传给后台的逻辑处理类来处理

Eclipse 的 Wizard 框架在界面分解上提供了一种很好的实践它可以将按钮区和其他界面区分离出来用类似 MVC 的方式实现了 Wizard 框架但这个实现并非没有瑕疵一个缺点是 wizard 是一个 plugin这样的话就减少了可重用性不能移植到 eclipse 以外的环境另一个缺点就是它引入了很大的复杂性而且在一些对界面元素的控制上丧失了一些精细控制的能力这可能是它过度地强调了自动化和用户扩展的方便性的缘故比如用户不能将自己的逻辑插入按钮区的按钮事件控制中而只能在自定义区的界面元素 Listener 中设定按钮区状态如果用户自定义的界面元素很多就需要很多个 Listener 来组合判断一个按钮状态(如是否进行下一步)这样的话就很影响性能而且无端地多了一堆复杂的逻辑判断也就是说本来只需在按钮 Listener 事件中处理的逻辑现在要分散在各个界面元素的 Listener 中去处理这也正是设计上一个值得反复强调的普遍问题当你要保持架构或设计的完美性时必然会以丧失其他特性为代价世上永远没有完美的东西我们只关注适合我们的

我下面要提出的这个架构模式的灵感来自于我的一个真实项目一个用 RSA(Rational Software Architect)/Eclipse 建模的项目在 RSA 环境中读写模型都必须在一个特有的 context 下才能操作这就意味着我在界面的启动之前必须封装好输入数据关闭之后返回输出数据而不是直接处理数据必须对输入/输出数据对象进行封装正如前面提到的这种情况界面设计中很普遍所以在模式命名时我用了组装器assembler 这个词有一层意思是输入/输出数据对象的组装另一层意思就是界面部件(界面元素的集合)的组装这里的组装还有更深层次的涵义就是指界面部件的可装配性可以在运行时动态组装而且这个模式可以用任何语言(JavaC++ 等)来实现在这里我会从一个简单的设计模型开始一步步走向一个完整的架构借此也向大家展示一个架构设计的思维历程本文中给出了 Eclipse SWT(Standard Widget Toolkit) 的示例

界面分解

在 Eclipse SWT 中有几个重要的界面部件一个是Shell界面的最外层容器类似 Java Swing 中的 Frame另一个就是 Composite界面元素的集合的容器类似 Java Swing 中的 Panel我们的界面分解将从 Composite 开始(Shell 本身是不需要分解的)我们可以在 Shell 中装配上一个空的 Composite 然后我们的具体界面元素都定义在这个 Composite 里这样就把 Composite 逻辑从 Shell 中分离出来了因此我们现在有了 个类(目前我们用概念类来表示)

把 Composite 逻辑从 Shell 中分离出来

Editor : 该类处理 Shell 的逻辑如显示show关闭close它负责创建和销毁 EditorComposite

EditorComposite: 该类处理 Composite 的界面逻辑如创建界面元素

有两点值得注意第一Editor 负责 EditorComposite 的创建和销毁也就是生命周期的管理那么我们可以想到如果我们的界面需要 transaction事务或 session会话的管理那么我们完全可以让 Editor 来负责这项职责而不是分散在各个 EditorComposite 中怎么扩展界面的事务功能可能会很复杂这已经超出本文的讨论范围我只是从架构的层面来分析可能有的可扩展性第二一个 Editor 可以包括多个 EditorComposite比如我们的属性页此时我们在Shell中定义的空的 Composite 将会是一个 TabFolder 还有一种情况就是我们可以根据某种逻辑来判断我们需要装配哪个 EditorComposite这就要求我们有一个装配的行为

界面部件装配

当我们的装配逻辑很简单时我们可以定义一个 assemble() 方法来负责装配行为但是当我们的界面需要组装一系列 EditorComposite 时就会牵涉到选择逻辑选择逻辑不一定很复杂但我们还是应该把这种行为从 Editor 中分离出来这样 Editor 可以集中精力负责与用户交互方面的职责而装配行为被分配到一个新的类 EditorAssembler 中这样做还有一个好处就是我们一旦有新的 EditorComposite 需要添加时我们只需要改变 EditorAssembler 的代码而不用修改 Editor 的代码这就把变化隔离出来对 Editor 的修改关闭对装配行为的扩展开放这正是面向对象设计领域反复强调的基本原则开放封闭原则(OpenClose Principle)经过重构后的架构如下图

重构后的架构

EditorAssembler该类处理 EditorComposite 的创建还包括多个 EditorComposite 的选择逻辑

这里的选择逻辑我们可以用 if/else 或 switch/case 来硬编码如果逻辑不是很复杂而且今后的修改不会太频繁的话用这种方法就足够了当然可以考虑将多个 EditorComposite 的装载信息专门用一个资源/信息类来存储 target=_blank>存储这在 EditorComposite 比较多的情况下很有效这样每次添加 EditorComposite 就只需要改变这个资源类这是一个很有用的建模原则(为了简化我们的核心模型我在这里不将这个资源类表示出来)

如果进一步考虑到我们的组装逻辑会比较复杂或会比较容易改变甚至在运行时动态改变我们就可以将众多的 EditorComposite 和复杂的逻辑存储在一个元数据文件中如 XML 或配置文件这样有新的 EditorComposite 需要支持或修改装配逻辑时不用修改 EditorAssembler 类只要修改元数据文件即可这样就可以很动态的配置我们的界面这里会有一个架构权衡的问题元数据由它的优点也有它的缺点其一必须编写解析它的类复杂性增加了其二不需要编译是它的优点也是它的缺点对 XML 或配置文件我们可以随意修改只有在运行时发现异常才知道改错了而且也可能被人蓄意破坏掉所以我们只在真的需要很频繁地修改 EditorComposite 的配置或经常需要增加 EditorComposite 时才采用元数据方案在这里我倾向于采用资源类方案

IO 数据装配

模型设计进行到这里我们似乎缺少了对数据流的建模在一个标准的界面程序中我们首先会有一组输出数据比如按OK按钮之后我们需要将界面元素上的输入信息输出到后台逻辑类来处理或直接调用好几个逻辑类分别处理不同的界面元素输入信息了我们一般习惯上可能直接将这个数据传递到逻辑类来处理这样做三个缺点其一如果我们的数据读写处理要求必须在特定的 context 中才能进行这样的话我们不能在界面中直接调用后台逻辑处理类了其实这种限制并不罕见在一些涉及底层(比如协议层)的开发时经常会碰到只能读不能写的情况其二UI 的可替代性差假如我们今后需要一种方案可以在运行时可以替换不同的 UI 但输出的数据是一样的也就是说后台逻辑处理完全一致那么这种情况我们就需要每一个 UI 自己去调用后台逻辑类重复编码而且可能由于程序员的失误每一个 UI 用了一个逻辑类从而导致一个完全相同行为的类有了好几个不一致实现版本这样不仅严重违反了面向对象设计而且还可能产生难以预料的 bug难以维护其三UI 的可重用性差对于上面多个 UI 对应一种逻辑处理的例子由于 UI 依赖了后台逻辑类如果今后要修改逻辑类结构的话我们就需要修改每一个 UI如果我们还有一种需求是要支持一个 UI 在不同的环境下需要不同的后台逻辑类时我们可能要专门在一个 UI 中设置一个属性来标识后台将要使用的逻辑类这会很复杂

解决上面几个缺点只有一种方法就是将后台逻辑类与 UI 解耦如果我们把要处理的输出数据打包成一个输出数据对象从界面统一输出再由 UI 的调用者决定调用哪一个后台逻辑类来处理数据而不是 UI 自己决定调用行为

还有一个输入数据对象就很好理解了我们调用 UI 时可能某些界面元素需要的从环境中动态装载数据比如一个下列列表还有一些我们上一次配置好的数据这次需要更新也需要将已有数据导入所以我们需要一个输入数据对象这就得到下面的模型

输入数据对象

src=http://imgeducitycn/img_///jpg border= twffan=done>

InputDataObject该类封装了输入数据由 EditorComposite 负责解析这些数据

OutputDataObject该类封装了输出数据由 EditorComposite 负责产生这些数据

Editor 负责传输这两个数据对象

从上面的模型我们可以看出 Editor 类其实相当于一个 Facade所有的界面与用户的交互都由它负责集中调度管理Editor 会将装配行为分配给 EditorAssembler 类来处理它还负责临时存储输入输出数据当然如果我们有类似 transaction 或 session 之类的处理会由 Editor 委派到别的相关类去处理应用 Facade 设计模式我们可以给 Editor 改个名字叫 EditorFacade这样更能体现设计者的意图千万不要忽视类的命名设计是一门严肃的科学每一个细节我们都不能苟且对架构的设计更要严谨命名可以起到沟通的作用还能起到提醒的功能EditorFacade 提醒我们以后要给它添加新的行为是记住它是一个 Facade不能将不相干的职责分配进来

另外我发现添加了 InputDataObject 类后EditorComposite 就有两个职责装载界面元素初始化数据(一些需要从环境中动态获得的输入数据从 InputDataObject 对象中获得)和显示上一次编辑的数据(也从 InputDataObject 对象中获得)我们定义两个方法来分别处理loadDataInfo()装载初始化数据;showPreInfo()显示上一次编辑的数据当然一般来说这两个方法是私有的private因为这是 EditorComposite 自身的内部逻辑但我们在这个架构中让它成为公有的public是因为我们可以在 EditorAssembler 类中集中控制它的调用而且每一个 EditorComposite 都会有装载初始化数据和显示已有数据的行为那么为什么不抽象出来呢以便让 EditorComposite 的开发提供者更清楚自己的职责虽然这么做有点破坏 EditorComposite 的封装性和其中方法的私密性但从架构的角度来讲这种破坏是合适的值得的

再看看前面的 EditorAssembler 类它其实有两个职责一个是创建 EditorComposite还有一个就是从几个 EditorComposite 选择出一个的判断逻辑如果我们把这两个不相干的职责解耦应用 Factory 设计模式就可以将创建 EditorComposite 的工作委任给一个 EditorCompositeFactory 的新类

经过以上几项重构后得到以下概念类模型

概念类模型

src=http://imgeducitycn/img_///jpg border= twffan=done>

经过上面的分析建模我们可以开始实现架构了从上面的概念模型我们可以很容易地抽象出相应的接口来首先我们看看 EditorFacade 类基于我们上面的讨论不同的界面可能有不同的需求比如有的要支持 transaction事务那么 EditorFacade 的实现就会不同所以我们有必要提取出一个接口来表示下面列出了这个接口 IEditorFacade

清单IEditorFacadejava



Code highlighting produced by Actipro CodeHighlighter (freeware)


> public interface IEditorFacade {

public void show();

public IInputDataObject getInputData();

public void setInputData(IInputDataObject inputData);

public IOutputDataObject getOutputData();

public void setOutputData(IOutputDataObject outputData);

public boolean isFinishedOK();

public Composite getRootComposite();

public void setAssembler(IEditorAssembler assembler);

public void close(boolean status);

}

那么 EditorFacade 类的部分代码如下

清单EditorFacadejava



Code highlighting produced by Actipro CodeHighlighter (freeware)


> public class EditorFacade implements IEditorFacade {

private Shell shell;

// validate if editor is closed with OK or Cancel

private boolean finishedOK;

// input data

private IInputDataObject inputData;

// output data

private IOutputDataObject outputData;

private Composite composite;

private IEditorAssembler assembler;

private void createSShell() {

shell = new Shell();

shellsetLayout(new GridLayout());

createComponent();

}

private void createComponent() {

composite = new Composite(shell SWTNONE);

………

assemblercreate(this);

}

public void show() {

thisshellopen();

assemblershowPreInfo();

}

public EditorFacade(IEditorAssembler assembler IInputDataObject inputData) {

thisassembler = assembler;

thisinputData = inputData;

thiscreateSShell();

}

public Composite getRootComposite() {

return composite;

}

public void close(boolean status) {

finishedOK = status;

thisshellclose();

}

}

下一步我们将两个 IO 数据类定义出来很显然不同的界面会有不同的输入输出数据在这里我们只能定义出两个抽象的接口 IInputDataObject 和 IOutputDataObject它们继承了序列化 javaioSerializable 接口里面并无其它内容这里注意一点空的接口并非无意义它可以起到标识的作用另外它隐藏了具体实现在传递数据时传递者不用知道具体数据内容这样传递者类具有更好的重用性而且具体数据类也不用暴露给不该知道它的类传递者类这正是另一个面向对象的基本原则迪米特法则(LoD)不要和陌生人说话下面给出 IInputDataObject 的清单

清单IInputDataObjectjava



Code highlighting produced by Actipro CodeHighlighter (freeware)


> public interface IInputDataObject extends Serializable {

}

接下来我们看看 EditorAssembler 类的实现根据前面的讨论它封装了界面的装配逻辑一定会被修改的那么我们就需要一个接口 IEditorAssembler 来规范它的行为在这里我还给出了一个抽象类 AbstractEditorAssembler实现了装载单个 EditorComposite 的方法另外我还给出了一个具体的 EditorAssembler 类这是一个每次只装载一个 EditorComposite 的例子代码清单如下

清单IEditorAssemblerjava



Code highlighting produced by Actipro CodeHighlighter (freeware)


> public interface IEditorAssembler {

/**

* create editor body and init

* @param editor

*/

public void create(IEditorFacade editor);

/**

* create editor composite

* @param editor

* @param compositeClassID

* :composite class nameeg testviewTestComposite

* @return

*/

public IEditorComposite createComposite(IEditorFacade editor

String compositeClassID);

/**

* show exist info in UI for update

*/

public void showPreInfo();

}

public interface IEditorAssembler {

/**

* create editor body and init

* @param editor

*/

public void create(IEditorFacade editor);

/**

* create editor composite

* @param editor

* @param compositeClassID

* :composite class nameeg testviewTestComposite

* @return

*/

public IEditorComposite createComposite(IEditorFacade editor

String compositeClassID);

/**

* show exist info in UI for update

*/

public void showPreInfo();

}

清单AbstractEditorAssemblerjava



Code highlighting produced by Actipro CodeHighlighter (freeware)


> public abstract class AbstractEditorAssembler implements IEditorAssembler {

public IEditorComposite createComposite(IEditorFacade editor

String compositeClassID) {

IEditorComposite body;

body = EditorCompositeFactorycreateComposite(compositeClassID editor);

bodycreate(editorgetRootComposite());

bodysetEditor(editor);

return body;

}

…………………………………

}

相关rss> 相关keyword >

清单StandaloneEditorAssemblerjava



Code highlighting produced by Actipro CodeHighlighter (freeware)


> public class StandaloneEditorAssembler extends AbstractEditorAssembler {

private String compositeClassID;

private IEditorComposite bodyComposite;

/**

*

* @param compositeClassID

* :composite class qulified nameeg comibmXXComposite;

*/

public StandaloneEditorAssembler(String compositeClassID) {

positeClassID = compositeClassID;

}

public void create(IEditorFacade editor) {

bodyComposite = createComposite(editor compositeClassID);

if (bodyComposite != null)

bodyCompositeloadDataInfo();

}

public void showPreInfo() {

bodyCompositeshowPreInfo();

}

}

接下来是 EditorCompositeFactory 的实现这个类的实现比较简单只是根据类名产生类

清单EditorCompositeFactoryjava



Code highlighting produced by Actipro CodeHighlighter (freeware)


> public class EditorCompositeFactory {

/**

* create IEditorComposite

* @param clsName

* @param editor

* @return

*/

public static IEditorComposite createComposite(String clsName

IEditorFacade editor) {

IEditorComposite composite = null;

try {

Class cls = ClassforName(clsName);

if (cls != null)

composite = (IEditorComposite) clsnewInstance();

} catch (Exception e) {

eprintStackTrace();

}

if (composite != null) {

compositesetEditor(editor);

}

return composite;

}

}

最后就是 EditorComposite 的实现了很显然每个界面的 EditorComposite 都不一样所以我们在这里只定义了一个接口来规范一下行为具体的 EditorComposite 实现我会在代码附件中的测试包中给出

清单IEditorCompositejava



Code highlighting produced by Actipro CodeHighlighter (freeware)


> public interface IEditorComposite {

/** set up composite UI */

public void create(Composite parent);

/** set the current editor for shell close and data set */

public void setEditor(IEditorFacade editor);

/** show previous data information in UI */

public void showPreInfo();

public void loadDataInfo();

}

下面我们编写一些测试代码来测试它这个测试应用是要编写一个电话簿为了简单起见我只定义了一个 EditorCompositePhoneBookComposite 在编写组装逻辑时也只是示例性地改变了一下界面的标题和尺寸(详细代码见代码下载)

清单PhoneBookEditorAssemblerjava



Code highlighting produced by Actipro CodeHighlighter (freeware)


> public void create(IEditorFacade editor) {

if (compositeType == ) {

//it is a phone book

bodyComposite = createComposite(editor testPhoneBookComposite);

||

| XML error: The previous line is longer than the max of characters |

editorgetShell()setText(Phone Book);

editorgetShell()setSize( );

editorgetShell()redraw();

if (bodyComposite != null)

bodyCompositeloadDataInfo();

} else if (compositeType == ) {

//it is a memo book

bodyComposite = createComposite(editor testPhoneBookComposite);

||

| XML error: The previous line is longer than the max of characters |

editorgetShell()setText(Memo Book);

editorgetShell()setSize( );

editorgetShell()redraw();

if (bodyComposite != null)

bodyCompositeloadDataInfo();

}

}

清单Mainjava



Code highlighting produced by Actipro CodeHighlighter (freeware)


> public static void main(String[] args) {

//定义PhoneBook EditorAssembler

IEditorAssembler assembler = new PhoneBookEditorAssembler();

//定义PhoneBook 输入数据

IInputDataObject inputData = new PhoneBookInputDO(LYL );

//定义PhoneBook editor

EditorFacade editor = new EditorFacade(assembler inputData);

editorshow();

if (editorisFinishedOK()) {

//取出PhoneBook 输出数据

if (editorgetOutputData() instanceof PhoneBookOutputDO) {

PhoneBookOutputDO outputData = (PhoneBookOutputDO) editor

getOutputData();

String name = outputDatagetName();

String phone = outputDatagetPhone();

Systemoutprintln(name: + name + ; phone: + phone);

}

}

}

接下来我们可以看一下架构的实现模型注意我在画下面的 UML 图时采用了分层的方式所有的接口都会在上面一层实现在下面一层这种分层画 UML 图的方法有助于我们理清架构的思路也便于与开发组的其他成员沟通

架构的实现模型

alt= src=http://imgeducitycn/img_///jpg width= border= twffan=done>

至此我们完成了界面组装器的核心架构的实现注意这只是一种实现并不是界面组装模式的全部作为一种模式它必须有更广的外延下面我们将要探讨它的模式本质

这个模式是一种架构模式模式的定义有三个要素问题环境解决方案这在前面我们已经详细地论述过了在这里我们讨论一下其他的参量每个模式都有它自己独特的价值观那么界面组装器模式给我们提供了什么样的价值观呢?

首先它的精髓在于这种分解界面将界面和组装行为解耦的设计思想这在拥有多个界面的应用中很有益处当界面多的时候如果没有一个比较集中的调度控制方式来对这些界面进行管理就会形成界面行为无法规范风格各异更难以作 transaction 事务或 session 会话控制这在小型应用开发中也许不很明显但在一个大中型应用中对分散的不规范的界面行为进行控制将会是一场恶梦到最后可能整个开发组都沉浸于 bug 的修复和界面修改中而无暇顾及领域逻辑代码的编写而通过将界面和组装行为解耦就可以让开发人员集中精力于界面逻辑和领域逻辑的开发而不用每一个界面都去编写管理界面的代码其实这也是模式化的一个优点模式可以优化我们的架构可以规范开发行为因此也会节省开发成本

其二它将界面逻辑处理与领域逻辑处理(也就是数据逻辑处理)解耦我们将数据输入输出从界面模型中抽取出来没有与界面耦合在一起这就获得巨大的好处第一我们可以在界面之外来处理数据在我们的领域类中处理这些数据也就是说界面只是提供了一个定义数据的载体而这些数据是被领域逻辑类使用的而我们开发的主要精力也应该放在处理业务逻辑的领域类上第二现在我们将界面和领域类解耦这样我们的界面和领域类都可以独立地变化相互之间没有任何依赖这就很方便于我们开发人员的分工编写界面的开发组不用依赖于编写后台逻辑类的开发组第三在做单元测试unit test 时开发后台逻辑类的人员可以单独测试领域类而开发界面的人员也可以单独测试界面逻辑第四当我们有多套界面机制时我们的后台逻辑类可以很方便地接插上去比如我们要支持 GUI(SWT/Java Swing)和 Web 方式那么我们的领域类和数据类无需任何更改就可以方便的切换第五我们还能获得好处就是数据类的可重用如果我们没有输入输出数据类的封装行为那可能我们会将各条数据散落在界面类中直接处理这样当你要换一种界面机制时就必须重写这部分逻辑无法重用

作为一种模式它会有很多的变体也就是说它不拘泥于我们给出的这种外在实现方式它还有其它的实现例子中我们只是组装一个 EditorComposite我们当然可以一次组装几个 EditorComposite比如一个复杂的界面会有好几个 EditorComposite 组成或者像属性页并列着有好几个 EditorComposite我们只需要自己实现一个组装器类 Assembler 就可以又或者我们可以在运行界面时动态地在几个界面之间切换界面这可能会复杂一些也受限于平台或语言的技术实现但也并非不可实现

对于该模式的适用性我想它主要适用于那些每次装载一个 EditorComposite 或属性页的情况至于是否可以作为 Wizard 向导界面的实现架构还需进一步探索不过从这个模式的概念层次上来看它的关键的价值观是完全可以用于实现 Wizard 向导界面的只不过具体实现时可能会对现在的架构变动较大另外这个模式主要适用于 GUI 客户端界面对于 Web 形式的界面已经有别的模式可以考虑

我们还可以讨论一下界面组装器模式与别的模式之间的关系在界面架构界我们已经有了大名鼎鼎的 MVC 模式为什么还需要界面组装器模式呢?虽然 MVC 模式解决的也是界面与领域逻辑处理的解耦但它的出发点主要是针对一个业务逻辑处理后会有好几个界面同时需要更新显示也就是说它的贡献在于他的及时传播数据变更的能力这和我们的模式是不一致的我们主要解决界面的分解组装和数据剥离的问题当然他们在结构上有些相似之处我们的 EditorFacade 有点像 MVC 中的控制器

结束语

本文所讲述的界面组装器模式为我们提供了将界面和组装行为解耦将界面逻辑处理与领域逻辑处理解耦的价值观在 GUI 胖客户端型界面中可以大量应用笔者已经在几个大型项目中应用了它所以它的可行性是经过实践检验的当然任何模式不管是设计模式还是架构模式都有它的适用性只有合适的没有绝对的优劣我们是否应用模式是在于模式为我们提供的价值观是否和我们的需求期望符合而不是因为别的原因

上一篇:制作多个线程

下一篇:多线程实例——每一秒显示一次系统时间