“面向接口编程,而非实现”是什么意思?

191

阅读设计模式相关内容时,我们会经常遇到这个短语。

但我不理解它的含义,有人能为我解释一下吗?


6
可能是“什么是按接口编程”?的重复问题。 - Rahul Shah
8个回答

190

接口只是契约或签名,它们不知道任何实现细节。

使用接口编码意味着客户端代码始终持有由工厂提供的接口对象。工厂返回的任何实例都将是类型为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
}

alt text

这只是一个基本示例,原则的实际解释超出了本答案的范围。

编辑

我已经更新了上面的示例并添加了一个抽象的Speaker基类。在此更新中,我为所有扬声器添加了一个功能:“SayHello”。所有扬声器都会说“Hello World”。因此,这是一个具有相似功能的共同特征。请参考类图,您会发现Speaker抽象类实现了ISpeaker接口,并将Speak()标记为抽象,这意味着每个扬声器实现负责实现Speak()方法,因为它因Speaker而异。但是所有扬声器一致地说“Hello”。因此,在抽象Speaker类中,我们定义了一个方法,用于表示“Hello World”,并且每个Speaker实现将派生SayHello()方法。

考虑这样一种情况:如果SpanishSpeaker无法说“Hello”,那么您可以覆盖SpanishSpeakerSayHello()方法并引发适当的异常。

请注意,我们未对接口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
}

alt text


