C#学习(14)---接口、依赖反转、单元测试、接口隔离原则

从上节课来看,接口跟纯虚抽象类其实本质上很相似,可以说一模一样。所以接下来的过程中,请不要忘记,接口也是“类”,也可以声明变量,引用实例。

接口从现实意义的角度来看,像是一种“协议”“契约”,是建立在使用者和提供者之间的。对于使用者,它规定了,“我想要什么”“我能要什么”;对于提供者,它规定了“你可以给出什么”。由于接口是契约,故它必须对双方透明公开,对合同双方可见,即接口一定是public的。

本节课讲了接口的三层面/方面作用。下面逐一讲解。

第一个作用,接口发挥“契约”作用,减少重复代码:

using System.Collections;

namespace ConsoleAppPractice
{
    class Program
    {
        static void Main()
        {
            int[] nums1 = new int[] { 1, 2, 3, 4, 5 };
            ArrayList nums2 = new ArrayList() { 1, 2, 3, 4, 5 };
            Console.WriteLine(Calculator.Average(nums1));
            Console.WriteLine(Calculator.Average(nums2));
        }
    }
    class Calculator
    {
        public static double Average(IEnumerable vs)
        {
            double sum = 0;
            int count = 0;
            foreach (var item in vs)
            {
                sum += (int)item;
                count++;
            }
            if (count!=0)
            {
                return sum / count;
            }
            else
            {
                return double.NaN;
            }
        }
    }
}

本来要做不同类型的average函数,非常繁琐。但是如果引入了IEnumerable接口的话,就只用一个函数就行。

在这里,接口代表了协议:可以被迭代

ArrayList这个类就实现了这个接口。 

我在此处有些好奇这个IEnumerable内部具体逻辑是啥,怎么知道它能被迭代

这个是ArrayList里面的两个实现函数

 IEnumerable里面的抽象函数

 可以看到这玩意很抽象很基础,包含了一个可迭代数列应该具有的功能。

我猜想具体实现,看上上图,即ArrayList里边的方法。第一个应该是得到一个Enumerator的对象,里面存储着ArrayList该位置的数字。第二个应该是存储指定下标/索引的。【可能用于字典什么的?】然后得到的都是object的对象,所以都应该强制类型转换后再累加。

回到主题。

供(数组)需(方法)双方都遵循着同一个契约,“可以被迭代”

第二个作用,接口可以用来解耦,降低耦合度。但是其实解耦的对象跟接口之间是紧耦合。

下面来看实例。

namespace ConsoleAppPractice
{
    class Program
    {
        static void Main()
        {
            Engine engine = new Engine();
            Car car = new Car(engine);
            car.Run(3);
            Console.WriteLine(car.Speed);
        }
    }
    class Engine
    {
        public int RPM { get; set; }
        public void Work(int gas)
        {
            Console.WriteLine("Engine is working!");
            RPM = gas * 3000;
        }
    }
    class Car
    {
        private int speed;
        public int Speed
        {
            get { return speed; }
            set { speed = value/1000; }
        }

        private Engine _engine;
        public Car(Engine engine)
        {
            _engine = engine;
        }
        public void Run(int gas)
        {
            _engine.Work(gas);
            Speed = _engine.RPM;
        }
    }
}

在这段代码中,Car与Enigne产生了紧耦合,不利于工程维护。因而,此处宜使用接口。

类似的手机一例:

namespace ConsoleAppPractice
{
    class Program
    {
        static void Main()
        {
            IPhone phone1 = new Huawei();
            IPhone phone2 = new Xiaomi();
            PhoneUser user = new PhoneUser(phone2);
            user.Action();
        }
    }

    interface IPhone
    {
        void Call();
        void Receive();
    }
    class Huawei:IPhone
    {
        public void Call()
        {
            Console.WriteLine("Huawei is calling.");
        }
        public void Receive()
        {
            Console.WriteLine("Huawei recept a message.");
        }
    }
    class Xiaomi : IPhone
    {
        public void Call()
        {
            Console.WriteLine("Xiaomi is calling.");
        }
        public void Receive()
        {
            Console.WriteLine("Xiaomi recept a message.");
        }
    }
    class PhoneUser
    {
        private IPhone _phone;
        public PhoneUser(IPhone phone)
        {
            _phone = phone;
        }
        public void Action()
        {
            _phone.Call();
            _phone.Receive();
        }
    }
}

通过给使用者提供多个提供者,类似方法重载,来降低依赖程度。

可以说,代码中,如果有替换的地方,就一定有接口。

这里其实用的是SOLID的D,即依赖反转原则:

按照一般的思维方式,解决问题一般是这样:

但是这就造成了金字塔形的紧耦合,不好。因而,我们需要适当运用依赖反转,来降低耦合度。

类似于上述手机和使用者的例子。

 此时表示依赖关系的箭头从自上而下变成了自下而上。从直观角度,这就叫“反转”。这就是依赖反转原则的意思。

第三个作用,接口可以实现单元测试

namespace ConsoleAppPractice
{
    class Program
    {
        static void Main()
        {
            IPower powerer = new Powerer();
            Fan fan = new Fan(powerer);
            Console.WriteLine(fan.WorkCondition());
        }
    }
    interface IPower
    {
        int GetPower();
    }
    class Powerer : IPower
    {
        public int GetPower()
        {
            return 100;
        }
    }
    class Fan
    {
        private IPower _powerer;
        public Fan(IPower powerer)
        {
            _powerer = powerer;
        }
        public string WorkCondition()
        {
            int power = _powerer.GetPower();
            if (power<=0)
            {
                return "Won't work.";
            }
            else if (power < 100)
            {
                return "Work slow.";
            }
            else if (power < 200)
            {
                return "Work successfully.";
            }
            else
            {
                return "Warning!!";
            }
        }
    }
}

 在这里,如果要测试电量数值为其他时fan能否运作,需要反复修改此处return的数值,显然不符合开闭原则。

此处,就需要额外建一个测试项目。

具体操作过程见刘铁猛《C#语言入门详解》全集_哔哩哔哩_bilibili

下面放上test文件里边的代码。

namespace ConsoleApp1.test
{
    public class PowerTest
    {
        [Fact]
        public void PowerLowerThanZero()
        {
            IPower powerer = new PowerLowerThanZero_power();
            Fan fan = new Fan(powerer);
            var expected = "Won't work.";
            var actual = fan.WorkCondition();
            Assert.Equal(actual, expected);
        }
        [Fact]
        public void PowerHigherThan200()
        {
            IPower powerer = new PowerHigherThan200_power();
            Fan fan = new Fan(powerer);
            var expected = "Warning!!";
            var actual = fan.WorkCondition();
            Assert.Equal(actual, expected);
        }
    }

    class PowerLowerThanZero_power : IPower
    {
        public int GetPower()
        {
            return 0;
        }
    }

    class PowerHigherThan200_power : IPower
    {
        public int GetPower()
        {
            return 210;
        }
    }

但注意到每次都要新声明一个IPower的派生类,太繁琐了。所以此处引入Moq:

using ConsoleAppPractice;
using Moq;
using System;
using Xunit;

namespace ConsoleApp1.test
{
    public class PowerTest
    {
        [Fact]
        public void PowerLowerThanZero()
        {
            var mock = new Mock<IPower>();
            mock.Setup(ps => ps.GetPower()).Returns(() => 0);
            Fan fan = new Fan(mock.Object);
            var expected = "Won't work.";
            var actual = fan.WorkCondition();
            Assert.Equal(actual, expected);
        }
        [Fact]
        public void PowerHigherThan200()
        {
            var mock = new Mock<IPower>();
            mock.Setup(ps => ps.GetPower()).Returns(() => 300);
            Fan fan = new Fan(mock.Object);
            var expected = "Warning!!";
            var actual = fan.WorkCondition();
            Assert.Equal(actual, expected);
        }
    }
}

里边那堆东西是Lambda表达式。

四、接口隔离原则

我们都知道,接口是一种对供需方均做出了约束的协议。对于供方,“不会少给”很容易做到,因为接口要求供方必须实现其里面的所有方法,否则不能实例化。但对于需方来说,“不会多要”这一点则是软性的。

什么是不会多要呢?意思是说,需方应该全部用到提供的接口里的东西,接口中不能存在一些完全没被用过的方法,即接口不能太大。如果接口太大,可以将其拆分成多个小接口。

第一种违反接口隔离原则,是传进来的接口太大,有些方法用不到,故应该分为小接口

namespace ConsoleAppPractice
{
    class Program
    {
        static void Main()
        {
            ITank tank = new HeavyTank();
            tank.Run();
        }
    }
    
    interface IVehicle
    {
        void Run();
    }

    class Car : IVehicle
    {
        public void Run()
        {
            Console.WriteLine("A car is running!");
        }
    }

    class Truck : IVehicle
    {
        public void Run()
        {
            Console.WriteLine("A truck is running!");
        }
    }

    interface ITank
    {
        void Run();
        void Fire();
    }

    class HeavyTank : ITank
    {
        public void Fire()
        {
            Console.WriteLine("Boom!!!");
        }

        public void Run()
        {
            Console.WriteLine("A heavytank is running......"); 
        }
    }

    class LightTank : ITank
    {
        public void Fire()
        {
            Console.WriteLine("boom!");
        }

        public void Run()
        {
            Console.WriteLine("A lighttank is running......");
        }
    }
}

在这个例子中,我们只想把坦克作为代步工具,因而,ITank这个接口里的Fire方法完全用不到。

而事实上,我们确实可以把坦克这个事物在这个场景的意义下拆分为两方面,一方面是用于行走的轮子,一方面是用于攻击的火炮。因而,我们可以选择把ITank这个接口拆分为两个小接口,让tank全部都接上那两个小接口:

namespace ConsoleAppPractice
{
    class Program
    {
        static void Main()
        {
            IVehicle tank = new HeavyTank();
            tank.Run();
        }
    }
    
