什么是接口和抽象类?

谨记:设计严谨的软件重要的标准就是需要经的起测试,一个程序好不好被测试,测试发现问题能不能被良好的修复,程序状况能否被监控,这都有赖于对抽象类和接口的正确使用。

接口和抽象类,是高阶面向对象设计的起点。想要学习设计模式,必须有着对抽象类和接口的良好认知,和SOLID的认知,并在日常工作中正确的使用他们。

 

先简述一下SOLID的特指的五种原则,优秀的设计模式,都是参考于这五种原则的实现;

SOLID:

SRP: Single Responsibility Principle 单一职责原则

OCP: Open Closed Principle 开闭原则

LSP: Liskov Substitution Principle 里氏替换原则

ISP:  Interface Segregation Principle 接口隔离原则

DIP: Dependency Inversion Principle 依赖反转原则

 

什么是接口和抽象类:

  • 接口和抽象类都是 “软件工程产物”
  • 具体类——抽象类——接口:越来越抽象,内部实现的东西越来越少
  • 抽象类是未完全实现逻辑的类(可以有字段和非public成员,他们代表了 “具体逻辑”)
  • 抽象类为复用而生:专门作为基类来使用,也具有解耦的功能。
  • 抽象类封装的是确定的开放的是不确定的,推迟到合适的子类中去实现。
  • 接口是完全未实现逻辑的 “类”,(“纯虚类”;只有函数成员;成员默认为public且不能为private) 但在新的C#版本中,接口也能拥有属性,索引,事件,和方法的默认实现。
  • 接口为解耦而生:“高内聚,底耦合”,方便单元测试。
  • 接口是一个 “协约”,它规定你必须有什么。
  • 它们都不能实例化,只能用来声明变量,引用具体类的实例。

 

为做基类而生的 “抽象类”与 “开放/关闭原则”: (所有的规则都是为了更好的协作)

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

错误的功能封装实现:违反了开闭原则,每当新增功能就会新增代码

    class Program
    {
        static void Main(string[] args)
        {
            Vehicle vehicle = new Car();
            vehicle.Run("car");
        }
    }

    class Vehicle
    {
        public void Stop()
        {
            Console.WriteLine("stopped!");
        }

        public void Fill()
        {
            Console.WriteLine("Pay and fill...");
        }

        public void Run(string type)
        {
            //这时候又来一辆车 我们又得加代码了 破坏了 开闭原则
            switch (type)
            {
                case "car":
                    Console.WriteLine("car is running...");
                    break;
                case "truck":
                    Console.WriteLine("truck is running...");
                    break;
                default:
                    break;
            }
        }

    }

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

    class Truck : Vehicle
    {
        public void Run()
        {
            Console.WriteLine("truck is running...");
        }
    }
View Code

利用多态的机制优化上述代码:

class Program
{
    static void Main(string[] args)
    {
        Vehicle vehicle = new Car();
        vehicle.Run();
    }
}

class Vehicle
{
    public void Stop()
    {
        Console.WriteLine("stopped!");
    }

    public void Fill()
    {
        Console.WriteLine("Pay and fill...");
    }

    public virtual void Run()
    {
        Console.WriteLine("vehicle is running...");
    }

}

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

class Truck : Vehicle
{
    public override void Run()
    {
        Console.WriteLine("truck is running...");
    }
}
多态优化

我们可以发现vehicle的Run方法,我们基本上很少能用到,那我们去掉它的方法实现,一个虚方法却没有函数实现,这不就是纯虚方法了吗? 在C#中,纯虚方法的替代类型是abstract。

在现在这个方法中,它封装了确定的方法,开放了不确定的方法,符合了抽象类的设计,也遵循了开闭/原则。

class Program
{
    static void Main(string[] args)
    {
        Vehicle vehicle = new Car();
        vehicle.Run();
    }
}

//封装出了确定的方法 开放了不确定的方法
abstract class Vehicle
{
    public void Stop()
    {
        Console.WriteLine("stopped!");
    }

    public void Fill()
    {
        Console.WriteLine("Pay and fill...");
    }

    public abstract void Run();
}

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

class Truck : Vehicle
{
    public override void Run()
    {
        Console.WriteLine("truck is running...");
    }
}
抽象类优化

这个时候我们如果想要新增一个类型的汽车,只需要继承并实现它的抽象方法即可,这更符合开闭/原则。

现在我们让VehicleBase做为纯抽象类,这在C#中是一种常见的模式,我们可以分“代”的来完成开放的不确定方法,让方法慢慢变得清晰和确定。

class Program
{
    static void Main(string[] args)
    {
        Vehicle vehicle = new Car();
        vehicle.Run();
    }
}

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

