What does “program to interfaces, not implementations” mean?
人们在阅读设计模式时偶然发现了这个短语。
但我不明白,有人能给我解释一下吗?
Interfaces are just contracts or signatures and they don't know
anything about implementations.
对接口进行编码意味着,客户机代码始终持有由工厂提供的接口对象。工厂返回的任何实例都属于接口类型,任何工厂候选类都必须实现该类型。这样,客户机程序就不担心实现,接口签名决定了所有操作都可以做什么。这可用于在运行时更改程序的行为。它还可以帮助您从维护的角度编写更好的程序。
下面是一个基本的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | 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 } |
This is just a basic example and
actual explanation of the principle is
beyond the scope of this answer.
编辑
我已经更新了上面的示例并添加了一个抽象演讲者基类。在这个更新中,我将一个特性添加到了所有的垃圾邮件程序中"再见"。所有演讲者都说"你好,世界"。所以这是一个有类似功能的共同特征。参考类图,您会发现speaker抽象类实现了ispeaker接口,并将speak()标记为抽象的,这意味着每个speaker实现都负责实现speak方法,因为speak方法随speaker的不同而不同。但所有的演讲者都一致说"你好"。因此,在抽象的speaker类中,我们定义了一个称为"hello world"的方法,并且每个speaker实现都将派生出sayhello方法。
考虑一个SpanishSpeaker不能打招呼的情况,在这种情况下,您可以覆盖SpanishSpeaker的sayHello方法并引发适当的异常。
Please note that, we have
not made any changes to Interface
ISpeaker. And the client code and
SpeakerFactory also remain unaffected
unchanged. And this is what we achieve by Programming-to-Interface.
我们可以通过简单地添加一个基本抽象类演讲者和在每个实现中进行一些细微的修改来实现这种行为,从而保持原始程序不变。这是任何应用程序所需要的特性,它使您的应用程序易于维护。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | 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 } |
把一个接口看作一个对象与其客户机之间的契约。这就是接口指定对象可以做的事情,以及访问这些事情的签名。
实施是实际行为。例如,您有一个方法sort()。您可以实现快速排序或合并排序。只要接口不变,这对客户端代码调用排序就不重要。
像Java API和.NETFramework这样的库大量使用接口,因为数百万程序员使用所提供的对象。这些库的创建者必须非常小心,不要更改这些库中类的接口,因为它会影响所有使用库的程序员。另一方面,他们可以随心所欲地更改实现。
如果,作为一个程序员,您对实现进行编码,那么一旦它改变了,您的代码就停止工作。因此,以这种方式考虑接口的好处:
这意味着您应该尝试编写代码,以便它使用抽象(抽象类或接口)而不是直接实现。
通常,实现通过构造函数或方法调用注入到代码中。因此,您的代码知道接口或抽象类,并且可以调用在这个契约上定义的任何东西。当使用实际对象(接口/抽象类的实现)时,调用将在对象上操作。
这是
.NET中的一个例子是使用
1 2 3 4 5 6 | // myList can be _any_ object that implements IList public int GetListCount(IList myList) { // Do anything that IList supports return myList.Count(); } |
基类库(bcl)的另一个例子是
如果您要在Combustion Car时代编写Car类,那么很有可能将OilChange()作为该类的一部分实现。但是,当电动汽车被引进时,你会遇到麻烦,因为这些汽车不需要换油,也不需要实施。
这个问题的解决方案是在car类中有一个performmaintenance()接口,并在适当的实现中隐藏细节。每种汽车类型都将为performmaintenance()提供自己的实现。作为一辆车的主人,你所要处理的只是性能维护(performmaintenance),而不必担心在发生变化时进行调整。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | class MaintenanceSpecialist { public: virtual int performMaintenance() = 0; }; class CombustionEnginedMaintenance : public MaintenanceSpecialist { int performMaintenance() { printf("combustionEnginedMaintenance: We specialize in maintenance of Combustion engines "); return 0; } }; class ElectricMaintenance : public MaintenanceSpecialist { int performMaintenance() { printf("electricMaintenance: We specialize in maintenance of Electric Cars "); return 0; } }; class Car { public: MaintenanceSpecialist *mSpecialist; virtual int maintenance() { printf("Just wash the car "); 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; } |
附加说明:你是一个拥有多辆车的车主。你把你想外包的服务开拓出来。在我们的情况下,我们想把所有汽车的维修工作外包出去。
您不想担心将汽车类型与服务提供商关联起来。您只需指定何时计划维护并调用它。适当的服务公司应该介入并执行维护工作。
替代方法。
你自己去做这项工作。在这里你要做适当的维护工作。
第二种方法的缺点是什么?您可能不是找到最佳维护方法的专家。你的工作是驾驶汽车并享受它。不是为了维护它。
第一种方法的缺点是什么?找一家公司等会有开销。除非你是一家租车公司,否则这可能不值得你付出努力。
这句话是关于耦合的。使用面向对象编程的一个潜在原因是重用。例如,您可以将您的算法拆分为两个协作对象A和B。这可能对以后创建另一个算法很有用,该算法可能会重用两个对象中的一个或另一个。但是,当这些对象通信(发送消息-调用方法)时,它们会在彼此之间创建依赖关系。但是,如果您想使用一个对象而不使用另一个对象,那么您需要指定如果我们替换B,其他对象C应该为对象A做什么。这些描述称为接口。这允许对象A与依赖于接口的不同对象进行通信而不发生更改。您提到的语句指出,如果您计划重用算法的某些部分(或者更一般地说是程序),那么应该创建接口并依赖它们,因此如果您使用声明的接口,则可以随时更改具体实现,而不更改其他对象。
正如其他人所说,这意味着您的调用代码应该只知道一个抽象的父类,而不应该知道将要完成工作的实际实现类。
有助于理解这一点的是,为什么您应该总是编程到一个接口上。原因很多,但最容易解释的两个是
1)测试。
假设我把整个数据库代码放在一个类中。如果我的程序知道具体的类,我只能通过在该类上真正运行它来测试我的代码。我用->来表示"交谈"。
WorkerClass->DalClass不过,让我们为组合添加一个接口。
WorkerClass->IDA->DalClass。
所以dalclass实现了ida接口,worker类只通过这个调用。
现在,如果我们想为代码编写测试,我们可以制作一个简单的类,它的行为就像一个数据库。
WorkerClass->IDA->IFAKEDAL。
2)再利用
按照上面的示例,假设我们想要从SQL Server(具体的dalclass使用)转移到monogob。这需要大量的工作,但如果我们已经编程到一个接口,就不需要了。在这种情况下,我们只需编写新的DB类,并更改(通过工厂)
WorkerClass->IDA->DalClass
到
workerclass->idal->mongodbclass
接口描述功能。在编写命令式代码时,讨论您使用的功能,而不是特定的类型或类。