65.9K
CodeProject 正在变化。 阅读更多。
Home

通过 Roxy IoC 容器和代码生成器实现关注点分离和智能混入

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2018年2月6日

Apache

21分钟阅读

viewsIcon

11979

downloadIcon

114

使用 Roxy IoC 容器和代码生成器实现关注点分离。

引言

关注点分离

在软件工程中,关注点是负责功能单个“方面”的事物。我计划在本文后面给出分离关注点的具体编码示例,但现在,让我们进行纯粹的口头讨论(不看代码)。

假设你在画布上有一些视觉图形(无论是桌面应用程序还是 Web 应用程序,以及使用什么软件包都无关紧要)。以下列出并解释了一些与图形相关的关注点

  1. 图形承载着一些视觉信息 - 它的颜色、大小、位置、视觉内容 - 这是它的视觉关注点。
  2. 在大多数应用程序中,图形还对应于一些通过图形呈现或修改的有用数据,或两者兼而有之。数据以及显示或修改它的功能将代表图形的数据关注点。
  3. 假设我们可以从画布上移除图形 - “移除”功能代表图形的移除关注点。
  4. 还假设图形可以处于选中和未选中状态 - 它的视觉效果根据是否选中而变化。负责此功能的功能可以作为选择关注点被提取出来。

 

为了说明,请运行MultiConcernsTest示例应用程序。我稍后将解释代码,但现在让我们讨论应用程序功能。

当你启动应用程序时,你会看到以下窗口

应用程序列出了组织内的两个业务组“占星师”和“炼金术士”,每个组下有两个人。

点击包含组名称的顶部米色部分将选择该组(其边框将变粗)。点击一个人将选择该人及其所属组。一次只能选择一个组,并且一个组内一次只能选择一个人。如果你取消选择一个组 - 其所有人也将被取消选择。

你可以使用鼠标左键打开一个包含“移除”菜单项的菜单。如果你点击它,相应的图形将被移除 - 如果你点击业务组的“移除”,你将移除整个业务组,而如果你点击某个人的“移除”,你将只移除业务组中的那个人。

显然,对于人物图形,视觉关注点由实际的视觉控件及其功能表示。数据关注点由包含姓名和姓氏的功能组成。选择移除关注点由负责选择和移除图形的功能表示。

类似的关注点划分也可以在业务组图形层面进行。

请注意,所列的关注点几乎相互独立 - 唯一的相互依赖点是

  • 当一个项目被移除时,它也应该在视觉上被移除 - WPF(我用来实现这些示例的框架)会自动处理它 - 一旦视图模型改变,视图就会被修改。
  • 当一个项目被选中时,它的边框应该变粗 - 这是通过一些小的额外 XAML 功能实现的。

 

在下面的讨论中,我将跳过视觉-非视觉关注点划分的讨论。WPF 和其他框架非常擅长处理这种特定的关注点划分(如果你使用 MVVM 模式)。我将主要讨论数据选择移除关注点的划分。

最佳设计是将关注点分离到不同的类/接口中,完全单独构建和测试它们,然后以很少或不需要额外代码的方式将它们组合在一起。

这种关注点分离具有以下主要优点

  1. 复用 - 选择和移除功能可以提取到单独的类中,并用于不同的图形,甚至用于一些非视觉代码。
  2. 代码清晰度 - 当数据、选择和移除功能混合在一起时,它们会污染代码,以至于难以区分一个功能的开始和结束。
  3. 代码独立性 - 如果人们开始将各种关注点放在一起,他们将不可避免地在各种关注点之间创建更多的依赖关系。这将导致各种相互依赖的错误,当更改一个关注点对应的代码时,可能会导致另一个关注点中的错误。
  4. 测试 - 分开测试所有关注点比一起测试更容易更好。可以理解,如果你测试单个关注点而不是它们的笛卡尔积,则组合更少。

 

行为

C# 行为是一种允许修改另一个类行为的类。行为附加到它们修改的类对象上。它们可以在附加到对象时更新对象的属性,并为其事件添加处理程序。

大多数人只熟悉 WPF 视觉行为,然而,非视觉行为也非常有用,正如我在许多文章中展示的那样,例如基于视图-视图模型的 WPF 和 XAML 实现模式集合行为。行为在关注点分离方面非常有用,因为它们允许将特定的关注点功能提取到行为类中。

在本文后面,我将提供使用行为进行关注点分离的具体示例。

Roxy

Roxy 是一个代码生成器和 IoC 容器,我专门为关注点分离而构建。本文的重点是展示使用 Roxy 轻松干净地实现关注点分离的优势。

Roxy 是一个开源项目,可在 Github 上获取:Roxy

您可以使用 nuget 管理器从 nuget.org 下载 NP.Roxy 及其所需的所有 dll。

用法、实现继承和智能混入

在深入示例之前,让我们回顾一下面向对象编程中的继承概念以及较新的混入概念。

当我们谈论继承时,我们实质上谈论的是两个不同(但相关)的概念——使用继承实现继承

在 C# 和 Java 中,使用继承由接口以及抽象或虚拟超类成员表示。C# 和 Java 接口继承尽可能强大。特别是,它允许“智能”多重使用继承,以便来自多个接口的属性、方法和事件只要签名和名称匹配就可以合并为一个。

被广泛讨论的“Is A”关系正是与使用继承相关,与实现继承几乎无关——事实上,正是使用继承定义了对象的使用方式。

C# 和 Java 中的实现继承根本不强大。只允许单继承,因此没有智能合并各种类成员实现的功能。

一般来说,类功能可以通过下图来演示

用法由形状的边框表示。空白(白色)或阴影区域分别表示抽象或虚拟成员(属性或方法,甚至是它们的集合)。不同的白色区域可以表示不同用法关注点的区域。

通过将功能实现插入到类形状的空白(和/或)阴影区域(区域的边界决定其使用接口),可以使类按预期工作。

这就是我认为理想的实现继承的目的(仅仅是填补空白)。

在我看来,“理想的”多重实现继承应该提供以下功能

  1. 它应该提供轻松指定多个实现“超类”的功能。多个实现“超类”甚至可以是同一类型——它们的功能可以用来实现各种插件(上图中的空白区域)。
  2. 它不应该是“白盒”继承,即“超类”的所有内容都进入子类,而应该是“黑盒”继承,在这种情况下,开发人员需要指定“超类”的哪个成员应该在子类中可访问。
  3. 将抽象或虚拟的“空白”与某些“超类”功能匹配的最简单方法是通过名称——因此,开发人员应该被允许在子类中轻松重命名“超类”成员。

 

不幸的是,没有任何语言原生支持这种类型的继承——C++ 的多重实现继承与“理想”相去甚远,因此可能导致许多问题,并且被认为过于难以使用。

