接口与抽象类的区别

基本定义

  • 访问级别的区别
    • 接口中的成员方法必须是public,且必须省略
    • 抽象类中的成员方法只要不是private即可,可以是protected、internal
  • 本质的区别
    • 接口的本质是服务的调用者(消费者)与服务的提供者之间的契约,所以方法必须完全暴露出来
    • 抽象类的成员被protected修饰的只能给子类使用,被internal修饰的成员只能在本程序集中使用,因此抽象类中被protected和internal修饰的成员都不是给服务的调用者准备的,各有都有特点的可见目标

示例1:不使用接口

  • 对一组整数进行求和、求平均值的操作(不使用接口) ```csharp using System; using System.Collections;

namespace InterfaceExample { class Program { static void Main(string[] args) { //供方:int[]的基类是Array,Array类和ArrayList类都实现了IEnumerable接口,遵循契约,可以被迭代 int[] nums1 = new int[] { 1, 2, 3, 4, 5 }; ArrayList nums2 = new ArrayList { 1, 2, 3, 4, 5 };

  1. int x = Sum(nums1);
  2. double y = Avg(nums1);
  3. Console.WriteLine(x);//输出:15
  4. Console.WriteLine(y);//输出:3
  5. int a = Sum(nums2);
  6. double b = Avg(nums2);
  7. Console.WriteLine(a);//输出:15
  8. Console.WriteLine(b);//输出:3
  9. }
  10. //需方:要求被传进来的参数能被foreach迭代即可
  11. static int Sum(int[] nums)
  12. {
  13. int sum = 0;
  14. foreach (var n in nums)
  15. {
  16. sum += n;
  17. }
  18. return sum;
  19. }
  20. //需方:要求被传进来的参数能被foreach迭代即可
  21. static double Avg(int[] nums)
  22. {
  23. int sum = 0;
  24. double count = 0;
  25. foreach (var n in nums)
  26. {
  27. sum += n;
  28. count++;
  29. }
  30. return sum / count;
  31. }
  32. //需方
  33. static int Sum(ArrayList nums)
  34. {
  35. int sum = 0;
  36. foreach (var n in nums)
  37. {
  38. sum += (int)n;
  39. }
  40. return sum;
  41. }
  42. //需方
  43. static double Avg(ArrayList nums)
  44. {
  45. int sum = 0;
  46. double count = 0;
  47. foreach (var n in nums)
  48. {
  49. sum += (int)n;
  50. count++;
  51. }
  52. return sum / count;
  53. }
  54. }

}

  1. <a name="fQ4h1"></a>
  2. ## 示例2:使用接口
  3. - 由于供需双方都遵循可迭代的契约,使用接口对代码进行改写,如下:
  4. ```csharp
  5. using System;
  6. using System.Collections;
  7. namespace InterfaceExample
  8. {
  9. class Program
  10. {
  11. static void Main(string[] args)
  12. {
  13. //供方:int[]的基类是Array,Array类和ArrayList类都实现了IEnumerable接口,遵循契约,可以被迭代
  14. int[] nums1 = new int[] { 1, 2, 3, 4, 5 };
  15. ArrayList nums2 = new ArrayList { 1, 2, 3, 4, 5 };
  16. int x = Sum(nums1);
  17. double y = Avg(nums1);
  18. Console.WriteLine(x);//输出:15
  19. Console.WriteLine(y);//输出:3
  20. int a = Sum(nums2);
  21. double b = Avg(nums2);
  22. Console.WriteLine(a);//输出:15
  23. Console.WriteLine(b);//输出:3
  24. }
  25. //需方:要求被传进来的参数能被foreach迭代即可
  26. static int Sum(IEnumerable nums)
  27. {
  28. int sum = 0;
  29. foreach (var n in nums)
  30. {
  31. sum += (int)n;
  32. }
  33. return sum;
  34. }
  35. //需方:要求被传进来的参数能被foreach迭代即可
  36. static double Avg(IEnumerable nums)
  37. {
  38. int sum = 0;
  39. double count = 0;
  40. foreach (var n in nums)
  41. {
  42. sum += (int)n;
  43. count++;
  44. }
  45. return sum / count;
  46. }
  47. }
  48. }