23
编程到接口并不仅仅是关于引用变量的类型。它还意味着你不使用任何对实现的隐含假设。例如,如果你将List作为类型,你仍然可能会假设通过多次调用 get(i) 来实现快速的随机访问。 - Joachim Sauer
23
工厂模式与编程接口无关,但我认为这个解释让人觉得它们是其中的一部分。 - T .
其中一个图片的 URL 已经失效 :( - appbootup
1
你用什么UML工具创建这些图片的? - Adam Arold
“...客户端代码始终持有由工厂提供的接口对象。”不,这是不正确的。这不是原则所关注的内容。如果您编写List aList = new ArrayList(),则是针对List接口进行编码,而不是针对ArrayList实现(在此使用Java)。更重要的是,如果您使用没有单独接口的类,例如StringBuilder bldr = new StringBuilder()(它实现了CharSequence接口,但我们可以忽略这一点),您仍然是在针对接口进行编码,而不是实现(StringBuilder类的隐式公共接口)。 - Rogério
显示剩余5条评论

41

将接口视为对象与其客户端之间的契约。即接口指定了对象可以做什么以及访问这些内容的签名。

实现是实际的行为。例如,您有一个方法sort(),您可以实现QuickSort或MergeSort。只要接口不改变,调用sort的客户端代码就不会受到影响。

像Java API和.NET Framework这样的库大量使用接口,因为有数百万程序员使用提供的对象。这些库的创建者必须非常小心,不要更改这些库中的类的接口,因为这将影响所有使用该库的程序员。另一方面,他们可以随意更改实现方式。

如果作为程序员,您编写针对实现的代码,则一旦实现更改,您的代码将停止工作。因此,考虑接口的好处:

  1. 它隐藏了您不需要知道的内容,使对象更易于使用。
  2. 它提供了对象的行为契约,因此您可以依赖它。

这意味着您需要注意您将对象分配给执行的任务:在提供的示例中,您仅分配了一个排序任务,并不一定是稳定排序。 - penguat
就像库文档没有提及实现细节一样,它们只是包含类接口的描述。 - Joe Iddon

28
这意味着你应该尝试编写代码,使其使用抽象(抽象类或接口),而不是直接使用实现。通常,实现通过构造函数或方法调用注入到代码中。因此,你的代码知道接口或抽象类,并可以调用在此合同上定义的任何内容。由于使用了实际对象(接口/抽象类的实现),所以调用正在操作对象。这是Liskov替换原则 (LSP)的子集,也是SOLID原则中的L。在.NET中的一个例子是使用IList而不是ListDictionary进行编码,这样你就可以在代码中互换使用实现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
2
客户端不直接与界面交互,而是通过界面进行交互。对象通过方法(消息)与其他对象进行交互,而接口则是一种语言 - 当您知道某个对象(人)实现(讲)英语(IList)时,您可以在不需要了解该对象更多信息的情况下使用它(例如他也是意大利人),因为在该上下文中没有必要(如果您想要寻求帮助,您不需要知道他还会说意大利语,只需理解英语即可)。 - Gabriel Ščerbák
1
顺便提一句,在我看来,Liskov替换原则是关于继承的语义,与接口无关,这也可以在没有继承的语言中找到(如Google的Go语言)。 - Gabriel Ščerbák
@GabrielŠčerbák 我同意。接口隔离原则(ISP)应该比LSP更值得一提。ISP更正确,因为接口可以比实现更小,因此接口更抽象。 - Grim

9
如果您在内燃机汽车时代编写汽车类,那么很可能会将oilChange()实现为该类的一部分。但是,当电动汽车问世时,您会遇到麻烦,因为这些汽车不需要进行换油操作,也没有相应的实现。
解决问题的方法是在Car类中拥有一个performMaintenance()接口,并将细节隐藏在适当的实现中。每种类型的汽车都将为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. 您调用工作并自行完成它。在这里,您将执行适当的维护工作。 第二种方法的缺点是什么? 您可能不是找到最佳维护方式的专家。您的工作是驾驶车辆并享受它。而不是在维护业务中。
  7. 第一种方法的缺点是什么? 需要花费时间寻找公司等开销。除非您是租车公司,否则可能不值得这样做。


6
这个陈述涉及到耦合。使用面向对象编程的一个潜在原因是重用。例如,您可以将算法分为两个协作对象A和B。这对于以后创建另一个算法可能会有用,该算法可能会重用其中一个或两个对象中的另一个。但是,当这些对象通信(发送消息-调用方法)时,它们之间会创建依赖关系。但是,如果您想单独使用一个对象而不使用另一个对象,则需要指定替换B时对象C应该为对象A执行什么操作。这些描述称为接口。这使得对象A能够在不改变依赖接口的情况下与不同的对象进行通信。您提到的声明表示,如果您计划重用算法(或更一般地说,程序的某些部分),则应创建接口并依靠它们,这样,如果使用已声明的接口,您可以随时更改具体实现而不更改其他对象。

6
正如其他人所说的,这意味着您的调用代码应该只知道一个抽象的父类,而不是实际执行工作的实现类。
有助于理解这一点的原因是为什么您应该始终按照接口编程。有很多原因,但其中两个最容易解释的是:
1)测试。
假设我在一个类中拥有我的整个数据库代码。如果我的程序了解具体类,我只能通过针对该类运行来测试我的代码。使用 -> 表示 "与...通信"。
WorkerClass->DALClass
然而,让我们将接口添加到混合物中。
WorkerClass -> IDAL -> DALClass。
因此,DALClass实现了IDAL接口,并且worker class仅通过此调用。
现在,如果我们想为代码编写测试,我们可以改为创建一个简单的类,其行为就像数据库。
WorkerClass -> IDAL -> IFakeDAL。
2)重复使用
按照上面的例子,假设我们想从SQL Server(我们的具体DALClass使用)移动到MonogoDB。这需要大量的工作,但如果我们已经按照接口编程,则不需要。在这种情况下,我们只需编写新的DB类,并更改(通过工厂)
WorkerClass -> IDAL -> DALClass
改为
WorkerClass -> IDAL -> MongoDBClass

3

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


0
“面向接口编程,而不是实现”是与四人帮(Gang of Four)相关的设计原则,他们是一群撰写了《设计模式:可复用面向对象软件的基础》的作者。
主要思想是你的代码应该设计成依赖于抽象接口而不是具体实现。
举个例子,如果你有一个缓存接口,你的应用程序将使用它,而不用担心它背后是内存、文件还是Redis缓存的实现。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接