实现这种实现继承的最佳位置是将其作为新语言特性的一部分。将其作为语言的一部分将确保类型安全并允许编译器优化。我计划在未来的文章中详细讨论这一点。在此期间,由于我们没有内置此类特性,我们可以使用包装器/适配器概念来实现此功能(例如,参见使用基于 Roslyn 的 VS 扩展包装器生成器在 C# 中实现适配器模式和模拟多重继承)。

在许多其他语言中,这种“包装器”结构被称为“混入”(mixins)。Roxy 的主要目的之一是在 C# 中实现“混入”功能。此外,正如我计划在本文中演示并在后续文章中阐述的那样,Roxy 的混入是“智能的”,因为它们允许在“子类”中更改“超类”成员的名称,并将多个“超类”成员合并为单个“子类”成员。

示例

代码位置

代码可以从本文顶部下载(NP.Concerns.Demo.zip)文件,也可以在 Github 上获取:NP.Concerns.Demo

演示应用程序和示例简介

每个示例都演示了如何构建相同的 WPF 小型应用程序

它显示了一个组织,其中有两个部门——“占星师”和“炼金术士”,每个部门包含两个人。你可以选择一个部门或该部门中的一个人(选中的卡片边框较粗)。

当你选择一个人时,该人所属的部门也会被选中。当一个部门被取消选择时,其所有人员也会被取消选择。

上图显示了选中“炼金术士”部门及其中的“Michael Mont”。

如果你右键点击部门或人员卡片,你将获得一个上下文菜单,允许你移除卡片(无论是整个部门还是部门中的某个人)。

如上所述,我们几乎不关心视觉效果的实现方式——我们将主要关注视图模型。

有3个演示示例

  1. MultiConcernsTest - 提供了应用程序最简单的实现。我尽量减少了行为的使用(尽管我确实使用了一些来稍微减少代码量)。
  2. MultiConcernsBehaviorTest - 类似于第一个示例,但我将父子选择行为提取到一个单独的类中,以演示基于行为的关注点分离的优势。
  3. MultiConcernsRoxyTest - 展示了如何使用 Roxy 构建相同的应用程序。几乎没有跨关注点的代码 - 关注点是从单独的类中插入的,并且几乎所有内容都是通过配置实现的。

 

MultiConcernsTest

查看 MultiConcernsTest.sln 解决方案并运行应用程序。确保应用程序的行为与上面描述的一致:选择和移除功能正常。

应用程序的所有业务逻辑都是通过非视觉视图模型代码实现的。视图模型代码位于“ViewModels”项目文件夹下。

有两个核心视图模型类:PersonVMBusinessGroupVM。还有一个小型类 BusinessGroupsVM,它是业务组的集合。请注意,我使用了 SingleSelectionObservableCollection 类作为集合——它提供了确保集合中只有一个成员处于“已选择”状态的功能。这个类定义在我们 WPF 应用程序引用的 NP.Utilities 项目中。BusinessGroupsVM 类还包含集合行为,以方便从集合中移除项目——稍后将详细介绍。

基于上述类的测试数据在 MainWindow.xaml.cs 类中创建,并被指定为主窗口的 DataContext

public MainWindow()
{
    InitializeComponent();

    BusinessGroupsVM businessGroups = new BusinessGroupsVM();

    // build some test data
    BusinessGroupVM businessGroup1 = new BusinessGroupVM
    {
        Name = "Astrologists"
    };

    businessGroup1.People.Add(new PersonVM { FirstName = "Joe", LastName = "Doe" });
    businessGroup1.People.Add(new PersonVM { FirstName = "Jane", LastName = "Dane" });

    businessGroups.Add(businessGroup1);

    BusinessGroupVM businessGroup2 = new BusinessGroupVM
    {
        Name = "Alchemists"
    };

    businessGroup2.People.Add(new PersonVM { FirstName = "Michael", LastName = "Mont" });
    businessGroup2.People.Add(new PersonVM { FirstName = "Michelle", LastName = "Mitchell" });

    businessGroups.Add(businessGroup2);

    // assign business groups to be the DataContext. 
    this.DataContext = businessGroups;
}  

查看 PersonVM 类。各种(非数据)相关关注点基本上由类实现的接口提供:INotifyPropertyChangedIRemovableISelectableItem<PersonVM>

查看 NP.Utilities 项目中定义的 IRemovable 接口

public interface IRemovable
{
    event Action<iremovable> RemoveEvent;

    void Remove();
}  
</iremovable>

它极其简单 - 它有一个方法 Remove(),它应该触发 RemoveEvent。然后,IRemovable 项所属的集合可以将该项从自身中移除。

这是 ISelectableItem 接口

public interface ISelectableItem<T>
    where T : ISelectableItem<T>
{
    bool IsSelected { get; set; }

    event Action<ISelectableItem<T>> IsSelectedChanged;

    void SelectItem();
}  

IsSelected 属性指定项是否被选中。无论何时它发生变化,都应该触发 IsSelectedChanged 事件。SelectItem() 方法应该将 IsSelected 属性更改为 true

现在,让我们回到 PersonVM 代码 - 我使用 C# 区域来分离代码中的关注点

public class PersonVM : 
    INotifyPropertyChanged,
    IRemovable,
    ISelectableItem<PersonVM>
{
    #region Data_Concern_Region
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string FullName => FirstName + " " + LastName;
    #endregion Data_Concern_Region

    #region Notifiable_Concern_Region
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion Notifiable_Concern_Region


    #region Removeable_Concern_Region
    public event Action<IRemovable> RemoveEvent = null;

    public void Remove()
    {
        RemoveEvent?.Invoke(this);
    }
    #endregion Removeable_Concern_Region


    #region Selectable_Concern_Region
    public event Action<ISelectableItem<PersonVM>> IsSelectedChanged;

    bool _isSelected = false;
    public bool IsSelected
    {
        get => _isSelected;
        set
        {
            if (_isSelected == value)
                return;

            _isSelected = value;

            IsSelectedChanged?.Invoke(this);

            OnPropertyChanged(nameof(IsSelected));
        }
    }

    public void ToggleSelection()
    {
        this.IsSelected = !this.IsSelected;
    }

    public void  SelectItem()
    {
        this.IsSelected = true;
    }

    #endregion Selectable_Concern_Region
}  

你可以看到,数据关注点在类中定义,而其余关注点本质上由接口和实现它们的功能定义。

现在看看 BusinessGroupVM 类。这是它的类图

它具有所有相似的关注点和相似的可移除可选择关注点的实现。可通知关注点(带有PropertyChanged事件)在超类VMBase中实现——我也可以使用VMBase作为PersonVM的基类,但我想展示它作为单独关注点的实现。这是BusinessGroupVM代码

public class BusinessGroupVM : VMBase, IRemovable, ISelectableItem<BusinessGroupVM>
{
    #region Data_Concern_Region
    public string Name { get; set; }

    public SingleSelectionObservableCollection<PersonVM> People { get; } =
        new SingleSelectionObservableCollection<PersonVM>();
    #endregion Data_Concern_Region


    #region Removeable_Concern_Region
    public event Action<IRemovable> RemoveEvent = null;

    public void Remove()
    {
        RemoveEvent?.Invoke(this);
    }
    #endregion Removeable_Concern_Region


    #region Selectable_Concern_Region
    public event Action<ISelectableItem<BusinessGroupVM>> IsSelectedChanged;

    bool _isSelected = false;
    public bool IsSelected
    {
        get => _isSelected;
        set
        {
            if (_isSelected == value)
                return;

            _isSelected = value;

            IsSelectedChanged?.Invoke(this);

            OnPropertyChanged(nameof(IsSelected));
        }
    }

    public void ToggleSelection()
    {
        this.IsSelected = !this.IsSelected;
    }

    public void SelectItem()
    {
        this.IsSelected = true;
    }

    #endregion Selectable_Concern_Region

    IDisposable _behaviorsDisposable;
    public BusinessGroupVM()
    {
        _behaviorsDisposable = 
            this.People.AddBehavior // remove behavior
            (
                (person) => person.RemoveEvent += Person_RemoveEvent,
                (person) => person.RemoveEvent -= Person_RemoveEvent
            )
            .AddBehavior // select behavior
            (
                (person) => person.IsSelectedChanged += Person_IsSelectedChanged,
                (person) => person.IsSelectedChanged -= Person_IsSelectedChanged
            );

        this.IsSelectedChanged += BusinessGroupVM_IsSelectedChanged;
    }

    private void Person_RemoveEvent(IRemovable person)
    {
        this.People.Remove((PersonVM) person);
    }

    private void BusinessGroupVM_IsSelectedChanged(ISelectableItem<BusinessGroupVM> businessGroup)
    {
        if (!this.IsSelected)
        {
            // this will also set all the 
            // all the Person objects within 
            // People collection into not-selected state.
            foreach(PersonVM personVM in this.People)
            {
                personVM.IsSelected = false;
            }
        }
    }

    private void Person_IsSelectedChanged(ISelectableItem<PersonVM> person)
    {
        if (person.IsSelected)
            this.IsSelected = true;
    }
}  

类上定义了两个 CollectionBehavior,用于响应移除和选择人员的事件。

public BusinessGroupVM()
{
    _behaviorsDisposable = 
        this.People.AddBehavior // remove behavior
        (
            (person) => person.RemoveEvent += Person_RemoveEvent,
            (person) => person.RemoveEvent -= Person_RemoveEvent
        )
        .AddBehavior // select behavior
        (
            (person) => person.IsSelectedChanged += Person_IsSelectedChanged,
            (person) => person.IsSelectedChanged -= Person_IsSelectedChanged
        );

    this.IsSelectedChanged += BusinessGroupVM_IsSelectedChanged;
} 

集合行为在我的集合行为文章中进行了描述。它们允许在将项目添加到ObservableCollection中的每个项目上添加事件处理程序,并在项目被移除时(如果被移除)移除相应的事件处理程序。

上面的构造函数代码意味着,只要 PersonVM 对象属于 BusinessGroupVM 对象的 People 集合,其 RemoveEvent 就会有一个 Person_RemoveEvent 处理程序,并且 IsSelectedChanged 会有一个 Person_IsSelectedChanged 处理程序。

你可以看到 Person_RemoveEvent 只是将人员从 People 集合中移除,而 Person_IsSelectedChanged 确保当其任何 PersonVM 对象被选中时,BusinessGroupVM 对象也被选中。

还有一个 BusinessGroupVMIsSelectedChanged 事件处理程序。它确保当 BusinessGroupVM 对象被取消选中时,其所有人员也都被取消选中。

现在让我们再看看 BusinessGroupsVM

public class BusinessGroupsVM : SingleSelectionObservableCollection<businessgroupvm>
{
    IDisposable _behaviorsDisposable;
    public BusinessGroupsVM()
    {
        _behaviorsDisposable =
            this.AddBehavior
            (
                (businessGroup) => businessGroup.RemoveEvent += BusinessGroup_RemoveEvent,
                (businessGroup) => businessGroup.RemoveEvent -= BusinessGroup_RemoveEvent
            );
    }

    private void BusinessGroup_RemoveEvent(IRemovable businessGroupToRemove)
    {
        this.Remove((BusinessGroupVM)businessGroupToRemove);
    }
}  
</businessgroupvm>

如上所述,它是 BusingGroupVM 对象的 SingleSelectionObservableCollection。这样的集合确保其中一次只有一个项目被选中。

它还包含一个功能(作为 CollectionBehavior 实现),该功能确保当项目的 RemoveEvent 被触发时,该项目会从集合中移除。

MultiConcernsTest 实现中的问题

我们在第一个示例中发现了所有与关注点混合相关的问题。

注意,很多功能是重复的,特别是 IRemovableISelectedItem 接口的实现在 PersonVMBusinessGroupVM 类中完全相同。此外,负责从 BusinessGroupVMPeople 集合中移除 PersonVM 对象的功能与从 BusinessGroupsVM 集合中移除 BusinessGroupVM 对象的功能相同。

第二个主要问题是关注点全部混杂在一起——单个类(例如 PersonVMBusinessGroupVM)包含了所有关注点的实现,这些额外的代码稀释了类的主要目的(即包含和呈现有用数据)。

MultiConcernsBehaviorTest

尝试运行 MultiConcernsBehaviorTest 解决方案 - 你会发现它的行为与之前的示例完全相同。

这个示例与第一个非常相似,但它有两个基于行为的改进。我将从集合中移除 IRemovable 的功能提取到 RemovableCollectionBehavior 中。我还将当业务组被选中时取消选择人员,以及当其中一人被选中时选择业务组的功能提取到 ParentChildSelectionBehavior 中。

这两个行为都定义在 NP.Utilities 项目的 Behaviors 文件夹下。

以下是 RemovableCollectionBehavior 的代码

public class RemovableCollectionBehavior
{
    IDisposable _behaviorDisposable = null;

    IEnumerable<IRemovable> _collection;
    public IEnumerable<IRemovable> TheCollection
    {
        get => _collection;

        set
        {
            if (ReferenceEquals(_collection, value))
                return;

            _collection = value;

            _behaviorDisposable =
                _collection?.AddBehavior
                (
                    (item) => item.RemoveEvent += Item_RemoveEvent,
                    (item) => item.RemoveEvent -= Item_RemoveEvent
                );
        }
    }

    private void Item_RemoveEvent(IRemovable itemToRemove)
    {
        (TheCollection as IList).Remove(itemToRemove);
    }
}  

使用 AddBehavior 扩展方法,我们将 Item_RemoveEvent 事件处理程序分配给集合中的每个项目。该处理程序从集合中移除传入的项目。

ParentChildSelectionBehavior 的代码也很简单(尽管稍微复杂一些)

public class ParentChildSelectionBehavior<TParent, TChild>
    where TParent : class, ISelectableItem<TParent>
    where TChild : class, ISelectableItem<TChild>
{
    IDisposable _childrenBehaviorDisposable = null;

    TParent _parent;
    public TParent Parent
    {
        get => _parent;

        set
        {
            if (_parent.ObjEquals(value))
                return;

            if (_parent != null)
            {
                _parent.IsSelectedChanged -=
                    ParentChildSelectionBehavior_IsSelectedChanged;
            }

            _parent = value;

            if (_parent != null)
            {
                _parent.IsSelectedChanged +=
                    ParentChildSelectionBehavior_IsSelectedChanged;
            }
        }
    }

    ObservableCollection<TChild> _children;
    public ObservableCollection<TChild> Children
    {
        private get => _children;
        set
        {
            if (ReferenceEquals(_children, value))
                return;

            _children = value;

            _childrenBehaviorDisposable?.Dispose();
            _childrenBehaviorDisposable = _children.AddBehavior
             (
                child => child.IsSelectedChanged += Child_IsSelectedChanged,
                child => child.IsSelectedChanged -= Child_IsSelectedChanged
             );
        }
    }

    // unselect children if parent is unselected
    private void ParentChildSelectionBehavior_IsSelectedChanged(ISelectableItem<TParent> parent)
    {
        if (!parent.IsSelected)
        {
            foreach(TChild child in this.Children)
            {
                if (child.IsSelected)
                {
                    child.IsSelected = false;
                }
            }
        }
    }

    // select the parent if its child is selected. 
    private void Child_IsSelectedChanged(ISelectableItem<TChild> child)
    {
        if ((child.IsSelected) && (this.Parent != null))
        {
            this.Parent.IsSelected = true;
        }
    }
}  

该类的 Parent 属性应分配给父可选项目,该类的 Children 属性应分配给 ISelectedItem 子项的集合。

RemovableCollectionBehaviorBusinessGroupVM 类中用于从人员集合中移除人员对象,在 BusinessGroupsVM 中用于移除 BusinesGroupVM 对象。ParentChildSelectionBehaviorBusinessgroupVM 中用于控制 BusinessGroupVM 对象与其人员集合 People 中的 PersonVM 对象之间的选择交互。

你可以看到,视图模型的代码确实变得更小,更不易混淆,例如,BusinessGroupVM 文件的底部现在看起来是这样

ParentChildSelectionBehavior<BusinessGroupVM, PersonVM> _parentChildSelectionBehavior =
    new ParentChildSelectionBehavior<BusinessGroupVM, PersonVM>();

RemovableCollectionBehavior _removableCollectionBehavior =
    new RemovableCollectionBehavior();

public BusinessGroupVM()
{
    _removableCollectionBehavior.TheCollection = this.People;

    _parentChildSelectionBehavior.Parent = this;
    _parentChildSelectionBehavior.Children = this.People;
}  

而不是上一个示例中的30多行。此外,代码的重用性更好,因为通用行为也可以在其他代码中使用,甚至在这个示例中,RemovableCollectionBehavior 也在两个地方被重用。

MultiConcernsRoxyTest - Roxy 实现

重要提示

与之前的示例不同,这个演示项目使用了来自 nuget.org 的 NP.Roxy nuget 包。

Roxy 示例视图模型

这是本文的主要演示——本文正是为此而写。它位于 MultiConcernsRoxyTest 解决方案下。

该示例本质上包含两个代码视图模型(其余由 Roxy 生成)。

文件 PersonDataVM.cs 包含 IPersonDataVM 接口和 PersonDataVM

public interface IPersonDataVM
{
    string FirstName { get; set; }

    string LastName { get; set; }

    string FullName { get; }
}

public class PersonDataVM : IPersonDataVM
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string FullName => FirstName + " " + LastName;
}  