    interface IVehicle
    {
        void Run();
    }

    class Car : IVehicle
    {
        //
    }

    class Truck : IVehicle
    {
        //
    }

    interface IPowder
    {
        void Fire();
    }

    class HeavyTank : IPowder,IVehicle
    {
        public void Fire()
        {
            Console.WriteLine("Boom!!!");
        }

        public void Run()
        {
            Console.WriteLine("A heavytank is running......"); 
        }
    }

    class LightTank : IPowder, IVehicle
    {
        //
    }
}

但是请注意,这个也不能使用得太过火了,因为它可能会让只有一个方法的小接口越来越多,颗粒度增加。因而,应该把类的大小和接口大小都控制在一个范围内

第二种违反接口隔离原则的,是一个大接口由两个设计很好的小接口合并,传的时候却传了大接口,这就导致本来会用到的被隔绝在了门外。

比如说本例:

namespace ConsoleAppPractice
{
    class Program
    {
        static void Main()
        {
            ITank tank = new Car();

        }
    }
    
    interface IVehicle
    {
        void Run();
    }

    //

    interface IPowder
    {
        void Fire();
    }
    interface ITank : IPowder, IVehicle
    {

    }

    class HeavyTank : ITank
    {
        //
    }

    class LightTank : ITank
    {
        //
    }
}

此时编译器会报错,因为Car已经被你挡在外面了。

再比如说,我们之前一直常用到的IEnumerable这个接口,其实它有一个更大的接口,是ICollection。

为了做我们的例子,我们需要创建一个只接上IEnumerable的接口,而没有接上ICollection接口的类。

namespace ConsoleAppPractice
{
    class Program
    {
        static void Main()
        {
            int[] nums1 = new int[] { 1, 2, 3, 4, 5 };
            ArrayList nums2 = new ArrayList() { 1, 2, 3, 4, 5 };
            ReadOnlyCollection collection = new ReadOnlyCollection(nums1);
            Console.WriteLine(Calculator.Sum(nums1));
            Console.WriteLine(Calculator.Sum(nums2));
            Console.WriteLine(Calculator.Sum(collection));
        }
    }
    class Calculator
    {
        public static double Sum(IEnumerable vs)
        {
            double sum = 0;
            foreach (var item in vs)
            {
                sum += (int)item;
            }
            return sum;
        }
    }
    class ReadOnlyCollection : IEnumerable
    {
        private int[] _nums;
        public ReadOnlyCollection(int[] nums)
        {
            _nums = nums;
        }
        public IEnumerator GetEnumerator()
        {
            return new Enumerator(this);
        }
        class Enumerator : IEnumerator//把Enumerator放在类的里面,防止污染名称空间
        {
            private ReadOnlyCollection _collection;
            private int _head;
            public Enumerator(ReadOnlyCollection r)
            {
                _collection = r;
                _head = -1;
            }
            public object Current
            {
                get
                {
                    object o = _collection._nums[_head];//装箱
                    return o;
                }
            }

            public bool MoveNext()
            {
                if (++_head < _collection._nums.Length)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }

            public void Reset()
            {
                _head = -1;
            }
        }
    }
}

如果是这样,非常完美。【注:此处感兴趣可以看看P16讲表达式的时候,老师讲过了foreach的语法糖】

但是如果把Sum需求者的接口改为ICollection,立马报错第十五行,此时你已经把这个IEnumerable拒之门外了。

我们知道,这个功能只需要可迭代就行了。传IEnumerable明显比传ICollection合适。

第三个接口隔离的例子,我们将展现C#语言特有的功能,即显式接口实现

一个大接口可被拆分成若干个小接口。有没有一个办法,可以让只需要使用这个小接口时,接口的方法才能被看到呢?这个办法就是显式接口实现。

比如说下例:一个杀手是不能被别人发现自己有Kill这个方法的:

namespace ConsoleAppPractice
{
    class Program
    {
        static void Main()
        {
            var Jeck = new WarmKiller();
            
        }
    }

    interface IKiller
    {
        void Kill();
    }
    interface IGentleman
    {
        void Love();
    }

    class WarmKiller : IKiller, IGentleman
    {
        public void Kill()
        {
            Console.WriteLine("I will kill the enemy.");
        }

        public void Love()
        {
            Console.WriteLine("I love you.");
        }
    }
}

如果是这样的话,就会被发现有kill方法,很不合理。

这时候就得用到第二项,即显式实现方法。

 此时,除非显式声明变量:

 

 否则,是看不到Kill这个方法的。

 此时想看到love这个方法需要做的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值