前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >(翻译)LearnVSXNow! #9 - 创建我们第一个工具集-重构为服务

(翻译)LearnVSXNow! #9 - 创建我们第一个工具集-重构为服务

作者头像
明年我18
发布2019-09-18 14:28:59
4540
发布2019-09-18 14:28:59
举报
文章被收录于专栏:明年我18明年我18

在第6篇和第7篇里,我们创建了一个名为StartupToolset的示例package,并且手动地添加了一个菜单项和工具窗。在这篇文章里,我们将重构这个package,提取独立的服务模块出来。

我们这个示例package有很多地方可以重构:不仅可以做提取服务之类的结构调整,也可以封装可重用的代码,以便供以后调用或提高代码可读性。在下一篇文章里我们将封装可重用的代码,但在这一篇里,我们把精力放在服务上。

复制一份StartupToolset

为了在重构之前保留目前的StartupToolset的版本,我把这个package复制了一份,并命名为StartupToolsetRefactored。你可以参考第6篇和第7篇的内容自己来做一个副本:新建一个空的名为StartupToolsRefactored的package,并且根据第6篇的内容为它添加一个菜单项,根据第7篇的内容添加一个工具窗。

为了避免和前一个package冲突,要修改一下StartupToolsRefactored里的GUID,并且修改一下菜单命令的显示文本,这样就可以在界面上和旧版的package区分开来。

创建一个全局服务(global service)

在重构的第一步,我们将把“计算引擎”做成一个全局服务。这样的话别的package就可以调用我们这个服务的功能了。

到目前为止,“计算”的逻辑是直接嵌入到我们的工具窗的用户控件CaculationControl类里的。这段逻辑放在了CalculateButton_Click事件处理方法里,这样我们的代码看起来就非常简单并且容易懂。但是在这种结构下,计算逻辑和我们的package是紧耦合的:

代码语言:javascript
复制
public partial class CalculationControl : UserControl{  ...  private void CalculateButton_Click(object sender, EventArgs e)  {    try    {      int firstArg = Int32.Parse(FirstArgEdit.Text);      int secondArg = Int32.Parse(SecondArgEdit.Text);      int result = 0;      switch (OperatorCombo.Text)      {        case "+":          result = firstArg + secondArg;          break;        ...      }      ResultEdit.Text = result.ToString();    }    catch (SystemException) { ... }    ...  }}

最适合的改进方案是把这段计算逻辑放到一个独立的服务对象里。如果我们把这个服务对象做成一个全局的VSX服务的话,不仅我们的CalculationControl控件可以使用它,其他的package也一样可以使用它。OK,就这样做!

创建服务接口

每一个服务都必须至少提供一个接口来作为服务的“契约”,所以,不必惊讶,我们要创建接口(译者注:从技术上来讲,服务不一定非得需要接口,这一点我在这篇译文的后面会做些测试代码来说明)。我们可以把接口定义在我们的package程序集里,但是,别的package要想用这个服务的话,就不得不引用我们的整个package:我们通常不想这么做。

所以,我们用老配方:创建一个的单独的程序集来放置服务。这样我们的package和其他的package都可以引用它。

创建一个名为StartupToolsetInterfaces的类库项目,并在StartupToolsetRefactored项目里引用它。删除掉默认的Class1.cs文件,并添加一个CalculationService.cs文件。

如果你还记得我们在前面的例子中是怎样访问到全局服务的话,你一定会想起来GetService方法:

代码语言:javascript
复制
IVsUIShell uiShell = (IVsUIShell)GetService(typeof(SVsUIShell));

为得到uiShell这个服务对象,我们用到了两个类型:IVsUIShell是定义服务的接口;SVsUIShell是所谓的标记类型(markup type),用它来标识服务对象。你可能会问,我们为什么要用两个类型?只用一个接口类型不就够了吗?是的,一个接口类型就够了,但用两个类型可以提高灵活性:一个服务对象可以实现一个或多个接口,一个接口也可以被一个或多个服务对象实现(译者注:例如你有一堆的服务都是IXXXService类型的,但每个服务的具体实现有所不同,你就可以定义若干个标记类型来区分这些不同的服务)。用两个类型的话,我们即可以给服务对象起名(如SVsUIShell),也可以为服务接口起名(如IVsUIShell)。GetService的参数可以是实现了服务的类型,但也不一定非得这样。实际上,我们可以传任何类型给它,这个参数只是作为一个key来标识一个服务对象。

