“ โปรแกรมไปยังอินเทอร์เฟซไม่ใช่การใช้งาน” หมายความว่าอย่างไร


131

มีคนหนึ่งสะดุดกับวลีนี้เมื่ออ่านเกี่ยวกับรูปแบบการออกแบบ

แต่ฉันไม่เข้าใจมีใครช่วยอธิบายเรื่องนี้ให้ฉันได้ไหม


คำตอบ:


148

อินเทอร์เฟซเป็นเพียงสัญญาหรือลายเซ็นและพวกเขาไม่รู้อะไรเลยเกี่ยวกับการใช้งาน

การเข้ารหัสเทียบกับอินเทอร์เฟซรหัสไคลเอ็นต์จะถืออ็อบเจ็กต์อินเทอร์เฟซซึ่งจัดหาโดยโรงงานเสมอ อินสแตนซ์ใด ๆ ที่ส่งคืนโดยโรงงานจะเป็นอินเทอร์เฟซประเภทที่คลาสผู้สมัครจากโรงงานต้องนำไปใช้ ด้วยวิธีนี้โปรแกรมไคลเอ็นต์จะไม่กังวลเกี่ยวกับการนำไปใช้งานและลายเซ็นของอินเทอร์เฟซจะกำหนดสิ่งที่สามารถทำได้ สามารถใช้เพื่อเปลี่ยนลักษณะการทำงานของโปรแกรมในขณะทำงาน นอกจากนี้ยังช่วยให้คุณเขียนโปรแกรมได้ดีขึ้นจากมุมมองของการบำรุงรักษา

นี่คือตัวอย่างพื้นฐานสำหรับคุณ

public enum Language
{
    English, German, Spanish
}

public class SpeakerFactory
{
    public static ISpeaker CreateSpeaker(Language language)
    {
        switch (language)
        {
            case Language.English:
                return new EnglishSpeaker();
            case Language.German:
                return new GermanSpeaker();
            case Language.Spanish:
                return new SpanishSpeaker();
            default:
                throw new ApplicationException("No speaker can speak such language");
        }
    }
}

[STAThread]
static void Main()
{
    //This is your client code.
    ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English);
    speaker.Speak();
    Console.ReadLine();
}

public interface ISpeaker
{
    void Speak();
}

public class EnglishSpeaker : ISpeaker
{
    public EnglishSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak English.");
    }

    #endregion
}

public class GermanSpeaker : ISpeaker
{
    public GermanSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak German.");
    }

    #endregion
}

public class SpanishSpeaker : ISpeaker
{
    public SpanishSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak Spanish.");
    }

    #endregion
}

ข้อความแสดงแทน

นี่เป็นเพียงตัวอย่างพื้นฐานและคำอธิบายที่แท้จริงของหลักการอยู่นอกเหนือขอบเขตของคำตอบนี้

แก้ไข

ฉันได้อัปเดตตัวอย่างด้านบนและเพิ่มSpeakerคลาสฐานนามธรรม ในการอัปเดตนี้ฉันได้เพิ่มคุณสมบัติให้กับลำโพงทั้งหมดใน "SayHello" ผู้พูดทุกคนพูด "Hello World" นั่นเป็นคุณสมบัติทั่วไปที่มีฟังก์ชันคล้ายกัน อ้างถึงแผนภาพคลาสและคุณจะพบว่าSpeakerระดับนามธรรมใช้ISpeakerอินเตอร์เฟซและเครื่องหมายSpeak()เป็นนามธรรมซึ่งหมายความว่าการดำเนินงานแต่ละลำโพงเป็นผู้รับผิดชอบในการดำเนินการSpeak()วิธีเพราะมันแตกต่างจากการSpeaker Speakerแต่ผู้พูดทั้งหมดกล่าว "สวัสดี" เป็นเอกฉันท์ ดังนั้นในคลาส Abstract Speaker เราจึงกำหนดวิธีการที่ระบุว่า "Hello World" และSpeakerการนำไปใช้งานแต่ละครั้งจะได้มาซึ่งSayHello()วิธีการ

