接口隔离
协议:甲方,我不会多要;乙方,我不会少给。
如何判断:就看,接口中是否有没有被调用到的函数成员。
接口隔离原则:接口调用者,不能多要。如果多要了,那么实现这个接口的类,其实也违反了单一职责原理。
单一职责原理:一个类只做一件事,或者只做一组相关的事。
接口隔离原则是站在服务调用者的角度看待问题,单一职责问题是站在服务提供者的角度来分析。
所以说接口隔离原则和单一职责原理是同一个问题的两种描述。
如果出现了胖接口问题,那么我们就需要将胖接口拆分成多个小接口,每个小接口都是一个单一的功能。把本质不同的功能隔离开,这就是接口隔离原则的名称的由来。
如何实现?
通过一个接口对多个接口的继承来实现。将胖接口(一个接口由多个可拆分的功能函数成员组成)拆分成几个小接口。将多个函数成员,再分开放在多个接口中,细分。
类和类之间的继承,只能有一个基类,但是接口和接口之间的继承,可以多选。
但是在拆分时要把握一个度,一个平衡,过分拆解会使接口数目过多。最好是根据需要来分割。
接口隔离的例子
添加名称空间:
using System.Collections;
查看Array和ArrayList的定义。
两者都实现了ICollection和IEnumerable两个接口。
查看Icollection接口定义;
发现它比IEnumerable除了可以被迭代以外,还多了几个功能。Count知道里面有多少个元素,CopyTo将里面的元素复制到一个数组里面去。
引用之前的例子,两个数组都想求和。当时选用的是两者抽象出的接口IEnumerable。如果改成ICollection。如何。结果与原来的一致。
static void Main(string[] args)
{
int[] nums1 = new[] { 1, 2, 3, 4, 5, 6, 7 };
ArrayList nums2 = new ArrayList { 1, 2, 3, 4, 5, 6, 7 };
Console.WriteLine(Sum(nums1));
Console.WriteLine(Sum(nums2));
}
static int Sum(ICollection nums)
{
int sum = 0;
foreach (var n in nums)
{
sum += (int)n;
}
return sum;
}
对于我们的来说,只要IENumerable,就够了,我们并不需要ICollection提供的那么多功能。
那么问题:有没有那么一种集合:只实现了IEnumerable接口,不需要ICollection接口?
C#中没有,但是我们可以自己定义一个。
class ReadOnlyCollection : IEnumerable
{
// 继承接口,当外界迭代实例的时候,需要返回一个IEnumerator迭代器。
public IEnumerator GetEnumerator()
{
throw new NotImplementedException();
}
}
我们的类继承了IEnumerable,需要实现接口,那么当迭代实例的时候,需要返回一个IEnumerator类型的迭代器。
如果我们在外面声明一个IEnumerator迭代器类的话,容易污染我们整个命名空间。为此我们在类内声明一个IEnumerator迭代器类。
成员类:类是一个成员。既然自己实现一个迭代器,那么继承原有迭代器基础上,需要重写修改原来的迭代代码。
class ReadOnlyCollection : IEnumerable
{
// 继承接口,当外界迭代实例的时候,需要返回一个IEnumerator迭代器。
public IEnumerator GetEnumerator()
{
throw new NotImplementedException();
}
public class Enumerator : IEnumerator
{
public object Current => throw new NotImplementedException();
public bool MoveNext()
{
throw new NotImplementedException();
}
public void Reset()
{
throw new NotImplementedException();
}
}
}
在VS点冒号后事先的接口。通过VS智能助手可以生成类内类的代码如上。
using System;
using System.Collections;
namespace DataReader
{
class Program
{
static void Main(string[] args)
{
int[] nums1 = { 1, 2, 3, 4, 5, 6 };
var roc = new MyReadOnlyCollection(nums1);
foreach (var n in roc)
{
Console.WriteLine(n);
}
}
//static int Sum(ICollection nums)
//{
// int sum = 0;
// foreach (var n in nums)
// {
// sum += (int)n;
// }
// return sum;
//}
}
class MyReadOnlyCollection : IEnumerable
{
// 为了实现collection,我们新建整型数组来接收。
private int[] _array;
// 构造器
public MyReadOnlyCollection(int[] array)
{
_array = array;
}
// 继承接口,当外界迭代实例的时候,需要返回一个IEnumerator迭代器。
public IEnumerator GetEnumerator()
{
//当需要迭代时创建自己迭代器,实例化一个IEnumerator迭代器
//如果在外面声明一个迭代器的话,会污染这个命名空间。
//为此在类内声明这样一个类。成员类。
return new Enumerator(this);
}
public class Enumerator : IEnumerator
{
private readonly MyReadOnlyCollection _collection;
private int _head;
public Enumerator(MyReadOnlyCollection 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;
}
}
}
}
这样我们就获得了一个只读,只能迭代,不能删除元素,不能添加元素的集合。
此时再回到接口这部分,此时将roc传入到Sum中,是无法执行的。
因为我们的接口太胖了。ICollection东西太多了。传入的数组,没法提供接口要求的这么多东西。因此如上无法转换。如何修改,将ICollection,改为INEnumeratable。因为我们的求和,其实只用了foreach迭代,其他的用不到。
以上费劲的例子:证明了一个问题:接口隔离原则:调用者绝不多调,传入的接口不应该有用不到的功能。过多的限制反而导致主要目的无法达到。
接口显示实现
C#独有的。
《这个杀手不太冷》既是绅士又是杀手。
两个接口一个killer,一个gentleman。两个接口的方法。通过新类WarmKiller来实现。
class Program
{
static void Main(string[] args)
{
var wk = new WarmKiller();
wk.Love();
wk.kill();
}
}
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 for ever...");
}
}
此时的两个接口方法都可以被看到,但是我们不希望如此,希望杀手身份不被暴露。
在实现接口时:VS有两种方式来实现:如下图:一种是直接实现,一种是显示实现所有成员。
class Program
{
static void Main(string[] args)
{
var wk = new WarmKiller();
wk.Love();
wk.
}
}
interface IGentleman
{
void Love();
}
interface IKiller
{
void kill();
}
class WarmKiller : IGentleman, IKiller
{
public void Love()
{
Console.WriteLine("I will love you for ever...");
}
void IKiller.kill()
{
Console.WriteLine("Let me kill the enemy...");
}
}
如此显示的实现:此时在主函数中就不能直接看到杀手身份了。wk之后只有一个Love,kill无法被查看到。
如果此时接到任务,如何做?就需要我们显示的调用。但是此时无法看到Love方法。
var wk = new WarmKiller();
IKiller killer = wk;
killer.kill();
还可以如此:
IKiller killer = new WarmKiller();
killer.kill();
如果想要调用Love。
IKiller killer = new WarmKiller();
killer.kill();
WarmKiller wk = killer as WarmKiller;
wk.Love();
甚至:直接强制转换。
IKiller killer = new WarmKiller();
killer.kill();
WarmKiller wk = (WarmKiller)killer;
wk.Love();
反射机制
给定一个对象,在不用new操作符,也未知这个对象是什么静态类型的情况下:能够创建出一个同类型的对象。
- 不仅不用new操作符创建出这个对象。
- 还能访问这个对象所带有的各个成员。
- 这就相当于进一步解耦合。因为有new操作符的地方,后面一定要跟类型。一旦跟着类型,就形成了依赖,紧耦合。
- 反射不用new操作符,不用静态类型,此时的耦合可以忽略不计。
- 反射并非Csharp专有,在DotNet开发体系和Java开发体系中都非常重要。
- CSharp和Java都是托管类型的语言。C和C++都是原生类型的语言。两者最大的区别中:反射肯定算得上一个。
反射的第一个用途:与反射相关的技能:依赖注入Dependency Injection(DI)
DotNet core代表了Dotnet的未来。
反射直接应用案例:
ITank tank = new HeavyTank();
var t = tank.GetType();
object o = Activator.CreateInstance(t); // 激活器创建对象。
MethodInfo fireMi = t.GetMethod("Fire");
MethodInfo runMi = t.GetMethod("Run");
fireMi.Invoke(o, null);
runMi.Invoke(o, null);
通过GetType直接从内存中获取与对象关联的动态的类型的描述信息。激活器创建实例对象,依据前面获得的类型。
通过MethodInfo来給实例添加成员方法。然后通过Invoke来调用方法,此时需要传入方法需要的参数,但是本例中函数参数为空,故传入一个null。
依赖反转原则:DI:Dependency Inversion。DIP依赖反转原则 principle。
依赖注入:DI:Dependency Injection。
依赖反转是一个概念,依赖注入是在依赖反转概念基础之上,结合我们的接口、反射机制所形成的一种应用。
点击安装:封装好的依赖注入。
然后引用名称空间
using Microsoft.Extensions.DependencyInjection;
依赖注入最重要的东西:容器:Container。
// 接口的实现者就是服务的提供者。
var sc =new ServiceCollection(); // 本质就是一个容器。
// 往容器中装东西。第一个参数:接口是什么?第二个参数:哪个类实现的这个接口。
// ITank是一个静态类型,Typeof一下ITank就可以获得它的动态类型描述。
sc.AddScoped(typeof(ITank), typeof(HeavyTank));
// 一对类型放入了容器。
var sp = sc.BuildServiceProvider();
//此处以上是一次性的注册。注册以后不再有new操作符。我们从container中直接要对象。
ITank tank = sp.GetService<ITank>();
tank.Fire();
tank.Run();
这样做的好处是什么?
避免了使用new操作符来创建新实例,如果以往采用new操作符,一旦new后的类型修改了,那么所有的new出来的实例都要修改。
// 接口的实现者就是服务的提供者。
var sc =new ServiceCollection(); // 本质就是一个容器。
// 往容器中装东西。第一个参数:接口是什么?第二个参数:哪个类实现的这个接口。
// ITank是一个静态类型,Typeof一下ITank就可以获得它的动态类型描述。
sc.AddScoped(typeof(ITank), typeof(HeavyTank)); // 注入:用我们注册的类型创建我们需要的实例。注入到它的构造器中。
sc.AddScoped(typeof(IVehicle), typeof(Car)); //此处就是注入。
sc.AddScoped<Driver>();
// 一对类型放入了容器。
var sp = sc.BuildServiceProvider();
//此处以上是一次性的注册。注册以后不再有new操作符。我们从container中直接要对象。
var driver = sp.GetService<Driver>();
driver.Drive();
注入:用我们注册的类型创建我们需要的实例,注入到它的构造器中。注入到Driver构造器中。
反射的第二个功能:追求更松的耦合。太难了,战略回避。
更松的耦合,一般用于插件式编程。
插件:不与主体程序一起编译,但是却可以跟主体程序一起工作的组件,往往由第三方来提供。
插件的好处是:以主体程序为中心,生成一个生态圈。
主体程序会发布包含有程序开发接口API(Application Programming Interface,应用程序开发接口)这样的程序开发包:SDK(Software Development Kit)。使用SDK中的API,第三方在开发插件的时候就比较容易,开发出来的插件也就比较标准、高效的跟主体程序对接。
API不一定都是接口。也有可能是一组函数,也有可能是一组类,也有可能是一组接口。
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
namespace BabyStroller
{
class Program
{
static void Main(string[] args)
{
// 输出当前文件夹路径。
Console.WriteLine(Environment.CurrentDirectory);
// 在路径下面我们创建一个文件:名字叫Animals
// 引用名称空间:IO。得到我们新建的文件夹。
var folder = Path.Combine(Environment.CurrentDirectory, "Animals");
var files = Directory.GetFiles(folder); // 虽然为空但是可以为我们新建了一个数组类型。
var animalITypes=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)
{
animalITypes.Add(t);
}
}
}
while (true)
{
for (int i = 0; i < animalITypes.Count; i++)
{
Console.WriteLine($"{i + 1}.{animalITypes[i].Name}");
}
Console.WriteLine("=============================");
Console.WriteLine("Please choose animal:");
int index = int.Parse(Console.ReadLine());
if (index > animalITypes.Count || index < 1)
{
Console.WriteLine("No Such an animal.Try again!");
continue;
}
Console.WriteLine("How many times?");
int times = int.Parse(Console.ReadLine());
var t = animalITypes[index - 1];
var m = t.GetMethod("Voice");
var o = Activator.CreateInstance(t);
m.Invoke(o, new object[] {times});
}
}
}
//主程序如此,剩余的事插件来完成。
}