标记类型(markup type)不包含任何功能,它们仅仅用来标记一个类型,以区分其他类型。

我们也按照这种模式来做,在CalculationService.cs文件里,添加两个接口:一个服务接口和一个标记接口:

代码语言:javascript
复制
using System.Runtime.InteropServices;  namespace StartupToolsetInterfaces{  [Guid("D7524CAB-5029-402d-9591-FA0D59BBA0F0")]  [ComVisible(true)]  public interface ICalculationService  {    bool Calculate(string firstArgText, string secondArgText,       string operatortext, out string resultText);  }    [Guid("AF7F72EF-2B54-4798-B76A-21DC02CC04B7")]  public interface SCalculationService  {  }}

按照惯例,服务接口以“I”开头,标记接口以“S”开头。它们必须能够被COM识别,所以要加上Guid。另外,服务接口必须定义为ComVisible,这样非托管代码就可以检索到它。

创建服务类

我们把实现计算逻辑的服务实现类定义在我们的package里(不在StartupToolsetInterfaces类库项目里)。在StartupToolsetRefactored项目里,添加一个CalculationService.cs文件(此CalculationService.cs文件非彼CalculationService.cs 文件),并添加类似下面的代码:

代码语言:javascript
复制
using System;using StartupToolsetInterfaces;  namespace MyCompany.StartupToolsetRefactored{  public sealed class CalculationService: ICalculationService, SCalculationService  {    public bool Calculate(string firstArgText, string secondArgText,       string operatorText, out string resultText)    { ... }  }}

由于接口是定义在StartupToolsetInterface程序集里的,所以我们要using它们的命名空间。为了能正常的创建我们的服务,服务类必须既实现服务接口,又实现标记接口(markup type)。如果你没有实现标记接口,编译是没问题的,但这个服务对象实例是不会被创建的。由于标记类型实际上不包含任何方法,所以我们只需要实现Calculate方法就可以了。这个方法的实现可以从CalculationControl控件的CalculateButton_Click方法里复制过来,并且要做些调整:

代码语言:javascript
复制
public bool Calculate(string firstArgText, string secondArgText,   string operatorText, out string resultText){  try  {    int firstArg = Int32.Parse(firstArgText);    int secondArg = Int32.Parse(secondArgText);    int result = 0;    switch (operatorText)    {      case "+":        result = firstArg + secondArg;        break;      ...    }    resultText = result.ToString();  }  catch (SystemException)  {    resultText = "#Error";    return false;  }  return true;}

现在让我们修改一下CalculateButton_Click方法,来用这个service:

代码语言:javascript
复制
public partial class CalculationControl : UserControl{  ...  private void CalculateButton_Click(object sender, EventArgs e)  {    ICalculationService calcService = new CalculationService();    string result;    calcService.Calculate(FirstArgEdit.Text, SecondArgEdit.Text,                          OperatorCombo.Text, out result);    ResultEdit.Text = result;    LogCalculation(FirstArgEdit.Text, SecondArgEdit.Text, OperatorCombo.Text,      ResultEdit.Text);  }  ...}

运行StartupToolsetRefactored项目,并且试一下Calculate工具窗,你会发现它能够正常工作。这样就够了吗?不,还不够。现在我们有了服务对象,并且应用了它,但我们还需要告诉VS IDE这个服务的存在,这样别的package才能用它!

提供服务

在我们使我们的服务可见和可用之前,我们先来看一下VS IDE中服务体系的机制。在第5篇中,我讲了一下VS IDE中服务的基本概念,这一次让我们深入一些。

任何一个对象如果想调用一个服务的话,它必须要和service provider“对话”。service provider实现了IServiceProvider接口,并包含GetService方法:

代码语言:javascript
复制
public interface IServiceProvider{  object GetService(Type serviceType);}

很容易想象出来:一个service provider包含了一个预定义的服务集合。VS IDE本身就是一个service provider,然而,VS IDE可以动态的处理服务对象,因为已安装的package可以提供它们自己的服务给IDE。所以,还应该有一个service container,service container实现了IServiceContainer接口,该接口继承自IServiceProvider

