尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
java-design-patterns 之 Presentation Model 模式实战:用 Swing 专辑管理器剖析界面状态与业务逻辑的解耦设计
示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载Presentation Model表现模型是一种把视图View的状态与行为抽取到独立模型类中的架构模式本仓库presentation-model模块用 Java Swing 实现了一个专辑信息管理 GUI 作为完整示例。本文将围绕该模块的西班牙语文档与源码逐类拆解Album、DisplayedAlbums、PresentationModel、View的职责划分与数据同步机制并借助单元测试与类图说明其可测试性收益帮助你理解并直接复现这一模式。模式概述Presentation Model 是什么别称Presentation Model 也被称为Application Model它在 java-design-patterns 仓库中被归类为行为型模式核心标签是解耦Decoupling。目的El modelo de presentación extrae el estado y el comportamiento de la vista a una clase modelo que forma parte de la presentación.即表现模型把视图的状态与行为抽取出来放进一个属于表现层的模型类中。视图本身只负责加载这个模型的数据/状态并按照状态把数据渲染到屏幕上从而将如何展示与如何控制逻辑彻底分离。现实世界类比在仓库西班牙语文档给出的类比中GUI 程序如果把所有表现行为都堆在 view 类里会变得难以测试。Presentation Model 模式允许我们把行为与逻辑和视图分开——视图只负责从另一个类加载数据与状态再据此显示。用一句话概括就是一个把呈现presentation与控制control分割开来的模式。仓库模块结构与运行方式模块位于 presentation-model其目录组织如下presentation-model/ ├── etc/ # 类图与架构图png / puml ├── src/main/java/com/iluwatar/presentationmodel/ │ ├── Album.java # 数据模型一张专辑 │ ├── DisplayedAlbums.java # 专辑集合管理 │ ├── PresentationModel.java # 表现模型GUI 的状态与行为 │ ├── View.java # Swing 视图 │ └── App.java # 程序入口 ├── src/test/java/com/iluwatar/presentationmodel/ # 四个测试类 ├── README.md └── pom.xml运行演示程序时直接执行入口类即可。App.main创建View并调用createView()启动 Swing 窗口public final class App { private App() {} public static void main(final String[] args) { var view new View(); view.createView(); } }对应地presentation-model/pom.xml 中通过 maven-assembly-plugin 将com.iluwatar.presentationmodel.App配置为 manifest 主类mainClass因此也可以使用 Maven 打包后运行 jar 来启动 demo。核心参与类四个类各司其职整个示例由 4 个 Java 类组成它们恰好构成 Presentation Model 的完整闭环类角色职责Album数据模型保存一张专辑的 title、artist、isClassical、composerDisplayedAlbums数据集合维护将在 GUI 中显示的所有专辑列表PresentationModel表现模型管理 GUI 的选中状态与全部读写行为是 View 与数据之间的桥梁View视图只负责 Swing 控件的创建与存/取数据到表现模型Album纯数据载体Album.java 使用 Lombok 注解自动生成 getter/setter 与全参构造器Setter Getter AllArgsConstructor public class Album { private String title; private String artist; private boolean isClassical; /** only when the album is classical, composer can have content. */ private String composer; }关键业务约束是只有当isClassical为 true 时composer才有内容。这条规则在下面多个类中反复体现。DisplayedAlbums专辑集合DisplayedAlbums.java 内部持有一个ListAlbum并提供addAlbums方法按是否古典乐规则构造专辑Slf4j Getter public class DisplayedAlbums { private final ListAlbum albums; public DisplayedAlbums() { this.albums new ArrayList(); } public void addAlbums(final String title, final String artist, final boolean isClassical, final String composer) { if (isClassical) { this.albums.add(new Album(title, artist, true, composer)); } else { this.albums.add(new Album(title, artist, false, )); } } }注意isClassical false时 composer 被强制置为空字符串这一行为由 DisplayedAlbumsTest.java 的两个测试用例分别验证testAdd_true断言 composer 被保留testAdd_false断言 composer 被清空。PresentationModel整个模式的核心PresentationModel.java 是位于 View 和专辑数据之间、用于控制数据的类这正是模式名称的由来Slf4j public class PresentationModel { private final DisplayedAlbums data; private int selectedAlbumNumber; private Album selectedAlbum; public PresentationModel(final DisplayedAlbums dataOfAlbums) { this.data dataOfAlbums; this.selectedAlbumNumber 1; this.selectedAlbum this.data.getAlbums().get(0); } // ... 见下文 }构造器默认选中列表中的第一张专辑下标从 1 计数。它对外暴露了两种类型的操作选中状态管理public void setSelectedAlbumNumber(final int albumNumber) { LOGGER.info(Change select number from {} to {}, this.selectedAlbumNumber, albumNumber); this.selectedAlbumNumber albumNumber; this.selectedAlbum data.getAlbums().get(this.selectedAlbumNumber - 1); }字段读写带日志与业务约束例如public String getTitle() { return selectedAlbum.getTitle(); } public void setTitle(final String value) { LOGGER.info(Change album title from {} to {}, selectedAlbum.getTitle(), value); selectedAlbum.setTitle(value); } public boolean getIsClassical() { return selectedAlbum.isClassical(); } public String getComposer() { return selectedAlbum.isClassical() ? selectedAlbum.getComposer() : ; } public void setComposer(final String value) { if (selectedAlbum.isClassical()) { LOGGER.info(Change album composer from {} to {}, selectedAlbum.getComposer(), value); selectedAlbum.setComposer(value); } else { LOGGER.info(Composer can not be changed); } }这里可以看到composer 只对古典专辑有意义的约束在表现层再次被强化getComposer对非古典专辑返回空串setComposer在非古典状态下拒绝写入并记录日志。这正是把视图行为上移到模型类的典型体现——View 不再需要理解这些业务规则。此外PresentationModel还通过静态方法albumDataSet()提供内置演示数据集HQ、The Rough Dancer and Cyclical Night、The Black Light、Symphony No.5其中只有最后一首是古典乐、作曲者为 Sibelius以及getAlbumList()返回所有专辑标题数组供JList展示public static DisplayedAlbums albumDataSet() { var titleList new String[] {HQ, The Rough Dancer and Cyclical Night, The Black Light, Symphony No.5}; var artistList new String[] {Roy Harper, Astor Piazzola, The Black Light, CBSO}; var isClassicalList new boolean[] {false, false, false, true}; var composerList new String[] {null, null, null, Sibelius}; var result new DisplayedAlbums(); for (var i 1; i titleList.length; i) { result.addAlbums(titleList[i - 1], artistList[i - 1], isClassicalList[i - 1], composerList[i - 1]); } return result; }View只做渲染 存取的瘦视图View.java 用 Swing 构建界面控件包括专辑列表JList、标题/艺术家/作曲者三个文本框、古典乐复选框以及 Apply / Cancel 两个按钮。它的构造器即注入表现模型public View() { model new PresentationModel(PresentationModel.albumDataSet()); }存数据View → PresentationModelpublic void saveToMod() { LOGGER.info(Save data to PresentationModel); model.setArtist(txtArtist.getText()); model.setTitle(txtTitle.getText()); model.setIsClassical(chkClassical.isSelected()); model.setComposer(txtComposer.getText()); }取数据PresentationModel → Viewpublic void loadFromMod() { LOGGER.info(Load data from PresentationModel); txtArtist.setText(model.getArtist()); txtTitle.setText(model.getTitle()); chkClassical.setSelected(model.getIsClassical()); txtComposer.setEditable(model.getIsClassical()); txtComposer.setText(model.getComposer()); }注意txtComposer.setEditable(model.getIsClassical())文本框的可编辑状态直接由表现模型的状态决定视图不再自行判断业务规则。GUI 交互装配createView中的关键部分albumList.addMouseListener(new MouseAdapter() { Override public void mouseClicked(final MouseEvent e) { model.setSelectedAlbumNumber(albumList.getSelectedIndex() 1); loadFromMod(); // 切换选中项后从模型重新加载显示 } }); chkClassical.addActionListener(itemEvent - { txtComposer.setEditable(chkClassical.isSelected()); if (!chkClassical.isSelected()) { txtComposer.setText(); } }); apply.addMouseListener(new MouseAdapter() { Override public void mouseClicked(final MouseEvent e) { saveToMod(); // 应用修改先写入模型 loadFromMod(); // 再回读刷新界面 } }); cancel.addMouseListener(new MouseAdapter() { Override public void mouseClicked(final MouseEvent e) { loadFromMod(); // 取消修改直接从模型回读覆盖界面输入 } });Apply / Cancel 按钮的语义由此变得极其简单Apply saveToMod()loadFromMod()Cancel loadFromMod()。因为所有数据以模型为唯一事实来源撤销修改只需重放一次加载无需在视图层维护任何缓存或回滚逻辑。数据流与同步机制总结整个 Demo 的运行数据流可以归纳为两条主链路模型 → 视图加载PresentationModel通过getXxx()暴露状态View.loadFromMod()将状态写入各 Swing 控件触发时机为构造界面、切换专辑、点击 Apply / Cancel。视图 → 模型保存View.saveToMod()读取控件文本通过setXxx()写回PresentationModel进而修改选中的Album触发时机为点击 Apply。对选中专辑PresentationModel内部维护selectedAlbumNumber1 基与selectedAlbum引用getAlbumList()则始终反映DisplayedAlbums的全量数据两者配合即可在列表切换与字段编辑之间保持一致。可测试性Presentation Model 模式的最大卖点模式的核心动机就是通过 GUI 窗口测试表现逻辑往往很别扭、甚至不可能。把状态与行为放进PresentationModel后无需启动任何 Swing 窗口即可对表现逻辑做单元测试。仓库中的测试恰好印证了这一点PresentationTest.java 直接实例化PresentationModel并验证纯逻辑Test void testSetSelectedAlbumNumber_1() { PresentationModel model new PresentationModel(PresentationModel.albumDataSet()); final int selectId 2; model.setSelectedAlbumNumber(selectId); assertEquals(albumList[selectId - 1], model.getTitle()); } Test void testSetComposer_false() { PresentationModel model new PresentationModel(PresentationModel.albumDataSet()); String testComposer TestComposer; model.setIsClassical(false); model.setComposer(testComposer); assertEquals(, model.getComposer()); // 非古典专辑拒绝写作曲者 }ViewTest.java 则验证视图 ↔ 模型的双向同步即使createView()会创建真实 Swing 组件断言依然落在模型状态上Test void testSave_setArtistAndTitle() { View view new View(); view.createView(); view.getTxtArtist().setText(testArtist); view.getTxtTitle().setText(testTitle); view.saveToMod(); view.loadFromMod(); assertEquals(testTitle, view.getModel().getTitle()); assertEquals(testArtist, view.getModel().getArtist()); }这种结构让表现逻辑的测试与GUI 组件本身解耦也正是模式文档强调的适用前提。类图模块的 UML 类图presentation-model/etc/presentation-model.urm.pngPlantUML 源文件见 presentation-model/etc/presentation-model.urm.puml清晰展示了依赖方向View依赖PresentationModelPresentationModel依赖DisplayedAlbums与Album即依赖全部指向模型层视图不直接触碰数据集合。适用场景根据西班牙语文档当出现以下情况时适合使用 Presentation Model 模式通过 GUI 窗口测试表现逻辑很别扭、甚至不可能——需要把表现逻辑抽出来以便直接单元测试尚未确定最终使用哪种 GUI——表现模型与具体视图无关可在不同 UI 技术如 Swing、JavaFX间复用。结合英文版 README 的补充该模式在 JavaFX借助属性与绑定、Swing 应用以及 Android 的 MVVMViewModel中都有对应实践属于一次建模、多端视图的典型场景。相关模式对比西班牙语文档列出的相关模式包括Supervising Controller监督控制器控制器只处理复杂交互、视图自行管理简单绑定与 Presentation Model 在逻辑上移的程度上有所区别Passive View被动视图视图完全被动、不包含任何逻辑与 Presentation Model 同属从视图剥离逻辑家族但被动视图通常配合 Presenter 使用。仓库中还可对照其他界面模式模块进一步理解差异例如 model-view-controller、model-view-presenter 与 model-view-viewmodel它们都致力于关注点分离区别在于交互模型与逻辑归属不同。这些模式与 Presentation Model 一样可在同一个设计模式仓库中横向对比学习。小结本模块用一个 5 个类、约 200 行的 Swing Demo 完整演示了 Presentation Model 模式Album承载数据、DisplayedAlbums管理集合、PresentationModel汇聚全部状态与行为含业务约束与日志、View退化为纯粹的渲染与存取层。通过saveToMod/loadFromMod两个方向的同步以及不依赖真实窗口的单元测试你可以直观体会到把表现逻辑从视图剥离带来的可测试性与可维护性收益。若需在真实项目中落地只需将本文的Album/DisplayedAlbums替换为业务领域模型把PresentationModel扩展为对应界面的状态容器并让视图控件全部经由模型读写即可。赞分享示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载相关推荐java-design-patterns 项目实战解析用 Hexagonal Architecture端口与适配器在 Java 中解耦核心业务逻辑java design patterns 项目实战解析用 Hexagonal Architecture端口与适配器在 Java 中解耦核心业务逻辑 本文以示例工程教程Java 数据映射器模式Data Mapper实战以 java-design-patterns 的>Java 数据映射器模式Data Mapper实战以 java design patterns 的 data mapper 模块为例解耦数据存储与业务逻示例工程教程如何快速掌握Enchantment CrackerMinecraft附魔破解的终极指南如何快速掌握Enchantment CrackerMinecraft附魔破解的终极指南 Enchantment Cracker是一款专为Minecraft玩家创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

