C#学习笔记(二十四)接口隔离、反射、特性、依赖注入

接口隔离

协议:甲方,我不会多要;乙方,我不会少给。

如何判断:就看,接口中是否有没有被调用到的函数成员。

接口隔离原则:接口调用者,不能多要。如果多要了,那么实现这个接口的类,其实也违反了单一职责原理。

单一职责原理:一个类只做一件事,或者只做一组相关的事。

接口隔离原则是站在服务调用者的角度看待问题,单一职责问题是站在服务提供者的角度来分析。

所以说接口隔离原则单一职责原理是同一个问题的两种描述。

如果出现了胖接口问题,那么我们就需要将胖接口拆分成多个小接口,每个小接口都是一个单一的功能。把本质不同的功能隔离开,这就是接口隔离原则的名称的由来。

如何实现?

通过一个接口对多个接口的继承来实现。将胖接口(一个接口由多个可拆分的功能函数成员组成)拆分成几个小接口。将多个函数成员,再分开放在多个接口中,细分。

类和类之间的继承,只能有一个基类,但是接口和接口之间的继承,可以多选。

但是在拆分时要把握一个度,一个平衡,过分拆解会使接口数目过多。最好是根据需要来分割。


接口隔离的例子

添加名称空间:

using System.Collections;

查看Array和ArrayList的定义。

两者都实现了ICollectionIEnumerable两个接口。

查看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});
            }
        }
    }
    //主程序如此,剩余的事插件来完成。
}

使用 .NET Core 3.0 的 AssemblyLoadContext 实现插件热加载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值