“구현이 아닌 인터페이스에 프로그래밍”이란 무엇입니까?


답변:


148

인터페이스는 단지 계약 또는 서명일 뿐이며 구현에 대해서는 아무것도 모릅니다.

인터페이스 수단에 대한 코딩으로 클라이언트 코드는 항상 팩토리에서 제공하는 Interface 객체를 보유합니다. 팩토리가 리턴 한 모든 인스턴스는 팩토리 후보 클래스가 구현해야하는 인터페이스 유형입니다. 이런 식으로 클라이언트 프로그램은 구현에 대해 걱정하지 않고 인터페이스 서명에 따라 모든 작업을 수행 할 수 있습니다. 런타임시 프로그램의 동작을 변경하는 데 사용할 수 있습니다. 또한 유지 관리 관점에서 훨씬 더 나은 프로그램을 작성하는 데 도움이됩니다.

다음은 기본적인 예입니다.

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"를 사용합니다. 이것이 유사한 기능을 가진 공통 기능입니다. 클래스 다이어그램을 참조하면 Speakerabstract 클래스 구현 ISpeaker인터페이스를 발견하고 abstract로 표시합니다. Speak()이는 각 Speaker 구현이 ~에 Speak()따라 다르므로 메소드 구현을 담당 함을 의미합니다 . 그러나 모든 화자는 만장일치로 "안녕하세요"라고 말합니다. 따라서 추상 스피커 클래스에서 "Hello World"라고하는 메소드를 정의하면 각 구현에서 메소드 가 파생됩니다 .SpeakerSpeakerSpeakerSayHello()

SpanishSpeakerHello를 말할 수없는 경우를 고려하여 SayHello()스페인어 스피커 의 메소드를 대체하고 적절한 예외를 발생시킬 수 있습니다 .

인터페이스 ISpeaker는 변경되지 않았습니다. 또한 클라이언트 코드와 SpeakerFactory도 변경되지 않은 상태로 유지됩니다. 그리고 이것이 우리가 Programming-to-Interface로 달성 한 것 입니다.

그리고 기본 추상 클래스 스피커와 각 구현에 약간의 수정을 추가하여 원래 프로그램을 변경하지 않고이 동작을 달성 할 수 있습니다. 이것은 모든 응용 프로그램에서 원하는 기능이며 응용 프로그램을 쉽게 유지 관리 할 수 ​​있습니다.

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유형을 a 로 사용하는 경우 여전히을 반복적으로 호출하여 임의 액세스가 빠르다고 가정 할 수 있습니다 get(i).
Joachim Sauer

16
팩토리는 인터페이스 프로그래밍과 직교하지만,이 설명이 마치 마치 구성 요소의 일부인 것처럼 보인다고 생각합니다.
T.

@ 툰 : 동의합니다. 인터페이스 프로그래밍에 대한 매우 기본적이고 간단한 예를 제공하고 싶었습니다. 소수의 조류 및 동물 클래스에 IFlyable 인터페이스를 구현하여 질문자를 혼동하고 싶지 않았습니다.
이것. __curious_geek

@이. 대신 추상 클래스 또는 파사드 패턴을 사용하는 경우 여전히 "인터페이스에 대한 프로그램"이라고합니까? 아니면 명시 적으로 인터페이스를 사용해야하고 클래스에서 구현해야합니까?
never_had_a_name

1
이미지를 만들기 위해 어떤 uml 도구를 사용하셨습니까?
Adam Arold

29

인터페이스를 객체와 클라이언트 간의 계약으로 생각하십시오. 즉, 인터페이스는 객체가 수행 할 수있는 작업과 해당 작업에 액세스하기위한 서명을 지정합니다.

구현은 실제 행동입니다. 예를 들어 sort () 메소드가 있다고 가정하십시오. QuickSort 또는 MergeSort를 구현할 수 있습니다. 인터페이스가 변경되지 않는 한 정렬을 호출하는 클라이언트 코드에는 중요하지 않습니다.

Java API 및 .NET Framework와 같은 라이브러리는 인터페이스를 많이 사용합니다. 수백만 명의 프로그래머가 제공된 객체를 사용하기 때문입니다. 이 라이브러리의 제작자는 라이브러리를 사용하는 모든 프로그래머에게 영향을 미치므로 이러한 라이브러리의 클래스에 대한 인터페이스를 변경하지 않도록 매우주의해야합니다. 반면에 그들은 원하는만큼 구현을 변경할 수 있습니다.

프로그래머로서 구현에 대해 코드를 작성하면 코드가 변경되는 즉시 코드 작동이 중지됩니다. 인터페이스의 이점을 다음과 같이 생각하십시오.

  1. 객체를보다 간단하게 사용하기 위해 알 필요가없는 것을 숨 깁니다.
  2. 객체의 작동 방식에 대한 계약을 제공하므로

그것은 당신이해야 할 일을 계약하고 있다는 것을 알고 있어야한다는 것을 의미합니다 : 예제에서 당신은 반드시 정렬에 대해서만 계약을하고, 반드시 안정적인 정렬은 아닙니다.
penguat

따라서 라이브러리 문서에서 구현을 언급하지 않는 방법과 마찬가지로 포함 된 클래스 인터페이스에 대한 설명 일뿐입니다.
Joe Iddon

17

즉, 구현 대신 직접 추상화 (추상 클래스 또는 인터페이스)를 사용하도록 코드를 작성해야합니다.

일반적으로 구현은 생성자 또는 메서드 호출을 통해 코드에 주입됩니다. 따라서 코드는 인터페이스 또는 추상 클래스에 대해 알고 있으며이 계약에 정의 된 모든 것을 호출 할 수 있습니다. 실제 객체 (인터페이스 / 추상 클래스 구현)가 사용됨에 따라 호출이 객체에서 작동합니다.

이것은 원칙 Liskov Substitution Principle의 L 인 (LSP) 의 부분 집합입니다 SOLID.

.NET의 예 IListList또는 대신 코드를 작성하는 Dictionary것이므로 IList코드에서 상호 교환 가능 하게 구현되는 모든 클래스를 사용할 수 있습니다 .

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

BCL (Base Class Library)의 또 다른 예는 ProviderBase추상 클래스입니다.이 클래스는 일부 인프라를 제공하며, 중요한 경우 모든 제공자 구현을 코드화 할 경우 상호 교환 가능하게 사용할 수 있습니다.


그러나 클라이언트는 어떻게 인터페이스와 상호 작용하고 빈 메소드를 사용할 수 있습니까?
never_had_a_name

1
클라이언트는 인터페이스와 상호 작용하지 않고 인터페이스를 통해 상호 작용합니다.) 객체는 메소드 (메시지)를 통해 다른 객체와 상호 작용하고 인터페이스는 일종의 언어입니다. 특정 객체 (사람)가 영어 (IList)를 구현 (말)한다는 것을 알면 ), 해당 객체에 대해 더 알 필요가 없이도 사용할 수 있습니다 (그는 또한 이탈리아 인 임). 그 맥락에서 필요하지 않기 때문에 (도움을 요청하려는 경우 그가 이탈리아어를 말하는지 알 필요가없는 경우) 영어를 이해한다면).
Gabriel Ščerbák

BTW. IMHO Liskov 대체 원칙은 상속의 시맨틱에 관한 것이며 인터페이스와는 아무런 관련이 없으며 상속되지 않은 언어에서도 찾을 수 있습니다 (Google에서 이동).
Gabriel Ščerbák

5

Combustion-Car 시대에 자동차 클래스를 작성한다면이 클래스의 일부로 oilChange ()를 구현할 가능성이 큽니다. 그러나 전기 자동차가 도입되면 이러한 자동차와 관련된 오일 교환이없고 구현이 없기 때문에 문제가 발생할 수 있습니다.

이 문제에 대한 해결책은 Car 클래스에 performMaintenance () 인터페이스를 가지고 적절한 구현 내에서 세부 사항을 숨기는 것입니다. 각 Car 유형은 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. 당신은 일을 불러 내고 스스로한다. 여기서는 적절한 유지 보수 작업을 수행합니다.

    두 번째 접근 방식의 단점은 무엇입니까? 유지 관리를 수행하는 가장 좋은 방법을 찾는 데 전문가가 아닐 수도 있습니다. 당신의 임무는 자동차를 운전하고 즐기는 것입니다. 그것을 유지하는 사업에 있지 않아야합니다.

    첫 번째 접근법의 단점은 무엇입니까? 렌터카 회사가 아니라면 노력할 가치가 없을 수 있습니다.


4

이 진술은 커플 링에 관한 것입니다. 객체 지향 프로그래밍을 사용하는 잠재적 인 이유 중 하나는 재사용입니다. 예를 들어, 두 개의 협업 오브젝트 A와 B로 알고리즘을 분할 할 수 있습니다. 이는 나중에 두 알고리즘 중 하나를 재사용 할 수있는 다른 알고리즘을 작성하는 데 유용 할 수 있습니다. 그러나 이러한 개체가 통신 할 때 (메시지 보내기-메서드 호출) 서로간에 종속성을 만듭니다. 그러나 다른 것을 사용하지 않고 하나를 사용하려면 B를 대체하면 객체 A에 대해 다른 객체 C가해야 할 일을 지정해야합니다. 이러한 설명을 인터페이스라고합니다. 이를 통해 객체 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.