zoukankan      html  css  js  c++  java
  • 什么是接口和抽象类?

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

    接口和抽象类,是高阶面向对象设计的起点。想要学习设计模式,必须有着对抽象类和接口的良好认知,和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...");
                }
            }
    
        }
    显示接口实现
  • 相关阅读:
    mac安装protobuf2.4.1时报错./include/gtest/internal/gtest-port.h:428:10: fatal error: 'tr1/tuple' file not found和google/protobuf/message.cc:175:16: error: implicit instantiation of undefined template
    java基础六 [异常处理](阅读Head First Java记录)
    安装和使用iOS的包管理工具CocoaPods
    Node.js的知识点框架整理
    java基础五 [数字与静态](阅读Head First Java记录)
    java基础四 [构造器和垃圾回收](阅读Head First Java记录)
    Appium学习路-安装篇
    Dell笔记本Ubuntu无线网卡驱动安装
    Ubuntu系统使用命令禁用触摸板等输入设备
    linux(ubuntu) 查看系统设备信息
  • 原文地址:https://www.cnblogs.com/Xieyiincuit/p/15395784.html
Copyright © 2011-2022 走看看