关于设计模式:我如何知道何时创建界面?

How will I know when to create an interface?

在我的开发学习中,我觉得我必须学习更多关于接口的知识。

我经常读到关于它们的文章,但似乎我无法抓住它们。

我读过一些例子,比如:动物类,有一个用于"行走"、"跑步"、"获取腿"等的iAnimal接口,但是我从来没有在做什么,我觉得"嘿,我应该在这里使用接口!"

我错过了什么?为什么我很难理解这个概念?我只是被一个事实吓坏了,那就是我可能从来没有意识到对它的具体需求——主要是由于理解它们的某些方面的缺失!这让我觉得自己在成为一名开发人员方面缺少了一些顶级的东西!如果有人有过这样的经历,并且有了突破,我会很感激关于如何理解这个概念的一些提示。谢谢您。


它解决了这个具体问题:

你有4种不同类型的A,B,C,D。在整个代码中,您都有如下内容:

1
2
3
4
a.Process();
b.Process();
c.Process();
d.Process();

为什么不让它们实现IProcessable,然后呢?

1
2
3
4
List<IProcessable> list;

foreach(IProcessable p in list)
    p.Process();

当你添加50种类型的类,比如说,所有的类都做同样的事情时,这将有更好的扩展性。

另一个具体问题:

你看过System.Linq.Enumerable吗?它定义了许多扩展方法,这些方法对实现IEnumerable的任何类型进行操作。因为实现IEnumerable的任何东西基本上都说"我支持无序foreach类型模式中的迭代",所以您可以为任何可枚举类型定义复杂的行为(count、max、where、select等)。


我很喜欢吉米的回答,但我觉得我需要补充一些东西。整件事的关键是"我能处理"中的"能"。它表示实现接口的对象的能力(或属性,但表示"内在质量",而不是C属性)。iAnimal可能不是一个很好的接口示例,但是如果您的系统有很多可以行走的东西,iWalkable可能是一个很好的接口。您可能拥有从动物派生的类,例如狗、牛、鱼、蛇。前两个可能实现iWalkable,后两个不行走,所以他们不会。现在你问"为什么不再有一个超类,行走动物,狗和牛的起源?"答案是当你有一些完全在遗传树之外的东西也可以行走时,比如机器人。机器人可以实现我的网络,但可能不会从动物身上获得。如果你想要一个可以行走的东西的列表,你可以把它输入Iwalkable,你可以把所有的行走动物和机器人放在列表中。

现在,用类似ipersistable的软件y来替换iWalkable,这个类比就更接近于您在实际程序中看到的情况。


当相同功能的实现不同时使用接口。

当需要共享公共的具体实现时,请使用抽象/基类。


把一个接口想象成一个契约。这是一种说法,"这些类应该遵循这些规则。"

所以在iAnimal示例中,这是一种说法,"我必须能够在实现iAnimal的类上调用run、walk等。"

为什么这个有用?您可能希望构建一个依赖于这样一个事实的函数:例如,您必须能够在对象上调用run和walk。您可以有以下内容:

1
2
3
4
5
6
7
8
9
public void RunThenWalk(Monkey m) {
    m.Run();
    m.Walk();
}

public void RunThenWalk(Dog d) {
    d.Run();
    d.Walk();
}

…对所有你知道能跑能走的物体重复这个步骤。但是,通过IAnimal接口,您可以定义函数一次,如下所示:

1
2
3
4
public void RunThenWalk(IAnimal a) {
    a.Run();
    a.Walk();
}

通过对接口进行编程,您基本上信任这些类来实现接口的意图。所以在我们的例子中,我们的想法是"我不在乎他们怎么跑和怎么走,只要他们跑和怎么走。只要他们履行了协议,我的跑步就有效。它在不了解其他课程的情况下运行良好。"

在这个相关问题上也有一个很好的讨论。


别担心那么多。很多开发人员很少需要编写接口。您将经常使用.NET框架中可用的接口,但是如果您不觉得很快就需要编写一个接口,那就没有什么奇怪的了。

我总是给别人举一个例子,如果你有帆船班和毒蛇班。他们分别继承了船类和汽车类。现在假设您需要遍历所有这些对象并调用它们的Drive()方法。您可以编写如下代码:

1
2
3
4
5
if(myObject is Boat)
    ((Boat)myObject).Drive()
else
    if (myObject is Car)
        ((Car)myObject).Drive()

写起来要简单得多:

1
((IDrivable)myObject).Drive()

当您希望能够对多个类型使用单个变量时,Jimmy是正确的,但是所有这些类型都通过接口声明实现了相同的方法。然后可以在接口类型变量上调用它们的主方法。

