接口&抽象类

一、接口和抽象类简要概述

  • 接口和抽象类都是"软件工程产物"
  • 具体类 -> 抽象类 -> 接口:越来越抽象,内部实现的东西越来越少
    • 如果是一个方法成员的话,它的方法体就是它的实现
    • 字段也代表一种实现,它实现了类怎么去存储数据
  • 抽象类是未完全实现逻辑的类(可以有字段和非public成员,它们代表了"具体逻辑")
  • 抽象类为复用而生:专门作为基类来使用,也具有解耦功能
  • 封装确定的,开放不确定的,推迟到合适的子类中去实现
  • 接口是完全未实现逻辑的"类"("纯虚类";只有函数成员;成员全部public)
  • 接口为解耦而生:"高内聚,低耦合",方便单元测试
  • 接口是一个"协约",早已为工业生产所熟知(有分工必有协作,有协作必有协约)
  • 它们都不能实例化,只能用来声明变量、引用具体类(concrete class)的实例

二、实际案例

为作基类而生的"抽象类" & 开放/关闭原则(开闭原则)

1. 抽象类的感性认识

一个被abstract所修饰的方法,只有返回值、方法名、和参数列表,没有方法体(连花括号都没有,不能有任何逻辑实现),这个就是一个完全没有被实现的方法,也就是抽象方法。abstract public void Study();抽象方法不能是private,因为它会被抽象类的子类去实现,private的话子类都没办法访问,何谈去实现这个抽象方法;internal、public、protected都可以,因为子类可以访问到

一旦一个类中有了抽象方法或者其他抽象成员,这个类就成了抽象类,抽象类前面必须加abstract修饰

csharp 复制代码
abstract class Student // 抽象类
{
    abstract public void Study(); // 抽象方法
}

抽象类中含有为被实现的函数成员(没有具体的行为),编译器不允许实例化一个抽象类

一个类不能被实例化,就只剩下俩个作用:

  • 作为基类;让别人从自己这边派生,在派生类中把没有实现的函数成员给实现了
  • 抽象类作为基类,可以去声明变量,用基类类型的变量去引用一个子类类型的实例,在子类里面已经实现了这个抽象类中未被实现的方法;也就是多态:父类变量引用子类实例;当变量和实例之间有代差时,会产生多态效果
    • 抽象方法被子类所实现有点像用override方法去重写virtual方法,所以抽象方法在某些编程语言中(c++)也被成为纯虚方法;
    • 虚方法有方法体,等着子类用override去重写这个方法;抽象方法连方法体都没有,所以abstract方法又被称为纯虚方法

抽象类:函数成员没有被完全实现 的类

抽象类中可以有若干个函数成员,但函数成员里面至少有一个像刚才那样没有被实现的

若这些函数成员都被实现了,那就是具体类(concrete class)

2. 开闭原则的感性认识

如果不是为了修Bug或者添加新功能的话,闲着没事别老去修改一个类的代码,特别时这个类当中的函数成员代码

我们应该封装那些不变的、稳定的、固定的、和确定的成员,把哪些不确定的、有可能改变的成员声明为抽象成员,并且留给子类去实现

3. 实际案例分析

案件分析:你有一辆小汽车(car),有run和stop功能;十年后,你买了一辆卡车(truck),也有run和stop功能;二十年后,你可以会买一辆拖拉机(tractor),也有run和stop功能;...

其中无论什么样类型的车,stop功能是完全一样的,只不过run方法各有不同

此时就可以抽离出来它们的共性,都是交通工具(vehicle),都有stop和run功能

  • stop功能完全一样,可以直接放在vehicle这个基类中
  • run功能各不相同,可以通过多态去实现(父类用virtual虚函数,子类用override)
