谈到WPF的开发,就不能不说到MVVM,一说到MVVM,就会提及MVC、MVP等概念,那么这样一关联下来就会产生很多概念,到最后就很容易变成以概念来阐述概念,最终的结果可想而知,大家可能会一头雾水、不知所云,所以我用“漫谈WPF开发”这个小标题来阐述一下我对WPF开发的理解,当然只是自己对这些技术的总结和经验,错误之处在所难免,也希望大家能够谅解!
从2007年接触WPF和Silverlight以来,也做过一些项目了,对他们也有一些自己的理解,当然在开发这些项目的过程中也在使用其他的一些技术做项目,比如WinForm、ASP.NET(ASP.NET MVC一个项目没做完就被终止)等等,感觉不论是采用什么技术,最基本的东西都不会变,比如对数据库和文件的访问、对日志和异常的处理、对报表的展现、对打印的实现、对性能的提升、对用户的友好等等。
那么这些项目也为我们积累了不少经验,有技术上的也有其他方面的:
其实归根到底就是要分清关系,理清思绪,既要处理好与机器的关系,也要处理好与人的关系,只有这样才能把产品或者项目做成功,我也在不断学习当中,所以如果大家有一些这方面的问题和建议,我们也可以互相讨论。
前面不知所云的漫谈了一通,那么我们到底该怎么认识WPF项目的开发呢?
我个人的观点是和其他技术一样,假如这是一个比较小的而且需求改动很小的项目,那么我不建议用一些高深的技术,因为它要的是马上看到效果和时间上的优势,所以应该抛弃我们的技术思想。
当遇到一个比较大型的项目而且需求可能变动很大,那我们得慎重考虑系统的构架了,因为很多时候我们都会发现我们的系统无法再扩展了,这就是一个很大的“杯具”了。
那么作为一个项目,我们怎样才能在事前做好呢?我觉得有以下几个方面:
前面谈到了一些项目管理整体把握,总结就是大道至简、适可而止!那么我们如何才能在具体项目中引用一些其他技术呢?感觉思绪有点乱了,还是就此打住,等到了讲WPF具体项目或者具体技术的时候再讲,不然就真的一发不可收拾了。
以上转自:https://www.cnblogs.com/KnightsWarrior/archive/2010/07/09/1774059.html
为什么要提倡 “Design Pattern” 呢?根本原因是为了代码复用,增加可维护性。那么怎么才能实现代码复用呢?
开闭原则具有理想主义的色彩,它是面向对象设计的终极目标。其他几条,则可以看做是开闭原则的实现方法。设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。
高内聚、低耦合、可扩展、可复用。
-== 一个类只负责一个职责。==
里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出。其严格表述如下:
如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1代换o2时,程序P的行为没有变化,那么类型S是类型T的子类型。这个定义比较拗口且难以理解,因此我们一般使用它的另一个通俗版定义:
里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠。
例如有两个类,一个类为BaseClass,另一个是SubClass类,并且SubClass类是BaseClass类的子类,那么一个方法如果可以接受一个BaseClass类型的基类对象base的话,如:method1(base),那么它必然可以接受一个BaseClass类型的子类对象sub,method1(sub)能够正常运行。反过来的代换不成立,如一个方法method2接受BaseClass类型的子类对象sub为参数:method2(sub),那么一般而言不可以有method2(base),除非是重载方法。
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
在使用里氏代换原则时需要注意如下几个问题:
最小知识原则(Principle of Least Knowledge,PLK,也叫迪米特法则)
迪米特法则来自于1987年美国东北大学(Northeastern University)一个名为“Demeter”的研究项目。迪米特法则又称为最少知识原则(LeastKnowledge Principle, LKP),其定义如下:迪米特法则(Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。
如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。
迪米特法则还有几种定义形式,包括:不要和“陌生人”说话、只与你的直接朋友通信等,在迪米特法则中,对于一个对象,其朋友包括以下几类:
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
在将迪米特法则运用到系统设计中时,要注意下面的几点:在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只要有可能,一个类型应当设计成不变类;在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。这里的“接口”往往有两种不同的含义:一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象;另外一种是指某种语言具体的“接口”定义,有严格的定义和结构,比如Java语言中的interface。对于这两种不同的含义,ISP的表达方式以及含义都有所不同:
当把“接口”理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的一个接口,此时,这个原则可以叫做“角色隔离原则”。
如果把“接口”理解成狭义的特定语言的接口,那么ISP表达的意思是指接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。在面向对象编程语言中,实现一个接口就需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便,为了使接口的职责单一,需要将大接口中的方法根据其职责不同分别放在不同的小接口中,以确保每个接口使用起来都较为方便,并都承担某一单一角色。接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或业务逻辑类)所需的方法即可,这种机制也称为“定制服务”,即为不同的客户端提供宽窄不同的接口。
** 依赖倒转原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。**
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。
在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。
在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象
合成复用原则又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle, CARP),其定义如下:
合成复用原则(Composite Reuse Principle, CRP):尽量使用对象组合,而不是继承来达到复用的目的。
合成复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用功能的目的。简言之:复用时要尽量使用组合/聚合关系(关联关系),少用继承。
在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的内部细节通常对子类来说是可见的,所以这种复用又称“白箱”复用,如果基类发生改变,那么子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用(如类没有声明为不能被继承)。
以下是模式的简要说明:
1)MVVM - 用作模型转换器并替代代码隐藏.提高了可测试性,为ViewModel编写单元测试要容易得多.
2)依赖注入 - 用于提高类的可测试性(您可以将特定类的单元测试与其他类别分开编写)以及以更简单的方式更改实现的可能性(更改记录器,缓存提供程序,Web服务等)
3)命令 - 默认情况下可以应用于Button和MenuItem控件,如果无法执行操作,则禁用控件.也用于MVVM模式,作为代码隐藏事件的替代.
WPF中已经使用的经典书中的其他模式:
创建型模式(6个)
结构型模式(7个)
行为型模式(11个)
包含:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
包含:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
包含:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
并发型模式、线程池模式。
常使用事件(event)进行解耦,外部代码通过订阅事件来解耦,实现对内部状态的观察
在 Process 类中有很多事件,可以用来捕获另一个进程中的输出,错误等
public event DataReceivedEventHandler OutputDataReceived;
public event DataReceivedEventHandler ErrorDataReceived;
通常这两个事件我们就可以获取到另外一个进程中的输出信息,除此之外还有很多的类在使用事件,相信你也用过很多
迭代器模式将一个聚合对象保存数据的职责,和它遍历数据的职责分离,在引入迭代器后,聚合对象只负责保存数据,而遍历数据交给迭代器来完成。迭代器模式的介绍将参考.NET中的迭代器来进行说明。
模式产生的原因
在软件开发中,为了可以更加方便的操作聚合对象,同时可以很灵活的为聚合对象增加不同的遍历方法,我们需要一个解决方案可以让我们访问一个聚合对象但又不需要暴露它的内部结构。迭代器模式为我们解决了这个问题。它提供一种可以顺序访问一个聚合对象各个元素的方法且不会暴露该对象的内部表示。
模式的灵感来源
在现实生活中,人们有两种方法来操作电视机实现开机,关机,换台等操作。
一种方法是使用电视机本身提供的操作面板上的按钮来说直接实现,
另一种则是通过遥控器来间接控制电视机。
遥控器的出现为电视机的操作带带来了极大的方便,用户并不需要知道电视机中是怎么存储频道的就可以实现换台。
由图可知,迭代器模式由4个对象构成:
抽象聚合对象是所有的聚合对象的父类,它提供了一个获得迭代器的方法,在.NET中这个方法被封装在了另一个接口中,其实为了更加彻底的解耦,我们也可以采用.NET的方法,将GetIterator单独封装在一个接口中。
具体的聚合对象,负责存储数据和获得迭代器,以方便遍历。
抽象迭代器中有如何遍历聚合对象的基础方法,在具体迭代器中我们需要实现这些方法。
具体迭代器负责实现抽象迭代器中的方法,并且要实现自己独有的遍历方法,一般我们可以在具体迭代器中书写当前迭代器的迭代逻辑
// 聚集抽象
public interface IEnumerable
{
/// Returns an enumerator that iterates through a collection.
/// An object that can be used to iterate through the collection.
IEnumerator GetEnumerator();
}
// 迭代器抽象
public interface IEnumerator
{
/// Advances the enumerator to the next element of the collection.
///
///
/// if the enumerator was successfully advanced to the next element; if the enumerator has passed the end of the collection.
/// The collection was modified after the enumerator was created.
bool MoveNext();
/// Gets the element in the collection at the current position of the enumerator.
/// The element in the collection at the current position of the enumerator.
object Current { get; }
/// Sets the enumerator to its initial position, which is before the first element in the collection.
/// The collection was modified after the enumerator was created.
void Reset();
}
Array 和 List 各自实现了自己的迭代器,感兴趣可以去看下源码
using System.Collections.Generic;
namespace Iterator.Iterator.Question3
{
public abstract class MyIterator
{
protected List<int> MyList;
protected int Index;
public MyIterator(string name, List<int> list)
{
MyList = list;
}
public abstract bool MoveNext();
public abstract object Current();
public abstract void Reset();
}
}
namespace Iterator.Iterator.Question3
{
public interface INewGetIterator
{
MyIterator GetIterator();
}
}
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Iterator.Iterator.Question3
{
public class MCollectionAgent : MyIterator
{
private MyIterator _iterator;
public MCollectionAgent(string name, List<int> list) : base(name, list)
{
switch (name)
{
case "AsOrder":
_iterator = new AsOrder(name, list);
break;
case "DesOrder":
_iterator = new DesOrder(name, list);
break;
}
}
public override bool MoveNext()
{
return _iterator.MoveNext();
}
public override object Current()
{
return _iterator.Current();
}
public override void Reset()
{
_iterator.Reset();
}
}
}
抽象聚合对象类:
using System.Collections.Generic;
using Iterator.Iterator.Example;
namespace Iterator.Iterator.Question3
{
public abstract class CollectionObject : INewGetIterator
{
protected List<int> MyList;
public abstract MyIterator GetIterator();
}
}
具体聚合对象A:
using System.Collections.Generic;
namespace Iterator.Iterator.Question3
{
public class CollectionA : CollectionObject
{
private string _name;
public CollectionA(string name)
{
_name = name;
MyList = new List<int>()
{
1,2,3,4,5,6,7,8,9
};
}
public override MyIterator GetIterator()
{
return new MCollectionAgent(_name, MyList);
}
}
}
升序遍历迭代器:
using System.Collections.Generic;
namespace Iterator.Iterator.Question3
{
public class AsOrder : MyIterator
{
public override bool MoveNext()
{
Index++;
if (Index < MyList.Count)
{
return true;
}
return false;
}
public override object Current()
{
return MyList[Index];
}
public override void Reset()
{
Index = -1;
}
public AsOrder(string name, List<int> list) : base(name,list)
{
Index = -1;
}
}
}
降序遍历迭代器:
using System.Collections.Generic;
namespace Iterator.Iterator.Question3
{
public class DesOrder : MyIterator
{
public DesOrder(string name, List<int> list) : base(name, list)
{
Index = MyList.Count;
}
public override bool MoveNext()
{
Index--;
if (Index >= 0)
{
return true;
}
return false;
}
public override object Current()
{
return MyList[Index];
}
public override void Reset()
{
Index = MyList.Count;
}
}
}
Program类:
using System;
using Iterator.Iterator.Question3;
namespace Iterator
{
internal class Program
{
public static void Main(string[] args)
{
//降序遍历
CollectionObject collectionObject = new CollectionA("DesOrder");
MyIterator iterator = collectionObject.GetIterator();
while (iterator.MoveNext())
{
Console.WriteLine(iterator.Current());
}
}
}
}
迭代器模式总结
迭代器模式的优点:
加粗样式迭代器模式支持以不同的方式遍历一个聚合对象,在同一个聚合对象中可以定义多种遍历方法。
迭代器简化了聚合对象,现在的聚合对象将不需要再考虑自己提供数据遍历的方法。
迭代器模式的缺点:
迭代器模式的使用会增加系统的复杂性。
迭代器模式的设计难度比较大,需要充分考虑系统将来的扩展。
命令模式是指:通过把一个请求封装成一个对象,从而可以使用不同的请求例如命令等将客户端参数化;可以将请求排队或者记录请求日志,可以支持请求撤销操作;命令模式是一个对象行为模式,别名为动作模式或者事务模式;
命令模式在将一个请求封装成一个对象后,无需了解请求激活的动作或者接收该请求后处理的细则;
这是一种两台机器之间通信性质的模式,命令模式解耦了发送者和接受者之间的联系,发送者调用一个操作,接收者执行该操作,发送者无需知道接收者对于该操作的任何接口。
单请求单接收
接口说明:
单请求多接收
其中将多个命令封装到一个List中,从而实现一个请求将有多个接受者进行处理;
请求支持撤销动作
-其中在Command方法中加入撤销动作undo(),即可实现该命令的撤销;
策略模式定义了一系列的算法,并且将每个算法封装成一个对象,同时这些算法还可以相互替代。
asp.net core 中的认证和授权,我觉得就是策略模式的应用,在使用 [Authorize] 的时候会使用默认的 policy,也可以指定要使用的策略 [Authorize(“Policy1”)] 这样就会使用另外一种策略 Policy1,policy 还是比较简单的
policy 是用来根据用户的认证信息来控制授权访问的,而认证则是根据当前上下文(请求上下文、线程上下文、环境上下文等)的信息进行认证从而获取用户信息的过程
而不同的认证模式(Cookie/JWT/自定义Token等)其实是不同的处理方法,也就是策略模式中不同的算法实现,指定哪种认证模式,就是使用哪种算法实现来获取用户信息
说明:
namespace CLZReportGen.ReportUtility
{
///
/// 环境类
///
public class ReportContext
{
public DateTime dtFrom, dtTo;
public CustomLogger logger;
public CLZ_CELUE cELUE;
//持有抽象策略角色的引用,用于客户端调用
private IStrategy strategy;
public IStrategy GetStrategy()
{
return strategy;
}
//设置所需策略
public void SetStrategy(IStrategy strategy)
{
this.strategy = strategy;
}
//根据设置的策略类返回对应的结果
public void ExecStrategyMethod(string reportType)
{
strategy.Exec(dtFrom, dtTo, cELUE, logger);
}
}
}
namespace CLZReportGen.ReportUtility
{
public interface IStrategy
{
bool Exec(DateTime dtFrom, DateTime dtTo, CLZ_CELUE cELUE,CustomLogger logger);
}
}
namespace CLZReportGen.ReportUtility
{
public class MReport : IStrategy
{
public bool Exec(DateTime dtFrom, DateTime dtTo, CLZ_CELUE cELUE, CustomLogger logger)
{
MReportModel mReportModel = new MReportModel(logger, dtFrom, dtTo, cELUE);
return mReportModel.templateMethod();
}
}
}
namespace CLZReportGen.ReportUtility
{
public class QReport : IStrategy
{
public bool Exec(DateTime dtFrom, DateTime dtTo, CLZ_CELUE cELUE, CustomLogger logger)
{
throw new NotImplementedException();
}
}
}
namespace CLZReportGen.ReportUtility
{
public class YReport : IStrategy
{
public bool Exec(DateTime dtFrom, DateTime dtTo, CLZ_CELUE cELUE,CustomLogger logger)
{
throw new NotImplementedException();
}
}
}
namespace CLZReportGen.ReportUtility
{
public class CallReport
{
///
/// 执行月报生产策略
///
///
///
public static void CallMReportTask(DateTime dtFrom, DateTime dtTo, CLZ_CELUE cELUE, CustomLogger logger)
{
ReportContext reportContext = new ReportContext();
IStrategy strategy = new MReport();
strategy.Exec(dtFrom, dtTo, cELUE,logger);
;
}
}
}
策略模式和命令模式区别
asp.net core 中间件的设计就是责任链模式的应用和变形,每个中间件根据需要处理请求,并且可以根据请求信息自己决定是否传递给下一个中间件,我也受此启发,封装了一个 PipelineBuilder 可以轻松构建中间件模式代码
app.UseStaticFiles();
app.UseResponseCaching();
app.UseResponseCompression();
app.UseRouting();
app.UseCors(builder => builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapControllerRoute(name: "areaRoute", "{area:exists}/{controller=Home}/{action=Index}");
endpoints.MapDefaultControllerRoute();
});
PipelineBuilder 实际示例:
var requestContext = new RequestContext()
{
RequesterName = "Kangkang",
Hour = 12,
};
var builder = PipelineBuilder.Create<RequestContext>(context =>
{
Console.WriteLine($"{context.RequesterName} {context.Hour}h apply failed");
})
.Use((context, next) =>
{
if (context.Hour <= 2)
{
Console.WriteLine("pass 1");
}
else
{
next();
}
})
.Use((context, next) =>
{
if (context.Hour <= 4)
{
Console.WriteLine("pass 2");
}
else
{
next();
}
})
.Use((context, next) =>
{
if (context.Hour <= 6)
{
Console.WriteLine("pass 3");
}
else
{
next();
}
})
;
var requestPipeline = builder.Build();
foreach (var i in Enumerable.Range(1, 8))
{
Console.WriteLine();
Console.WriteLine($"--------- h:{i} apply Pipeline------------------");
requestContext.Hour = i;
requestPipeline.Invoke(requestContext);
Console.WriteLine("----------------------------");
Console.WriteLine();
}
asp.net core 中的各种 Builder, HostBuilder/ConfigurationBuilder 等,这些 Builder 大多既是 Builder 又是 Director,Builder 本身知道如何构建最终的 Product(Host/Configuration)
var host = new HostBuilder()
.ConfigureAppConfiguration(builder =>
{
// 注册配置
builder
.AddInMemoryCollection(new Dictionary<string, string>()
{
{"UserName", "Alice"}
})
.AddJsonFile("appsettings.json")
;
})
.ConfigureServices((context, services) =>
{
// 注册自定义服务
services.AddSingleton<IIdGenerator, GuidIdGenerator>();
services.AddTransient<IService, Service>();
if (context.Configuration.GetAppSetting<bool>("XxxEnabled"))
{
services.AddSingleton<IUserIdProvider, EnvironmentUserIdProvider>();
}
})
.Build()
;
dotnet 中有两个数据结构 Stack/Queue 这两个数据都实现了 ICloneable 接口,内部实现了深复制
来看 Stack 的 Clone 方法实现:
public virtual Object Clone()
{
Contract.Ensures(Contract.Result<Object>() != null);
Stack s = new Stack(_size);
s._size = _size;
Array.Copy(_array, 0, s._array, 0, _size);
s._version = _version;
return s;
}
WPF、WinForm 中都有控件的概念,这些控件的设计属于是组合模式的应用,所有的控件都会继承于某一个共同的基类, 使得单个对象和组合对象都可以看作是他们共同的基类对象
之前介绍的适配器模式中有提到 asp.net core 3.x 里引入了 ServiceProviderFactory, 使得 .net core 可以更方便的集成第三方的依赖注入框架,这里使用了适配器模式通过 ServiceFactoryAdapter 来适配各种不同的第三方的依赖注入框架