接口隔离原则(interface segregation principle):
基本定义
其内容可形象地比喻为契约当中的甲乙两方,契约的甲方实行”我不会多要”,乙方实行”我不会少给”原则。
在代码中乙方的”我不会少给”是比较容易约束的,因为当一个类在实现一个接口的时候(即作为服务的提供者时),其必须实现接口中的所有方法,如果未完全实现,它就是一个抽象类,不能实例化,那么它就不是一个完整的服务提供者。
但是甲方的”我不会多要”,它其实是一个软性的规定,是设计方面的问题,需要用设计原则来约束和控制。如何判断甲方”有没有多要”,就看传给调用者的接口类型中有无一直没被调用的函数成员。
何为胖接口及缺点
胖接口由两个或者两个以上本质不同的接口合并起来的,当把胖接口传给调用者时,只有其中一部分被调用,另一部分为多余的。违反接口隔离原则,造成胖接口不同的原因有两种:
1,由设计失误造成:即将太多的功能包含到一个接口中。
- 带来的问题为:实现此接口的类同时违反了单一职责原则(single responsibility principle:一个类应该只做一件事或者只做一组相关的事)。多数情况下接口隔离原则和单一职责原则本身就是相关的,只是接口隔离原则是站在服务调用者的角度上看接口类型,而单一职责原则是站在服务提供者的角度看的。
- 解决方案为:将胖接口拆分为多个单一功能的小接口,即把本质不同的功能隔离开。
- 示例:背景为一个女driver开车上班被追尾,男生安慰她以后开坦克Tank上班就可以了。如下: ```csharp using System;
namespace IspExample { class Program { static void Main(string[] args) { var driver = new Driver(new Car()); driver.Drive(); } }
class Driver
{
private IVehicle _vehicle;
public Driver(IVehicle vehicle)
{
_vehicle = vehicle;
}
public void Drive()
{
_vehicle.Run();
}
}
interface IVehicle
{
void Run();
}
class Car : IVehicle
{
public void Run()
{
Console.WriteLine("Car is running...");
}
}
class Truck : IVehicle
{
public void Run()
{
Console.WriteLine("Truck is running...");
}
}
interface ITank
{
void Fire();
void Run();
}
class LightTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!");
}
public void Run()
{
Console.WriteLine("Ka Ka Ka...");
}
}
class MediumTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!!");
}
public void Run()
{
Console.WriteLine("Ka! Ka! Ka!...");
}
}
class HeavyTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!!!");
}
public void Run()
{
Console.WriteLine("Ka!! Ka!! Ka!!...");
}
}
}
女driver要开Tank了,方法是将Driver类中的IVehicle类型字段改成ITank类型字段。并在Main()中使用。如下:
```csharp
class Program
{
static void Main(string[] args)
{
var driver = new Driver(new MediumTank());
driver.Drive();
}
}
class Driver
{
private ITank _tank;
public Driver(ITank tank)
{
_tank = tank;
}
public void Drive()
{
_tank.Run();
}
}
Driver类中已将胖接口传入进来,此时产生了胖接口,因为女driver只会使用Tank的Run()功能,永远不会去用Tank的Fire()功能。
因上例违反了接口隔离原则,应将胖接口ITank分裂成接口IWeapon和接口IVehicle,即将ITank接口中的2个方法分别隔离到2个接口中。如下:
using System;
namespace IspExample
{
class Program
{
static void Main(string[] args)
{
var driver = new Driver(new LightTank());
driver.Drive();
}
}
class Driver
{
private IVehicle _vehicle;
public Driver(IVehicle vehicle)
{
_vehicle = vehicle;
}
public void Drive()
{
_vehicle.Run();
}
}
interface IVehicle
{
void Run();
}
class Car : IVehicle
{
public void Run()
{
Console.WriteLine("Car is running...");
}
}
class Truck : IVehicle
{
public void Run()
{
Console.WriteLine("Truck is running...");
}
}
interface IWeapon
{
void Fire();
}
interface ITank:IVehicle,IWeapon
{
}
class LightTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!");
}
public void Run()
{
Console.WriteLine("Ka Ka Ka...");
}
}
class MediumTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!!");
}
public void Run()
{
Console.WriteLine("Ka! Ka! Ka!...");
}
}
class HeavyTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!!!");
}
public void Run()
{
Console.WriteLine("Ka!! Ka!! Ka!!...");
}
}
}
应注意:在使用接口隔离原则和单一职责原则时,不应使用过度,否则会产生很多很细碎的、里面只有一个方法的接口和类,导致接口和类的颗粒度太小,使用此两原则时一定要把握一个度,把接口和类的大小控制在一定范围内。
2,传给调用者的胖接口本身是由两个设计合理的小接口合并而来的。会导致有可能把原本合格的服务提供者拒之门外。
- 示例:写一个函数,用于计算一组整数之和。注:ICollection实现了IEnumerable接口。
```csharp using System; using System.Collections;
namespace IspExample2 { class Program { static void Main(string[] args) { int[] nums1 = { 1,2,3,4,5}; ArrayList nums2 = new ArrayList { 1,2,3,4,5}; var nums3 = new ReadOnlyCollection(nums1); Console.WriteLine(Sum(nums1)); Console.WriteLine(Sum(nums2));
foreach (var n in nums3)
{
Console.WriteLine(n);
}
}
static int Sum(ICollection nums)
{
int sum = 0;
foreach (var n in nums)
{
sum += (int)n;
}
return sum;
}
}
class ReadOnlyCollection : IEnumerable
{
private int[] _array;
public ReadOnlyCollection(int[] array)
{
_array = array;
}
public IEnumerator GetEnumerator()
{
return new Enumerator(this);
}
public class Enumerator : IEnumerator
{
private ReadOnlyCollection _collection;
private int _head;
public Enumerator(ReadOnlyCollection collection)
{
_collection = collection;
_head = -1;
}
public object Current
{
get
{
object o = _collection._array[_head];
return o;
}
}
public bool MoveNext()
{
if (++_head<_collection._array.Length)
{
return true;
}
else
{
return false;
}
}
public void Reset()
{
_head = -1;
}
}
}
}
Sum函数无法处理nums3,因为传进去的接口太胖,虽然只用得着迭代,但是传进去的参数类型为ICollection,这样把一些合格的服务提供者给挡在外面了,此时将Sum方法的形参类型改成IEnumerable即可,因为在服务使用者里面只用得着迭代,就更加符合接口隔离原则,即"调用者不会多要"。
```csharp
class Program
{
static void Main(string[] args)
{
int[] nums1 = { 1,2,3,4,5};
ArrayList nums2 = new ArrayList { 1,2,3,4,5};
var nums3 = new ReadOnlyCollection(nums1);
Console.WriteLine(Sum(nums1));
Console.WriteLine(Sum(nums2));
Console.WriteLine(Sum(nums3));
}
static int Sum(IEnumerable nums)
{
int sum = 0;
foreach (var n in nums)
{
sum += (int)n;
}
return sum;
}
}
显示接口实现
C#语言特有的功能,能把隔离出来的接口隐藏起来,直到显示地使用接口类型的变量去引用一个实现了此接口的具体类的实例,此接口中隐藏的方法才能被使用。
示例:有一个杀手,具有Love和Kill两面性。将其隔离成IGentleman和IKiller两个接口,并用WarmKiller类都普通地实现这两个接口。
using System;
namespace IspExample3
{
class Program
{
static void Main(string[] args)
{
var wk = new WarmKiller();
wk.Kill();
wk.Love();
}
}
interface IGentleman
{
void Love();
}
interface IKiller
{
void Kill();
}
class WarmKiller : IGentleman,IKiller
{
public void Kill()
{
Console.WriteLine("Let me kill the enemy...");
}
public void Love()
{
Console.WriteLine("I will love you forever...");
}
}
}
此时wk.时既能看到Kill方法,也能看到Love方法。在代码上看是没问题,但是在设计上是有问题的,因为一个杀手不能轻易地暴露自己的Kill方法。
如果一个接口中的方法,不需要轻易地被人调用,就应该将接口显示地实现。如下:将WarmKiller显示实现IKiller所有成员。
using System;
namespace IspExample3
{
class Program
{
static void Main(string[] args)
{
IKiller killer = new WarmKiller();
killer.Kill();
var wk = (WarmKiller)killer;
wk.Love();
var wk2 = new WarmKiller();
wk2.Love();
}
}
interface IGentleman
{
void Love();
}
interface IKiller
{
void Kill();
}
class WarmKiller : IGentleman,IKiller
{
public void Love()
{
Console.WriteLine("I will love you forever...");
}
void IKiller.Kill()
{
Console.WriteLine("Let me kill the enemy...");
}
}
}
这时wk2只能看到Love()方法,killer只能看到Kill()方法,若killer要使用Love()方法,则必须进行强制类型转换。
反射Reflection
反射不是C#语言的功能,是.net框架的功能,因此vb.net,F#等也能使用反射机能。
基本定义
一个对象,能在不使用new操作符,也不知道此对象具体是何种静态类型的情况下,创建出一个同类型的对象,
还能访问原对象的所有成员。
反射会使得耦合更松,甚至耦合弱到忽略不计。
反射在.net,java开发体系中是非常重要的,反射是C#和Java这些托管类型的语言与C和C++原生类型的语言最大的区别之一,单元测试,依赖注入,泛型编程都是基于反射机制。.net和C#在设计上十分精妙,一般不会直接接触到反射的底层,大部分是使用封装好的反射。
反射的用途
很多时候,程序的逻辑并不能够在写程序的时候就能确定的,有时是需要在用户和程序进行交互的时候才能确定,此时程序是处在运行状态的,是动态状态的。如果想让程序员在静态状态下,即程序还在编写的情况下,去预测和枚举用户有可能做的操作的话,程序就可能变得十分臃肿,有可能写出成百上千的if else语句,那么写出的程序就一定十分难维护,可读性就十分地低,更重要的是很多时候在编写程序时根本无法完全枚举用户做何种操作。这时程序就需要一种以不变应万变的能力,此能力就是反射。
- .net平台有.net framework(运行在windows下)和.net core(跨平台)两个大的版本,他们都具有反射机制,但是他们的类库在调用时不太一样,学会一种,另一种api查手册就可以了,原理是一致的。
- 反射是动态的,在内存中去拿对象与他类型绑定的描述,再用这些描述去创建新的对象,这些过程会影响程序的性能,因此不要程序中盲目地、过多地使用反射机制,以免影响程序运行的性能。
示例:用的是第一个例子,来表达反射基本原理,大部分情况不会直接这样用,而是用封装好的反射。
using System;
using System.Reflection;
namespace IspExample
{
class Program
{
static void Main(string[] args)
{
ITank tank = new HeavyTank();
//=============华丽的分割线============
//分割线以下不再用静态类型,即new HeavyTank和ITank
var t = tank.GetType();
//tank.GetType()得到的是这个对象在内存中在运行时与他关联的动态类型的一些描述信息,而不是静态类型
object o = Activator.CreateInstance(t);
MethodInfo fireMi = t.GetMethod("Fire");
//此处Fire,必须和ITank的Fire()大小写一致,后续可用SDK中的API来约束,防止第三方写错
MethodInfo runMi = t.GetMethod("Run");
fireMi.Invoke(o, null);
runMi.Invoke(o,null);
}
}
...
}
依赖注入
封装好的反射其中最重要的一个功能就是依赖注入DI(Dependency Injection),与依赖反转DIP不一样,不要搞混淆。但没有DIP就没有DI,依赖反转是一个概念,依赖注入是在此概念的基础上结合接口和反射机制所形成的应用。
依赖注入要借助于依赖注入框架,在解决方案→管理NuGet程序包→搜索Dependency Injection,安装框架Microsoft.Extensions.DependencyInjection。
依赖注入中有一个最重要元素叫Container容器,把各种各样的类型和对应的接口放入容器ServiceCollection中,后续要实例时,就向容器中要。
在注册类型时还可以告知容器,创建对象时是每次都创建新对象还是只创建一个单例模式(每次要都是同一个实例)。
using System;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;//引用名称空间
namespace IspExample
{
class Program
{
static void Main(string[] args)
{
var sc = new ServiceCollection();//ServiceCollection为容器
sc.AddScoped(typeof(ITank),typeof(HeavyTank));
//往容器装东西,不能将ITank作为参数,因为ITank是静态类型,而typeof(ITank)是动态类型,才能拿到动态类型的描述。
var sp = sc.BuildServiceProvider();
//=====================华丽的分割线=====================
//分割线以上是一次的注册,可以在程序启动时注册
//分割线以下代表在程序的其他可以看到ServiceProvider的各个地方都可以这么用,不在有new操作符,从Container中要对象
ITank tank = sp.GetService<ITank>();
//好处是,在程序的很多地方都用到了ITank所引用的实例,如果有一天程序升级,ITank接口对应的实现类不再是HeavyTank,而是MediumTank,只需改sc.AddScoped(typeof(ITank),typeof(MediumTank))即可。
tank.Fire();
tank.Run();
ITank tank2 = sp.GetService<ITank>();
tank2.Run();
}
}
...
}
以上是依赖注入的最基本用法,以下是依赖注入更厉害的用法。
using System;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;//引用名称空间
namespace IspExample
{
class Program
{
static void Main(string[] args)
{
var sc = new ServiceCollection();//ServiceCollection为容器
sc.AddScoped(typeof(ITank),typeof(HeavyTank));
sc.AddScoped(typeof(IVehicle), typeof(Car));
sc.AddScoped<Driver>();
//往容器装东西,不能将ITank作为参数,因为ITank是静态类型,而typeof(ITank)是动态类型,才能拿到动态类型的描述。
var sp = sc.BuildServiceProvider();
//=====================华丽的分割线=====================
//分割线以上是一次的注册,可以在程序启动时注册
//分割线以下代表在程序的其他可以看到ServiceProvider的各个地方都可以这么用,不在有new操作符,从Container中要对象
var driver = sp.GetService<Driver>();
//不需要像之前var driver = new Driver(new Car())创建Driver实例,会自动在容器中找。注入体现在用注册的类型sc.AddScoped()创建的实例,注入到Driver构造器中,
driver.Drive();
}
}
}
插件式编程
以不变应万变的能力,使用反射追求更松的耦合,具体表现就是插件式编程。
插件式就是不与主体程序一起编译,但是却可以和主体程序一起工作的组件,往往由第三方提供。
好处是可以以主体程序为中心,生产一个生态圈,有人不断更新主体程序,有人不断用插件添加新功能。比如office,visual studio。
主体程序一般都会发布包含有程序开发接口(API-Application Programming Interface)的程序开发包(SDK-Software Development Kit)。使用SDK中的API,第三方在开发插件就比较容易,开发出的插件能比较标准,高效地和主体程序对接。API里面不一定都是接口,也有可能是一组函数,或一组类。
示例:有一个婴儿车的生产商,婴儿车上面板上有一些按钮,第一排是小动物头像,第二排是数字,小孩按下动物头像,点下数字,婴儿车就会发出对应次数动物的声音。婴儿车出厂前会默认提供一两种动物,并提供了一个USB口,用来扩展第三方开发的其他动物声音,其他厂商开发出的动物声音通过U盘,插入婴儿车的USB口,这样小朋友就可以了解更过的动物。
纯反射方法
首先,创建主体程序,创建可执行程序Console App(.NET Core),解决方案名称为BabyStroller,项目名称为BabyStroller.App。
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Loader;
namespace BabyStroller.App
{
class Program
{
static void Main(string[] args)
{
var folder = Path.Combine(Environment.CurrentDirectory,"Animals");
// 将两个路径合成一个路径,自动处理路径分隔符的问题,folder为Animals文件夹所在路径
var files = Directory.GetFiles(folder);//返回folder目录中文件的名称
var animalTypes = new List<Type>();
foreach (var file in files)
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
//加载指定路径上的程序集文件的内容
var types = assembly.GetTypes();
foreach (var t in types)
{
if (t.GetMethod("Voice")!=null)
{
animalTypes.Add(t);
}
}
}
while (true)
{
for (int i = 0; i < animalTypes.Count; i++)
{
Console.WriteLine($"{i+1}.{animalTypes[i].Name}");
}
Console.WriteLine("======================");
Console.WriteLine("Please choose animal:");
int index = int.Parse(Console.ReadLine());
if (index>animalTypes.Count || index<1)
{
Console.WriteLine("No such an animal.Try again!");
Console.WriteLine();
continue;
}
Console.WriteLine("How many times?");
int times = int.Parse(Console.ReadLine());
var t = animalTypes[index - 1];
var m = t.GetMethod("Voice");
var o = Activator.CreateInstance(t);
m.Invoke(o, new object[] { times });
Console.WriteLine();
}
}
}
}
然后,第三方开发出开发小动物插件程序,创建Class Library(.NET Core),解决方案名称为Animals,项目名称为Animals.Lib,并在Animals.Lib类库中添加Cat类和Sheep类。
using System;
namespace Animals.Lib
{
public class Cat
{
public void Voice(int times)
{
for (int i = 0; i < times; i++)
{
Console.WriteLine("Meow!");
}
}
}
}
using System;
namespace Animals.Lib
{
public class Sheep
{
public void Voice(int times)
{
for (int i = 0; i < times; i++)
{
Console.WriteLine("Baa!");
}
}
}
}
在Animals解决方案中继续添加Animals.Lib2类库,并在Animals.Lib2类库中添加Cow类和Dog类。
using System;
namespace Animals.Lib2
{
public class Cow
{
public void Voice(int times)
{
for (int i = 0; i < times; i++)
{
Console.WriteLine("Moo!");
}
}
}
}
using System;
namespace Animals.Lib2
{
public class Dog
{
public void Voice(int times)
{
for (int i = 0; i < times; i++)
{
Console.WriteLine("Woof!");
}
}
}
}
把编译后生成的Animals.Lib.dll和Animals.Lib2.dll两个文件放入BabyStroller文件夹下的Animals文件夹中,并在主体程序下运行程序。如下:<br /><br />
使用SDK方法
运用纯反射方法,很容易犯错,比如插件开发商,开发时不小心把Voice()写错成小写voice(),这样主体程序就会把错写的类过滤掉。为了帮助第三方的插件开发商避免不必要的错误,减轻开发成本,一般第一方会发布一个SDK,让第三方快速开发。
将以上代码进行改写,在主体程序BabyStroller解决方案中,添加BabyStroller.SDK类库项目,并在BabyStroller.SDK类库中添加IAnimal接口。所有第三方的插件开发商都要实现IAnimal接口,保证里面都有Voice()方法
using System;
namespace BabyStroller.SDK
{
public interface IAnimal
{
void Voice(int times);
}
}
继续在BabyStroller.SDK类库中UnfinishedAttribute特征类。特征主要考虑到开发商有时候开发出来的某个动物没有开发完,但是也在类库中,只要在没有开发完的类中添加[Unfinished]特征即可,这样主程序判断类有[Unfinished]特征后就不会Load这个类。
using System;
namespace BabyStroller.SDK
{
public class UnfinishedAttribute:Attribute
{
}
}
SDK就可以发布了,把编译后生成的BabyStroller.SDK.dll和配置说明书放到指定的网盘中,供第三方使用。
主体程序添加项目引用BabyStroller.SDK
第三方Animals.Lib和Animals.Lib2也都添加项目引用BabyStroller.SDK
然后改写第三方的所有类,对每个都实现IAnimal接口。代码如下:
using BabyStroller.SDK;
using System;
namespace Animals.Lib
{
public class Cat : IAnimal
{
public void Voice(int times)
{
for (int i = 0; i < times; i++)
{
Console.WriteLine("Meow!");
}
}
}
}
如果Cow类没有开发完,不想让他出现在面板上,那么使用[Unfinished]标注一下即可。
using BabyStroller.SDK;
using System;
using System.Collections.Generic;
using System.Text;
namespace Animals.Lib2
{
[Unfinished]
public class Cow: IAnimal
{
public void Voice(int times)
{
for (int i = 0; i < times; i++)
{
Console.WriteLine("Moo!");
}
}
}
}
将新编译后生成的Animals.Lib.dll和Animals.Lib2.dll两个类库文件,重新放入BabyStroller文件夹下的Animals文件夹中。
最后改写主体程序,如下:
using BabyStroller.SDK;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
namespace BabyStroller.App
{
class Program
{
static void Main(string[] args)
{
var folder = Path.Combine(Environment.CurrentDirectory,"Animals");
// 将两个路径合成一个路径,自动处理路径分隔符的问题,folder为Animals文件夹所在路径
var files = Directory.GetFiles(folder);//返回folder目录中文件的名称
var animalTypes = new List<Type>();
foreach (var file in files)
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
//加载指定路径上的程序集文件的内容
var types = assembly.GetTypes();
foreach (var t in types)
{
if (t.GetInterfaces().Contains(typeof(IAnimal)))
{
var isUnfinished = t.GetCustomAttributes().Any(a => a.GetType() == typeof(UnfinishedAttribute));
if (isUnfinished)
{
continue;
}
animalTypes.Add(t);
}
}
}
while (true)
{
for (int i = 0; i < animalTypes.Count; i++)
{
Console.WriteLine($"{i+1}.{animalTypes[i].Name}");
}
Console.WriteLine("======================");
Console.WriteLine("Please choose animal:");
int index = int.Parse(Console.ReadLine());
if (index>animalTypes.Count || index<1)
{
Console.WriteLine("No such an animal.Try again!");
Console.WriteLine();
continue;
}
Console.WriteLine("How many times?");
int times = int.Parse(Console.ReadLine());
var t = animalTypes[index - 1];
var o = Activator.CreateInstance(t);
var a = o as IAnimal;
a.Voice(times);
Console.WriteLine();
}
}
}
}
此时第一方程序和第三方插件通过SDK组合在一起了。运行结果如下,被[Unfinished]标注的Cow类被过滤掉了。