迭代器模式-为什么不公开内部表示很重要?


24

我正在阅读C#设计模式要点。我目前正在阅读有关迭代器模式的信息。

我完全了解如何实现,但不了解重要性或看不到用例。书中给出了一个示例,其中有人需要获取对象列表。他们可以通过公开公共财产(例如IList<T>或)来做到这一点Array

这本书写道

问题是这两个类的内部表示都暴露给外部项目。

内部代表什么?事实是arrayIList<T>?我真的不明白为什么这对消费者(程序员称呼这)是一件坏事……

然后,这本书说这种模式通过公开其GetEnumerator功能而起作用,因此我们可以GetEnumerator()以此方式调用和公开“列表”。

我认为这种模式在某些情况下(与所有模式一样)都有位置,但是我看不到何时何地。



4
因为,实现可能希望从使用数组更改为使用链表,而无需更改使用者类。如果消费者知道返回的确切类,这将是不可能的。
MTilsted 2015年

11
这不是为消费者坏事知道,这对消费者是一件坏事依靠。我不喜欢“信息隐藏”一词,因为它使您相信信息像密码一样是“私有的”,而不是像手机内部那样是“私有的”。隐藏内部组件是因为它们与用户无关,而不是因为它们是某种秘密。您只需要在这里拨号,在这里交谈,在这里聆听即可。
曼队长

假设我们有一个很好的“界面” F,让我的知识(方法)abc。一切都很好,也有很多不同之处,但F对我而言只是如此。将方法视为“约束”或合同的条款F将类提交给正在做的事情。假设我们添加一个d尽管如此,因为我们需要它。这增加了一个额外的约束,每次我们这样做时,我们都会对Fs 施加越来越多的约束。最终(最坏的情况)只有一件事情可以成为一件事情,F因此我们也可能没有。而且F限制如此之多,只有一种方法可以做到。
亚历克·蒂尔

@船长,多么美妙的评论。是的,我明白了为什么要“抽象化”很多东西,但是关于了解和依赖的转折是……坦白地说。在阅读您的文章之前,我没有考虑过一个重要的区别。
MyDaftQuestions 2015年

Answers:


56

软件是一种许诺和特权的游戏。承诺超出承诺范围或超出协作者需求绝不是一个好主意。

这尤其适用于类型。编写可迭代集合的目的在于,它的用户可以对其进行迭代-不多也不少。暴露具体类型Array通常会带来许多其他保证,例如,您可以根据自己选择的功能对集合进行排序,更不用说法线Array可能会允许协作者更改存储在其中的数据的事实。

即使您认为这是一件好事(“如果渲染器注意到缺少新的导出选项,也可以直接对其进行修补!整洁!”),但总体而言,这会降低代码库的一致性,因此很难原因-使代码易于理解是软件工程的首要目标。

现在,如果您的协作者需要访问许多东西,以确保它们不会丢失任何东西,则可以实现一个Iterable接口并仅公开该接口声明的那些方法。这样,明年,当标准库中出现质量更好,效率更高的数据结构时,您将能够切换基础代码并从中受益,而无需在任何地方都固定客户端代码。不承诺超出期望还有其他好处,但是仅此一项就太大了,实际上,不需要其他任何好处。


12
Exposing the concrete type Array usually creates many additional promises, e.g. that you can sort the collection by a function of your own choosing...-太好了 我只是没想到。是的,它带回一个“迭代”,并且只有一个迭代!
MyDaftQuestions 2015年

18

隐藏实现是OOP的核心原则,并且在所有范例中都是一个好主意,但是对于支持延迟迭代的语言中的迭代器(或使用特定语言的任何称呼)而言,这尤其重要。

公开可迭代对象(甚至IList<T>是类似接口)的具体类型的问题不在公开它们的对象中,而是在使用它们的方法中。例如,假设您有一个用于打印Foos 列表的函数:

void PrintFoos(IList<Foo> foos)
{
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
    }
}

您只能使用该函数来打印foo列表-但前提是它们实现了 IList<Foo>

IList<Foo> foos = //.....
PrintFoos(foos);

但是,如果您要打印列表中每个偶数索引的项目怎么办?您需要创建一个新列表:

IList<Foo> everySecondFoo = new List<T>();
bool isIndexEven = true;
foreach (foo; foos)
{
    if (isIndexEven)
    {
        everySecondFoo.Add(foo);
    }
    isIndexEven = !isIndexEven;
}
PrintFoos(everySecondFoo);

这很长,但是使用LINQ,我们可以将它写成单层,实际上更易读:

PrintFoos(foos.Where((foo, i) => i % 2 == 0).ToList());

现在,您最后注意到了.ToList()吗?这会将惰性查询转换为列表,因此我们可以将其传递给PrintFoos。这需要分配第二个列表,并两次传递项目(第一个列表中的一个创建第二个列表,第二个列表中的另一个打印它)。另外,如果我们有这个:

void Print6Foos(IList<Foo> foos)
{
    int counter = 0;
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
        ++ counter;
        if (6 < counter)
        {
            return;
        }
    }
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0).ToList());

如果foos有成千上万的条目怎么办?我们将必须仔细阅读所有这些文档,并分配一个庞大的列表,仅打印其中的6张!

输入枚举数-迭代器模式的C#版本。与其让我们的函数接受列表,不如让我们接受Enumerable

void Print6Foos(Enumerable<Foo> foos)
{
    // everything else stays the same
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0));

现在Print6Foos可以懒洋洋地遍历列表的前6个项目,而我们不需要触摸其余的项目。

这里不公开内部表示是关键。当Print6Foos接受一个列表时,我们必须给它一个列表-一种支持随机访问的东西-因此我们必须分配一个列表,因为签名不能保证它只会遍历整个列表。通过隐藏实现,我们可以轻松地创建Enumerable仅支持功能实际需要的更有效的对象。


6

公开内部表示实际上绝不是一个好主意。它不仅使代码难以推理,而且也难以维护。假设您选择了任何内部表示形式-假设IList<T>-并公开了此内部形式。任何使用您代码的人都可以访问列表,并且代码可以依赖于内部表示形式为列表。

无论出于何种原因,您都打算IAmWhatever<T>在以后的某个时间点将内部表示形式更改为。取而代之的是,只需要简单地更改类的内部结构,就必须依靠类型的内部表示来更改代码和方法的每一IList<T>。当您是唯一使用该类的人时,这很麻烦并且充其量也容易出错,但是使用该类可能会破坏其他人的代码。如果您只是公开了一组公共方法而没有公开任何内部结构,则您可能已经更改了内部表示形式,而无需在类外注意到任何代码行,就好像没有任何变化一样工作。

这就是为什么封装是任何重要软件设计中最重要的方面之一的原因。


6

您说的越少,您必须说的越少。

您要求的越少,则被告知的内容就越少。

如果您的代码仅公开IEnumerable<T>,仅支持GetEnumrator()它,则可以用任何其他可以支持的代码代替IEnumerable<T>。这增加了灵活性。

如果您的代码仅使用IEnumerable<T>它,则任何实现的代码都可以支持它IEnumerable<T>。同样,还有额外的灵活性。

例如,所有linq-to-objects仅取决于IEnumerable<T>。尽管它快速路径的某些特定实现IEnumerable<T>以一种测试和使用的方式来执行此操作,但它总是可以退回到仅使用GetEnumerator()IEnumerator<T>返回的实现上。这比在数组或列表之上构建时具有更大的灵活性。


好的明确答案。非常恰当的是,您要使其简短,因此请遵循第一句话中的建议。太棒了!
Daniel Hollinrake

0

措辞我喜欢用镜子@CaptainMan的评论是:‘这是罚款消费者知道内部细节是坏的客户需求。要知道内部细节’

如果客户必须输入arrayIList<T>输入其代码,则当这些类型是内部详细信息时,消费者必须正确输入。如果它们是错误的,则使用者可能不会编译,甚至会得到错误的结果。这样产生的行为与“总是隐藏实现细节,因为您不应该公开这些细节”的其他答案相同,但是开始挖掘不公开的优点。如果您从未公开实现细节,那么您的客户将永远无法使用它来束缚自己!