代码语言:javascript
复制
public interface IServiceContainer: IServiceProvider{  void AddService(...); // --- Overloaded  void RemoveService(...); // --- Overloaded}

AddServiceRemoveService方法提供了我们所期望的service container的功能。一个VSPackage本身就是一个service container(当然也是一个service provider),因为Package类实现了IServiceContainer

service container并不是一个平面的东东,它可能包含parent container。当添加或移除一个服务的时候,我们可以把这个服务传给它的parent container,VS IDE就是用这种结构来管理全局服务的。另外,VS IDE用SProfferService服务来管理全局服务,不过MPF帮我们屏蔽了SProfferService:如果我们的package继承自Package基类的话,我们很少会用到它。

好了,让我们看看怎样才能把CalculationService提供给VS IDE。我们需要做下面的几步:

  1. 第一步:需要一个方法,该方法负责创建相应类型的服务对象。
  2. 第二步:在package上注明该package能提供的服务类型。
  3. 第三步:为服务对象的创建添加初始化代码。

第一步:添加负责创建服务对象的方法

服务对象只会被创建一次,然后所有的调用方都用这同一个实例。我们可以在package初始化的时候实例化服务对象,也可以在第一个调用者请求这个服务的时候才去实例化它。

在这里我们打算用第二种方式,所以我们需要一个创建服务对象的回调方法。在我们的package类中,添加一个CreateService的方法:

代码语言:javascript
复制
private object CreateService(IServiceContainer container, Type serviceType){  if (container != this)  {    return null;  }  if (typeof(SCalculationService) == serviceType)  {    return new CalculationService();  }  return null;}

这个回调方法有两个参数:container是请求这个服务的容器,serviceType是请求的服务类型。如果能够创建服务的话,该方法必须返回服务实例,否则必须返回null。在上面这个代码段里,我们只接受是package本身的container,并且只能创建SCalculationService类型的服务。

第二步:声明能提供的服务

就像菜单命令和工具窗那样,我们必须在package那里附加一个attribute,以声明该package能提供的服务:

代码语言:javascript
复制
[ProvideService(typeof(SCalculationService))] public sealed class StartupToolsetRefactoredPackage : Package { ... }

ProvideService属性的作用是:regpkg.exe利用这个attribute去注册我们的服务,并使我们的package能够按需加载(在第一次调用package的服务的时候,如果package没有加载,则加载package)。

每个服务默认以类型的名字作为服务名,当然也可以通过设置这个attribute的ServiceName属性来更改服务名。

第三步:添加初始化代码

我们的package通过ProvideServiceAttribute使外面的事件知道它的服务的存在,但是为了服务实例能被创建,我们还得添加一些初始化代码才行。这段代码最好放在package的构造函数里:

代码语言:javascript
复制
public sealed class StartupToolsetRefactoredPackage : Package{  public StartupToolsetRefactoredPackage()  {    IServiceContainer serviceContainer = this;    ServiceCreatorCallback creationCallback = CreateService;    serviceContainer.AddService(typeof(SCalculationService),       creationCallback, true);  }  ...}

Package类显示地实现了IServiceContainer接口,是没有公开的AddService方法的,所以我们必须把this转换成IServiceContainer类型的对象。AddService方法有很多重载,我们用其中的接受3个参数的那个:要添加的服务的类型、当服务第一次调用时会被调用的回调方法、以及是否把这个服务传递给parent container的标记。我们把最后一个参数设成true,这样就可以确保我们的服务可以被全局访问。

使用服务

现在,所有其他package都可以用松耦合的方式来使用我们的计算服务了。但是我们在CalculationButton_Click方法里是直接实例化它的:

代码语言:javascript
复制
ICalculationService calcService = new CalculationService();string result;calcService.Calculate(FirstArgEdit.Text, SecondArgEdit.Text,                      OperatorCombo.Text, out result);

我们最好修改一下它,以便从IDE里得到服务实例:

代码语言:javascript
复制
private void CalculateButton_Click(object sender, EventArgs e){  ICalculationService calcService =     Package.GetGlobalService(typeof (SCalculationService)) as ICalculationService;  if (calcService != null)  {    string result;    calcService.Calculate(FirstArgEdit.Text, SecondArgEdit.Text,                          OperatorCombo.Text, out result);    ResultEdit.Text = result;  }}

一些试验

到目前为止,我们的package已经使用了我们创建的服务了。接下来,我建议你对代码做些临时的改动,并看看我们的package会有什么变化。

为了能够清楚地看到这些变化,我建议你在CalculateButton_Click方法的最下面调用LogCalculationToOutput方法,这样就可以看到我们的package在执行的时候输出来的调试信息:

代码语言:javascript
复制
private void CalculateButton_Click(object sender, EventArgs e){  ...  LogCalculationToOutput(FirstArgEdit.Text, SecondArgEdit.Text,     OperatorCombo.Text, ResultEdit.Text);}

我们将对代码做些小的改动,并且每次改动都会使我们的服务不可用:当我们需要得到这个服务的实例的时候,我们只能得到空引用。在这个过程中不会有任何错误提示,但是在output窗口里,我们可以发现这个服务不会正常工作。例如,如果我们想计算“1+2”,我们期待在output窗口中能看到“1 + 2 = 3”,但是我们只能看到“1 + 2 = ”。

我强烈建议你做这一下这些改动,并检查改动后的结果,因为服务开发者经常会犯类似的错误,并且不知道错在哪了。所以,求你了,做一下下面的试验(每次试验完要记得“undo”这一次的修改)。

试验1:在CalculationService类声明那里,注释掉对SCalculationService接口的实现

代码语言:javascript
复制
public sealed class CalculationService: ICalculationService   // , SCalculationService{  public bool Calculate(string firstArgText, string secondArgText,     string operatorText, out string resultText)  { ... }}

package照样可以编译通过,但是这个服务对象是没法被创建的。因为当我们调用GetService方法的时候,这个方法认为返回的服务对象能够转换成参数里指定的类型。在我们的例子中我们是通过GetService(typeof(SCalculationService))调用的,但返回的CalculationService类的实例是不能够转换成SCalculationService类型的,因为它并没有实现SCalculationService接口。

试验2:在调用AddService方法时,把最后一个参数从true改成false

代码语言:javascript
复制
public StartupToolsetRefactoredPackage(){  IServiceContainer serviceContainer = this;  ServiceCreatorCallback creationCallback = CreateService;  serviceContainer.AddService(typeof(SCalculationService), creationCallback,    false);}

这样改后,我们也得不到服务的实例了,这是因为Package.GetGlobalService方法找的是所有公开给VS IDE的服务,但是我们把AddService的最后一个参数改成false之后,我们的服务就不是公开的了。

用本地的方式使用服务

到目前为止我们都是通过调用Package.GetGlobalService方法来得到服务实例的,看起来像是这个服务是别的package而不是我们的package提供的。其实,我们可以用GetService方法:

代码语言:javascript
复制
private void CalculateButton_Click(object sender, EventArgs e){  ICalculationService calcService =     GetService(typeof (SCalculationService)) as ICalculationService;  ...}

这样改动后,我们的package照样运行正常!但是这个GetService方法是从哪里来的呢?CalculateControl用户控件和我们的package没有直接的联系,它继承自UserControl类,UserControl又继承自System.ComponentModel.Component,而这个类实现了IServiceProvider接口,还记得不,这个接口定义了GetService方法!但是,属于用户控件的GetService方法是怎么知道我们的package会提供这个服务的?我们并没有在这个用户控件里直接引用package啊。

原因就是VS IDE的Siting机制。当我们的package加载到IDE的时候,它被site了,并且得到了一个parent IServiceProvider;当我们的工具窗里的用户控件加载到内存的时候,这个控件也被site到工具窗中,所以也会有一个parent IServiceProvider,这两个service provider是同一个对象。用户控件的GetService方法在执行的时候,会查找整个IServiceProvider链。在这个链中,它会调用到我们的package的GetService方法并最终得到这个服务对象。这是一种本地访问服务的方式。如果注释掉package上附加的ProvideService属性的话(译者注:仅注释掉是不够的,要卸载package然后再注册),我们的package也可以正常运行,但是这个服务就不再是一个全局服务了,别的package不一定能够再使用它。(译者注:在别的package请求这个服务时,无法知道这个服务在哪个package内,所以也就无法使用这个服务,但是如果我们的package已经加载了,那么别的package依然可以得到这个服务,因为在我们package的构造函数里,我们把这个服务加到了parent service container里)

总结

原来的StartupToolset里的计算逻辑是耦合在工具窗的用户控件里的,在这篇文章里,我们把这段逻辑抽了出来做成了一个全局服务。为创建这个服务,我们在一个单独的程序集里添加了两个接口:

  1. 服务接口声明了服务的功能(契约)。
  2. 标记类型(无成员的接口)被用作GetService的参数。

在package项目中,我们添加了一个服务实现类,实现了服务接口和标记接口,并探讨了服务的机制和使服务能被全局访问的步骤。我们的服务实例在第一次被请求时才会创建。另外,我们还知道了怎样以全局和本地的方法来访问服务。

在下一篇里,我们继续重构这个package,并创建可重用的代码。

原文链接:http://dotneteers.net/blogs/divedeeper/archive/2008/01/31/LearnVSXNow9.aspx

到这里这篇译文就已经结束了,但我还想再多说明一些东西:

1。服务一定需要定义成接口吗?

如果单单从技术上来看,服务不一定非得需要接口。为了说明这一点,我们在StartupToolsetInterfaces项目里添加一个MyServiceClass.cs文件,并添加如下代码:

代码语言:javascript
复制
public class MyServiceClass{    public int Caculate(int i)    {        return i*i;    }}

然后用这篇译文里的方法添加ProvideService、回调方法,并在package的构造函数里调用AddService。然后,新建一个带菜单项的package,并添加对StartupToolsetInterfaces的引用,然后在菜单项的事件处理方法里,添加如下代码:

代码语言:javascript
复制
MyServiceClass myService = GetService(typeof(MyServiceClass)) as MyServiceClass;if (myService != null){    MessageBox.Show(myService.Caculate(3).ToString());}

运行起来后,点击这个package的菜单,是不是弹出了一个消息框,并显示9?所以,服务不一定非得用接口,但用接口会更好,可以使结构更好,又或者可以使非托管代码可以访问这个服务(我并没有验证过)。

2。服务的GUID是干什么用的?

在上面这个示例服务MyService里,我们并没有给他加GUID,但原文作者给出的例子却加了GUID,那么这个GUID是干嘛用的呢?其实,GUID无非就是标识这个服务而已。然后打开注册表,在HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\9.0Exp\Configuration\Services下面就可以找到这个GUID,是ProvideService这个attribute指定的SCalculationService接口的GUID。如果没有给它加GUID,regpkg在注册的时候,会自动产生一个GUID,所以,一般情况下也不用给服务指定GUID。

但在某些情况下,这个GUID还是有用的。比如由于某种原因,我们的package不能够引用StartupToolsetInterfaces项目,但是在package里又想用它的service,我们就可以在package项目里加一个接口或类(该接口或类可以是空的),然后给他一个GUID,GUID的值是StartupToolsetInterfaces里的SCalculationService的GUID:

代码语言:javascript
复制
[Guid("AF7F72EF-2B54-4798-B76A-21DC02CC04B7")]class MyService{ //空的   }

然后把自己定义的这个接口类型传给GetService方法,这样就照样可以得到这个服务的实例(是一个object类型的),然后通过反射来调用它的方法了。

代码语言:javascript
复制
object service = GetService(typeof(MyService));if(service != null){    //反射调用其方法}

当然,通过反射来调用看起来很怪,应该有其他方式可以用“强类型”的方式使用这个服务,例如像使用COM对象那样,定义interop类型,但我缺少这方面的知识,所以没有去验证怎样使用。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2010-03-20 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 复制一份StartupToolset
  • 创建一个全局服务(global service)
  • 创建服务接口
  • 创建服务类
  • 提供服务
  • 第一步:添加负责创建服务对象的方法
  • 第二步:声明能提供的服务
  • 第三步:添加初始化代码
  • 使用服务
  • 一些试验
  • 试验1:在CalculationService类声明那里,注释掉对SCalculationService接口的实现
  • 试验2:在调用AddService方法时,把最后一个参数从true改成false
  • 用本地的方式使用服务
  • 总结
  • 1。服务一定需要定义成接口吗?
  • 2。服务的GUID是干什么用的?
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档