马德拉岛、酒与蛋糕全解析:从大西洋花园到经典磅蛋糕

马德拉岛、酒与蛋糕全解析:从大西洋花园到经典磅蛋糕

1. 先说清楚:Madeira到底是一座岛、一杯酒,还是一块蛋糕?很多人第一次接触"Madeira"这个词,场景可能各不相同:有人是在旅游博主的照片里看到一片悬崖峭壁上的绿色岛屿,有人是在红酒架前被酒标上的…

📅 2026/10/2 16:50:44
Codex 100个真实案例 - 用AI写俄罗斯方块(触屏手机也能玩)

Codex 100个真实案例 - 用AI写俄罗斯方块(触屏手机也能玩)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/2 16:45:43
Vscode 代码插件 Code Runner 一键运行代码:把 settings.json 改到 TaoToken 统一 Key 通道

Vscode 代码插件 Code Runner 一键运行代码:把 settings.json 改到 TaoToken 统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/2 16:45:43
MORE NEWS

更多资讯

📰

精通线程池:核心参数、执行流程与生产调优实战

“精通线程池”这几个字,写的时候挺爽,面试的时候是真慌。我面过不少候选人,简历上写着精通并发编程,问到底层逻辑,十个里有八个卡在“线程池是先扩容还是先入队”这种问题上。线程池这东西,背会八个参数不…

📰

Esri 2020全球土地覆盖数据下载全攻略:分幅规则、工具原理与避坑指南

1. 从 Esri 2020 全球土地覆盖数据说起:它到底是什么先交代一个背景:Esri 在 2020 年发布了一版10米分辨率全球土地覆盖数据,这几乎是目前公开可下载的、全球尺度下分辨率最高的一套地表分类产品。所谓“土地覆盖”(Land Cover&am…

📰

拆解ABS“玄学”:Simulink单轮模型从零搭建与门限控制仿真

1. 先给ABS的“玄学”祛个魅:它到底在控制什么很多人第一次接触ABS模型,都会觉得这玩意儿透着点说不清道不明的气质。明明是一套缸内压力切换的逻辑,怎么放到不同路面上结果就完全不一样,调参跟猜谜似的。这个感觉其实不奇怪&…

📰

Claude Code配置入门与排错:从Node.js到settings.json完整指南

Claude 配置这个话题,最近被问的次数实在有点多。有人装完claude命令敲下去报错说找不到,有人登录到一半卡在授权页面,还有人把settings.json改了几十行结果原来看起来正常的对话反而全废了。说实话,Claude Code 的配置难度不算高…

📰

PLC子程序编程详解:从梯形图杂乱到模块化结构

写PLC程序这几年,我最大的体会是:很多设备逻辑不复杂,但梯形图一多就乱,改一个动作要找半天触点,调试的时候谁都救不了你。后来我把程序里那些反复出现的逻辑全拆成了子程序,整个工程量直接从“一团毛线”变…

📰

计算机毕业设计|基于springboot + vue校园拼单团购系统(源码+数据库+文档)

校园拼单团购系统 目录 基于springboot vue校园拼单团购系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取: 基于springboot vue校园拼单团购系统 一、前言 博主介绍&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