我们需要一个类唯一的原因是它包含 FullName 属性的代码。

文件 BusinessGroupDataVM.cs 包含 IBusinessGroup 接口

public interface IBusinessGroup
{
    string Name { get; set; }

    ObservableCollection<ISelectableRemovablePerson> People { get; }
}  

这些都是“手写”的视图模型功能。你可以看到,它只包含数据,没有其他任何关注点。

还有一个文件 RoxyModelAssembly.cs。它包含一些非常简单的接口和一个静态类 RoxyModelAssembler,用于将具有各种关注点的完整类组装在一起。本节其余部分的解释将重点关注此文件。

Roxy 功能简介

将来我计划撰写更多文章详细介绍 Roxy 的功能。然而,在这里,我只对 Roxy 类型生成进行简要介绍,以便解释 RoslynModelAssembly.cs 文件的内容。

ITypeConfig 类型的对象对于创建 Roxy 生成的类型至关重要。它们包含有关生成类型的完整信息。它们可以被修改,直到它们的 ConfigurationCompleted() 方法被调用。之后它们将变为“冻结”——不允许任何修改。

ITypeConfig 对象通常通过调用静态 Roxy 方法 Core.FindOrCreateTypeConfig<...>(string className = null) 获取。

此方法或其各种重载的泛型类型参数是我们主要关注的。

