前言
在第十六篇,我把一个Edit.java重构成多个单独的类文件。
il0vec:从零开始写文本编辑器(十六):从Editor.java分化成多个单独类文件zhuanlan.zhihu.com但是结果仍然不理想,类的列表超过了一个显示器屏幕的范围。本来是没有打算分类成代码树组织的。但考虑到新功能的不断添加,所以从本篇起就要正式用代码树组织类文件了。
重构后的代码树结构
卷 文档与开发软件 的文件夹 PATH 列表
卷序列号为 AAA3-83B6
D:.
└─editor
└─frame
└─main
├─dialog
│ ├─hexviewer
│ └─preference
│ ├─apply_close
│ ├─category
│ └─content
├─menubar
├─statusbar
├─tabbedpanel
│ └─scrollpanel
│ ├─list
│ │ └─linenum
│ └─textpanel
└─token
└─keywords_java
我的命名很随意,但方向是向“GUI界面结构”收敛的。即GUI的窗口可以联想出源代码的类层级路径,反之亦然。
保持这个重构方向不变,这样代码越迭代,组织结构就越趋近GUI观察层级包含结果。
例如:
观察下面目录层级
│ └─scrollpanel
│ ├─list
│ │ └─linenum
│ └─textpanel
可以对应出GUI界面的滚动面板,它的行头视图是JList列表控件,它的视口控件是JTextPane。
这样就很方便维护了。
代码树结点的命名
命名主要以GUI控件类名为主,比如:frame dialog menubar,只是我去掉了 swing的默认 J 前缀。
命名还有部分是非GUI类,比如:词法分析 TokenReader 模块,由于它的上游数据是来自代码编辑器的文本框(JTextPane),所以我把 TokenReader 也放到 JTextPane 附近。目前是放到 TabbedPanel 的同一级,后续会小调整,但距离 JTextPane 不会太远。
访问权限的微调
- 一阶段:任意访问权限
是写在同一个Editor.java类中,所有权限都是随意访问的,即使写成私有 private,仍然在同一个源文件中可以互相访问,所以在初始时,没有考虑访问权限的设计。
- 二阶段:包级私有权限(缺省)
重构成多个在editor同一目录下的几十个类,相访问对方的方法,只需把private 提升为 protected,用eclipse自动修改为缺省。
- 三阶段:就是本篇使用的代码树组织
对于逻辑是调用JDK静态方法的类,就直接写成静态方法。
比如:偏好 Preference 和 全局资源 Res 模块,
package editor;
import java.util.prefs.Preferences;
public class PreferenceEditor {
private static Preferences preferences;
public static Preferences getPreferences() {
if (preferences == null) {
preferences = Preferences.userNodeForPackage(Editor.class);
}
return preferences;
}
}
package editor;
import javax.swing.KeyStroke;
public class Res {
private static HashMapStringsEn hashMapStringsEn;
private static HashMapStringsZh hashMapStringsZh;
private static HashMapAccelerator hashMapAccelerators;
private static Lang langCurrent;
public static void init() {
langCurrent = Lang.chinese;
hashMapStringsEn = new HashMapStringsEn();
hashMapStringsZh = new HashMapStringsZh();
hashMapAccelerators = new HashMapAccelerator();
}
public static String getString(String stringId) {
try {
if (langCurrent == Lang.chinese) {
return hashMapStringsZh.get(stringId);
} else {
return hashMapStringsEn.get(stringId);
}
} catch (NullPointerException e) {
e.printStackTrace();
System.out.printf("Error: %s.init() not invokedn", Res.class.getName());
return null;
}
}
public static KeyStroke getAccelerator(String stringId) {
try {
return hashMapAccelerators.get(stringId);
} catch (NullPointerException e) {
e.printStackTrace();
System.out.printf("Error: %s.init() not invokedn", Res.class.getName());
return null;
}
}
}
对于实体类,它需要创建实例来使用。
多数是设计为私有成员和公有get-set-add-remove等方法,这些是按源码的风格来写。也没什么多讲的。
有一个耗时的重构内容:匿名类的重新组织,主要是各种事件侦听回调。这个还真不好讲清楚。我试试打个比方。
在一阶段也写了很多匿名事件侦听类,比如:
- 菜单项“偏好”,显示“个人偏好”对话框。
- “个人偏好”对话框点击“应用”按钮,会记录偏好的字体设置,并立即更新编辑框的字体。
其中1)是好重构的,因为它是单向关联。然而2)是双向关联,有点麻烦。所以重构为添加多个单独的回调。
首先,在偏好对话框内部,仍然匿名类完成记录偏好字体设置的逻辑。
private ActionListener actionListenerApply = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for (int i = 0; i < panelPreferenceContent.getComponentCount(); i++) {
Component component = panelPreferenceContent.getComponent(i);
if (component instanceof PanelPreferenceFont) {
PanelPreferenceFont panelPreferenceFont = (PanelPreferenceFont) component;
panelPreferenceFont.apply();
} else if (component instanceof PanelPreferenceMultiDocument) {
PanelPreferenceMultiDocument panelPreferenceMultiDocument = (PanelPreferenceMultiDocument) component;
panelPreferenceMultiDocument.apply();
} else if (component instanceof PanelPreferenceRecentFiles) {
PanelPreferenceRecentFiles panelRecentFiles = (PanelPreferenceRecentFiles) component;
panelRecentFiles.apply();
} else if (component instanceof PanelPreferenceDeveloper) {
PanelPreferenceDeveloper panelPreferenceDeveloper = (PanelPreferenceDeveloper) component;
panelPreferenceDeveloper.apply();
} else {
// more branch.
}
}
}
};
然后 FrameMain 初始时,就绑定了处理更新编辑框字体更新的逻辑
public class FrameMain extends JFrame {
...
private void init() {
...
dialogPreference = new DialogPreference();
dialogPreference.addActionListenerApply(actionListenerDialogPreferenceApply);
}
...
private ActionListener actionListenerDialogPreferenceApply = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Font font = PreferenceFont.fontRead();
if (font != null) {
tabbedPaneEditors.setFont(font);
} // end if
}
};
...
}
最后要配合实现修改字体的逻辑,很多小细节的设计,比如:
- 用包装器思想,来穿透多层更新到编辑器的字体;
- 用多态思想,重写标签面板的setFont()方法,来更新内部的所有标签下的文本编辑控件的字体;
- 用多态思想,重写滚动面板的setFont()方法,来同步更新行号和代码的字体。
- ...
这些细节都是迭代完成的,起始我也没想清楚。
但有一个结构是我一直很清楚的。就是前面提过的向“GUI界面结构”收敛。
比如:PanelPreferenceApplyAndClose 类
package editor.frame.main.dialog.preference.apply_close;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JPanel;
import editor.Res;
public class PanelPreferenceApplyAndClose extends JPanel {
private static final long serialVersionUID = 1L;
private JButton buttonApply;
private JButton buttonClose;
public PanelPreferenceApplyAndClose() {
super();
setLayout(new FlowLayout(FlowLayout.RIGHT));
{
JButton button = new JButton(Res.getString("apply"));
buttonApply = button;
add(button);
}
{
JButton button = new JButton(Res.getString("cancel"));
buttonClose = button;
add(button);
}
}
public JButton getButtonApply() {
return buttonApply;
}
public JButton getButtonClose() {
return buttonClose;
}
}
它是对应偏好对话框底部的“应用”和“取消”按钮的父面板类。
在这里没有直接写任何按钮处理逻辑,因为应该把逻辑由上游实现。为了这个目的,要开放按钮的接口。当然也可以直接写几对addActionListener/removeActionListener,但是计划会变的,比如:将来可能有禁用按钮状态等功能。所以这个类就只完成布局的逻辑,不干别的事情。
另一处类似的重构是菜单项,直接上代码
private void initMenuItemActions() {
{
MenuFile menuFile = menuBarMain.getMenuFile();
menuFile.getMenuItemNew().addActionListener(actionListenerNew);
menuFile.getMenuItemOpen().addActionListener(actionListenerOpen);
menuFile.getMenuItemSave().addActionListener(actionListenerSave);
menuFile.getMenuItemSaveAs().addActionListener(actionListenerSaveAs);
menuFile.getMenuItemExit().addActionListener(actionListenerExit);
}
{
MenuView menuView = menuBarMain.getMenuView();
menuView.getMenuItemHexView().addActionListener(actionListenerHexView);
}
{
MenuTest menuTest = menuBarMain.getMenuTest();
menuTest.getMenuItemDivZero().addActionListener(actionListenerDivZero);
menuTest.getMenuItemMDI().addActionListener(actionListenerMDI);
menuTest.getMenuItemToken().addActionListener(actionListenerToken);
menuTest.getMenuItemCodeColor().addActionListener(actionListenerCodeColor);
}
{
MenuWindow menuWindow = menuBarMain.getMenuWindow();
menuWindow.getMenuItemPreference().addActionListener(actionListenerPreference);
}
{
MenuHelp menuHelp = menuBarMain.getMenuHelp();
//
}
}
其实有一个好玩的比喻
一家里有两兄弟吵架分一个苹果,哥哥和弟弟互相不服,都要自己来分,想给自己多分一部分。这时让哥哥或弟弟分都不合适。所以应该有一个高于他们的人来处理,应该是爸爸。这样由爸爸来分苹果,就公平了,不管怎么分,都是爸爸的逻辑问题,不公平找爸爸,而不是哥哥弟弟之间互相扯皮(双向关联)。
哪么,爸爸是哪个类呢?
答:从源代码树中,向上级找类,找到一个公共的最近父类。
点到即止,就说到这里吧!
以上~