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





5.00/5 (10投票s)
使用 Roxy IoC 容器和代码生成器实现关注点分离。
引言
关注点分离
在软件工程中,关注点是负责功能单个“方面”的事物。我计划在本文后面给出分离关注点的具体编码示例,但现在,让我们进行纯粹的口头讨论(不看代码)。
假设你在画布上有一些视觉图形(无论是桌面应用程序还是 Web 应用程序,以及使用什么软件包都无关紧要)。以下列出并解释了一些与图形相关的关注点
- 图形承载着一些视觉信息 - 它的颜色、大小、位置、视觉内容 - 这是它的视觉关注点。
- 在大多数应用程序中,图形还对应于一些通过图形呈现或修改的有用数据,或两者兼而有之。数据以及显示或修改它的功能将代表图形的数据关注点。
- 假设我们可以从画布上移除图形 - “移除”功能代表图形的移除关注点。
- 还假设图形可以处于选中和未选中状态 - 它的视觉效果根据是否选中而变化。负责此功能的功能可以作为选择关注点被提取出来。
为了说明,请运行MultiConcernsTest
示例应用程序。我稍后将解释代码,但现在让我们讨论应用程序功能。
当你启动应用程序时,你会看到以下窗口
应用程序列出了组织内的两个业务组“占星师”和“炼金术士”,每个组下有两个人。
点击包含组名称的顶部米色部分将选择该组(其边框将变粗)。点击一个人将选择该人及其所属组。一次只能选择一个组,并且一个组内一次只能选择一个人。如果你取消选择一个组 - 其所有人也将被取消选择。
你可以使用鼠标左键打开一个包含“移除”菜单项的菜单。如果你点击它,相应的图形将被移除 - 如果你点击业务组的“移除”,你将移除整个业务组,而如果你点击某个人的“移除”,你将只移除业务组中的那个人。
显然,对于人物图形,视觉关注点由实际的视觉控件及其功能表示。数据关注点由包含姓名和姓氏的功能组成。选择和移除关注点由负责选择和移除图形的功能表示。
类似的关注点划分也可以在业务组图形层面进行。
请注意,所列的关注点几乎相互独立 - 唯一的相互依赖点是
- 当一个项目被移除时,它也应该在视觉上被移除 - WPF(我用来实现这些示例的框架)会自动处理它 - 一旦视图模型改变,视图就会被修改。
- 当一个项目被选中时,它的边框应该变粗 - 这是通过一些小的额外 XAML 功能实现的。
在下面的讨论中,我将跳过视觉-非视觉关注点划分的讨论。WPF 和其他框架非常擅长处理这种特定的关注点划分(如果你使用 MVVM 模式)。我将主要讨论数据、选择和移除关注点的划分。
最佳设计是将关注点分离到不同的类/接口中,完全单独构建和测试它们,然后以很少或不需要额外代码的方式将它们组合在一起。
这种关注点分离具有以下主要优点
- 复用 - 选择和移除功能可以提取到单独的类中,并用于不同的图形,甚至用于一些非视觉代码。
- 代码清晰度 - 当数据、选择和移除功能混合在一起时,它们会污染代码,以至于难以区分一个功能的开始和结束。
- 代码独立性 - 如果人们开始将各种关注点放在一起,他们将不可避免地在各种关注点之间创建更多的依赖关系。这将导致各种相互依赖的错误,当更改一个关注点对应的代码时,可能会导致另一个关注点中的错误。
- 测试 - 分开测试所有关注点比一起测试更容易更好。可以理解,如果你测试单个关注点而不是它们的笛卡尔积,则组合更少。
行为
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 中的实现继承根本不强大。只允许单继承,因此没有智能合并各种类成员实现的功能。
一般来说,类功能可以通过下图来演示
用法由形状的边框表示。空白(白色)或阴影区域分别表示抽象或虚拟成员(属性或方法,甚至是它们的集合)。不同的白色区域可以表示不同用法关注点的区域。
通过将功能实现插入到类形状的空白(和/或)阴影区域(区域的边界决定其使用接口),可以使类按预期工作。
这就是我认为理想的实现继承的目的(仅仅是填补空白)。
在我看来,“理想的”多重实现继承应该提供以下功能
- 它应该提供轻松指定多个实现“超类”的功能。多个实现“超类”甚至可以是同一类型——它们的功能可以用来实现各种插件(上图中的空白区域)。
- 它不应该是“白盒”继承,即“超类”的所有内容都进入子类,而应该是“黑盒”继承,在这种情况下,开发人员需要指定“超类”的哪个成员应该在子类中可访问。
- 将抽象或虚拟的“空白”与某些“超类”功能匹配的最简单方法是通过名称——因此,开发人员应该被允许在子类中轻松重命名“超类”成员。
不幸的是,没有任何语言原生支持这种类型的继承——C++ 的多重实现继承与“理想”相去甚远,因此可能导致许多问题,并且被认为过于难以使用。
实现这种实现继承的最佳位置是将其作为新语言特性的一部分。将其作为语言的一部分将确保类型安全并允许编译器优化。我计划在未来的文章中详细讨论这一点。在此期间,由于我们没有内置此类特性,我们可以使用包装器/适配器概念来实现此功能(例如,参见使用基于 Roslyn 的 VS 扩展包装器生成器在 C# 中实现适配器模式和模拟多重继承)。
在许多其他语言中,这种“包装器”结构被称为“混入”(mixins)。Roxy 的主要目的之一是在 C# 中实现“混入”功能。此外,正如我计划在本文中演示并在后续文章中阐述的那样,Roxy 的混入是“智能的”,因为它们允许在“子类”中更改“超类”成员的名称,并将多个“超类”成员合并为单个“子类”成员。
示例
代码位置
代码可以从本文顶部下载(NP.Concerns.Demo.zip)文件,也可以在 Github 上获取:NP.Concerns.Demo
演示应用程序和示例简介
每个示例都演示了如何构建相同的 WPF 小型应用程序
它显示了一个组织,其中有两个部门——“占星师”和“炼金术士”,每个部门包含两个人。你可以选择一个部门或该部门中的一个人(选中的卡片边框较粗)。
当你选择一个人时,该人所属的部门也会被选中。当一个部门被取消选择时,其所有人员也会被取消选择。
上图显示了选中“炼金术士”部门及其中的“Michael Mont”。
如果你右键点击部门或人员卡片,你将获得一个上下文菜单,允许你移除卡片(无论是整个部门还是部门中的某个人)。
如上所述,我们几乎不关心视觉效果的实现方式——我们将主要关注视图模型。
有3个演示示例
- MultiConcernsTest - 提供了应用程序最简单的实现。我尽量减少了行为的使用(尽管我确实使用了一些来稍微减少代码量)。
- MultiConcernsBehaviorTest - 类似于第一个示例,但我将父子选择行为提取到一个单独的类中,以演示基于行为的关注点分离的优势。
- MultiConcernsRoxyTest - 展示了如何使用 Roxy 构建相同的应用程序。几乎没有跨关注点的代码 - 关注点是从单独的类中插入的,并且几乎所有内容都是通过配置实现的。
MultiConcernsTest
查看 MultiConcernsTest.sln 解决方案并运行应用程序。确保应用程序的行为与上面描述的一致:选择和移除功能正常。
应用程序的所有业务逻辑都是通过非视觉视图模型代码实现的。视图模型代码位于“ViewModels”项目文件夹下。
有两个核心视图模型类:PersonVM
和 BusinessGroupVM
。还有一个小型类 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
类。各种(非数据)相关关注点基本上由类实现的接口提供:INotifyPropertyChanged
、IRemovable
和 ISelectableItem<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
对象也被选中。
还有一个 BusinessGroupVM
的 IsSelectedChanged
事件处理程序。它确保当 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 实现中的问题
我们在第一个示例中发现了所有与关注点混合相关的问题。
注意,很多功能是重复的,特别是 IRemovable
和 ISelectedItem
接口的实现在 PersonVM
和 BusinessGroupVM
类中完全相同。此外,负责从 BusinessGroupVM
的 People
集合中移除 PersonVM
对象的功能与从 BusinessGroupsVM
集合中移除 BusinessGroupVM
对象的功能相同。
第二个主要问题是关注点全部混杂在一起——单个类(例如 PersonVM
或 BusinessGroupVM
)包含了所有关注点的实现,这些额外的代码稀释了类的主要目的(即包含和呈现有用数据)。
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
子项的集合。
RemovableCollectionBehavior
在 BusinessGroupVM
类中用于从人员集合中移除人员对象,在 BusinessGroupsVM
中用于移除 BusinesGroupVM
对象。ParentChildSelectionBehavior
在 BusinessgroupVM
中用于控制 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)
,其中
TImplementedInterface
是生成的类型实现的接口。如果它不应该实现任何接口,则应传入NP.Roxy.NoInterface
类型参数。TSuperClass
是生成的类型将扩展的类。如果不需要扩展任何类,应传入NP.Roxy.NoClass
类型参数。TWrappedInterface
是一个特殊接口,它定义了生成的类包装的封装(mixin)对象。这些对象为生成的类的未定义、抽象或虚拟方法和属性提供实现。从某种意义上说,这些对象是生成类的实现继承“超类”。
Core.FindOrCreateTypeConfig<...>(string className = null)
方法的 className
参数允许指定生成类的名称(当然,在 NP.Generated
命名空间中必须是唯一的)。如果没有传入 className
,Roxy
将根据传入方法的泛型类型参数生成一个默认类名。
一旦 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>
没什么大不了的。
除了 SelectableItem
和 Removable
类,我还使用了在上一节示例中解释的 RemovableCollectionBehavior
和 ParentChildSelectionBehavior
类。
RoxyModelAssembler 解释
现在我们准备解释 ITypeConfig
对象如何在 RoxyModelAssembler
静态类中使用。
静态方法 RoxyModelAssembler.AssembleSelectableRemovablePerson()
配置 PersonVM
类型,该类型还实现了 ISelectableItem
、IRemovable
和 INotifiablePropertyChanged
接口。
// 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
,后者结合了 ISelectableItem
、IRemovable
和 INotifiablePropertyChanged
接口。我将 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
类,该类结合了业务组数据关注点、可选择、可移除、可通知关注点以及 RemovableCollectionBehavior
和 ParentChildSelectionBehavior
。
// 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
,后者定义了 RemovableCollectionBehavior
(IRemovableCollectionBehaviorWrapper
接口被提取出来,因为创建 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<...>()
来创建生成类型的对象。
结论
本文主要观点是
- 关注点分离对于构建良好、可重用和可测试的代码至关重要。
- 有两种类型的继承——使用继承和实现继承,它们松散地映射到 C# 或 Java 接口实现和基类扩展。
- 应用程序中的每个类都有多个关注点。
- 每个关注点都有使用和实现两方面。
- 通过使用 C# 或 Java 语言的接口继承,并能够将多个同名和同签名的成员合并到同一成员中,可以轻松合并多个关注点的使用方面。
- 仅凭 C#、Java 或 C++ 语言手段,很难合并多个关注点的实现方面。我之所以提出 Roxy IoC 容器和代码生成器,主要是为了弥补这一缺点。
- 我主张引入新的语言特性和一种特殊类型的多重继承或混入,以改善各种关注点实现方面的合并。这将允许优化编译并提供更好的类型安全特性。这种“多重实现继承”的特性如下:
- 它应该是“黑盒”继承,只继承指定的功能——而不是通常的“白盒”继承,其中所有受保护和公共功能都被继承。
- 将“超类”功能重命名并映射到“子类”名称应该很容易。
- 应该提供各种方法来合并多个映射到同一“子类”成员的“超类”成员。
- 同一个“超类”类型可以以不同的名称多次出现在类型的“超类”中。这些“超类”的成员可以映射到“子类”的不同成员。
我计划撰写更多关于 Roxy 的文章,深入描述其各种功能。
致谢
我要感谢我亲爱的妻子帕齐特(Pazit)帮助我编辑这篇文章,并确保其内容清晰且语法正确。
我一直在使用一个很棒的工具 - C# 快速图表工具来生成 UML 图。向其创作者致敬。