然而,使用接口还有第二个原因。当项目架构师与实现编码员不同时,或者有几个实现编码员和一个项目经理。负责人可以编写一大堆接口,并查看系统的互操作性,然后让开发人员用实现类填充接口。这是确保多个人编写兼容类的最佳方法,并且他们可以并行地编写。


我喜欢军队类比。

中士不在乎你是软件开发人员、音乐家还是律师。你被当作士兵对待。

uml

中士更容易不去管他正在工作的人的具体细节,把每个人都当作士兵抽象的东西来对待(……当他们没有表现得像士兵时惩罚他们)。

人们能像士兵一样行动的能力被称为多态性。

接口是帮助实现多态性的软件结构。

为了实现简单,需要抽象细节是您问题的答案。

Polymorphism, which etymologically means"many forms," is the ability to treat an object of any subclass of a base class as if it were an object of the base class. A base class has, therefore, many forms: the base class itself, and any of its subclasses.

(..) This makes your code easier for you to write and easier for others to understand. It also makes your code extensible, because other subclasses could be added later to the family of types, and objects of those new subclasses would also work with the existing code.


在我的经验中,直到我开始使用模拟框架进行单元测试,才产生创建接口的驱动力。很明显,使用接口将使模拟变得更容易(因为框架依赖于虚拟的方法)。一开始,我就看到了从实现中将接口抽象到类的价值。即使我没有创建实际的接口,我现在也尝试使我的方法成为虚拟的(提供一个可以被重写的隐式接口)。

我发现还有很多其他的原因来加强重构接口的良好实践,但是单元测试/模拟的东西提供了最初的"aha时刻"实践经验。

编辑:为了澄清,通过单元测试和模拟,我总是有两个实现——实际的、具体的实现和测试中使用的备用模拟实现。一旦您有了两个实现,接口的价值就变得显而易见了——从接口的角度来处理它,这样您就可以随时替换实现。在本例中,我将用模拟接口替换它。我知道如果我的类是正确构造的,我可以在没有实际接口的情况下完成这项工作,但是使用实际接口会加强这一点并使其更清晰(对读者来说更清楚)。如果没有这种推动力,我想我不会欣赏接口的价值,因为我的大多数类都只有一个具体的实现。


一些非编程示例可能会帮助您了解编程中接口的适当使用。

电气设备和电网之间有一个接口——这是关于插头和插座形状以及它们之间的电压/电流的一组约定。如果您想实现一个新的电气设备,只要您的插头遵循规则,它将能够从网络上获得服务。这使得扩展非常容易,并且消除或降低了协调成本:您不必通知电力供应商您的新设备如何工作,并且就如何将新设备插入网络达成单独的协议。

各国都有标准轨距。这使得工程公司和建造火车在铁轨上运行的工程公司之间的分工成为可能,铁路公司可以在不重新构建整个系统的情况下更换和升级火车。

企业提供给客户机的服务可以描述为一个接口:定义良好的接口强调服务并隐藏手段。当您将信件放入邮箱时,您希望邮政系统在给定的时间内递送信件,但您对信件的递送方式没有任何期望:您不需要知道,邮政服务可以灵活选择最符合要求和当前情况的递送方式。一个例外是客户选择航空邮件的能力——这不是现代计算机程序员设计的那种接口,因为它揭示了太多的实现。

自然的例子:我不太喜欢eats()、makessound()、movess()等例子。它们确实描述了行为,这是正确的,但是它们没有描述交互以及如何启用交互。使自然界中的相互作用得以实现的界面的明显例子是与繁殖有关,例如,花为蜜蜂提供了一个特定的界面,从而可以进行授粉。