พิจารณากรณีที่SpanishSpeakerไม่สามารถกล่าวสวัสดีได้ในกรณีนี้คุณสามารถแทนที่SayHello()วิธีการสำหรับผู้พูดภาษาสเปนและเพิ่มข้อยกเว้นที่เหมาะสมได้

โปรดทราบว่าเราไม่ได้ทำการเปลี่ยนแปลงใด ๆ กับ Interface ISpeaker และรหัสไคลเอนต์และ SpeakerFactory ยังคงไม่ได้รับผลกระทบไม่เปลี่ยนแปลง และนี่คือสิ่งที่เราประสบความสำเร็จโดยการเขียนโปรแกรมเพื่อการเชื่อมต่อ

และเราสามารถบรรลุพฤติกรรมนี้ได้เพียงแค่เพิ่มลำโพงคลาสนามธรรมพื้นฐานและการปรับเปลี่ยนเล็กน้อยในการใช้งานแต่ละครั้งจึงทำให้โปรแกรมดั้งเดิมไม่เปลี่ยนแปลง นี่เป็นคุณสมบัติที่ต้องการของแอปพลิเคชันใด ๆ และทำให้แอปพลิเคชันของคุณสามารถบำรุงรักษาได้ง่าย

public enum Language
{
    English, German, Spanish
}

public class SpeakerFactory
{
    public static ISpeaker CreateSpeaker(Language language)
    {
        switch (language)
        {
            case Language.English:
                return new EnglishSpeaker();
            case Language.German:
                return new GermanSpeaker();
            case Language.Spanish:
                return new SpanishSpeaker();
            default:
                throw new ApplicationException("No speaker can speak such language");
        }
    }
}

class Program
{
    [STAThread]
    static void Main()
    {
        //This is your client code.
        ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English);
        speaker.Speak();
        Console.ReadLine();
    }
}

public interface ISpeaker
{
    void Speak();
}

public abstract class Speaker : ISpeaker
{

    #region ISpeaker Members

    public abstract void Speak();

    public virtual void SayHello()
    {
        Console.WriteLine("Hello world.");
    }

    #endregion
}

public class EnglishSpeaker : Speaker
{
    public EnglishSpeaker() { }

    #region ISpeaker Members

    public override void Speak()
    {
        this.SayHello();
        Console.WriteLine("I speak English.");
    }

    #endregion
}

public class GermanSpeaker : Speaker
{
    public GermanSpeaker() { }

    #region ISpeaker Members

    public override void Speak()
    {
        Console.WriteLine("I speak German.");
        this.SayHello();
    }

    #endregion
}

public class SpanishSpeaker : Speaker
{
    public SpanishSpeaker() { }

    #region ISpeaker Members

    public override void Speak()
    {
        Console.WriteLine("I speak Spanish.");
    }

    public override void SayHello()
    {
        throw new ApplicationException("I cannot say Hello World.");
    }

    #endregion
}

ข้อความแสดงแทน


19
การเขียนโปรแกรมไปยังอินเทอร์เฟซไม่เพียง แต่เกี่ยวกับประเภทของตัวแปรอ้างอิงเท่านั้น นอกจากนี้ยังหมายความว่าคุณจะไม่ใช้สมมติฐานโดยปริยายใด ๆ เกี่ยวกับการใช้งานของคุณ ตัวอย่างเช่นถ้าคุณใช้เป็นชนิดที่ยังคงของคุณจะยังคงได้สมมติว่าเข้าถึงโดยสุ่มเป็นไปอย่างรวดเร็วด้วยซ้ำเรียกList get(i)
Joachim Sauer

16
โรงงานตั้งฉากกับการเขียนโปรแกรมกับอินเทอร์เฟซ แต่ฉันคิดว่าคำอธิบายนี้ทำให้ดูเหมือนว่าเป็นส่วนหนึ่งของมัน
.

