什么是接口和抽象類?


謹記:設計嚴謹的軟件重要的標准就是需要經的起測試,一個程序好不好被測試,測試發現問題能不能被良好的修復,程序狀況能否被監控,這都有賴於對抽象類和接口的正確使用。

接口和抽象類,是高階面向對象設計的起點。想要學習設計模式,必須有着對抽象類和接口的良好認知,和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...");
            }
        }

    }
顯示接口實現


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM