168

在阅读设计模式时会偶然发现这句话。

但是我不明白,谁能给我解释一下?

4

7 回答 7

173

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

针对接口进行编码意味着,客户端代码始终持有一个由工厂提供的接口对象。工厂返回的任何实例都是任何工厂候选类必须实现的接口类型。这样客户端程序就不用担心实现了,接口签名决定了所有操作可以做什么。这可以用来改变程序在运行时的行为。从维护的角度来看,它还可以帮助您编写更好的程序。

这是一个基本的例子。

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()方法,因为它从SpeakerSpeaker。但所有发言者一致说“你好”。因此,在抽象的 Speaker 类中,我们定义了一个“Hello World”方法,每个Speaker实现都会派生该SayHello()方法。

考虑一个SpanishSpeaker不能说你好的情况,所以在这种情况下你可以覆盖SayHello()西班牙语演讲者的方法并引发适当的异常。

请注意,我们没有对 Interface ISpeaker 进行任何更改。并且客户端代码和 SpeakerFactory 也保持不变。这就是我们通过Programming-to-Interface实现的。

我们可以通过简单地添加一个基本抽象类 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
}

替代文字

于 2010-04-23T10:31:59.440 回答
35

将接口视为对象与其客户之间的契约。那就是接口指定对象可以做的事情,以及访问这些事情的签名。

实现是实际的行为。例如,假设您有一个方法 sort()。您可以实现 QuickSort 或 MergeSort。只要接口没有改变,这对于调用 sort 的客户端代码应该无关紧要。

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

如果作为一名程序员,您针对实现进行编码,那么一旦它更改您的代码就会停止工作。所以这样想接口的好处:

  1. 它隐藏了您不需要知道的事情,使对象更易于使用。
  2. 它提供了对象行为方式的约定,因此您可以依赖它
于 2010-04-23T10:39:36.210 回答
25

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

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

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

.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抽象类——它提供了一些基础设施,并且重要的是,如果您针对它进行编码,则所有提供者实现都可以互换使用。

于 2010-04-23T10:31:04.617 回答
7

如果您要在 Combustion-Car 时代编写一个 Car 类,那么您很有可能将 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. 您确定适用于所有汽车的工作(可以是新的界面界面)。
  5. 提出了一种提供服务的机制。基本上你将提供实现。
  6. 你调用工作并自己做。在这里,您将完成适当的维护工作。

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

    第一种方法的缺点是什么?有寻找公司等的开销。除非你是一家租车公司,否则可能不值得付出努力。

于 2014-03-11T05:46:54.357 回答
5

这句话是关于耦合的。使用面向对象编程的一个潜在原因是重用。因此,例如,您可以将算法拆分为两个协作对象 A 和 B。这对于以后创建另一个算法可能很有用,该算法可能会重用两个对象中的一个或另一个。但是,当这些对象进行通信(发送消息 - 调用方法)时,它们会在彼此之间创建依赖关系。但是如果你想使用一个而不使用另一个,你需要指定如果我们替换 B,其他对象 C 应该为对象 A 做什么。这些描述称为接口。这允许对象 A 与依赖接口的不同对象进行通信而无需更改。您提到的声明说,如果您打算重用算法的某些部分(或更一般地说是程序),您应该创建接口并依赖它们,

于 2010-04-23T10:33:34.230 回答
4

正如其他人所说,这意味着您的调用代码应该只知道一个抽象父级,而不是将完成工作的实际实现类。

有助于理解这一点的是为什么您应该始终对接口进行编程。原因有很多,但最容易解释的两个是

1) 测试。

假设我将整个数据库代码放在一个类中。如果我的程序知道具体的类,我只能通过真正针对该类运行它来测试我的代码。我使用 -> 来表示“交谈”。

WorkerClass -> DALClass 但是,让我们添加一个接口。

工人类-> IDAL-> DALClass。

所以 DALClass 实现了 IDAL 接口,工作类只通过它调用。

现在,如果我们想为代码编写测试,我们可以改为创建一个像数据库一样的简单类。

WorkerClass -> IDAL -> IFakeDAL。

2) 重用

按照上面的示例,假设我们要从 SQL Server(我们的具体 DALClass 使用)迁移到 MonogoDB。这将需要大量工作,但如果我们已经对接口进行了编程,则不会。在这种情况下,我们只需编写新的 DB 类,然后(通过工厂)进行更改

WorkerClass -> IDAL -> DALClass

WorkerClass -> IDAL -> MongoDBClass

于 2013-10-29T21:46:43.647 回答
2

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

于 2010-09-23T23:30:13.947 回答