“编程接口而不是实现”是什么意思?


Answers:


148

接口只是合同或签名,它们对实现一无所知。

根据接口方式进行编码,客户端代码始终包含一个由工厂提供的Interface对象。工厂返回的任何实例的类型都是任何工厂候选类都必须实现的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”。这是具有相似功能的共同特征。参考类图,您会发现Speaker抽象类实现ISpeaker接口并将其标记Speak()为抽象,这意味着每个Speaker实现都负责实现该Speak()方法,因为它从Speaker到有所不同Speaker。但是所有发言者都一致说“你好”。因此,在抽象的Speaker类中,我们定义了一个表示“ Hello World” SpeakerSayHello()方法,每个实现都将派生该方法。

考虑一种情况,SpanishSpeaker您不能说“你好”,那么在这种情况下,您可以覆盖“ SayHello()西班牙语使用者” 的方法并提出适当的例外。

请注意,我们尚未对Interface ISpeaker进行任何更改。客户端代码和SpeakerFactory也保持不变。这是我们通过“ 编程到接口”实现的

而且,我们可以通过在每个实现中简单地添加一个基本抽象类Speaker和一些小的修改来实现此行为,从而使原始程序保持不变。这是任何应用程序都需要的功能,它使您的应用程序易于维护。

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
对接口进行编程不仅与参考变量的类型有关。这也意味着您不会对实现使用任何隐式假设。例如,如果您使用a List作为类型,那么您仍然可以通过重复调用来假设随机访问是快速的get(i)
Joachim Sauer 2010年

16
工厂与接口编程正交,但是我认为这种解释似乎使它们似乎是其中的一部分。
T。

@Toon:同意你的观点。我想提供一个非常基本和简单的示例,以便进行接口编程。我不想通过在很少的鸟类和动物类上实现IFlyable接口来使提问者感到困惑。
这个。__curious_geek

@这个。如果我改用抽象类或外观模式,是否仍将其称为“接口程序”?还是我必须明确使用接口并在类上实现它?
never_had_a_name

1
您使用什么uml工具创建图像?
亚当·阿罗德

29

将接口视为对象与其客户之间的契约。也就是说,该接口指定对象可以执行的操作,以及用于访问这些操作的签名。

实现是实际的行为。举例来说,您有一个sort()方法。您可以实现QuickSort或MergeSort。只要接口不更改,这对客户端代码调用sort都没有关系。

诸如Java API和.NET Framework之类的库大量使用了接口,因为数百万的程序员使用了提供的对象。这些库的创建者必须非常小心,不要更改这些库中类的接口,因为它将影响使用该库的所有程序员。另一方面,他们可以根据自己的喜好更改实现。

如果作为程序员,您针对实现进行编码,则一旦更改,您的代码就会停止工作。因此,以这种方式考虑接口的好处:

  1. 它隐藏了您不需要知道的东西,使对象更易于使用。
  2. 它提供了对象行为方式的约定,因此您可以依靠它

这确实意味着您需要知道要收缩对象要做什么:在提供的示例中,您仅收缩某种排序,而不一定是稳定排序。
penguat 2010年

与库文档中没有提及实现的方式类似,它们只是对包含的类接口的描述。
乔·伊登

17

这意味着您应该尝试编写代码,使其使用抽象(抽象类或接口)而不是直接实现。

通常,通过构造函数或方法调用将实现注入到您的代码中。因此,您的代码了解接口或抽象类,并且可以调用此合同上定义的任何内容。当使用实际对象(接口/抽象类的实现)时,调用在该对象上进行。

这是Liskov Substitution Principle(L)SOLID原则的(LSP)的子集。

.NET中的示例将使用IList而不是Listor 进行编码Dictionary,因此您可以使用IList在代码中可互换实现的任何类:

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

基类库(BCL)中的另一个示例是ProviderBase抽象类-它提供了一些基础结构,并且重要的是,如果您对它进行编码,则可以互换使用所有提供程序实现。


但是客户端如何与接口交互并使用其空方法?
never_had_a_name

1
客户端不与接口交互,而是通过接口交互:)对象通过方法(消息)与其他对象交互,并且接口是一种语言-当您知道某些对象(人)实现(说)英语(IList)时),则无需知道该对象(他也是意大利人)就可以使用它,因为在这种情况下不需要(如果您想寻求帮助,您不需要知道他也说意大利语)如果您懂英语)。
加百利Ščerbák2010年

顺便说一句。IMHO Liskov替换原理与继承的语义有关,与接口无关,接口也可以在没有继承的语言中找到(来自Google)。
加百利Ščerbák2010年

5

如果您要在“燃烧汽车”时代编写汽车类,那么您很有可能将oilChange()作为该类的一部分来实现。但是,当引入电动汽车时,您会遇到麻烦,因为这些汽车不涉及换油,也没有实施。

解决该问题的方法是在Car类中具有performMaintenance()接口,并在适当的实现中隐藏详细信息。每种Car类型都会为performMaintenance()提供其自己的实现。作为汽车的拥有者,您所要做的就是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. 您确定适合所有汽车的工作(可以是新的接口Interface)。
  5. 提供了一种提供服务的机制。基本上,您将提供实现。
  6. 您可以调用工作并自己完成。在这里,您将进行适当的维护工作。

    第二种方法的缺点是什么?您可能不是寻找最佳维护方法的专家。您的工作是驾驶汽车并享受它。不从事维护业务。

    第一种方法的缺点是什么?寻找公司等会产生开销。除非您是租车公司,否则可能不值得花大力气。


4

该声明是关于耦合的。使用面向对象编程的一个潜在原因是重用。因此,例如,您可以将算法分配给两个协作对象A和B。这对于以后创建另一个算法可能很有用,因为后者可能会重用两个对象中的一个或另一个。但是,当这些对象进行通信(发送消息-调用方法)时,它们之间会建立依赖关系。但是,如果要不使用另一个而使用另一个,则需要指定如果替换B,其他对象C应该对对象A做什么。这些描述称为接口。这允许对象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

接口描述功能。在编写命令式代码时,请谈论您正在使用的功能,而不是特定的类型或类。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.