@ ตูน: เห็นด้วยกับคุณ. ฉันต้องการให้ตัวอย่างพื้นฐานและเรียบง่ายสำหรับการเขียนโปรแกรมไปยังอินเทอร์เฟซ ฉันไม่ต้องการสร้างความสับสนให้กับผู้ถามด้วยการใช้อินเทอร์เฟซ IFlyable กับคลาสนกและสัตว์ไม่กี่ตัว
นี่ __curious_geek

@นี้. ถ้าฉันใช้คลาสนามธรรมหรือรูปแบบซุ้มแทนจะยังคงเรียกว่า "โปรแกรมไปยังอินเทอร์เฟซ" หรือไม่? หรือฉันต้องใช้อินเทอร์เฟซอย่างชัดเจนและนำไปใช้กับคลาส?
never_had_a_name

1
คุณใช้เครื่องมือ uml อะไรในการสร้างภาพ
Adam Arold

29

คิดว่าอินเทอร์เฟซเป็นสัญญาระหว่างออบเจ็กต์กับลูกค้า นั่นคืออินเทอร์เฟซระบุสิ่งที่วัตถุสามารถทำได้และลายเซ็นสำหรับการเข้าถึงสิ่งเหล่านั้น

การนำไปปฏิบัติเป็นพฤติกรรมที่แท้จริง ตัวอย่างเช่นคุณมี method sort () คุณสามารถใช้ QuickSort หรือ MergeSort สิ่งนี้ไม่ควรมีความสำคัญกับการเรียกรหัสไคลเอ็นต์ตราบใดที่อินเทอร์เฟซไม่เปลี่ยนแปลง

ไลบรารีเช่น Java API และ. NET Framework ใช้อินเทอร์เฟซเป็นจำนวนมากเนื่องจากโปรแกรมเมอร์หลายล้านคนใช้อ็อบเจ็กต์ที่ให้มา ผู้สร้างไลบรารีเหล่านี้จะต้องระมัดระวังอย่างยิ่งที่จะไม่เปลี่ยนอินเทอร์เฟซไปยังคลาสในไลบรารีเหล่านี้เนื่องจากจะส่งผลกระทบต่อโปรแกรมเมอร์ทุกคนที่ใช้ไลบรารี ในทางกลับกันพวกเขาสามารถเปลี่ยนการใช้งานได้มากเท่าที่ต้องการ

หากในฐานะโปรแกรมเมอร์คุณเขียนโค้ดกับการนำไปใช้งานทันทีที่มีการเปลี่ยนแปลงโค้ดของคุณจะหยุดทำงาน ลองนึกถึงประโยชน์ของอินเทอร์เฟซด้วยวิธีนี้:

  1. มันซ่อนสิ่งที่คุณไม่จำเป็นต้องรู้ทำให้วัตถุใช้งานง่ายขึ้น
  2. มันให้สัญญาว่าวัตถุจะทำงานอย่างไรเพื่อให้คุณสามารถพึ่งพาสิ่งนั้นได้

หมายความว่าคุณต้องตระหนักถึงสิ่งที่คุณกำลังทำสัญญากับออบเจ็กต์ที่จะทำ: ในตัวอย่างมีให้คุณทำสัญญาเฉพาะการจัดเรียงเท่านั้นไม่จำเป็นต้องเป็นการจัดเรียงที่มั่นคง
penguat

คล้ายกับที่เอกสารของไลบรารีไม่ได้กล่าวถึงการนำไปใช้งาน แต่เป็นเพียงคำอธิบายของอินเทอร์เฟซคลาสที่รวมอยู่
Joe Iddon

17

หมายความว่าคุณควรพยายามเขียนโค้ดของคุณเพื่อให้ใช้นามธรรม (คลาสนามธรรมหรืออินเทอร์เฟซ) แทนการนำไปใช้โดยตรง

โดยปกติการใช้งานจะถูกแทรกลงในโค้ดของคุณผ่านตัวสร้างหรือการเรียกใช้เมธอด ดังนั้นรหัสของคุณจึงรู้เกี่ยวกับอินเทอร์เฟซหรือคลาสนามธรรมและสามารถเรียกอะไรก็ได้ที่กำหนดไว้ในสัญญานี้ เนื่องจากมีการใช้อ็อบเจ็กต์จริง (การใช้อินเทอร์เฟซ / คลาสนามธรรม) การโทรจะดำเนินการกับอ็อบเจ็กต์

นี่คือชุดย่อยของLiskov Substitution Principle(LSP), L ของSOLIDหลักการ

ตัวอย่างใน. NET จะใช้รหัสIListแทนListหรือDictionaryดังนั้นคุณสามารถใช้คลาสใดก็ได้ที่ใช้IListแทนกันในโค้ดของคุณ:

// myList can be _any_ object that implements IList
public int GetListCount(IList myList)
{
    // Do anything that IList supports
    return myList.Count();
}

อีกตัวอย่างหนึ่งจาก Base Class Library (BCL) คือProviderBaseคลาสนามธรรมซึ่งมีโครงสร้างพื้นฐานบางอย่างและที่สำคัญหมายความว่าการใช้งานของผู้ให้บริการทั้งหมดสามารถใช้แทนกันได้หากคุณเขียนโค้ดกับมัน


แต่ไคลเอนต์จะโต้ตอบกับอินเทอร์เฟซและใช้วิธีการว่างเปล่าได้อย่างไร?
never_had_a_name

1
ไคลเอนต์ไม่โต้ตอบกับอินเทอร์เฟซ แต่ผ่านอินเทอร์เฟซ :) อ็อบเจ็กต์โต้ตอบกับอ็อบเจ็กต์อื่นผ่านเมธอด (ข้อความ) และอินเทอร์เฟซเป็นภาษาประเภทหนึ่ง - เมื่อคุณรู้ว่าอ็อบเจ็กต์บางอย่าง (บุคคล) ใช้ (พูด) อังกฤษ (IList ) คุณสามารถใช้มันได้โดยไม่จำเป็นต้องรู้เพิ่มเติมเกี่ยวกับวัตถุนั้น (เขาก็เป็นคนอิตาลีเช่นกัน) เพราะมันไม่จำเป็นในบริบทนั้น (หากคุณต้องการขอความช่วยเหลือคุณไม่จำเป็นต้องรู้ว่าเขาพูดภาษาอิตาลีด้วย ถ้าคุณเข้าใจภาษาอังกฤษ)
Gabriel Ščerbák

BTW หลักการแทนที่ของ IMHO Liskov เป็นเรื่องเกี่ยวกับความหมายของการสืบทอดและไม่มีส่วนเกี่ยวข้องกับอินเทอร์เฟซซึ่งสามารถพบได้ในภาษาที่ไม่มีการสืบทอด (ไปจาก Google)
Gabriel Ščerbák

5

หากคุณต้องการเขียนคลาสรถยนต์ในยุคสันดาป - รถยนต์มีโอกาสมากที่คุณจะใช้ oilChange () เป็นส่วนหนึ่งของคลาสนี้ แต่เมื่อมีการเปิดตัวรถยนต์ไฟฟ้าคุณจะประสบปัญหาเนื่องจากไม่มีการเปลี่ยนถ่ายน้ำมันสำหรับรถยนต์เหล่านี้และไม่มีการนำไปใช้

วิธีแก้ปัญหาคือต้องมีอินเตอร์เฟส performMaintenance () ในคลาส Car และซ่อนรายละเอียดไว้ในการใช้งานที่เหมาะสม รถแต่ละประเภทจะจัดเตรียมการใช้งานของตัวเองสำหรับ performMaintenance () ในฐานะเจ้าของรถสิ่งที่คุณต้องจัดการคือการดำเนินการบำรุงรักษา () และไม่ต้องกังวลกับการปรับตัวเมื่อมีการเปลี่ยนแปลง