abstract class Vehicle : VehicleBase
{
    public override void Fill()
    {
        Console.WriteLine("pay and fill...");
    }

    public override void Stop()
    {
        Console.WriteLine("stopped!");
    }
}

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

class Truck : Vehicle
{
    public override void Run()
    {
        Console.WriteLine("truck is running...");
    }
}
纯抽象类的优化

让我们在思考下去,纯抽象类,不就是接口的默认实现模式吗,我们将纯抽象类改成接口。

class Program
{
    static void Main(string[] args)
    {
        Vehicle vehicle = new Car();
        vehicle.Run();
    }
}

interface IVehicle
{
    void Stop();
    void Fill();
    void Run();
}

abstract class Vehicle : IVehicle
{
    public void Fill()
    {
        Console.WriteLine("pay and fill...");
    }

    public void Stop()
    {
        Console.WriteLine("stopped!");
    }
    abstract public void Run();
}

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

class Truck : Vehicle
{
    public override void Run()
    {
        Console.WriteLine("truck is running...");
    }
}
接口的优化

这是不是就熟悉多了,这是一种良好的设计方法了。

接口是一种契约

他不仅仅约束提供服务方应如何去实现这个服务,例如需要返回一个什么样的结果。也约束了消费方如何去消费这个服务,例如你应该提供怎么样的操作。

现在没有接口,我们要实现来自于两个消费者的需求,这两个需求的内部逻辑都是一样的,给一串数字求和,求平均数,但是因为他们的产品不同,所以入参不同。

static void Main(string[] args)
        {
            ArrayList nums1 = new ArrayList() { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
            int[] nums2 = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

            Console.WriteLine(Sum(nums1));
            Console.WriteLine(Avg(nums1));

            Console.WriteLine(Sum(nums2));
            Console.WriteLine(Avg(nums2));
        }

        static int Sum(int[] nums)
        {
            int result = 0;
            foreach (int x in nums) result += x;
            return result;
        }

        static int Avg(int[] nums)
        {
            int result = 0;
            foreach (int x in nums) result += x;
            return result / nums.Length;
        }

        static int Sum(ArrayList nums)
        {
            int result = 0;
            foreach (int x in nums) result += x;
            return result;
        }

        static int Avg(ArrayList nums)
        {
            int result = 0;
            foreach (int x in nums) result += x;
            return result / nums.Count;
        }
无接口消费

但我们程序员观察了内部的逻辑,发现这两个参数在内部都使用了foreach,突然灵光一闪,能用foreach实现迭代不就证明他们都实现了IEnumerable接口吗,于是优化了代码:

static void Main(string[] args)
        {
            ArrayList nums1 = new ArrayList() { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
            int[] nums2 = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

            Console.WriteLine(Sum(nums1));
            Console.WriteLine(Avg(nums1));

            Console.WriteLine(Sum(nums2));
            Console.WriteLine(Avg(nums2));
        }

        static int Sum(IEnumerable nums)
        {
            int result = 0;
            foreach (int x in nums) result += x;
            return result;
        }

        static int Avg(IEnumerable nums)
        {
            int result = 0;
            int count = 0;
            foreach (int x in nums) { result += x; count++; }
            return result / count;
        }
利用IEnumerable

没错,这其实也是多态的一种实现。

接口的解耦

紧耦合:一件需要警惕的事情,现在有这样一个场景,你负责开发了一个引擎功能,在其他团队负责的小车功能中,他们需要你的引擎功能作为基础,想想看这个时候如果你的功能出了错误,他们的工作也就白做了,不仅如此,他们工作的进度,也取决于你的开发进度。

我们使用接口来让耦合变得更松弛,现在我们都在使用手机,但是手机的品牌有很多供我们选择,但他们都为我们提供了相同的服务,这些服务便可以被接口所决定和约束,你必须要有打电话,接电话,发送短信的功能等等,你才能称的上是手机。并且我们使用手机不可能只使用这一个手机,我们可能会使用不同的手机,而换手机这个操作也不能让我们受到影响。

static void Main(string[] args)
{
    var userPhone = new UserPhone(new Xiaomi());
    userPhone.Use();
}

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

    public void Use()
    {
        _phone.Send();
        _phone.Call();
        _phone.Recived();
    }
}


interface IPhone
{
    void Call();
    void Send();
    void Recived();
}

class Huawei : IPhone
{
    public void Call()
    {
        Console.WriteLine("Huawei call");
    }

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

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

class Xiaomi : IPhone
{
    public void Call()
    {
        Console.WriteLine("Xiaomi call");
    }

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

    public void Send()
    {
        Console.WriteLine("Xiaomi Send");
    }
}
使用接口解耦合

依赖反转原则

我们在日常编程中,我们惯性的自顶向下的思维,会将大问题分解成一个个小的问题,可能会产生一层层的耦合。

在这个例子中,耦合度很高,driver只能驾驶Car,而不能驾驶其他汽车。

  引入IVehicle接口,将车的类型于Driver解耦,现在Diver可以利用多态来掌控不同类型的汽车。注意箭头的指向!现在依赖反转了。

 引入抽象类,作为Driver们的基类,它依赖了IVehicle,现在组合方式成了 2 X 2,Driver们和Car耦合大大降低了。

接口提供的易测试性

现在有一个小例子,厂家生产了电风扇,电风扇不同的电流对应了电风扇不同的状态。我们试着来一步步优化并测试这个方法。

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

        class PowerSupply
        {
            public int GetPower() => 100;
        }

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

            public void Work()
            {
                int power = _powerSupply.GetPower();
                if (power <= 0)
                {
                    Console.WriteLine("Don't Work");
                }
                else if (power < 100)
                {
                    Console.WriteLine("Low");
                }
                else if(power < 200)
                {
                    Console.WriteLine("Work Fine");
                }
                else
                {
                    Console.WriteLine("Warning");
                }
               
            }
        }
       
    }