最常见的方法重载是 Core.FindOrCreateTypeConfig<TImplementedInterface, TSuperClass, TWrappedInterface>(string className = null),其中

  1. TImplementedInterface 是生成的类型实现的接口。如果它不应该实现任何接口,则应传入 NP.Roxy.NoInterface 类型参数。
  2. TSuperClass 是生成的类型将扩展的类。如果不需要扩展任何类,应传入 NP.Roxy.NoClass 类型参数。
  3. TWrappedInterface 是一个特殊接口,它定义了生成的类包装的封装(mixin)对象。这些对象为生成的类的未定义、抽象或虚拟方法和属性提供实现。从某种意义上说,这些对象是生成类的实现继承“超类”。

Core.FindOrCreateTypeConfig<...>(string className = null) 方法的 className 参数允许指定生成类的名称(当然,在 NP.Generated 命名空间中必须是唯一的)。如果没有传入 classNameRoxy 将根据传入方法的泛型类型参数生成一个默认类名。

 

一旦 ITypeConfig 对象被创建和配置,并且它们的 ConfigurationCompleted() 方法被调用,就可以通过调用静态方法 Core.GetInstanceOfGeneratedType<T>(string className = null, params object[] args); 来获取生成类型的新实例,其中 T 可以是基类名或实现的接口名,args 是生成类型的构造函数参数 - 通常我们使用默认构造函数,因此 args 数组为空。

