基于OEA框架的客户化设计(一) 总体设计

    这篇文章还是对工作内容的总结,主要是总结一下这几天做的产品的客户化工作内容。

    关于产品线工程中客户化的理论知识和概念,请见金根的《产品线工程》。具体的,OEA框架中的客户化理论,见:《软件产品线工程方法:如何在OpenExpressApp做客户化工作》。

    本文主要从以下几个方面来叙述如何在OEA框架中设计和实现客户化框架:

  1. OEA客户化框架设计目标
  2. 方案设计
  3. 具体实现

设计目标

  1. 支持实体类的扩展。
  2. 支持实体扩展包的动态加载。
  3. 支持界面扩展及界面扩展包的动态加载。
  4. 各版本间自定义界面元素,可以基于现有的特定版本修改一些内容。
  5. 各版本间支持自定义内容文件,如果没有使用,则使用默认版本的内容文件。(内容文件是指:图片、帮助文档等。)

    解释一下,基于OEA框架的GIX4项目是以领域实体为中心的架构。主版本中的领域实体,代表了产品功能“7、2、1”中的7和2 。7是所有版本都应该有的领域实体,2是可以进行配置以说明是否具备的领域实体,而1就是在主干之外,为特定版本开发的实体。所以以上目标中,支持对“2”的定制和对“1”的扩展是最重要的。

    由于时间仓促,目前只能以上述内容为目标,以后可能还会添加一些内容。如,枚举值的客户化,DailyBuild客户化等。

方案设计

    本次设计经过组内讨论,确定了具体的设计方向。这里主要对最重要的两项进行详细的叙述。

配置?

    一般来说,要实现客户化,使用配置可能是最直接的想法。一开始我想也没想就觉得可能客户化的内容需要存储在配置文件中,可能是一个自定义的XML文档。但是,后来和朋友聊天过程中灵光一闪,真的要采用配置吗?这里根本不需要在运行时动态改变应用程序的行为,只要在编译期能够编译出不同的版本即可,所以我决定使用“应用程序定义”的方式来完成“配置”。而“定义”与配置不同点在于,定义是用代码写死的,程序运行期间不可更改。编译期根据定义编译不同的版本。

    其实后来知道,产品线工程中的重点之一就是对产品的“可变性”进行管理。而可变性的实现机制有很多种,主要分三类:适配、替换、扩展,具体内容见:《软件产品线工程方法:如何在OpenExpressApp做客户化工作》。

    设计之初,我认为客户化的应用程序配置应该满足:

  1. 可以有公共的配置,子配置如果设置了同样的项,则重写公共的配置。
  2. 简单可用的配置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类具体的代码:

/// <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方法进行自己的版本信息定义,如,通用版本的实现:

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,然后调用其初始化操作和冻结方法即可:

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();
    }

配置文件:

<add key="AppDefinitionClass" value="Common.Definition.AppDefinition, Common.Definition"/>

    限于篇幅,今天就先总结到此。下一篇主要是把客户化框架的设计讲完,然后再下一篇可能是GIX4项目中分离原有DLL的应用。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏CSDN技术头条

和各种诡异 Bug 打交道 13 年,我总结了 18 条经验

作者 | Henrik Warne 翻译 | 郑芸 在《程序员,你会从 Bug 中学习么?》一文中,我写了我是怎样追踪这些年遇到的最有趣 bug 的。最近我重新...

28280
来自专栏java一日一条

我的编码习惯 - 参数校验和国际化规范

今天我们说说参数校验和国际化,这些代码没有什么技术含量,却大量充斥在业务代码上,很可能业务代码只有几行,参数校验代码却有十几行,非常影响代码阅读,所以很有必要把...

11110
来自专栏用户画像

3.1.2覆盖和交换

早期的计算机系统中,主存容量小,虽然主存中仅存放一道用户程序,但是存储空间放不下用户进程的现象也经常发生,这一矛盾可以用覆盖基础来解决。 覆盖的基本思想是:...

10010
来自专栏腾讯云流计算

Data Artisans Streaming Ledger ——流数据处理中串行化的ACID事务

Data Artisans Streaming Ledger,在data Artisans的River Edition上已经可用,提供串行化(一致性事务处理机制...

36610
来自专栏java达人

多线程设计模式解读6-single threaded Execution模式(附分布式环境下的操作)

Single Threaded Execution模式主要是用于确保同一时间内只能让一个线程执行处理,说通俗点就是对synchronized的标准化使用方式,这...

12540
来自专栏数据之美

玩转 SHELL 脚本之:Shell 命令 Buffer 知多少?

1、问题: 下午有同学问了这么一个问题: tail -n +$(tail -n1 /root/tmp/n) -F /root/tmp/ip.txt 2>...

49260
来自专栏ImportSource

并发编程-多线程带来的风险

Java 对于线程的支持是一把双刃剑。 当它通过提供语言以及库的支持简化了并发应用程序的开发的同时,也提高了开发人员的门槛,因为要有更多的program使用到线...

42060
来自专栏24K纯开源

在Adobe Html5 Extension的使用Nodejs的问题

    之前为一个客户开发过一个基于Adobe Premiere的Html5扩展。原本是在Adobe Premiere Pro 2015下面进行调试开发的。一切...

9320
来自专栏FreeBuf

一名代码审计新手的实战经历与感悟

blueCMS介绍 个人认为,作为一个要入门代码审计的人,审计流程应该从简单到困难,逐步提升。因此我建议大家的审计流程为——DVWA——blueCMS——其他小...

44060
来自专栏小怪聊职场

HTTP|GET 和 POST 区别?网上多数答案都是错的!

375100

扫码关注云+社区

领取腾讯云代金券