csharp 复制代码
using System;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Vehicle v_car = new Car();
            v_car.Run();
            v_car.Stop();

            Vehicle v_truck = new Truck();
            v_truck.Run();
            v_truck.Stop();

            Vehicle v_tractor = new Tractor();
            v_tractor.Run();
            v_tractor.Stop();
        }
    }

    class Vehicle 
    {
        public void Stop() { Console.WriteLine("Stopped!!!"); }
        public virtual void Run() { Console.WriteLine("Vehicle running"); }
    }

    class Car : Vehicle 
    {
        public override void Run() { Console.WriteLine("Car running"); }
    }
    class Truck : Vehicle 
    {
        public override void Run() { Console.WriteLine("Truck running"); }
    }
    class Tractor : Vehicle
    {
        public override void Run() { Console.WriteLine("Tractor running"); }
    }
}

问题发现了:基类Vehicle中的public virtual void Run() { Console.WriteLine("Vehicle running"); }压根没有用,因为子类都override重写父类的方法了,故可以把方法体给直接删掉

没有方法体的虚函数,可以直接用abstract修饰,得到抽象方法,public abstract void Run();

有抽象方法的类必须是抽象类,abstract class Vehicle

抽象类的子类去通过override重写抽象方法,这样就看的简洁很多

csharp 复制代码
using System;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Vehicle v_car = new Car();
            v_car.Run();
            v_car.Stop();

            Vehicle v_truck = new Truck();
            v_truck.Run();
            v_truck.Stop();

            Vehicle v_tractor = new Tractor();
            v_tractor.Run();
            v_tractor.Stop();
        }
    }

    abstract class Vehicle
    {
        public void Stop() { Console.WriteLine("Stopped!!!"); }
        public abstract void Run(); // 改动唯一位置
    }

    class Car : Vehicle 
    {
        public override void Run() { Console.WriteLine("Car running"); }
    }
    class Truck : Vehicle 
    {
        public override void Run() { Console.WriteLine("Truck running"); }
    }
    class Tractor : Vehicle
    {
        public override void Run() { Console.WriteLine("Tractor running"); }
    }
}

若抽象类里面全是抽象方法会咋样?

定义一个VehicleBase类,里面全是抽象方法,让Vehicle类去继承这个VehicleBase类

csharp 复制代码
abstract class VehicleBase
{
    abstract public void Stop();
    abstract public void Run();
}

完整代码:

csharp 复制代码
using System;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Vehicle v_car = new Car();
            v_car.Run();
            v_car.Stop();

            Vehicle v_truck = new Truck();
            v_truck.Run();
            v_truck.Stop();

            Vehicle v_tractor = new Tractor();
            v_tractor.Run();
            v_tractor.Stop();
        }
    }

    abstract class VehicleBase
    {
        abstract public void Stop();
        abstract public void Run();
    }


    abstract class Vehicle: VehicleBase
    {
        public override void Stop() { Console.WriteLine("Stopped!!!"); }
        // public abstract void Run(); // 因为VehicleBase基类已经有了抽象方法Run,这里就没必要再次去声明了
    }

    class Car : Vehicle 
    {
        public override void Run() { Console.WriteLine("Car running"); }
    }
    class Truck : Vehicle 
    {
        public override void Run() { Console.WriteLine("Truck running"); }
    }
    class Tractor : Vehicle
    {
        public override void Run() { Console.WriteLine("Tractor running"); }
    }
}

把VehicleBase抽象类(abstract)替换成接口interface

  • 接口要求所有成员都必须是public,故原先的public就可以去掉了
  • 接口本身就包含纯抽象类的含义,所有的成员一定都是抽象的,所以abstract也可以去掉
  • 接口的抽象成员abstract被去掉了,所以子类对应重写override也可需要去掉
  • 接口作为基类命名方式一般以I开头,如IVehicle
  • 接口有ABC,抽象类可以继承接口,实现AB,对于C可以通过abstract保留抽象,让另一个类去继承抽象类即可