用于类型配置的接口实现

我正在使用位于 NP.Utilities 项目下的 ISelectableItem<T>IRemovable 接口的通用实现。对应类的名称是 SelectableItem<T>Removable。以下是 SelectableItem<T> 代码

public class SelectableItem<T> : VMBase, ISelectableItem<T>, INotifyPropertyChanged
    where T : ISelectableItem<T>
{
    bool _isSelected = false;

    [XmlIgnore]
    public bool IsSelected
    {
        get
        {
            return _isSelected;
        }

        set
        {
            if (_isSelected == value)
                return;

            _isSelected = value;

            IsSelectedChanged?.Invoke(this);

            OnPropertyChanged(nameof(IsSelected));
        }
    }

    public event Action<ISelectableItem<T>> IsSelectedChanged;

    public void SelectItem()
    {
        this.IsSelected = true;
    }

    public void ToggleSelection()
    {
        this.IsSelected = !this.IsSelected;
    }
}  

你可以看到,SelectableItem 还实现了可通知关注点,并在其 IsSelected 属性更改时触发 PropertyChanged 事件。

这是 Removable 代码

public class Removable : IRemovable
{
    public event Action<iremovable> RemoveEvent;

    public void Remove()
    {
        RemoveEvent?.Invoke(this);
    }
}  
</iremovable>

没什么大不了的。

除了 SelectableItemRemovable 类,我还使用了在上一节示例中解释的 RemovableCollectionBehaviorParentChildSelectionBehavior 类。

RoxyModelAssembler 解释

现在我们准备解释 ITypeConfig 对象如何在 RoxyModelAssembler 静态类中使用。

静态方法 RoxyModelAssembler.AssembleSelectableRemovablePerson() 配置 PersonVM 类型,该类型还实现了 ISelectableItemIRemovableINotifiablePropertyChanged 接口。

// assembles the functionality for PersonVM with 
// selectable and removable capabilities
public static void AssembleSelectableRemovablePerson()
{
    // create type config
    ITypeConfig typeConfig =
        Core.FindOrCreateTypeConfig<ISelectableRemovablePerson, PersonDataVM, ISelectableRemovablePersonWrapper>();

    // set the first argument for PropertyChanged event 
    // to map into 'this' pointer
    typeConfig.SetEventArgThisIdx(nameof(INotifyPropertyChanged.PropertyChanged), 0);

    // complete the configuration
    typeConfig.ConfigurationCompleted();
}  

如您所见,它实现了接口 ISelectableRemovablePerson(也在同一文件中定义)

public interface ISelectableRemovableItem<T> : 
    ISelectableItem<T>, 
    IRemovable, 
    INotifyPropertyChanged
   where T : ISelectableItem<T>
{
}

// represents PersonVM - 
// the IPersonDataVM with Selectable and
// Removable functionality
public interface ISelectableRemovablePerson :
    IPersonDataVM, ISelectableRemovableItem<ISelectableRemovablePerson>
{

}  

