接口隔离原则、反射、特性、依赖注入

接口隔离原则
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
五种原则当中的
i
上一章中的接口,即契约。
契约就是在说两件事,甲方说自己不会多要,乙方会在契约里说自己不会少给。
乙方的不会少给是比较容易做到的,作为服务的提供者,实现一个接口的时候,必须要实现接口里面的所有的方法,如果没有实现所有的方法,那么就会留下抽象方法,自己就变成了一个抽象类,那么仍然不能够实例化,不是一个完整的服务提供者,不是一个具体类。
实现了一个接口的完整服务提供者,接口里面的方法是一定必须实现的。乙方不能少给,这是强制性的,硬性的规定。如果有方法没有实现完全,编译器会检查到报错。
但是甲方不会多要,却是一个软性的规定。所以说这是一个设计方面的问题,需要使用设计原则去约束。 编译器没法去检查,甲方是否多要了。
怎么才能知道,甲方有没有多要呢?
就看传给这个调用者的接口类型里,有没有一直没被调用到的方法成员。如果有,那就说明,传进来的接口类型太大了,太胖了。换句话来说,当一个接口太胖了,那么这个接口就是由两个或两个以上的本质不同的小接口合并起来的。所以当把大接口传进来之后,只有一部分被调用到了,而其中一部分就多余出来了。
这种大接口产生的原因的不同,违反接口隔离原则,所带来的不好的后果有两种。
第一种情况
胖接口设计的时候有问题,把太多的功能包含到这个接口里了。把这种含有太多功能的接口传给调用者,这样其中必然有一些功能调用不到。
这样子的话,实现这个接口的类,同时也违反了,单一职责原则。
单一职责原则和接口隔离原则,就是一个硬币的两面,实际上就是一回事。
只是接口隔离原则,是从服务的调用者的角度上来看。单一职责原则,是站在服务提供者的角度上来看这个接口。
针对这种情况,我们的解决方法就是将胖接口拆分。拆分小接口,每个小接口都描述单一的功能。将本质不同的功能隔离开来,然后再使用接口封装起来。
接口隔离原则:就是服务的调用者不会多要
c#
using System;
namespace IspExample {
/* 背景故事:一个女生开车把车撞了
* 她男朋友就哄她说,下次给她买个坦克开
*/
internal class Program {
static void Main(string[] args) {
/* 我们想要给driver传入的是交通工具
* 而不是想要传入能开炮的东西
* 所以这里是ITank接口设计的不合理
* 应该让ITank继承IVehicle
*/
Driver driver = new Driver(new Car());
driver.Drive();
Console.ReadKey();
}
}
/* 此时我们想让Driver类
* 也能开Tank,无论怎么改都要改动Driver类
* 假如直接将IVehicle接口改为ITanke接口
* 那么的确可以使用这个接口,开坦克了,但是这样
* 就传进来了一个胖接口,因为那个女生开坦克是当做车开的
* Fire方法永远都不会被调用到了
* 那么这个设计就违反了接口隔离原则
* 解决办法就是,将这个胖接口,分成两个小接口
*/
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();
}
/* 继承多个接口
* 让ITank既继承自IVehicle又继承自IWeapon
*/
interface ITank : IVehicle, IWeapon{
}
//原本的ITank
//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
中的代码,改成这种那么就服务的提供者就没有了
Car
和Truck
这两个了
c#class Driver { private ITank _tank; public Driver(ITank tank) { _tank = tank; } public void Drive() { _tank.Run(); } }
c#
using System;
using System.Collections;
namespace IspExample2 {
/* 之前有个例子中
* 要求计算一组整数的和,
* 接口对服务调用者的约束就是,这组整数能够被迭代
* 也就是要求,组整数的类型实现了IEnumerable接口
*/
internal 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));
/* 此时,Sum这个方法是没办法处理nums3的
* 因为设置的传入接口太胖了
* 我们自己定义的集合只继承了IEnumerable接口
* 而没有继承ICollection接口
* 我们实际上,只需要有迭代器的数据类型就能够传入
* 所以不应该将传入的接口设置的这么胖
*/
}
//static int Sum(ICollection nums)
//原本设置的接口
//下面是更改后的接口
static int Sum(IEnumerable nums) {
int sum = 0;
foreach (int i in nums) {
sum += i;
}
return sum;
}
}
//有可能我们会设计出一个只实现了IEnumerable接口
//而没有实现ICollection接口的类
//自己写一个只读的集合
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;
/* 为什么要初始化为-1是有原因的
* 迭代器是先调用判断是否越界的方法的
* 而这个时候++_head,然后读取数组的值的时候,就正好是1了
*/
}
//只读属性
public object Current {
get {
/* 这个属性需要拿到传进集合的数组,
* 所以是必须传进来一个collection的
* 因为要求返回为Object,所以还不能直接返回整数类型,需要装箱
*/
Object o = _collection._array[_head];
//_head++;
return o;
}
}
public bool MoveNext() {
if (++_head < _collection._array.Length)
return true;
else return false;
}
public void Reset() {
_head = -1;
}
}
}
}
第三种情况
专门用来展示,显式接口实现
c#语言在接口隔离方面,做得比其他语言都要好,都要彻底。不但能做到接口隔离,甚至还能做到把隔离出来的接口隐藏起来。
直到显式的使用这种接口类型的变量,去引用一个实现了这个接口的具体类的实例的时候,这个接口内的方法才能被看见,才能被使用。
杀手不太冷的主角,一面是暖男,一面是冷酷无情的杀手
c#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IspExample3 {
/* 爱憎分明的杀手
* 一面是暖男
* 另一面是冷酷无情的
*/
internal class Program {
static void Main(string[] args) {
/* 从设计的角度来讲
* 杀手在大街上不应该被人随便就能认出来
* 即,Kill()方法,不应该能显示出来。
* 如果一个接口的方法,我们不想被人轻易地调用的话
* 就不应该被人轻易地看到
*/
IKiller killer = new WarmKiller();
killer.Kill();
//如果想要调用Love方法有以下几种方式:
var wk = (IGentleman)killer;
//var wk = (WarmKiller)killer;
//var wk = killer as WarmKiller;
wk.Love();
}
}
interface IGentleman {
void Love();
}
interface IKiller {
void Kill();
}
class WarmKiller : IGentleman, IKiller {
public void Love() {
Console.WriteLine("I will love you for ever...");
}
/* 这个方法只有
* IKiller类型引用的实例才能够调用这个方法
*/
void IKiller.Kill() {
throw new NotImplementedException();
}
}
}
反射
反射不是C#语言的功能,而是.Net框架的功能。
给一个对象,反射能在不适用
new
操作符,并且也不知道这个对象是什么静态类型的情况下,创建出同类型的对象。还能访问这个对象所带有的各个成员。
这就相当于进一步的解耦。因为在有
new
操作符的地方,一定会跟类型,一旦跟了类型,就有了依赖。而且这种依赖还是紧耦合的。现在创建对象可以不适用new
操作符,可以不出现静态类型,那么很多时候这个耦合甚至可弱到忽略不计。Java开发体系也有这个机制。
可以说,这是C#和Java这些托管类型语言与c/c++这些原生类型语言最大的区别之一。
之前学的单元测试,下面要学的依赖注入,还有以后要学的泛型编程,都是基于反射机制的。处于.Net和Java很底层的东西
这么底层的东西,原理一定是很深奥,很复杂。 但是呢,.Net和C#在设计方面十分精妙。一般情况下,我们在使用反射,但是却感觉不到反射的存在。也就是说,一般不会直接接触到反射,大部分情况下,都是使用一些已经封装好了的反射。
很多时候 程序的逻辑,不是我们在写程序的时候就能够确定的。有时候,这个逻辑是到了用户跟程序进行交互的时候,才能确定。这个时候程序已经处在运行状态了。
也就是已经处在动态期了,已经离开开发和编译环境了。如果我们在开发程序的时候,就枚举用户所有能够进行的操作,那么这个程序就会变得非常难维护。
或者我们不可能考虑到用户所能进行的所有情况。所以这个时候,程序需要一种 以不变应万变 的能力,这个能力就是反射。
接下来,用两个例子演示反射的神奇功能:
反射原理
.Net平台有两个大的版本:运行在windows上的
.Net FramWork
和可以扩平台的.NET Core
。两个平台都有反射机制,但是类库不太一样。现在使用的是.Net Core
程序,以后使用.Net FramWork
去做反射的时候,可以去查一下对应的API。注意,反射是动态的在内存中进行操作,不要过多的使用反射机制,不然会对程序的性能有所影响。
反射的原理
直接使用反射
c#
using System.Reflection;
namespace IspExample4 {
/* 背景故事:一个女生开车把车撞了
* 她男朋友就哄她说,下次给她买个坦克开
*/
internal class Program {
static void Main(string[] args) {
ITank tank = new HeavyTank();
//===========分割线===========
/* new 后面的HeavyTank就是静态类型
* GetType()方法可以获得静态类型的一些信息
* 比如这个类型包含哪些方法,哪些属性
*/
var t = tank.GetType();
/* Activator 是激活器
* CreateInstance创建t类型的实例,不知道具体的类型
* 所以创建的是object类型的
*/
object o = Activator.CreateInstance(t);
//接下来使用反射
//获得t类型的名叫Fire的方法
MethodInfo fireMi = t.GetMethod("Fire");
MethodInfo runMi = t.GetMethod("run");
/* public object? Invoke(object? obj, object?[]? parameters);
* 第二个参数指的是该方法的是否需要传入一些参数
* 即该方法的参数列表
*/
fireMi.Invoke(o, null);
runMi.Invoke(o, null);
//这里是直接用的反射,但是大部分情况下不会这么用
}
}
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();
}
/* 继承多个接口
* 让ITank既继承自IVehicle又继承自IWeapon
*/
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!!...");
}
}
}
依赖注入
(DI -- Dependency Injection)缩写是DI。
依赖反转原则的缩写也是DI,Dependency Inversion。
所以此DI非彼DI,但是如果没有依赖反转,也就没有依赖注入。
依赖反转是一个概念,依赖注入是在这个概念的基础之上,结合接口和反射机制,所形成的一种应用。
依赖注入最重要的就是有一个容器Container
,现在使用的容器是 Microsoft.Extensions.DependencyInjection: 这是.NET Core内置的依赖注入容器,提供了基本的DI功能1。也就是ServiceProvider
,把各种各样的类型和这些类型对应的接口放到容器里面。要实例的时候,就向容器要。注册类型的时候,还可以指定以后创建对象的时候,是每次创建一个新对象,还是创建一个单例模式,每次都传同一个实例。这个容器怎么使用,不在这里讲,这里主要是看DependencyInjection
怎么用。
这篇文章有助于理解依赖注入
[C#\]理解和入门依赖注入 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/592698341)
依赖注入需要借助依赖注入的框架

引入名称空间

```c#
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace IspExample4 {
internal class Program {
static void Main(string[] args) {
/* 反射有一种很重要的容器
* 叫做container
* 这种容器,就不在这里展开了
*/
//服务的提供者
var sc = new ServiceCollection();
/* typeof()方法用于获取类型的动态信息
* AddKeyedScoped()方法有很多重载方法,
* 我们现在使用的这个
* 第一个参数是基接口
* 第二个是谁实现了这个接口
* 假如我们没有使用反射,程序中new 了很多的HeavyTank对象
* 当有一天我们需要改成MediumTank的时候,就需要改很多个地方
* 而使用了反射,就只需要改成
* sc.AddKeyedScoped(typeof(ITank), typeof(MediumTank));
* 现在这是基础用法
*/
sc.AddScoped(typeof(ITank), typeof(HeavyTank));
var sp = sc.BuildServiceProvider();
//============分割线==============
ITank tank = sp.GetService