csharp 复制代码
using System;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Vehicle v_car = new Car();
            v_car.Run();
            v_car.Stop();

            Vehicle v_truck = new Truck();
            v_truck.Run();
            v_truck.Stop();

            Vehicle v_tractor = new Tractor();
            v_tractor.Run();
            v_tractor.Stop();
        }
    }

    interface IVehicle // 定义接口
    {
        void Stop();
        void Run();
    }


    abstract class Vehicle: IVehicle
    {
        public void Stop() { Console.WriteLine("Stopped!!!"); }
        public abstract void Run(); // 可以不实现接口中的Run方法,把Run方法搞成抽象方法,留给子类去实现
    }

    class Car : Vehicle 
    {
        public override void Run() { Console.WriteLine("Car running"); }
    }
    class Truck : Vehicle 
    {
        public override void Run() { Console.WriteLine("Truck running"); }
    }
    class Tractor : Vehicle
    {
        public override void Run() { Console.WriteLine("Tractor running"); }
    }
}

三、接口

接口是由抽象类进化而来

  • 接口中的"抽象方法"必须都是public的,因为必须都是public,所以都不需要写
    • 这个成员访问级别public,决定了接口的本质
    • 接口的本质:服务的调用者/服务的消费者服务的提供者之间的契约(contract);契约就必须透明,对双方都可见;契约使得自由合作成为可能
    • 自由合作:既约束服务的使用者,也约束服务的提供者
  • 抽象类中的抽象方法只要不是private就行
    • 抽象类中的protected修饰的成员只能够让自己的子类看得到
    • internal,出了当前这个程序集别人也都看不见
    • protected和internal都不是给功能调用者准备的,各自都有各自特定的可见目标

接口与单元测试

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

1. 接口案例

对一组整数求和和求平均值操作

整数分别由int类型的数组和ArrayList数组存放

常规做法:重载两个函数即可

缺点:代码重复

csharp 复制代码
using System;
using System.Collections;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            int[] nums1 = new int[] { 1, 2, 3, 4, 5};
            ArrayList nums2 = new ArrayList() { 11, 12, 13, 14, 15 };
            Console.WriteLine($"int数组求和:{Sum(nums1)}");
            Console.WriteLine($"int数组求平均值:{Average(nums1)}");

            Console.WriteLine($"ArrayList求和:{Sum(nums2)}");
            Console.WriteLine($"ArrayList求平均值:{Average(nums2)}");
        }

        static int Sum(int[] nums) 
        {
            int sum = 0;
            foreach (int num in nums)
            {
                sum += num;
            }
            return sum;
        }
        static double Average(int[] nums) 
        {
            int sum = 0;double count = 0;
            foreach (int num in nums)
            {
                sum += num;
                count++;
            }
            return sum / count;
        }

        static int Sum(ArrayList nums) 
        {
            int sum = 0;
            foreach (int num in nums)
            {
                sum += num; // ArrayList存储的元素类型是object,正常需要强转一下,sum += (int)num; 新版编译器会自动转换类型
            }
            return sum;
        }

        static double Average(ArrayList nums)
        {
            int sum = 0; double count = 0;
            foreach (int num in nums)
            {
                sum += num; // ArrayList存储的元素类型是object,正常需要强转一下,sum += (int)num; 新版编译器会自动转换类型
                count++;
            }
            return sum / count;
        }
    }
}

可以明显看到,代码重复了

int数组和ArrayList数组都实现了可迭代的IEnumerable接口

所有 C# 数组(int []、string []、double []、自定义类 [])都隐式实现:

IEnumerable

IEnumerable

ICollection

IList

这是 CLR 底层规定,数组是特殊类型,编译器会自动让它支持枚举。

故,可通过传入IEnumerable接口类型变量进行迭代求和和求平均值