依赖与紧耦合

基本定义

  • 依赖:面向对象是现实世界的抽象,因此在面向对象世界中,类与类之间也有合作和分工。合作的术语在软件中称为依赖。依赖越直接,耦合越紧。

示例1:紧耦合示例

using System;

namespace InterfaceExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var engine = new Engine();
            var car = new Car(engine);
            car.Run(3);
            Console.WriteLine(car.Speed);
        }
    }

    class Engine
    {
        public int RPM { get; private set; }
        public void Work(int gas)
        {
            this.RPM = gas * 1000;
        }
    }

    class Car
    {
        private Engine _engine;//此处已经产生了紧耦合,Car完全依赖在Engine类上
        public Car(Engine engine)
        {
            _engine = engine;
        }

        public int Speed { get; private set; }
        public void Run(int gas)
        {
            _engine.Work(gas);
            this.Speed = _engine.RPM / 100;
        }
    }
}

示例2:松耦合示例

  • 紧耦合缺点:如果基础类Engine类中逻辑出了问题,与之耦合的Car类也必将出现问题,而且比较难发现问题所在(几十个上百个类的紧耦合)。这样即不好调试,也会影响团队工作,因此在开发时,尽可能避免紧耦合,接口可以有效地降低耦合度,如下示例:人和手机之间的松耦合关系 ```csharp using System;

namespace InterfaceExample { class Program { static void Main(string[] args) { var user = new PhoneUser(new NokiaPhone()); user.UsePhone(); //输出:Nokia calling… // Hello! // Nokia message ring… // Hello,Nokia!

        //如果NokiaPhone坏了,只需改成EricssonPhone即可,不需要改PhoneUser、NokiaPhone和EricssonPhone类中的任何代码
        var user2 = new PhoneUser(new EricssonPhone());
        user2.UsePhone();
        //输出:Ericsson calling...
        //      Hi,This's Tim!
        //      Ericsson ring...
        //      Good evening!

        // var user2 = new PhoneUser(new EricssonPhone());中也有耦合,
        //可以通过反射实现不用改new EricssonPhone(),在程序外的某个配置文件中设置即可
    }
}

class PhoneUser
{
    private IPhone _phone;
    public PhoneUser(IPhone phone)
    {
        _phone = phone;
    }

    public void UsePhone()
    {
        _phone.Dail();
        _phone.PickUp();
        _phone.Receive();
        _phone.Send();
    }
}

interface IPhone
{
    void Dail();
    void PickUp();
    void Send();
    void Receive();
}

class NokiaPhone : IPhone
{
    public void Dail()
    {
        Console.WriteLine("Nokia calling...");
    }

    public void PickUp()
    {
        Console.WriteLine("Hello!");
    }

    public void Receive()
    {
        Console.WriteLine("Nokia message ring...");
    }

    public void Send()
    {
        Console.WriteLine("Hello,Nokia!");
    }
}

class EricssonPhone : IPhone
{
    public void Dail()
    {
        Console.WriteLine("Ericsson calling...");
    }

    public void PickUp()
    {
        Console.WriteLine("Hi,This's Tim!");
    }

    public void Receive()
    {
        Console.WriteLine("Ericsson ring...");
    }

    public void Send()
    {
        Console.WriteLine("Good evening!");
    }
}

}


- 在代码中,若有可以代换的地方,那么一定会有接口的存在。接口就是为了解耦而生,松耦合最大的好处就是为了让功能的提供方变得可替换,从而降低了不能替换所带来的高风险和高成本

<a name="NI2zQ"></a>
# 依赖反转原则

<a name="OMocz"></a>
## 基本定义

- 解耦在代码中的表现即为依赖反转,单元测试其实就是依赖反转在开发中直接应用和直接受益者
- 依赖反转:用于"平衡"自顶向下,逐步求精的思维方式,如下:

![image.png](https://cdn.nlark.com/yuque/0/2021/png/21507654/1624893392230-f4d77518-3e59-4be1-9393-e6d0a6db30c3.png#clientId=u60f315d9-11a3-4&from=paste&height=147&id=u838fd934&margin=%5Bobject%20Object%5D&name=image.png&originHeight=337&originWidth=646&originalType=binary&ratio=1&size=104224&status=done&style=none&taskId=ue8617df0-ade5-44c9-bec5-5daac1d27d4&width=281)     ![image.png](https://cdn.nlark.com/yuque/0/2021/png/21507654/1624893431927-20b02e8c-29a2-4f9a-a8dd-b889e8bbd545.png#clientId=u60f315d9-11a3-4&from=paste&height=153&id=ua4f58dd8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=357&originWidth=670&originalType=binary&ratio=1&size=127581&status=done&style=none&taskId=u54846e4c-79f0-46b5-85d2-c3be1acb3b5&width=288)

- 逐步进化的原理图过程如下:![image.png](https://cdn.nlark.com/yuque/0/2021/png/21507654/1624894145946-c0218220-6bb4-451a-95c3-a8352cf62cb0.png#clientId=u60f315d9-11a3-4&from=paste&height=760&id=uf90d5e6e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=760&originWidth=1006&originalType=binary&ratio=1&size=285700&status=done&style=stroke&taskId=u46cb65f5-0c4b-40d5-b77b-7aebd553f88&width=1006)

<a name="ZbsQz"></a>
## 示例:接口、解耦、依赖倒置原则如何被单元测试所应用。

<a name="MzH7j"></a>
### 示例1:紧耦合方式
```csharp
using System;