class MaintenanceSpecialist {
    public:
        virtual int performMaintenance() = 0;
};

class CombustionEnginedMaintenance : public MaintenanceSpecialist {
    int performMaintenance() { 
        printf("combustionEnginedMaintenance: We specialize in maintenance of Combustion engines \n");
        return 0;
    }
};

class ElectricMaintenance : public MaintenanceSpecialist {
    int performMaintenance() {
        printf("electricMaintenance: We specialize in maintenance of Electric Cars \n");
        return 0;
    }
};

class Car {
    public:
        MaintenanceSpecialist *mSpecialist;
        virtual int maintenance() {
            printf("Just wash the car \n");
            return 0;
        };
};

class GasolineCar : public Car {
    public: 
        GasolineCar() {
        mSpecialist = new CombustionEnginedMaintenance();
        }
        int maintenance() {
        mSpecialist->performMaintenance();
        return 0;
        }
};

class ElectricCar : public Car {
    public: 
        ElectricCar() {
             mSpecialist = new ElectricMaintenance();
        }

        int maintenance(){
            mSpecialist->performMaintenance();
            return 0;
        }
};

int _tmain(int argc, _TCHAR* argv[]) {

    Car *myCar; 

    myCar = new GasolineCar();
    myCar->maintenance(); /* I dont know what is involved in maintenance. But, I do know the maintenance has to be performed */


    myCar = new ElectricCar(); 
    myCar->maintenance(); 

    return 0;
}

คำอธิบายเพิ่มเติม: คุณเป็นเจ้าของรถที่มีรถยนต์หลายคัน คุณแกะออกบริการที่คุณต้องการจ้าง ในกรณีของเราเราต้องการจ้างงานซ่อมบำรุงรถยนต์ทั้งหมด

  1. คุณระบุสัญญา (อินเทอร์เฟซ) ที่เป็นประโยชน์สำหรับรถยนต์และผู้ให้บริการทั้งหมดของคุณ
  2. ผู้ให้บริการมีกลไกในการให้บริการ
  3. คุณไม่ต้องการกังวลเกี่ยวกับการเชื่อมโยงประเภทรถกับผู้ให้บริการ คุณเพียงระบุเวลาที่คุณต้องการกำหนดเวลาการบำรุงรักษาและเรียกใช้ บริษัท ผู้ให้บริการที่เหมาะสมควรเข้ามาดำเนินการบำรุงรักษา

    แนวทางอื่น

  4. คุณระบุการทำงาน (อาจเป็นอินเทอร์เฟซอินเทอร์เฟซใหม่) ที่ดีสำหรับรถยนต์ทุกคันของคุณ
  5. คุณมีกลไกในการให้บริการ โดยทั่วไปคุณจะให้การใช้งาน
  6. คุณเรียกร้องงานและลงมือทำเอง ที่นี่คุณจะทำงานบำรุงรักษาที่เหมาะสม

    ข้อเสียของแนวทางที่ 2 คืออะไร? คุณอาจไม่ใช่ผู้เชี่ยวชาญในการค้นหาวิธีที่ดีที่สุดในการบำรุงรักษา งานของคุณคือขับรถและสนุกกับมัน ไม่ต้องอยู่ในธุรกิจของการบำรุงรักษามัน

    ข้อเสียของแนวทางแรกคืออะไร? มีค่าใช้จ่ายในการหา บริษัท ฯลฯ เว้นแต่คุณจะเป็น บริษัท รถเช่าอาจไม่คุ้มค่ากับความพยายาม


4

คำแถลงนี้เกี่ยวกับการมีเพศสัมพันธ์ เหตุผลหนึ่งที่เป็นไปได้สำหรับการใช้โปรแกรมเชิงวัตถุคือการนำกลับมาใช้ใหม่ ตัวอย่างเช่นคุณสามารถแยกอัลกอริทึมของคุณออกเป็นสองอ็อบเจ็กต์ที่ทำงานร่วมกัน A และ B สิ่งนี้อาจมีประโยชน์สำหรับการสร้างอัลกอริทึมอื่นในภายหลังซึ่งอาจนำมาใช้อย่างน้อยหนึ่งอ็อบเจ็กต์ทั้งสอง อย่างไรก็ตามเมื่อวัตถุเหล่านั้นสื่อสารกัน (ส่งข้อความ - วิธีการโทร) พวกมันจะสร้างการอ้างอิงซึ่งกันและกัน แต่ถ้าคุณต้องการใช้โดยไม่ใช้อีกชิ้นหนึ่งคุณต้องระบุสิ่งที่ควรทำสำหรับอ็อบเจ็กต์ A อื่น ๆ หากเราแทนที่ B คำอธิบายเหล่านั้นเรียกว่าอินเทอร์เฟซ สิ่งนี้ช่วยให้อ็อบเจ็กต์ A สื่อสารโดยไม่มีการเปลี่ยนแปลงกับอ็อบเจ็กต์อื่นที่อาศัยอินเทอร์เฟซ ข้อความที่คุณกล่าวถึงบอกว่าหากคุณวางแผนที่จะนำบางส่วนของอัลกอริทึมกลับมาใช้ใหม่ (หรือโดยทั่วไปคือโปรแกรม) คุณควรสร้างอินเทอร์เฟซและพึ่งพาพวกเขา


2

ดังที่คนอื่นกล่าวไว้นั่นหมายความว่ารหัสการโทรของคุณควรรู้เกี่ยวกับผู้ปกครองที่เป็นนามธรรมเท่านั้นไม่ใช่คลาสการใช้งานจริงที่จะทำงานได้

สิ่งที่ช่วยให้เข้าใจว่านี่คือเหตุผลที่คุณควรตั้งโปรแกรมให้กับอินเทอร์เฟซเสมอ มีหลายสาเหตุ แต่สองประการที่ง่ายที่สุดในการอธิบายคือ

1) การทดสอบ

สมมติว่าฉันมีรหัสฐานข้อมูลทั้งหมดในคลาสเดียว ถ้าโปรแกรมของฉันรู้เกี่ยวกับคลาสคอนกรีตฉันสามารถทดสอบโค้ดของฉันได้โดยการรันเทียบกับคลาสนั้นจริงๆ ฉันใช้ -> เพื่อหมายถึง "พูดคุยกับ"

WorkerClass -> DALClass อย่างไรก็ตามเรามาเพิ่มอินเทอร์เฟซในการผสมผสานกัน

WorkerClass -> IDAL -> DALClass

ดังนั้น DALClass จึงใช้อินเทอร์เฟซ IDAL และคลาสผู้ปฏิบัติงานจะเรียกใช้สิ่งนี้เท่านั้น

ตอนนี้ถ้าเราต้องการเขียนการทดสอบโค้ดเราสามารถสร้างคลาสง่ายๆที่ทำหน้าที่เหมือนฐานข้อมูลแทนได้

WorkerClass -> IDAL -> IFakeDAL

2) ใช้ซ้ำ

ตามตัวอย่างด้านบนสมมติว่าเราต้องการย้ายจาก SQL Server (ซึ่ง DALClass ที่เป็นรูปธรรมของเราใช้) ไปยัง MonogoDB สิ่งนี้จะต้องใช้เวลามาก แต่ไม่ใช่ถ้าเราตั้งโปรแกรมไว้ในอินเทอร์เฟซ ในกรณีนั้นเราแค่เขียนคลาส DB ใหม่และเปลี่ยน (ผ่านทางโรงงาน)

WorkerClass -> IDAL -> DALClass

ถึง

WorkerClass -> IDAL -> MongoDBClass


1

อินเทอร์เฟซอธิบายความสามารถ เมื่อเขียนโค้ดที่จำเป็นให้พูดถึงความสามารถที่คุณใช้มากกว่าประเภทหรือคลาสเฉพาะ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.