类耦合不易测试

引入接口解耦合:

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


        interface IPowerSupply
        {
            int GetPower();
        }

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

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

            public void Work()
            {
                int power = _powerSupply.GetPower();
                if (power <= 0)
                {
                    Console.WriteLine("Don't Work");
                }
                else if (power < 100)
                {
                    Console.WriteLine("Low");
                }
                else if(power < 200)
                {
                    Console.WriteLine("Work Fine");
                }
                else
                {
                    Console.WriteLine("Warning");
                }
               
            }
        }
       
    }
接口接触耦合

接下来我们写了一个xUnit测试项目,对现有接口进行测试。

public class DeskFanTest
    {
           
        [Fact]
        public void PowerSupplyThenZero_Ok()
        {
            var fan = new DeskFan(new PowerSupplyThenZero());
            var expected = "Work Fine";

            var actual = fan.Work();

            Assert.Equal(expected, actual);
        }
      
        [Fact]
        public void PowerSupplylessThen200_Bad()
        {
            var fan = new DeskFan(new PowerSupplyThen200());
            var expected = "Work Fine";

            var actual = fan.Work();

            Assert.Equal(expected, actual);
        }
    }

    class PowerSupplyThenZero : IPowerSupply
    {
        public int GetPower()
        {
            return 140;
        }
    }

    class PowerSupplyThen200 : IPowerSupply
    {
        public int GetPower()
        {
            return 210;
        }
    }
}
测试接口

但我们这个测试项目其实还是有问题的,我们的测试用例都需要一个个来创建,比较麻烦,我们这里引入Mock。

public class DeskFanTest
    {           
        [Fact]
        public void PowerSupplyThenZero_Ok()
        {
            var mockPower = new Mock<IPowerSupply>();
            mockPower.Setup(s=>s.GetPower()).Returns(100);

            var fan = new DeskFan(mockPower.Object);
            var expected = "Work Fine";

            var actual = fan.Work();

            Assert.Equal(expected, actual);
        }
      
        [Fact]
        public void PowerSupplylessThen200_Bad()
        {
            var mockPower = new Mock<IPowerSupply>();
            mockPower.Setup(s => s.GetPower()).Returns(300);
            var fan = new DeskFan(mockPower.Object);
            var expected = "Warning";

            var actual = fan.Work();

            Assert.Equal(expected, actual);
        }
    } 
Mock测试

具有良好的测试性对于一个持续集成的环境也是有很大帮助的,每当我们 check in 代码时,持续集成工具都会先 run一遍测试项目,如果这一次的代码 check in 使之前的测试项目不能通过了,那这一次 check in 则是失败的。

 

接口的D/I原则

我们说到接口是服务提供方和服务消费方之间的协议,那现在服务提供方提供了消费方在规定协议中的功能接口,但消费方对功能的需求的软性需求,则有可能服务提供方还多提供了服务,但这些服务可能永远都没有被用到,那么这个接口就太胖了,造成这种原因可能是一个接口中包含了多个其他场景的功能,那么它的设计违反了接口隔离原则的。而实现了这个接口的类也违反了单一职责原则

以下的这个例子:声明了两个接口IVehicle和ITank,但是呢这两个接口中都封装了相同的Run方法,并且ITank的Fire方法也未曾被使用过(并且Fire应该是属于武器部分),还有呢就是我们如果想让Driver开坦克,就必须要该代码了。所以这个接口可以被重新隔离。

public class Program
    {
        public static void Main(string[] args)
        {
            Driver driver= new Driver(new Car());
            driver.Run();
        }

        class Driver
        {
            private readonly IVehicle _vehicle;
            public Driver(IVehicle vehicle)
            {
                _vehicle = vehicle;
            }

            public void Run()
            {
                _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 ITank
        {
            void Run();
            void Fire();
        }

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

            public void Run()
            {
                Console.WriteLine("small tank is running...");
            }
        }

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

            public void Run()
            {
                Console.WriteLine("big tank is running...");
            }
        }
    }
违反接口隔离原则

我们引入IWeapon接口来隔离Fire和Run,并且让ITank也遵守IVehicle的规定,那么Driver就能把Tank当Car开了。

public class Program
    {
        public static void Main(string[] args)
        {
            Driver tankDriver= new Driver(new BigTank());
            tankDriver.Run();

            Driver carDriver = new Driver(new Car());
            carDriver.Run();
        }

        class Driver
        {
            private readonly IVehicle _vehicle;
            public Driver(IVehicle vehicle)
            {
                _vehicle = vehicle;
            }

            public void Run()
            {
                _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();
        }

        interface ITank : IVehicle, IWeapon
        {

        }

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

            public void Run()
            {
                Console.WriteLine("small tank is running...");
            }
        }

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

            public void Run()
            {
                Console.WriteLine("big tank is running...");
            }
        }
    }
接口隔离

 

我们用另一个例子来理解 “合适的” 接口,还记得上面我们数组元素相加的例子吗,我们想要的功能其实就是对int[]类型的元素进行迭代累加而已,不需要其他的功能,所以我们使用的不是IList或者ICollection这些 “胖接口” 因为这些里面的功能我们都用不到,所以我们提供IEnumerable接口即可。这更符合接口隔离原则,太胖的接口还会使得阻碍我们服务的提供。

public class Program
    {
        public static void Main(string[] args)
        {
            int[] nums = new int[] { 1, 2, 3, 4, 5 };
            var roc = new ReadOnlyCollection(nums);

            var result1 = Sum(nums);
            Console.WriteLine(result1); // 15

            //会出错 因为ICollection是一个胖接口
            //我们只需要使用迭代功能即可 胖接口挡住了我们合格的服务
            var result2 = Sum(roc);

            var result3 = Sum1(roc);
            Console.WriteLine(result3); //15
        }

        static int Sum(ICollection collection)
        {
            int result = 0;
            foreach (var item in collection)
            {
                result += (int)item;
            }

            return result;
        }

        static int Sum1(IEnumerable collection)
        {
            int result = 0;
            foreach (var item in collection)
            {
                result += (int)item;
            }

            return result;
        }

    }

    class ReadOnlyCollection : IEnumerable
    {
        private readonly int[] _array;

        public ReadOnlyCollection(int[] array)
        {
            _array = array;
        }

        public IEnumerator GetEnumerator()
        {
            return new Enumerator(this);
        }

        public class Enumerator : IEnumerator
        {
            private readonly ReadOnlyCollection _readOnlyCollection;
            private int _head;
            public Enumerator(ReadOnlyCollection readOnlyCollection)
            {
                _readOnlyCollection = readOnlyCollection;
                _head = -1;
            }

            public bool MoveNext()
            {
                return ++_head < _readOnlyCollection._array.Length;
            }

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

            public object Current
            {
                get
                {
                    object o = _readOnlyCollection._array[_head];
                    return o;
                }
            }
        }

    }
胖接口对有效接口的阻挡

 

对接口的进一步隔离还可以使用显示接口:现在有一个这样的例子,有一个男孩其实是一个超级英雄,他白天在学校里当学生,晚上惩奸除恶还不想被人知道。

public class Program
    {
        public static void Main(string[] args)
        {
            //student只能访问到learn方法
            var student = new Boy();
            student.Learn();

            //想要调用KillBadGuy 你需要让他变成英雄
            IHero hero = student;
            hero.KillBadGuy();
        }

        public interface IStudent
        {
            void Learn();
        }

        public interface IHero
        {
            void KillBadGuy();
        }

        public class Boy : IStudent, IHero
        {
            public void Learn()
            {
                Console.WriteLine("I am a Student Keep learn");
            }

            void IHero.KillBadGuy()
            {
                Console.WriteLine("kill the bad guy...");
            }
        }

    }
显示接口实现
posted @ 2021-10-11 23:39  zhouslthere  阅读(359)  评论(0编辑  收藏  举报