namespace InterfaceExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var fan = new DeskFan(new PowerSupply());
            Console.WriteLine(fan.Work());
        }
    }

    class PowerSupply
    {
        public int GetPower()
        {
            return 100;
        }
    }

    class DeskFan
    {
        private PowerSupply _powerSupply;
        public DeskFan(PowerSupply powerSupply)
        {
            _powerSupply = powerSupply;
        }

        public string Work()
        {
            int power = _powerSupply.GetPower();
            if (power<=0)
            {
                return "Won't work.";
            }
            else if (power<100)
            {
                return "Slow";
            }
            else if (power<200)
            {
                return "Work fine";
            }
            else
            {
                return "Warning";
            }
        }
    }
}
  • 此时如果想测试DeskFan是否工作,必须直接改写PowerSupply类中GetPower的值,违反了开闭原则,即不能随意改变一个类中的代码。如果不止是一个DeskFan或者其他供电设备连在PowerSupply上,那么与之相连的供电设备会因为PowerSupply改变了值而把设备内部烧坏。

示例2:使用接口进行改写并进行单元测试

using System;

namespace InterfaceExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var fan = new DeskFan(new PowerSupply());
            Console.WriteLine(fan.Work());
        }
    }

    public interface IPowerSupply
    {
        int GetPower();
    }

    public class PowerSupply:IPowerSupply
    {
        public int GetPower()
        {
            return 100;
        }
    }

    public class DeskFan
    {
        private IPowerSupply _powerSupply;
        public DeskFan(IPowerSupply powerSupply)
        {
            _powerSupply = powerSupply;
        }

        public string Work()
        {
            int power = _powerSupply.GetPower();
            if (power<=0)
            {
                return "Won't work.";
            }
            else if (power<100)
            {
                return "Slow";
            }
            else if (power<200)
            {
                return "Work fine";
            }
            else
            {
                return "Explode!";
            }
        }
    }
}
  • 为了更方便测试,再去创建一个专门测试的类,专门输出超出范围的电流,再将此类的实例传给DeskFan。测试过程应在单元测试项目中完成,而非Main函数中
  • 创建单元测试方法:在Solution -> Add -> New Project -> xUnit Test Project -> 命名为InterfaceExample.Tests(即xxxx.Tests格式) -> 创建好后添加项目引用,如下:

image.png

  • 在InterfaceExample.Tests名称空间中写入以下代码: ```csharp using System; using Xunit;

namespace InterfaceExample.Tests { public class DeskFanTests { [Fact] public void PowerLowerThanZero_OK() { var fan = new DeskFan(new PowerSupplyLowerThanZero()); var expected = “Won’t work.”; var actual = fan.Work(); Assert.Equal(expected, actual); } }

class PowerSupplyLowerThanZero : IPowerSupply
{
    public int GetPower()
    {
        return 0;
    }
}

}


- 打开:VS菜单栏 -> 测试 -> 测试资源管理器,并展开左侧弹出的测试栏,在最底层中的PowerLowerThanZero_OK中右键运行,此时所有运行结果都为绿勾,如下:

![image.png](https://cdn.nlark.com/yuque/0/2021/png/21507654/1624896891189-8ec295b8-4431-40c9-9d3c-c8de10a8caa5.png#clientId=u60f315d9-11a3-4&from=paste&height=246&id=u19b27ce3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=246&originWidth=538&originalType=binary&ratio=1&size=21967&status=done&style=none&taskId=u9885efa5-3fae-4ebb-9598-bd399bf20e3&width=538)

<a name="p0J3f"></a>
### 示例3:单元测试不通过的情况

- 如果此时在InterfaceExample.Tests名称空间中继续增加测试case,故意将测试中的var expected = "Warning!";与对应DeskFan类中值写成不一样,如下:
```csharp
using System;
using Xunit;

namespace InterfaceExample.Tests
{
    public class DeskFanTests
    {
        [Fact]
        public void PowerLowerThanZero_OK()
        {
            var fan = new DeskFan(new PowerSupplyLowerThanZero());
            var expected = "Won't work.";
            var actual = fan.Work();
            Assert.Equal(expected, actual);
        }

        [Fact]
        public void PowerHigherThan200_Warning()
        {
            var fan = new DeskFan(new PowerSupplyHigherThan200());
            var expected = "Warning!";
            var actual = fan.Work();//DeskFan中等于220的结果为"Explode!"
            Assert.Equal(expected, actual);//两者不相等
        }
    }

    class PowerSupplyLowerThanZero : IPowerSupply
    {
        public int GetPower()
        {
            return 0;
        }
    }

    class PowerSupplyHigherThan200 : IPowerSupply
    {
        public int GetPower()
        {
            return 220;
        }
    }
}
  • 以上PowerHigherThan200_Warning方法中两者结果不相等,在测试中不会通过,打红叉,如下:

image.png

  • 可在出错的测试case,PowerHigherThan200_Warning方法中打断点,右击单元测试中的PowerHigherThan200_Warning进行调试,逐步进行调试,发现进入DeskFan的Work方法后返回值为Explode!,与PowerHigherThan200_Warning方法中的不一致,如下:

image.png
image.png

  • 将Work方法中的”Explode!”改为”Warning!”,重新测试即可通过。如下:

image.png

示例4:使用Mock进行单元测试

  • 在工作中写测试case与写代码同等重要,如果没有测试case来监控代码的话,就不确定写出代码的可靠程度
  • 上例中,存在一个很大的问题,即在单元测试中为了测试不同的情况,就得创建不同的接口实现类,会造成看起来很丑的类越来越多,使用Mock可以解决这一问题,从而简化单元测试
    • 首先右击InterfaceExample.Tests -> 管理NuGet程序包 -> 搜索Moq -> 进行安装
    • 引用Moq名称空间
    • 对InterfaceExample.Tests名称空间中测试代码进行改写,如下: ```csharp using System; using Xunit; using Moq;

namespace InterfaceExample.Tests { public class DeskFanTests { [Fact] public void PowerLowerThanZero_OK() { var mock = new Mock(); mock.Setup(ps => ps.GetPower()).Returns(() => 0); var fan = new DeskFan(mock.Object); var expected = “Won’t work.”; var actual = fan.Work(); Assert.Equal(expected, actual); }

    [Fact]
    public void PowerHigherThan200_Warning()
    {
        var mock = new Mock<IPowerSupply>();
        mock.Setup(ps => ps.GetPower()).Returns(() => 220);
        var fan = new DeskFan(mock.Object);
        var expected = "Warning!";
        var actual = fan.Work();
        Assert.Equal(expected, actual);
    }
}

} ```

  • 继续运行测试case看结果,都为对勾
  • 运用MockFramework,可以简化单元测试,而不用专门创建很多用来测试的类

接口与单元测试

  • 接口的产生:自底向上(重构),自顶向下(设计)
  • C#接口的实现(隐式,显示,多接口)
  • 语言对面向对象设计的内建支持,依赖反转,接口隔离,开闭原则…

image.png