csharp 复制代码
using System;
using System.Collections;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            int[] nums1 = new int[] { 1, 2, 3, 4, 5};
            ArrayList nums2 = new ArrayList() { 11, 12, 13, 14, 15 };
            Console.WriteLine($"int数组求和:{Sum(nums1)}");
            Console.WriteLine($"int数组求平均值:{Average(nums1)}");

            Console.WriteLine($"ArrayList求和:{Sum(nums2)}");
            Console.WriteLine($"ArrayList求平均值:{Average(nums2)}");
        }

        static int Sum(IEnumerable nums) 
        {
            int sum = 0;
            foreach (int num in nums)
            {
                sum += num;
            }
            return sum;
        }
        static double Average(IEnumerable nums) 
        {
            int sum = 0;double count = 0;
            foreach (int num in nums)
            {
                sum += num;
                count++;
            }
            return sum / count;
        }
    }
}

2. 依赖

人是群居生物,在这个社会上生活避免不了合作

在面向对象世界里的合作成为依赖

依赖的同时就出现了耦合,依赖越直接,耦合越紧

依赖案例

现实世界中,汽车有引擎,汽车能否正常工作,依赖于引擎

引擎不工作,汽车肯定开不起来

一个Engine引擎类

  • int型的RPM转速属性,set方法为private,因为没办法从外界设置发动机转速,发动机转速是输出
  • public类型、返回值为void的Work方法、参数是int类型的gas表示油耗大小,RPM = gas * 1000;
csharp 复制代码
class Engine 
{
    public int RPM { get; private set; } // 转速,外界只能get,不可以set
    public void Work(int gas) 
    {
        this.RPM = gas * 1000; // 假设转速 = 油耗 * 1000
    }
}

一个Car汽车类

  • private的Engine类型字段(此时Car已经依赖到Engine上了)
  • 构造函数传入一个Engine
  • int型的Speed属性,set方法也设置为private
  • public类型、返回值为void的Run方法,参数是int类型的gas油耗,Car的Run表示让汽车动起来,也就是调用Engine和Work方法
  • Speed = Engine的RPM / 100,速度=发动机转速/100
csharp 复制代码
class Car
{
    private Engine engine_; // 此时Car已经依赖到Engine上了
    public Car(Engine engine)
    {
        this.engine_ = engine; // 创建Car对象的时候,必须给它一个Engine对象
    }

    public int Speed { get; private set; }
    public void Run(int gas) 
    {
        engine_.Work(gas); // 调用Engine的Work方法
        this.Speed = engine_.RPM / 100; // 假设车速 = 转速/100
    }
}

主函数调用:

csharp 复制代码
static void Main(string[] args)
{
    Engine engine = new Engine();
    Car car = new Car(engine);
    car.Run(10); // 传入一个油耗gas
    Console.WriteLine($"Car此时的速度为:{car.Speed}"); // 油耗10 == 转速10*1000 == 速度1000/100
}

完整代码:

csharp 复制代码
using System;
using System.Collections;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Engine engine = new Engine();
            Car car = new Car(engine);
            car.Run(10); // 传入一个油耗gas
            Console.WriteLine($"Car此时的速度为:{car.Speed}"); // 油耗10 == 转速10*1000 == 速度1000/100
        }

        class Engine
        {
            public int RPM { get; private set; } // 转速,外界只能get,不可以set
            public void Work(int gas)
            {
                this.RPM = gas * 1000; // 假设转速 = 油耗 * 1000
            }
        }

        class Car
        {
            private Engine engine_; // 此时Car已经依赖到Engine上了
            public Car(Engine engine)
            {
                this.engine_ = engine; // 创建Car对象的时候,必须给它一个Engine对象
            }

            public int Speed { get; private set; }
            public void Run(int gas) 
            {
                engine_.Work(gas); // 调用Engine的Work方法
                this.Speed = engine_.RPM / 100; // 假设车速 = 转速/100
            }
        }
    }
}

Car和Engine是紧耦合的,也就是说一旦Engine出现了问题,Car必定会出现问题

3. 消除依赖

引入接口可以解决紧耦合问题

接口本质就是一个契约,用来约束功能

