接口隔离

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

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

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

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

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

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

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

如何实现?

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

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

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

接口隔离的例子

第一种情况

胖接口设计的时候有问题,把太多的功能包含到这个接口里了。把这种含有太多功能的接口传给调用者,这样其中必然有一些功能调用不到。

这样子的话,实现这个接口的类,同时也违反了,单一职责原则。


单一职责原则和接口隔离原则,就是一个硬币的两面,实际上就是一回事。

只是接口隔离原则,是从服务的调用者的角度上来看。单一职责原则,是站在服务提供者的角度上来看这个接口。

针对这种情况,我们的解决方法就是将胖接口拆分。拆分小接口,每个小接口都描述单一的功能。将本质不同的功能隔离开来,然后再使用接口封装起来。

接口隔离原则:就是服务的调用者不会多要

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中的代码,改成这种

那么就服务的提供者就没有了CarTruck这两个了

class Driver {
     private ITank _tank;
     public Driver(ITank tank)
     {
         _tank = tank;
     }
     public void Drive() {
         _tank.Run();
     }
 }
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#语言在接口隔离方面,做得比其他语言都要好,都要彻底。不但能做到接口隔离,甚至还能做到把隔离出来的接口隐藏起来。

直到显式的使用这种接口类型的变量,去引用一个实现了这个接口的具体类的实例的时候,这个接口内的方法才能被看见,才能被使用。


杀手不太冷的主角,一面是暖男,一面是冷酷无情的杀手

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace lspExample2
{
    class Program
    {
        static void Main(string[] args)
        {
            var wk = new WarmKiller();
            IKiller killer = wk;
            killer.Kill();
            wk.Love();
        }

        interface IGentleman
        {
            void Love();
        }

        interface IKiller
        {
            void Kill();
        }

        class WarmKiller : IGentleman, IKiller
        {
            public void Love()
            {
                Console.WriteLine("I will love you");
            }

            void IKiller.Kill()
            {
                Console.WriteLine("I will kill you");
            }
        }
    }
}

反射

反射不是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。

注意,反射是动态的在内存中进行操作,不要过多的使用反射机制,不然会对程序的性能有所影响。

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怎么用。
依赖注入需要借助依赖注入的框架

引入名称空间

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<ITank>();
            tank.Fire();
            tank.Run();


            //更强大的功能:
            sc.AddScoped(typeof(IVehicle), typeof(MediumTank));
            sc.AddScoped<Driver>();
            var ss = sc.BuildServiceProvider();
            //===============分割线============
            var driver = ss.GetService<Driver>();
            driver.Drive();
            /* 当typeof()里面是Car的时候,就是调用的Car的Run方法
             * 当typeof()里面是MediumTank的时候,就是调用的MediumTank的Run方法
             */

        }
    }

    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!!...");
        }
    }
}
反射实现更松的耦合

这种更松的耦合一般使用在插件式编程中。好处是,以主体程序为中心,生成一个生态圈,在这个生态圈中,不断的更新主体程序,于是就有人不断用插件往上面添加新功能并且从中获利。微软的很多东西都是带有这种生态圈的。

插件,不与主体程序一起编译,但是与主体程序一起工作,往往由第三方提供。

主体程序和插件的关系,就是不变和万变的关系。

主体程序会发布包含有程序开发接口、API(Application Programming Interface)程序开发包,就是SDK(Software Development Kit)


反射开发会太自由了,发布API可以约束开发者开发第三方插件,同时也减轻开发者的一些劳动。

纯反射

这是指,不依赖于任何预先定义的接口或基类,而是完全通过反射来检查和操作程序集、类型、成员等。这种方式可以在运行时动态地创建对象、调用方法、访问字段和属性,甚至修改类型的行为

详解:C#--依赖注入

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