一个代码示例(将安得烈与我的另一个接口结合在一起的目的是什么),这也说明了为什么接口而不是抽象的类,在没有支持多重继承的语言上(C和Java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface ILogger
{
    void Log();
}
class FileLogger : ILogger
{
    public void Log() { }
}
class DataBaseLogger : ILogger
{
    public void Log() { }
}
public class MySpecialLogger : SpecialLoggerBase, ILogger
{
    public void Log() { }
}

注意,filelogger和databaselogger不需要接口(可能是logger抽象基类)。但是考虑到您需要使用第三方日志程序来强制您使用一个基类(假设它公开了您需要使用的受保护方法)。因为语言不支持多重继承,所以您将无法使用抽象基类方法。

底线是:尽可能使用一个接口来获得代码的额外灵活性。您的实现不那么紧密,因此它更适合于更改。


作为一个.NET开发人员,你的整个生活都是完全有可能的,而且永远不会写你自己的接口。毕竟,我们在没有它们的情况下存活了几十年,我们的语言仍然图灵完整。

我不能告诉你为什么你需要接口,但我可以给你一个我们在当前项目中使用它们的列表:

  • 在我们的插件模型中,我们按接口加载插件,并将该接口提供给插件编写器以符合。

  • 在我们的机器间消息传递系统中,消息类都实现了一个特定的接口,并使用该接口"展开"。

  • 我们的配置管理系统定义了一个用于设置和检索配置设置的接口。

  • 我们使用一个接口来避免讨厌的循环引用问题。(如果不必这样做,请不要这样做。)

  • 我想如果有一个规则,当你想在一个IS-A关系中对几个类进行分组时,应该使用接口,但是你不想在基类中提供任何实现。


    一旦需要强制类的行为,就应该定义一个接口。

    动物的行为可能涉及步行、进食、跑步等,因此,你把它们定义为界面。

    另一个实用的例子是ActionListener(或Runnable)接口。当需要跟踪特定事件时,您将实现它们。因此,需要在类(或子类)中提供actionPerformed(Event e)方法的实现。同样,对于可运行的接口,您提供了public void run()方法的实现。

    此外,您还可以通过任意数量的类实现这些接口。

    使用接口(Java)的另一个实例是实现C++中提供的多重继承。


    我时不时地使用接口,下面是我最新的用法(名称已被通用化):

    我在WinForm上有许多自定义控件,需要将数据保存到我的业务对象中。一种方法是分别调用每个控件:

    1
    2
    3
    myBusinessObject.Save(controlA.Data);
    myBusinessObject.Save(controlB.Data);
    myBusinessObject.Save(controlC.Data);

    这个实现的问题是,每当我添加一个控件时,我都必须进入"保存数据"方法并添加新的控件。

    我更改了控件以实现一个具有方法saveToBusinessObject(…)的isaveable接口,因此现在我的"save data"方法只迭代控件,如果它找到一个isaveable,它将调用saveToBusinessObject。所以现在,当需要一个新的控件时,某人所要做的就是在该对象中实现isaveable(并且永远不要碰其他类)。

    1
    2
    3
    4
    5
    6
    7
    foreach(Control c in Controls)
    {
      ISaveable s = c as ISaveable;

      if( s != null )
          s.SaveToBusinessObject(myBusinessObject);
    }

    接口常常无法实现的好处是您本地化了修改。一旦定义好,您将很少更改应用程序的整体流,但您通常会在细节级别进行更改。当您将详细信息保存在特定对象中时,processA中的更改不会影响processB中的更改。(基类也会给您带来这种好处。)

    编辑:另一个好处是行动的特殊性。就像在我的示例中一样,我只想保存数据;我不在乎它是什么类型的控件,也不在乎它是否可以执行其他操作——我只想知道是否可以将数据保存在控件中。它使我的保存代码非常清晰——没有检查它是文本、数字、布尔值还是其他什么,因为自定义控件处理了所有这些。


    假设你想模拟当你试图睡觉时可能发生的烦恼。

    接口前模型

    enter image description here

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Mosquito {
        void flyAroundYourHead(){}
    }

    class Neighbour{
        void startScreaming(){}
    }

    class LampJustOutsideYourWindow(){
        void shineJustThroughYourWindow() {}
    }

    正如你清楚地看到的那样,当你试图睡觉的时候,很多"事情"都会让人恼火。

    使用没有接口的类

    但说到使用这些类,我们有一个问题。他们没有共同点。您必须分别调用每个方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class TestAnnoyingThings{
        void testAnnoyingThinks(Mosquito mosquito, Neighbour neighbour, LampJustOutsideYourWindow lamp){
             if(mosquito != null){
                 mosquito.flyAroundYourHead();
             }
             if(neighbour!= null){
                 neighbour.startScreaming();
             }
             if(lamp!= null){
                 lamp.shineJustThroughYourWindow();
             }
        }
    }

    带接口的模型

    为了克服这个问题,我们可以引入一个ITerfaceenter image description here

    1
    2
    3
    4
    interface Annoying{
       public void annoy();

    }

    在类中实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Mosquito implements Annoying {
        void flyAroundYourHead(){}

        void annoy(){
            flyAroundYourHead();
        }
    }

    class Neighbour implements Annoying{
        void startScreaming(){}

        void annoy(){
            startScreaming();
        }
    }

    class LampJustOutsideYourWindow implements Annoying{
        void shineJustThroughYourWindow() {}

        void annoy(){
            shineJustThroughYourWindow();
        }
    }

    与接口一起使用

    这将使这些类的使用更加容易

    1
    2
    3
    4
    5
    class TestAnnoyingThings{
        void testAnnoyingThinks(Annoying annoying){
            annoying.annoy();
        }
    }


    最简单的例子就是支付处理器(PayPal,PDS等)。

    假设您创建了一个具有processAch和processCreditCard方法的接口IPaymentProcessor。

    现在可以实现一个具体的PayPal实现。使这些方法称为PayPal特定功能。

    如果您稍后决定需要切换到另一个提供者,您可以。只需为新的提供者创建另一个具体的实现。由于您所绑定的只是您的接口(契约),所以您可以交换出应用程序使用的接口,而不必更改使用它的代码。


    如果浏览.NET框架程序集并向下钻取到任何标准对象的基类,您将注意到许多接口(名为isomename的成员)。

    接口主要用于实现大型或小型框架。在我想写自己的框架之前,我对接口有着同样的感觉。我还发现理解接口帮助我更快地学习框架。当你想为任何事情写一个更优雅的解决方案时,你会发现一个接口很有意义。这就像是让一个班为工作穿上合适的衣服。更重要的是,接口允许系统变得更加自我记录,因为当类实现接口时,复杂的对象变得不那么复杂,这有助于对其功能进行分类。

    类在希望能够显式或隐式参与框架时实现接口。例如,IDisposable是一个公共接口,它为流行和有用的dispose()方法提供方法签名。在框架中,您或其他开发人员需要了解的关于类的所有信息是,如果它实现了IDisposable,那么您就知道((IDisposable)MyObject.Dispose()可以用于清理目的。

    经典示例:如果不实现IDisposable接口,则不能在C_中使用"using()"关键字构造,因为它要求可以将指定为参数的任何对象隐式转换为IDisposable。

    复杂示例:更复杂的例子是System.ComponentModel.Component类。此类同时实现IDisposable和IComponent。大多数,.NET对象(如果不是所有)都有一个与它们关联的可视化设计器,它们实现IComponent,以便IDE能够与组件交互。

    结论:随着您越来越熟悉.NET框架,当您在对象浏览器或.NET Reflector(免费)工具(http://www.red-gate.com/products/reflector/)中遇到新类时,首先要做的事情是检查它从哪个类继承,以及它实现的接口。.NET Reflector甚至比对象浏览器更好,因为它允许您查看派生类。这允许您了解从特定类派生的所有对象,从而潜在地了解您不知道存在的框架功能。在.NET框架中添加更新的或新的命名空间时,这一点尤其重要。


    它还允许您执行模拟单元测试(.net)。如果您的类使用接口,那么您可以在单元测试中模拟对象并轻松地测试逻辑(而不必实际访问数据库或Web服务等)。

    网址:http://www.nmock.org/


    考虑一下你正在做一个第一人称射击游戏。玩家有多种枪可供选择。

    我们可以有一个接口Gun,它定义了一个函数shoot()

    我们需要Gun类的不同子类,即ShotGunSniper等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ShotGun implements Gun{
        public void shoot(){
           \\shotgun implementation of shoot.
        }
    }

    Sniper implements Gun{
        public void shoot(){
           \\sniper implementation of shoot.
        }
    }

    射击类

    射手的盔甲里全是枪。让我们创建一个List来表示它。

    1
    List<Gun> listOfGuns = new ArrayList<Gun>();

    在需要的时候,射手使用switchGun()功能在枪中循环。

    1
    2
    3
    4
    public void switchGun(){
        //code to cycle through the guns from the list of guns.
        currentGun = //the next gun in the list.
    }

    当调用fire()时,我们可以使用上述函数设置当前枪,只需调用shoot()函数即可。

    1
    2
    3
    public void fire(){
        currentGun.shoot();
    }

    shoot函数的行为将根据Gun接口的不同实现而不同。

    结论

    当一个类函数依赖于另一个类中的一个函数时,创建一个接口,该函数将根据实现的类的实例(对象)改变其行为。

    例如,Shooter类的fire()功能要求枪支(SniperShotGun执行shoot()功能。所以如果我们换枪开火。

    1
    2
    shooter.switchGun();
    shooter.fire();

    我们改变了fire()函数的行为。


    正如一些人可能已经回答的那样,接口可以用于在类之间强制某些行为,这些行为不会以相同的方式实现。因此,通过实现一个接口,您可以说您的类具有该接口的行为。IAnimal接口不是一个典型的接口,因为dog、cat、bird等类是动物的类型,应该扩展它,这是继承的一个例子。相反,在这种情况下,接口更像是动物行为,例如irunable、iflyable、itrainable等。

    接口对许多事情都很好,其中一个关键的事情就是可插性。例如,声明一个具有List参数的方法将允许传递实现List接口的任何内容,从而允许开发人员在以后移除和插入不同的列表,而不必重写大量的代码。

    你可能永远不会使用接口,但是如果你从头开始设计一个项目,特别是某种框架,你可能会想熟悉它们。

    我建议阅读Coad、梅菲尔德和克恩在Java设计中关于接口的章节。他们的解释比一般的介绍性文章好一点。如果你不使用Java,你可以只阅读本章的开头,这只是概念。


    正如任何增加系统灵活性的编程技术一样,接口也增加了一定程度的复杂性。它们通常都很好,您可以在任何地方使用它(您可以为所有类创建一个接口),但是这样做,您将创建一个更复杂的系统,很难维护。

    像往常一样,这里有一个权衡:灵活性超过可维护性。哪个更重要?没有答案-这取决于项目。但请记住,每个软件都必须维护…

    所以我的建议是:在真正需要接口之前不要使用它们。(使用Visual Studio,您可以在2秒钟内从现有类中提取接口,因此不要着急。)

    既然如此,您什么时候需要创建一个接口?

    当我重构一个突然需要处理两个或更多类似类的方法时,我会这样做。然后我创建一个接口,将这个接口分配给两个(或更多)类似的类,并更改方法参数类型(用接口类型替换类类型)。

    它起作用了:o)

    一个例外:当我模拟对象时,界面更容易使用。所以我经常为此创建接口。

    PS:当我写"接口"时,我的意思是:"任何基类的接口",包括纯接口类。注意抽象类通常比纯接口更好,因为您可以向它们添加逻辑。

    你好,西尔文。


    使用接口有很多目的。

  • 用于多态行为。要调用具有对子类引用的inteface的子类的特定方法的位置。

  • 与类有契约以在需要时实现所有方法,就像最常用的是与COM对象的契约一样,在继承接口的dll上生成包装类;这些方法在后台调用,您只需要实现它们,但其结构与在COM dll中定义的结构相同,您可以n只通过它们公开的接口知道。

  • 通过在类中加载特定方法来减少内存使用。就像如果您有三个业务对象并且它们在一个类中实现一样,您可以使用三个接口。

  • 例如IUSER、IORDER、IORDERIEM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public interface IUser()
    {

    void AddUser(string name ,string fname);

    }

    // Same for IOrder and IOrderItem
    //


    public class  BusinessLayer: IUser, IOrder, IOrderItem

    {    
        public void AddUser(string name ,string fname)
        {
            // Do stuffs here.
        }

        // All methods from all interfaces must be implemented.

    }

    如果只想添加用户,请执行以下操作:

    1
    2
    3
    4
    5
    IUser user = new (IUser)BusinessLayer();

    // It will load  all methods into memory which are declared in the IUser interface.

    user.AddUser();

    当您成为一个库开发人员(为其他编码人员编码的人)时,界面将变得明显。我们中的大多数人从应用程序开发人员开始,在那里我们使用现有的API和编程库。

    尽管接口是一个契约,但还没有人提到接口是使代码的某些部分稳定的一个很好的方法。当它是一个团队项目(或者开发其他开发人员使用的代码)时,这尤其有用。下面是一个具体的场景:

    When you are developing code in a team, others will possibly be using the code you write. They'll be most happy when they code to your (stable) interfaces, and you'll be happy when you have the freedom to change your implementations (hidden behind the interface) without breaking your team's code. It's a variant of information hiding (interfaces are public, implementations are hidden from the client programmers). Read more about protected variations.

    另请参见有关接口编码的相关问题。


    扩大拉森纳尔所说的。接口是所有实现类都必须遵循的契约。因此,您可以使用一种称为合同编程的技术。这允许您的软件独立于实现。


    接口通常用于定义对象可以显示的行为。

    在.NET世界中,一个很好的例子是IDisposable接口,它用于任何使用必须手动释放的系统资源的Microsoft类。它要求实现它的类具有Dispose()方法。

    (dispose()方法也由vb.net和c的using语言结构调用,它只在IDisposable上工作)

    请记住,通过使用诸如EDCOX1×1(V.NET)、EDCOX1、2(C)、EDCOX1×3(Java)等构造,可以检查对象是否实现特定的接口。