假设手机本质就三个功能:打电话、接电话、发短信、收短信

当我有一台手机的时候,那我肯定知道手机有这四个功能

约束消费者:消费者肯定相信手机一定有这四个功能

约束手机厂家:无论哪个厂商,只要宣称自己的产品是手机,就必须去实现这四个功能

接口好处:

你一开始用的是xiaomi,突然手机坏了,那么就算换成huawei、vivo、oneplus等品牌手机,你也可以拿来直接用,因为你就用这四个功能,人和手机之间是解耦的

案例实现

  1. 调用手机接口IPhone,有四个功能
csharp 复制代码
interface IPhone 
{
    void Dail();
    void PickUp();
    void Send();
    void Receive();
}
  1. xiaomi手机实现IPhone接口
csharp 复制代码
class XiaomiPhone : IPhone
{
    public void Dail()
    {
        Console.WriteLine("Dail Xiaomi Phone");
    }

    public void PickUp()
    {
        Console.WriteLine("PickUp Xiaomi Phone");
    }

    public void Receive()
    {
        Console.WriteLine("Receive Xiaomi Phone");
    }

    public void Send()
    {
        Console.WriteLine("Send Xiaomi Phone");
    }
}
  1. Huawei手机实现IPhone接口
csharp 复制代码
class HuaweiPhone : IPhone
{
    public void Dail()
    {
        Console.WriteLine("Dail Huawei Phone");
    }

    public void PickUp()
    {
        Console.WriteLine("PickUp Huawei Phone");
    }

    public void Receive()
    {
        Console.WriteLine("Receive Huawei Phone");
    }

    public void Send()
    {
        Console.WriteLine("Send Huawei Phone");
    }
}
  1. 用户类
    • 内部有个private的IPhone接口类型字段
    • 构造器接受一个IPhone接口类型的变量
    • 一个UsePhone方法,调用手机的四个方法
csharp 复制代码
class PhoneUse 
{
    private IPhone phone_;
    public PhoneUse(IPhone phone)
    {
        this.phone_ = phone;
    }

    public void UsePhone() 
    {
        phone_.Dail();
        phone_.PickUp();
        phone_.Receive();
        phone_.Send();
    }
}
  1. 主函数
    • 创建手机用户,传入一个IPhone实例,xiaomi和huawei都实现了IPhone这个接口
    • 调用UsePhone方法
csharp 复制代码
static void Main(string[] args)
{
    //PhoneUse phoneUse = new PhoneUse(new XiaomiPhone());
    PhoneUse phoneUse = new PhoneUse(new HuaweiPhone()); // 换手机了,只需要修改手机对象即可
    phoneUse.UsePhone();
}

完整代码:

csharp 复制代码
using System;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            //PhoneUse phoneUse = new PhoneUse(new XiaomiPhone());
            PhoneUse phoneUse = new PhoneUse(new HuaweiPhone()); // 换手机了,只需要修改手机对象即可
            phoneUse.UsePhone();
        }

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

        class XiaomiPhone : IPhone
        {
            public void Dail()
            {
                Console.WriteLine("Dail Xiaomi Phone");
            }

            public void PickUp()
            {
                Console.WriteLine("PickUp Xiaomi Phone");
            }

            public void Receive()
            {
                Console.WriteLine("Receive Xiaomi Phone");
            }

            public void Send()
            {
                Console.WriteLine("Send Xiaomi Phone");
            }
        }

        class HuaweiPhone : IPhone
        {
            public void Dail()
            {
                Console.WriteLine("Dail Huawei Phone");
            }

            public void PickUp()
            {
                Console.WriteLine("PickUp Huawei Phone");
            }

            public void Receive()
            {
                Console.WriteLine("Receive Huawei Phone");
            }

            public void Send()
            {
                Console.WriteLine("Send Huawei Phone");
            }
        }

        class PhoneUse 
        {
            private IPhone phone_;
            public PhoneUse(IPhone phone)
            {
                this.phone_ = phone;
            }

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

在代码当中,如果有可以替换的地方,那么就一定有接口的存在

接口就是为了解耦合而生的

松耦合最大的好处:让功能的提供方变得可替换,从而降低功能的提供方不能被替换所带来的高风险和高成本

  • 高风险:服务的提供方本身有毛病,会导致依赖在它上面的其他类都不可以工作
  • 高成本:服务提供方的程序员开发进度慢了,有可能导致整个团队的工作受阻
    故开发的时候要尽可能的追求松耦合

四、依赖反转(依赖倒置)原则

解耦在代码中的表现就是依赖反转

依赖反转(dependency inversion principle)

依赖:服务的使用者和服务的提供者之间的依赖关系,服务的使用者依赖在服务的提供者上

依赖越直接,耦合越紧密;服务的提供者出现问题时,服务的使用者也会出现相应问题

人类解决问题的思维方式:自顶向下逐步求精

大问题的解决有赖于下面的小问题和中问题的解决,调用者依赖于功能的提供者之上的,主要箭头的方向,被依赖的放在下面

依赖反转(依赖倒置):提供一种新的思路,来平衡这种自顶向下逐步求精的这一单一思维方式

案例

无接口、无依赖倒置时的情况

  • 司机Driver依赖车Car
    • Driver类有个Car字段,调用Driver方法时会调用Car的Run方法
    • Driver和Car是解耦合的
  • Trucker和Truck也是解耦合的
  • Racer和RaceCar也是解耦合的
    问题:让Driver去试开Truck,传不过去,就相当于上面的求和和求平均值,用处理int数组的方法去处理ArrayList是有问题的

平常开Car,特殊情况也会开Truck,按无接口、无依赖倒置时的情况,Truck是没办法开的

通过接口实现依赖反转

创建IVehicle接口,接口有个Run方法;让Car和Truck类去实现IVehicle接口,这样Car

和Truck类里面也都有了Run方法

让Driver的字段不再是具体的Car或者Truck,而是IVehicle接口类型字段,这样就可以把Car类型的实例或Truck类型的实例交给Driver去引用,这样Driver去调用Driver方法时,访问的是IVehicle里面的Run方法,根据多态原则,最后给的是Car实例就调用Car的Run,给的Truck实例就调用Truck的Run

这样就利用接口把Driver和各种各样的车解耦了

当类实现一个接口时,类和接口之间的关系也是紧耦合

Car和Truck去实现IVehicle接口,这个箭头方向发送了变化,箭头的线有之前的向下,改成了现在的向上,依赖关系发生了反转,从图上看反转的是箭头

继续优化

当有多个服务的使用者和多个服务的提供者,都遵循一个接口时,就可以多种配对

Car和Truck两个服务的提供者,都实现了IVehicle接口

DriverBase抽象类,里面有IVehicle接口类型的字段,还有一个Drive方法

从这个DriverBase抽象类中派生出两个子类,一个是普通的Driver另一个是AIDriver,都有IVehicle接口类型字段,在Drive时,会分别调用自己的IVehicle类型的实例的Run方法

通过这样的桥接,Driver即可以开普通的车Car,也可以开卡车Truck

AIDriver可以开Car,也可以开Truck

再进一步优化就是设计模式了

五、单元测试(了解)

单元测试就是依赖反转的直接受益者

案例:生成电扇的厂家,里面有电源,电扇转的快输出高电流,转速慢输出低电流

电扇有自我保护功能,电流过大会断开或警报

分析:依赖关系------电扇依赖在电源上

先按常规,写个紧耦合,然后再通过接口把耦合松开,最后再进行单元测试

常规情况------紧耦合

  1. 电源类PowerSupply
    • 返回值为int的GetPower方法,电源是固定电源,永远输出固定值100
  2. 电扇类DeskFan
    • private的PowerSupply类型的字段
    • 构造函数中传入PowerSupply对象
    • 返回值为void的Work方法,负责拿到PowerSupply类中的电源值,根据这个电源值进行对应判断输出
  3. 主函数
    • 创建一个DeskFan对象,传入PowerSupply对象
    • 调用DeskFan对象的Work方法
csharp 复制代码
using System;

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

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

        class DeskFan
        {
            private PowerSupply powerSupply_;
            public DeskFan(PowerSupply powerSupply)
            {
                this.powerSupply_ = powerSupply;
            }
            public string Work()
            {
                int power = powerSupply_.GetPower();
                if (power <= 0) return $"Power is {power}. Won't work.";
                else if (power < 100) return $"Power is {power}. Slow.";
                else if (power < 200) return $"Power is {power}. Working.";
                else return "Warning.";
            }
        }
    }
}

