设计目标
- 支持实体类的扩展。
- 支持实体扩展包的动态加载。
- 支持界面扩展及界面扩展包的动态加载。
- 各版本间自定义界面元素,可以基于现有的特定版本修改一些内容。
- 各版本间支持自定义内容文件,如果没有使用,则使用默认版本的内容文件。(内容文件是指:图片、帮助文档等。)
解释一下,基于OEA框架的GIX4项目是以领域实体为中心的架构。主版本中的领域实体,代表了产品功能“7、2、1”中的7和2 。7是所有版本都应该有的领域实体,2是可以进行配置以说明是否具备的领域实体,而1就是在主干之外,为特定版本开发的实体。所以以上目标中,支持对“2”的定制和对“1”的扩展是最重要的。
由于时间仓促,目前只能以上述内容为目标,以后可能还会添加一些内容。如,枚举值的客户化,DailyBuild客户化等。
方案设计
本次设计经过组内讨论,确定了具体的设计方向。这里主要对最重要的两项进行详细的叙述。
配置?
一般来说,要实现客户化,使用配置可能是最直接的想法。一开始我想也没想就觉得可能客户化的内容需要存储在配置文件中,可能是一个自定义的XML文档。但是,后来和朋友聊天过程中灵光一闪,真的要采用配置吗?这里根本不需要在运行时动态改变应用程序的行为,只要在编译期能够编译出不同的版本即可,所以我决定使用“应用程序定义”的方式来完成“配置”。而“定义”与配置不同点在于,定义是用代码写死的,程序运行期间不可更改。编译期根据定义编译不同的版本。
其实后来知道,产品线工程中的重点之一就是对产品的“可变性”进行管理。而可变性的实现机制有很多种,主要分三类:适配、替换、扩展,具体内容见:《软件产品线工程方法:如何在OpenExpressApp做客户化工作》。
设计之初,我认为客户化的应用程序配置应该满足:
- 可以有公共的配置,子配置如果设置了同样的项,则重写公共的配置。
- 简单可用的配置API
最后,我定出了以下的实现目标:
主干版本中有应用程序定义类ConfigMain,客户A和客户B分别有自定义的配置类ConfigA,ConfigB。
各客户的版本中,分别把他自己的配置类和主配置类结合,然后以配置文件的方式注入到整个应用程序中。
当应用程序读取某个配置项时,直接从注入的配置类中获取;此时,按照一定的寻找顺序,定位该配置项。如客户A的配置类为ConfigA + ConfigMain,则在寻找时,应该先在ConfigA中寻找,如果找不到,则在ConfigMain中寻找。
文件组织方式
各客户版本需要不同的文件来运行,这些文件主要是一些内容文件,如图片,xml,也包含少量的DLL。毫无疑问地,客户化工作需要对它们进行管理。
DLL文件的组织比较简单,只需要各客户版本把自己的DLL放在一个版本特定的目录下,程序动态加载就行了。
这里我定出了以下规则:所有需要客户化的DLL都放在客户各自的文件夹根目录下。
但是这里需要注意,这些代码文件需要在应用程序定义被加载之后,才会被应用程序加载。所以应用程序定义类需要被直接DI进来,这样,客户版本信息就可以在这些DLL加载之前被访问到,也就可以继续加载这些DLL了。
内容文件的组织不同于代码,这些文件很可能在运行时也需要被替换。所以这里的策略不能再使用“定义”的方式。需要有一定的文件寻址算法。以下是暂定方案:
所有需要客户化的文件都放在/Files/中。版本通用文件,则直接放在/Files/Common/中。各客户有自己的文件夹,如客户A有文件夹/Files/A/。文件夹名在配置类中标明。
程序中,可以文件寻找引擎指定要使用的文件的相对路径,如使用LOGO,则指定/Images/Logo.jpg。如果客户A在A中已经建立了/Files/A/Images/Logo.jpg文件,则返回此文件;否则返回的应该是/Files/Common/Images/Logo.jpg。
方案总结
使用定义而不使用配置的方式,防止了不必要的程序代码的开发。但是要注意定义的API的简便和易用性。
文件组织方式使得各客户文件完全分离,简化了Buid 版本的代码开发。这里主要注意路径寻址的实现。
具体设计
应用程序定义类的实现
为支持属性值的重写和融合,应用程序定义类直接使用OO的继承实现,通用的定义类作为基类,分支版本直接从它派生下来并重写新的属性。使用OO的方式可以很好地实现属性值扩展,例如,我们可以使用装饰模式来实现复杂的属性定义。
应用程序定义类中,应该组合一些分支对象,来进行更细粒度的定义。
下图是本次客户化中应用程序定义类的结构:
图1 应用程序定义类的结构
Freeable表示所有定义都是可以被冻结的。这些定义在一开始被设置好版本的值后,将会被冻结,所以内容不再改变,变为“不可变类”。一,这是其运行期不需要改变的体现;二,不可变类是高效的类。
PathDefinition是所有内容文件的路径定义,它使用了PathProvider类来为其提供内容文件路径寻址算法,同时,它使用内容文件的相对路径从PathProvider中获取真实路径。
UIInfo是视图信息的载体,该类是定义的重点,留待下一篇中介绍。
AppDefinition是整个应用程序定义类的基类,以DI实现单例模式,作为全局唯一的访问点。目前,它包含了一个UIInfo对象来提供视图信息和一个PathDefinition来提供文件路径。
以下主要给出AppDefinition类具体的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
/// <summary>
/// 应用程序的主干版本定义。
/// 同时,也是分支版本定义的基类。
/// </summary>
public
abstract
class
AppDefinition : Definition
{
#region SingleTon
private
static
AppDefinition _instance;
/// <summary>
/// 提供一个静态字段,作为全局唯一的访问地址。
/// 注意:本类并没有直接设计为单例模式!
/// </summary>
public
static
AppDefinition Instance
{
get
{
if
(_instance ==
null
)
throw
new
InvalidOperationException(
"请先设置该属性。"
);
return
_instance;
}
set
{
if
(value ==
null
)
throw
new
ArgumentNullException(
"value"
);
if
(_instance !=
null
)
throw
new
InvalidOperationException(
"该属性只能被设置一次。"
);
_instance = value;
}
}
#endregion
/// <summary>
/// 查找文件路径的查找算法提供器。
/// </summary>
private
PathProvider _pathProvider;
/// <summary>
/// 在使用所有属性前,需要主动调用此方法来进行初始化。
/// </summary>
protected
override
void
InitCore()
{
base
.InitCore();
this
.CheckUnFrozen();
this
._pathProvider =
new
PathProvider();
if
(!
string
.IsNullOrWhiteSpace(
this
.BranchAppName))
{
this
._pathProvider.AddBranch(
this
.BranchAppName);
}
//初始化所有文件路径
this
.Pathes =
this
.CreatePathes();
this
.Pathes.SetPathProvider(
this
._pathProvider);
this
.Pathes.Initialize();
//创建元数据库
this
.UIInfo =
this
.DefineUI();
this
.UIInfo.Initialize();
}
protected
override
void
OnFrozen()
{
base
.OnFrozen();
FreezeChildren(
this
.UIInfo,
this
.Pathes);
}
/// <summary>
/// 分支版本名。
/// 同时,这个也是客户化文件夹的名字。
/// 分支版本定义,需要重写这个属性。
/// </summary>
protected
virtual
string
BranchAppName
{
get
{
return
null
;
}
}
#region 文件
/// <summary>
/// 创建所有路径的定义。
/// 子类重写此方法,用于添加更多的路径信息定义。
/// </summary>
/// <returns></returns>
protected
virtual
PathDefinition CreatePathes()
{
return
new
PathDefinition();
}
/// <summary>
/// 应用程序中所有使用到的需要客户化的路径集。
/// </summary>
public
PathDefinition Pathes {
get
;
private
set
; }
#endregion
#region DLL
/// <summary>
/// 获取所有此版本中需要加载的实体类Dll集合。
/// </summary>
/// <returns></returns>
public
string
[] GetEntityDlls()
{
return
this
._pathProvider.MapAllPathes(
"Library"
,
true
);
}
/// <summary>
/// 获取所有此版本中需要加载的模块Dll集合。
/// </summary>
/// <returns></returns>
public
string
[] GetModuleDlls()
{
return
this
._pathProvider.MapAllPathes(
"Module"
,
false
);
}
#endregion
#region UIInfo
/// <summary>
/// 应用程序中所有可用的视图信息。
/// </summary>
public
UIInfo UIInfo {
get
;
private
set
; }
/// <summary>
/// 子类重写此方法,用于初始化产品视图定义。
/// 重点实现!
/// </summary>
/// <returns></returns>
protected
virtual
UIInfo DefineUI()
{
return
new
UIInfo();
}
#endregion
}
|
子版本的定义需要重写父类的DefineUI方法进行自己的版本信息定义,如,通用版本的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
namespace
Common.Definition
{
/// <summary>
/// 通用版本的产品定义
/// </summary>
public
class
AppDefinition : OpenExpressApp.MetaModel.Customizing.AppDefinition
{
/// <summary>
/// 子类重写此属性以指定是否包含合同。
/// </summary>
protected
virtual
bool
IncludeContract
{
get
{
return
true
;
}
}
/// <summary>
/// 这里加入东方版本需要的特定的视图信息
/// </summary>
/// <returns></returns>
protected
override
UIInfo DefineUI()
{
var
ui =
base
.DefineUI();
ui.Entity<CBFGQBQItemTitle>()
.EntityProperty(t => t.Code).ShowInLookup().ShowInList().Set_ListMinWidth(200);
ui.Entity<CBSummary>()
.EntityProperty(t => t.PBSId).Show().Set_ListMinWidth(500);
ui.Entity<CBNormSummary>()
.EntityProperty(t => t.PBSId).Show().Set_ListMinWidth(100);
//在基类上定义视图信息,这个类的所有子类如果没有显式设置其它的值,则会使用基类的属性。
ui.Entity(
typeof
(BQNormItemTitleBase))
.EntityProperty(
"AllCode"
).Show().Set_ListMinWidth(200);
this
.DefineContractDll(ui);
return
ui;
}
/// <summary>
/// 定义合同模块的显示。
/// </summary>
/// <param name="ui"></param>
private
void
DefineContractDll(UIInfo ui)
{
if
(
this
.IncludeContract)
{
ui.Entity<ContractBudget>().UnVisible();
ui.Entity<RealContractBudget>().Visible();
ui.UnVisible(
CCN.ShowPCIBQitemCommand, CCN.EntityShowBQCommand,
CCN.MeasureShowBQCommand, CCN.ResourceShowBQCommand,
CCN.ProjectIndicatorsCalculationCommand
);
}
else
{
ui.UnVisible(
typeof
(RealContractBudget),
typeof
(ContractSubjectType),
typeof
(ProjectContractSubject),
typeof
(ContractIndicatorQueryObject)
);
ui.UnVisible(
CCCN.ShowPCIBQitemCommand, CCCN.EntityShowBQCommand,
CCCN.MeasureShowBQCommand, CCCN.ResourceShowBQCommand
);
}
}
}
}
|
程序在启动时,从配置中注入AppDefinition,然后调用其初始化操作和冻结方法即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public
partial
class
App : Application
{
public
App()
{
this
.InitAppDefinition();
}
/// <summary>
/// 定义软件运行时版本
/// </summary>
private
void
InitAppDefinition()
{
var
appDefType = Type.GetType(AppConfig.Instance.AppDefinitionClass,
true
,
true
);
AppDefinition.Instance = Activator.CreateInstance(appDefType)
as
AppDefinition;
AppDefinition.Instance.Initialize();
AppDefinition.Instance.Freeze();
}
|
配置文件:
1
|
<
add
key="AppDefinitionClass" value="Common.Definition.AppDefinition, Common.Definition"/>
|
限于篇幅,今天就先总结到此。下一篇主要是把客户化框架的设计讲完,然后再下一篇可能是GIX4项目中分离原有DLL的应用。
本文转自BloodyAngel博客园博客,原文链接:http://www.cnblogs.com/zgynhqf/archive/2010/09/20/1832019.html,如需转载请自行联系原作者