我喜欢从这个角度来考虑它,因为它打开了将实现隐藏到含义阴影中的概念,而不是苛刻的“总是隐藏实现细节”的概念。在很多情况下,公开实现是完全有效的。考虑一下实时设备驱动程序的情况,在这种情况下,跳过抽象层并将字节戳入正确位置的能力可能与是否进行定时有所不同。或者考虑一个开发环境,您没有时间制作“好的API”,而需要立即使用它们。在这样的环境中暴露底层API通常可能是确定截止期限与否。

但是,当您将那些特殊情况抛在脑后,着眼于更一般的情况时,隐藏实现就变得越来越重要。 公开实现细节就像绳索。如果使用得当,您可以使用它来做各种事情,例如攀登高山。使用不当,可能会套索。您对产品的使用方式了解得越少,则需要越小心。

我一次又一次看到的案例研究是,开发人员制作的API对隐藏实现细节不太谨慎。使用该库的第二个开发人员发现该API缺少他们想要的一些关键功能。他们决定编写滥用功能来访问隐藏的实现细节(这需要花费几秒钟),而不是编写功能请求(可能需要数月的时间)。这本身并不是一件坏事,但是API开发人员通常从未计划将其作为API的一部分。后来,当他们改进API并更改底层细节时,他们发现它们已链接到旧的实现,因为有人将其用作API的一部分。最糟糕的版本是有人使用您的API提供以客户为中心的功能,而现在您的公司 的薪水与这个有缺陷的API有关!只需在您对客户不了解的情况下公开实现细节,就足以证明可以在您的API周围套上头套!


1
回复:“或者考虑一个没有足够时间来制作'好的API'并需要立即使用东西的开发环境'”:如果您发现自己处于这样的环境中,并且由于某种原因不想退出(或不确定是否要这样做),我推荐这本书《死亡三月:生存“不可能的任务”项目的完整软件开发人员指南》。它在这个问题上有很好的建议。(另外,我建议您改变主意,不要退出。)
ruakh 2015年

@ruakh我发现了解如何在没有时间编写良好API的环境中工作始终很重要。坦白说,就像我们喜欢象牙塔的方法一样,实际的代码才是真正付账的方法。我们越早承认,我们越早开始以平衡的方式接近软件,使API的质量与生产性代码达到平衡,以匹配我们的确切环境。我已经看到太多年轻的开发人员陷入了混乱的理想中,却没有意识到他们需要灵活运用这些理想。(我也是那位年轻的开发人员。仍然从5年的“良好API”设计中恢复过来)
Cort Ammon-恢复莫妮卡(Monica

1
是的,实码付账。良好的API设计是确保您将继续能够生成真实代码的方式。当项目变得难以维护并且无法创建新错误而无法修复错误时,糟糕的代码将停止为自己付出代价。(无论如何,这是一个错误的难题。好的代码所需要的不是时间,而是骨干。)
ruakh 2015年

1
@ruakh我发现,没有“好的API”,就可以制作出好的代码。如果有机会,好的API会很好,但是将它们视为必需品会引起更多的争执。考虑:人体的API太可怕了,但我们仍然认为它们是工程学上的小壮举=)
Cort Ammon-恢复莫妮卡

我要举的另一个例子是启发人们进行计时的灵感。与我一起工作的小组之一有一些他们需要开发的设备驱动程序,它们具有严格的实时要求。他们有2个API可以使用。一个很好,一个不那么好。好的API无法安排时间,因为它隐藏了实现。较小的API公开了该实现,因此您可以使用特定于处理器的技术来创建有效的产品。优良的API礼貌地摆在架子上,并且它们继续满足时序要求。
Cort Ammon-恢复莫妮卡2015年

0

您不一定知道数据的内部表示形式是什么-或将来可能会变成这种形式。

假设您有一个表示为数组的数据源,则可以使用常规的for循环对其进行迭代。

现在说您要重构。您的老板要求数据源是数据库对象。此外,他想选择从API,哈希表,链表,旋转的磁鼓,麦克风或外蒙古的牛屑实时计数中提取数据。

好吧,如果只有一个for循环,则可以轻松地修改代码以按期望的方式访问数据对象。

如果您有1000个试图访问数据对象的类,那么现在您将遇到一个很大的重构问题。

迭代器是访问基于列表的数据源的标准方法。这是一个常见的界面。使用它会使您的代码数据源不可知。

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.