可以看到,若要改变状态,只能去修改PowerSupply的GetPower方法

若有其他设备用到这个PowerSupply,则修改后会影响到其他设备工作

故,引入接口,进行解耦

接口------解耦合

  1. 定义接口IPowerSupply,包含返回值为int的GetPower方法,因为是接口,故不需要实现
  2. PowerSupply类去实现IPowerSupply接口
  3. DeskFan类中PowerSupply改为IPowerSupply接口
csharp 复制代码
using System;

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

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

        public class DeskFan
        {
            private IPowerSupply powerSupply_;
            public DeskFan(IPowerSupply powerSupply)
            {
                this.powerSupply_ = powerSupply;
            }
            public string Work()
            {
                int power = powerSupply_.GetPower();
                if (power <= 0) return $"Power is {power}. Won't work.";
                else if (power < 100) return $"Power is {power}. Slow.";
                else if (power < 200) return $"Power is {power}. Working.";
                else return "Warning.";
            }
        }
    }
}

单元测试

打开测试资源管理器,测试------测试资源管理器

添加一个测试项目

添加一个xUnit测试项目

一般测试项目命名:被测试项目名字.Tests,例如这里的ConsoleApp1.Tests

目前本人工作上暂时用不到这个,故作为了解,不再详细学习了,需要了解的同学们可以自行网上查阅资料学习

六、总结

接口方法是虚线------纯虚方法

abstract方法是长虚线------比纯虚方法稍微实现了一点,但没有完全实现

virtual方法、override方法和普通方法都是实线------地地道道已经有逻辑了,有方法体

  1. 接口方法无论由哪些类来实现它时,方法的修饰符一定是public
  2. 抽象类到virtual方法 或者 抽象类到非virtual的实现方法,要加override
  3. virtual方法到override方法也要加override
  4. 有个接口,接口里面有方法,想直接拿一个类来实现它,看到的那个方法一定是public
  5. 用一个抽象类去实现一个接口,看到的方法一定是public abstract方法
  6. 用一个类实现接口方法,并且这个方法想让它可以被其他子类重写,看到的方法时public virtual方法
  7. 被重写后看到的方法是public override
相关推荐
新手小新2 小时前
C#学习笔记1-在VS CODE部署C#开发环境
笔记·学习·c#
rockey6275 小时前
AScript动态脚本多语言环境支持
sql·c#·.net·script·eval·function·动态脚本
ou.cs6 小时前
c# SemaphoreSlim保姆级教程
开发语言·网络·c#
龙侠九重天6 小时前
ML.NET 实战:快速构建分类模型
分类·数据挖掘·c#·.net
fengyehongWorld7 小时前
C# 创建Worker,杀死指定程序的线程
c#
Nuopiane11 小时前
C#基础(1)堆栈、GC与Marshal
unity·c#
FuckPatience11 小时前
Visual Studio C# 项目中文件后缀简介
开发语言·c#
游乐码19 小时前
c#泛型约束
开发语言·c#
hoiii18719 小时前
C# 基于 LumiSoft 实现 SIP 客户端方案
前端·c#