The Clifton Method - 第二部分





5.00/5 (7投票s)
服务管理器 - 通过接口规范进行实例化
系列文章
引言
本文建立在我所谓的软件开发“Clifton Method”的基础上。本系列的第一篇文章是 模块管理器,虽然本文中的代码可以独立运行,但最终目的是与模块管理器协同工作。
通过接口规范实例化具体实例的概念应该是 依赖倒置原则 的一个众所周知的模式。正如维基百科的链接所总结的那样
在面向对象编程中,依赖倒置原则是指一种特定的解耦软件模块的方式。遵循此原则时,从高级、策略设置模块到低级、依赖模块的传统依赖关系会被反转,从而使高级模块独立于低级模块的实现细节。该原则指出:
- 高级模块不应依赖于低级模块。两者都应依赖于抽象。
- 抽象不应依赖于细节。细节应依赖于抽象。
DIP 解决了什么问题?
与模块管理器类似,服务管理器提供了解耦对象之间依赖关系的能力。“依赖缠绕”的应用程序直接实例化类。我们可以举出许多依赖缠绕的例子。
在这些情况下,高级组件和低级组件之间的依赖关系在实现中是硬编码的。如果需求发生变化,代码将需要大量返工,以替换或抽象出低级依赖项。当低级组件本身与更高级别的组件交互时,问题会变得更加复杂,例如,当用户界面事件由控制器处理时,需要数据库 I/O,这可能会影响用户界面。
依赖倒置原则解耦了依赖关系,这样,**无论是**高级组件还是低级组件都可以更改而**不会**破坏**代码**。这导致实现看起来更像这样。
在此图中,高级和低级组件都已被抽象,因此应用程序(图中未显示)可以使用接口而不是具体类来实现,从而在“应用程序想要做什么”与“组件如何做”之间实现了高度解耦。
额外的劳动值得吗?
上面的图表说明,程序员需要付出更多的工作(有时是相当多的工作)来创建接口、指定接口行为,并且在许多情况下,编写实现接口行为的包装器。例如,UI 抽象的更准确的视图可能如下所示:
程序员(不是开发者,除非他们是同一个人)必须在耦合和抽象之间取得的平衡,通常由以下问题决定:
- 我确信哪些组件我将坚持使用?
- 哪些组件的具体实现将需要发生变化,不仅是由于当前的需求,还由于不可预见的未来变化?
不幸的是,这两个问题的答案通常需要水晶球!
尽管如此,当找到一个折衷点时,最终的应用程序将更健壮、更易于更改,通常寿命更长,并且未来的更改成本更低。这种方法的一个直接用途是服务可以轻松模拟,这有助于单元测试和应用程序开发,无论是服务尚未实现,还是例如,与可能不可用的硬件相关。
服务管理器
服务管理器是依赖倒置原则的一个轻量级实现。简而言之,组件,无论是高级还是低级,都通过将具体实现与接口关联来进行实例化。
单例实例化
这是一个最小化的示例:
using System;
using Clifton.Core.ServiceManagement;
namespace ServiceManagerDemo
{
public interface IAnimal : IService
{
void Speak();
}
public interface ICat : IAnimal { }
public interface IDog : IAnimal { }
public class Cat : ServiceBase, ICat
{
public void Speak()
{
Console.WriteLine("Meow");
}
}
public class Dog : ServiceBase, IDog
{
public void Speak()
{
Console.WriteLine("Woof");
}
}
class Program
{
static void Main(string[] args)
{
ServiceManager svcMgr = new ServiceManager();
svcMgr.RegisterSingleton<ICat, Cat>();
svcMgr.RegisterSingleton<IDog, Dog>();
IAnimal animal1 = svcMgr.Get<ICat>();
IAnimal animal2 = svcMgr.Get<IDog>();
animal1.Speak();
animal2.Speak();
}
}
}
在此示例中,请注意:
IAnimal
是任何派生自它的东西的纯抽象规范。- “具体”接口
ICat
和IDog
派生自IAnimal
,但不包含任何具体实现。 Cat
和Dog
的具体实现分别派生自ICat
和IDog
,并实现了IAnimal
行为。- 在此示例中,我们告诉服务管理器,“
Cat
”和“Dog
”服务是单例——它们只实例化一次,随后的 Get 调用将返回唯一实例。
接口 IService
和抽象类 ServiceBase
由服务管理器提供,稍后将讨论。
这个架构有趣之处在于接口本身被抽象化了。
-
一个通用的接口
IAnimal
描述了抽象概念的行为。 -
派生自
IAnimal
的空子接口提供了将“具体”接口与“具体”实现者映射的方法,从而确定要实例化哪个“动物”。 -
通常,“具体”接口不包含进一步的行为规范,尽管可能有理由认为这很有用/必要。
注册单例时,它实际上会立即由服务管理器实例化。
非单例实例化
服务通常是单例,但也可以实例化多个服务实例。在此示例中,我们将创建一个 Cat
的多个实例。
using System;
using Clifton.Core.ServiceManagement;
namespace ServiceManagerInstanceDemo
{
public interface IAnimal : IService
{
string Name { get; set; }
void Speak();
}
public interface ICat : IAnimal { }
public abstract class Animal : ServiceBase
{
public string Name { get; set; }
public abstract void Speak();
}
public class Cat : Animal, ICat
{
public override void Speak()
{
Console.WriteLine(Name + " says 'Meow'");
}
}
public static class InstanceDemo
{
public static void Demo()
{
ServiceManager svcMgr = new ServiceManager();
svcMgr.RegisterInstanceOnly<ICat, Cat>();
IAnimal cat1 = svcMgr.Get<ICat>();
cat1.Name = "Fido";
IAnimal cat2 = svcMgr.Get<ICat>();
cat2.Name = "Morris";
cat1.Speak();
cat2.Speak();
}
}
}
请注意
- 服务通过
RegisterInstanceOnly
而不是RegisterSingleton
进行注册。 - 一个 `abstract` 类实现了 `Name` 属性,但指定 `Speak` 仍然是抽象的,因为 `Name` 属性现在是所有动物共有的。
我们还可以通过 `Get` 方法中的一个函数来指定在 `Get` 调用中为属性赋值,例如:
IAnimal cat2 = svcMgr.Get<ICat>(cat => cat.Name = "Morris");
独占服务
通常,一种服务是所有其他实现者的排他者。应用程序可能只需要一个数据库服务。不同的安装可能需要连接到不同的数据库,但应用程序同时连接到*两个不同的*数据库服务的情况不会发生。这为接口实现创建了一个更简单的场景——在我的示例中,“Animal
”服务,如果被视为独占服务,则由 Cat
或 Dog
实现,而我们永远不需要同时实现两者。
using System;
using Clifton.Core.ServiceManagement;
namespace ServiceManagerExclusiveDemo
{
public interface IAnimal : IService
{
void Speak();
}
public class Cat : ServiceBase, IAnimal
{
public void Speak()
{
Console.WriteLine("Meow");
}
}
public static class ExclusiveDemo
{
public static void Demo()
{
ServiceManager svcMgr = new ServiceManager();
svcMgr.RegisterSingleton<IAnimal, Cat>();
IAnimal animal = svcMgr.Get<IAnimal>();
animal.Speak();
}
}
}
如上面的代码所示,实现更简单。
运行时确定服务获取
服务管理器不是依赖注入框架(DIF)——您会注意到,在上面的单例和实例示例中,代码仍然需要引用映射到具体实现的具体接口。
IAnimal animal1 = svcMgr.Get<ICat>();
IAnimal animal2 = svcMgr.Get<IDog>();
这里,代码仍然指定了具体接口。通常,您会希望从具体接口实例化类,但可能存在具体接口在编译时未知的多种情况,而是需要在运行时确定。这可以通过 DIF 来处理,DIF 会在运行时将具体实例注入到属性中,但恕我直言,这增加了许多不必要的复杂性。
这个问题可以通过指定类型来解决,而不是将其作为泛型参数,而是作为实际的 Type
参数,您可以从其他地方(配置文件等)获取它。这是一个相当简化的示例,其中我们首先假设某个过程将具体接口与具体实现者进行映射。
public static void RegisterServices()
{
svcMgr = new ServiceManager();
svcMgr.RegisterSingleton<ICat, Cat>();
svcMgr.RegisterSingleton<IDog, Dog>();
}
假设我们要调用一个将使用 IAnimal
服务的操作,但它不知道使用哪个具体接口。我们有两种选择。我们可以像这样调用(此处 IDog
和 ICat
在我的演示命名空间中实现):
ByTypeDemo.ByTypeParameter(typeof(ServiceManagerByTypeDemo.ICat));
ByTypeDemo.ByTypeParameter(typeof(ServiceManagerByTypeDemo.IDog));
并像这样实现方法(请注意,`static` 的使用仅是为了演示方便):
public static void ByTypeParameter(Type someAnimal)
{
IAnimal animal = svcMgr.Get<IAnimal>(someAnimal);
animal.Speak();
}
然而,这有一个缺点,就是失去了类型一致性——我们不再保证 someAnimal
实现 IAnimal
,这意味着如果它没有,我们将得到一个运行时错误。
更好的调用和实现如下:
public static void ByGenericParameter<T>() where T : IAnimal
{
IAnimal animal = svcMgr.Get<T>();
animal.Speak();
}
第二种方法要好得多,因为它在编译时强制了泛型参数的类型。实现者只需要知道泛型接口 IAnimal
,而不需要知道具体接口 ICat
和 IDog
,但尽管如此,仍可确保泛型参数的类型为 IAnimal
。
实现细节
服务管理器本身实现为一个服务。
public class ServiceManager : ServiceBase, IServiceManager
这意味着,结合模块管理器,服务管理器可以作为服务加载,而服务管理器提供的服务本身也是抽象和可替换的(只要您实现了 IServiceManager
!)。
线程安全
服务管理器旨在线程安全,因此它管理的三个信息块使用 ConcurrentDictionary
。
protected ConcurrentDictionary<Type, Type> interfaceServiceMap;
protected ConcurrentDictionary<Type, IService> singletons;
protected ConcurrentDictionary<Type, ConstructionOption> constructionOption;
唯一需要锁定的时间是创建或返回先前创建的单例时——我们需要这里的锁来防止两个线程同时尝试创建单例,或者一个线程创建单例而另一个线程尝试获取单例。
/// <summary>
/// Return a registered singleton or create it and register it if it isn't registered.
/// </summary>
protected IService CreateOrGetSingleton<T>(Action<T> initializer)
where T : IService
{
Type t = typeof(T);
IService instance;
lock (locker)
{
if (!singletons.TryGetValue(t, out instance))
{
instance = CreateAndRegisterSingleton<T>(initializer);
}
}
return instance;
}
实例注册
实例注册涉及向接口服务映射添加条目,并保留该类型被视为实例的事实。
public virtual void RegisterInstanceOnly<I, S>()
where I : IService
where S : IService
{
Type interfaceType = typeof(I);
Type serviceType = typeof(S);
Assert.Not(interfaceServiceMap.ContainsKey(interfaceType),
"The service " + GetName<S>() + " has already been registered.");
interfaceServiceMap[interfaceType] = serviceType;
constructionOption[interfaceType] = ConstructionOption.AlwaysInstance;
}
单例注册
单例注册涉及向接口服务映射添加条目并实例化单例。
public virtual void RegisterSingleton<I, S>(Action<I> initializer = null)
where I : IService
where S : IService
{
Type interfaceType = typeof(I);
Type serviceType = typeof(S);
Assert.Not(interfaceServiceMap.ContainsKey(interfaceType),
"The service " + GetName<S>() + " has already been registered.");
interfaceServiceMap[interfaceType] = serviceType;
constructionOption[interfaceType] = ConstructionOption.AlwaysSingleton;
RegisterSingletonBaseInterfaces(interfaceType, serviceType);
// Singletons are always instantiated immediately so that they can be initialized
// for global behaviors. A good example is the global exception handler services.
CreateAndRegisterSingleton<I>(initializer);
}
创建和注册
对于单例和服务实例,当创建实例时,会创建、注册并调用初始化方法(下文讨论)。
protected virtual IService CreateAndRegisterSingleton<T>(Action<T> initializer = null)
where T : IService
{
IService instance = CreateInstance<T>(initializer);
Register<T>(instance);
instance.Initialize(this);
return instance;
}
protected IService CreateInstance<T>(Action<T> initializer)
where T : IService
{
Type t = typeof(T);
IService instance = (IService)Activator.CreateInstance(interfaceServiceMap[t]);
initializer.IfNotNull((i) => i((T)instance));
return instance;
}
如果我们使用 C# 6.0,我们可以将 IfNotNull
扩展方法替换为:
initializer?.((T)instance));
但我还没有将代码库升级到 C# 6.0!
获取服务实例
当我们请求服务时,服务管理器会根据注册情况检查我们想要单例还是实例。
public virtual T Get<T>(Action<T> initializer = null)
where T : IService
{
IService instance = null;
VerifyRegistered<T>();
Type interfaceType = typeof(T);
switch (constructionOption[interfaceType])
{
case ConstructionOption.AlwaysInstance:
instance = CreateInstance<T>(initializer);
instance.Initialize(this);
break;
case ConstructionOption.AlwaysSingleton:
instance = CreateOrGetSingleton<T>(initializer);
break;
default:
throw new ApplicationException("Cannot determine whether the service " +
GetName<T>() + " should be created as a unique instance or as a singleton.");
}
return (T)instance;
}
根据应用程序需求获取实例或单例
如果我们之前没有将服务注册为单例或实例服务,我们就必须通过调用以下任一方法明确说明我们想要的实例类型:
/// <summary>
/// If allowed, returns a new instance of the service implement interface T.
/// </summary>
public virtual T GetInstance<T>(Action<T> initializer = null)
where T : IService
{
VerifyRegistered<T>();
VerifyInstanceOption<T>();
IService instance = CreateInstance<T>(initializer);
instance.Initialize(this);
return (T)instance;
}
/// <summary>
/// If allowed, creates and registers or
/// returns an existing service that implements interface T.
/// </summary>
public virtual T GetSingleton<T>(Action<T> initializer = null)
where T : IService
{
VerifyRegistered<T>();
VerifySingletonOption<T>();
IService instance = CreateOrGetSingleton<T>(initializer);
return (T)instance;
}
此机制允许我们根据应用程序的需求获取单例或实例。服务管理器允许我们同时拥有单例和多个实例,但这是一种不常见的情况,通常,我们在注册时指定预期用途,以便服务管理器可以断言应用程序正按预期使用该服务。
独占服务
即使服务实现了派生自抽象接口的具体接口,“抽象”接口的魔力是通过遍历接口层次结构并注册所有接口来处理的。
/// <summary>
/// Singletons are allowed to also register their base type
/// so that applications can reference singleton services by the common type
/// rather than their instance specific interface type.
/// </summary>
protected virtual void RegisterSingletonBaseInterfaces(Type interfaceType, Type serviceType)
{
Type[] itypes = interfaceType.GetInterfaces();
foreach (Type itype in itypes)
{
interfaceServiceMap[itype] = serviceType;
constructionOption[itype] = ConstructionOption.AlwaysSingleton;
}
}
这有点危险,因为没有断言说存在一个且仅一个通用接口与独占服务相关联。为了做到这一点,我们可以使用通用接口上的属性来指示任何派生的具体接口都是独占的,但我还没有实现它。
IService
所有服务都必须派生自 IService
,它提供编译时验证,确保服务已正确注册并且服务实例已正确获取。
namespace Clifton.Core.ServiceManagement
{
public interface IService
{
IServiceManager ServiceManager { get; }
void Initialize(IServiceManager srvMgr);
void FinishedInitialization();
}
}
服务必须实现 ServiceManager
属性和上述两个方法,或者派生自 ServiceBase
,后者提供默认实现。
namespace Clifton.Core.ServiceManagement
{
/// <summary>
/// A useful base class for a default implementation of IService methods.
/// </summary>
public abstract class ServiceBase : IService
{
public IServiceManager ServiceManager { get; set; }
public virtual void Initialize(IServiceManager svcMgr)
{
ServiceManager = svcMgr;
}
public virtual void FinishedInitialization()
{
}
}
}
为什么这有必要或有用,尤其是因为服务管理器本身从不调用这些方法?
- 在我的方法中,服务通常与上一篇文章中讨论的模块管理器结合使用。服务实现为模块,一个模块可以(尽管通常不是)实现多个服务。因此,它需要能够注册服务,这需要服务管理器的一个实例。
- 在所有服务注册完成之前,服务无法使用。这与依赖注入框架(DIF)有点不同,在 DIF 中,所有服务都在应用程序“运行”之前注入。因为我的目的是避免 DIF 的额外复杂性和调试困难,所以我更喜欢“更接近底层”的方法。这意味着注册是一个两步过程:
- 初始化每个模块中的服务管理器实例,允许模块注册其服务。
- 调用
FinishedInitialization
,它告诉模块所有服务都已注册,现在它可以对它依赖的服务执行任何最终初始化。
我们将在下一篇文章中看到模块管理器和服务管理器如何协同工作。
结论
使用服务管理器是程序员工具箱中实现依赖倒置原则的重要工具之一。使用服务管理器实现的应用程序将逐步遵循 SOLID 面向对象编程原则。
- 单一职责原则:一个类应该只有一个职责(即,软件规范中只有一个潜在的更改会影响类的规范)。
- 开闭原则:“软件实体……应该对扩展开放,对修改关闭。”
- 里氏替换原则:“程序中的对象应该能够被其子类的实例替换,而不会改变程序的正确性。”
- 接口隔离原则:“许多客户端特定的接口比一个通用的接口更好。”
- 依赖倒置原则:应该“依赖于抽象。不要依赖于具体实现。”
这里提供的服务管理器开始为后三个原则(L、I 和 D)奠定坚实的基础。使用此处提供的代码及其泛型参数的使用,或您自己的类似实现,可以在解耦具体实现的同时提供编译时类型检查,从而实现一个灵活的应用程序,并表现出对高级和低级对象的良好独立性。
历史
- 2016 年 8 月 25 日:初始版本