IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发测试 -> C#学习(14)---接口、依赖反转、单元测试、接口隔离原则 -> 正文阅读

[开发测试]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这个方法需要做的。

  开发测试 最新文章
pytest系列——allure之生成测试报告(Wind
某大厂软件测试岗一面笔试题+二面问答题面试
iperf 学习笔记
关于Python中使用selenium八大定位方法
【软件测试】为什么提升不了?8年测试总结再
软件测试复习
PHP笔记-Smarty模板引擎的使用
C++Test使用入门
【Java】单元测试
Net core 3.x 获取客户端地址
上一篇文章      下一篇文章      查看所有文章
加:2021-12-24 18:47:11  更:2021-12-24 18:47:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/18 4:34:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码