(我还展示了它扩展的接口 - ISelectableRemovableItem

ISelectableRemovablePerson 接口继承自另一个接口 ISelectableRemovableItem,后者结合了 ISelectableItemIRemovableINotifiablePropertyChanged 接口。我将 ISelectableRemovableItem 接口提取出来,因为它也可以用于 BusingGroupVM 功能。

此接口还从 IPersontDataVM 接口获取其数据关注点。

回到 Core.FindOrCreateTypeConfig<ISelectableRemovablePerson, PersonDataVM, ISelectableRemovablePersonWrapper>(); 方法,它的第二个泛型类型参数是 PersonDataVM 类型,因此生成的类将继承自 PersonDataVM,其属性 FullName 将从该类获取实现。

 public string FullName => FirstName + " " + LastName;  

生成类型的其余属性、事件和方法将来自 ISelectableRemovablePersonWrapper 接口定义的包装对象。

// Wrapper interface for implementing 
// ISelectableRemovableItem interface
public interface ISelectableRemovableWrapper<T>
    where T : ISelectableItem<T>
{
    SelectableItem<T> Selectable { get; }

    Removable Removable { get; }
}


// the wrapper for 
// implementing ISelectableRemovablePerson
public interface ISelectableRemovablePersonWrapper :
    ISelectableRemovableWrapper<ISelectableRemovablePerson>
{
}  

(我还展示了它扩展的接口 - ISelectableRemovableWrapper

此接口继承自 ISelectableRemovableWrapper。它包含两个成员 Selectable(类型为 SelectableItem)和 Removable(类型为 RemovableItem)。这些成员定义了可选择可移除关注点的实现。ISelectableRemovableWrapper 被提取出来以便在 BusinessGroupVM 生成中重用。

查看静态方法 RoxyModelAssembler.AssembleSelectableRemovableBusinessGroup(),该方法用于组装 BusinessGroupVM 类,该类结合了业务组数据关注点、可选择可移除可通知关注点以及 RemovableCollectionBehaviorParentChildSelectionBehavior

// Assembles the BusinessGroupVM functionality with 
// Selectable and Removable implementations and also 
// with ParentChildSelectionBehavior and RemovableCollectionBehavior.
public static void AssembleSelectableRemovableBusinessGroup()
{
    // get the type config object
    ITypeConfig typeConfig =
        Core.FindOrCreateTypeConfig<ISelectableRemovableBusinessGroup, ISelectableRemovableBusinessGroupWrapper>("BusinessGroupVM");

    // Adds the initialization of People collection to
    // an empty SingleSelectionObservableCollection collection within the constructor
    typeConfig.SetInit<SingleSelectionObservableCollection<ISelectableRemovablePerson>>(nameof(IBusinessGroup.People));

    // set the first argument for PropertyChanged event 
    // to map into 'this'
    typeConfig.SetEventArgThisIdx(nameof(INotifyPropertyChanged.PropertyChanged), 0);

    // maps Parent property of wrapped ParentChildSelectionBehavior
    // into 'this' field of the generated type. 
    typeConfig.SetThisMemberMap
    (
        nameof(ISelectableRemovableBusinessGroupWrapper.TheParentChildSelectionBehavior),
        nameof(ParentChildSelectionBehavior<ISelectableRemovableBusinessGroup, ISelectableRemovablePerson>.Parent)
    );

    // maps Children property of wrapped ParentChildCollectionBehavior
    // into People property of the generated type. 
    typeConfig.SetMemberMap
    (
        nameof(ISelectableRemovableBusinessGroupWrapper.TheParentChildSelectionBehavior),
        nameof(ParentChildSelectionBehavior<ISelectableRemovableBusinessGroup, ISelectableRemovablePerson>.Children),
        nameof(IBusinessGroup.People)
    );


    // maps TheCollection property of wrapped RemovableCollectionBehavior
    // into People property of the generated type. 
    typeConfig.SetMemberMap
    (
        nameof(ISelectableRemovableBusinessGroupWrapper.TheRemovableCollectionBehavior),
        nameof(RemovableCollectionBehavior.TheCollection),
        nameof(IBusinessGroup.People)
    );

    // specifies that the configuration is completed.
    typeConfig.ConfigurationCompleted();
}  

请注意,这里使用了 Core.FindOrCreateTypeConfig<...>(...) 方法的不同重载,它只接受两个参数——要实现的接口和包装器接口。

ISelectableRemovableBusinessGroup 是要实现的接口

// represents IBusinessGroup with Selectable and
// Removable functionality
public interface ISelectableRemovableBusinessGroup :
    IBusinessGroup,
    ISelectableRemovableItem<ISelectableRemovableBusinessGroup>
{

}  

就像上面讨论的 ISelectableRemovablePerson 一样,它继承自 ISelectableRemovableItem,后者提供了可选择可移除可通知类型的关注点使用方面。

这些关注点的实现侧由接口 ISelectableRemovableBusinessGroupWrapper 指定。

// Wrapper for the RemovableCollectionBehavior
public interface IRemovableCollectionBehaviorWrapper
{
    RemovableCollectionBehavior TheRemovableCollectionBehavior { get; }
}


// Wrapper for RemovableCollectionBehavior and 
// ParentChildSelectionBehavior
public interface ISelectableRemovableBusinessGroupWrapper :
    ISelectableRemovableWrapper<ISelectableRemovableBusinessGroup>,
    IRemovableCollectionBehaviorWrapper
{
    ParentChildSelectionBehavior<ISelectableRemovableBusinessGroup, ISelectableRemovablePerson> TheParentChildSelectionBehavior { get; }
}  

该接口定义了 ParentChildSelectionBehavior。它还继承自 ISelectableRemovableWrapper 接口,该接口指定了可选择可移除可通知关注点的实现(如上所述)。最后,它还继承自 IRemovableCollectionBehaviorWrapper,后者定义了 RemovableCollectionBehaviorIRemovableCollectionBehaviorWrapper 接口被提取出来,因为创建 BusinessGroupsVM 实现也需要它)。

其余内容在代码中都有详细的文档

// Adds the initialization of People collection to
// an empty SingleSelectionObservableCollection collection within the constructor
typeConfig.SetInit<SingleSelectionObservableCollection<ISelectableRemovablePerson<<(nameof(IBusinessGroup.People));  

在构造函数中,将 People 集合初始化为空的 SingleSelectionObservableCollection<ISelectableRemovablePerson> 集合。

也有名称映射,例如

// maps Parent property of wrapped ParentChildSelectionBehavior
// into 'this' field of the generated type. 
typeConfig.SetThisMemberMap
(
    nameof(ISelectableRemovableBusinessGroupWrapper.TheParentChildSelectionBehavior),
    nameof(ParentChildSelectionBehavior<iselectableremovablebusinessgroup, iselectableremovableperson="">.Parent)
);  
</iselectableremovablebusinessgroup,>

将生成类型的 this 映射到 ParentChildSelectionBehavior.Parent 属性。

最后,用于生成 BusingsGroupsVM 集合代码的静态方法是 RoxyModelAssembler.AssembleBusinessGroupsCollection()

// Assembles the BusinessGroupsVM collection. 
public static void AssembleBusinessGroupsCollection()
{
    ITypeConfig typeConfig =
        Core.FindOrCreateTypeConfig<NoInterface, SingleSelectionObservableCollection<ISelectableRemovableBusinessGroup>, IRemovableCollectionBehaviorWrapper>("BusinessGroupsVM");

    // maps TheCollection property of RemovableCollectionBehavior
    // into 'this' of the generated object. 
    typeConfig.SetThisMemberMap
    (
        nameof(IRemovableCollectionBehaviorWrapper.TheRemovableCollectionBehavior),
        nameof(RemovableCollectionBehavior.TheCollection)
    );

    typeConfig.ConfigurationCompleted();
}  

ITypeConfig 对象未指定实现接口,但指定了基类 SingleSelectionObservableCollection<ISelectableRemovableBusinessGroup>。包装器接口是 IRemovableCollectionBehaviorWrapper,它只包含 RemoveableCollectionBehavior。行为的 TheCollection 属性映射到生成对象的 this

生成测试对象的代码

现在,生成测试对象(WPF 窗口的 DataContext)的代码不同了,因为我们不能使用生成对象的构造函数,而必须使用 Roxy 功能来完成。

此代码位于 MainWindow.xaml.cs 文件中的 MainWindow 类构造函数内。

public MainWindow()
{
    InitializeComponent();

    // create the generated types
    RoxyModelAssembler.AssembleSelectableRemovablePerson();
    RoxyModelAssembler.AssembleSelectableRemovableBusinessGroup();
    RoxyModelAssembler.AssembleBusinessGroupsCollection();

    // get the data context as BusinessGroupsVM type
    SingleSelectionObservableCollection<ISelectableRemovableBusinessGroup> dataContext =
        Core.GetInstanceOfGeneratedType<SingleSelectionObservableCollection<ISelectableRemovableBusinessGroup>>();

    // set the data context of the main window
    this.DataContext = dataContext;

   // create businessGroup1 as BusingGroupVM object
   ISelectableRemovableBusinessGroup businessGroup1 = 
        Core.GetInstanceOfGeneratedType<ISelectableRemovableBusinessGroup>();

    businessGroup1.Name = "Astrologists";

    // add businessGroup1 to the data context collection
    dataContext.Add(businessGroup1);

    // create person1 as PersonVM object
    ISelectableRemovablePerson person1 = Core.GetInstanceOfGeneratedType<ISelectableRemovablePerson>();

    //set properties
    person1.FirstName = "Joe";
    person1.LastName = "Doe";

    // add person1 object to businessGroup1
    businessGroup1.People.Add(person1);

    // create and add person2
    ISelectableRemovablePerson person2 = Core.GetInstanceOfGeneratedType<ISelectableRemovablePerson>();
    person2.FirstName = "Jane";
    person2.LastName = "Dane";
    businessGroup1.People.Add(person2);

    // create and add businessGroup2
    ISelectableRemovableBusinessGroup businessGroup2 =
        Core.GetInstanceOfGeneratedType<ISelectableRemovableBusinessGroup>();
    businessGroup2.Name = "Alchemists";
    dataContext.Add(businessGroup2);

    // create and add person3 
    ISelectableRemovablePerson person3 = Core.GetInstanceOfGeneratedType<ISelectableRemovablePerson>();
    person3.FirstName = "Michael";
    person3.LastName = "Mont";
    businessGroup2.People.Add(person3);

    // create and add person4
    ISelectableRemovablePerson person4 = Core.GetInstanceOfGeneratedType<ISelectableRemovablePerson>();
    person4.FirstName = "Michelle";
    person4.LastName = "Mitchell";
    businessGroup2.People.Add(person4);
}  

注意,我们使用 Core.GetInstanceOfGeneratedType<...>() 来创建生成类型的对象。

结论

本文主要观点是

  1. 关注点分离对于构建良好、可重用和可测试的代码至关重要。
  2. 有两种类型的继承——使用继承和实现继承,它们松散地映射到 C# 或 Java 接口实现和基类扩展。
  3. 应用程序中的每个类都有多个关注点。
  4. 每个关注点都有使用实现两方面。
  5. 通过使用 C# 或 Java 语言的接口继承,并能够将多个同名和同签名的成员合并到同一成员中,可以轻松合并多个关注点的使用方面。
  6. 仅凭 C#、Java 或 C++ 语言手段,很难合并多个关注点的实现方面。我之所以提出 Roxy IoC 容器和代码生成器,主要是为了弥补这一缺点。
  7. 我主张引入新的语言特性和一种特殊类型的多重继承或混入,以改善各种关注点实现方面的合并。这将允许优化编译并提供更好的类型安全特性。这种“多重实现继承”的特性如下:
    1. 它应该是“黑盒”继承,只继承指定的功能——而不是通常的“白盒”继承,其中所有受保护和公共功能都被继承。
    2. 将“超类”功能重命名并映射到“子类”名称应该很容易。
    3. 应该提供各种方法来合并多个映射到同一“子类”成员的“超类”成员。
    4. 同一个“超类”类型可以以不同的名称多次出现在类型的“超类”中。这些“超类”的成员可以映射到“子类”的不同成员。

 

我计划撰写更多关于 Roxy 的文章,深入描述其各种功能。

致谢

我要感谢我亲爱的妻子帕齐特(Pazit)帮助我编辑这篇文章,并确保其内容清晰且语法正确。

我一直在使用一个很棒的工具 - C# 快速图表工具来生成 UML 图。向其创作者致敬。

© . All rights reserved.