12

花了一些时间在 Haskell 和其他函数式语言中玩耍后,我开始欣赏从概括描述问题而来的设计的简单性。虽然模板编程的许多方面可能远非简单,但有些用途很常见,我认为它们不会妨碍清晰(尤其是函数模板)。我发现模板通常可以简化当前设计,同时自动添加一些未来阻力。为什么要将它们的功能降级给库编写者?

另一方面,有些人似乎避免使用像瘟疫这样的模板。十年前我可以理解这一点,当时泛型类型的概念对于大多数编程社区来说都是陌生的。但是现在所有流行的静态类型 OO 语言都支持一种或另一种形式的泛型。增加的熟悉度似乎需要对保守态度进行调整。

最近有人向我表达了这样一种保守的态度:

你永远不应该做任何比必要的更通用的东西——软件开发的基本规则。

老实说,我很惊讶看到这种说法如此不屑一顾,好像它应该是不言而喻的。就我个人而言,我发现它远非不言而喻,除非您另有说明,否则像 Haskell 这样的语言都是通用的。话虽如此,我想我明白这种观点的来源。

在我的脑海里,我确实有类似的规则在喋喋不休。现在它处于最前沿,我意识到我总是从整体架构的角度来解释它。例如,如果您有一个类,您不想在其中加载大量您可能有一天会使用的功能。如果您只需要一个具体版本,请不要费心制作接口(尽管可模拟性可能与此相反)。像这样的东西...

然而,我不做的是在微观层面上应用这个原则。如果我有一个没有理由依赖于任何特定类型的小型实用函数,我将制作一个模板。

那你怎么看,所以?你认为什么是过度概括?这条规则是否根据上下文有不同的适用性?你甚至同意这是一条规则吗?

4

7 回答 7

14

过度概括让我发疯。我不害怕模板(近在咫尺),我喜欢通用解决方案。但我也喜欢解决客户支付的问题。如果这是一个为期一周的项目,为什么我现在要资助一个为期一个月的盛会,它不仅会通过新税收等明显的未来可能变化,而且可能会通过发现新卫星或火星上的生命继续发挥作用?

将其带回模板,客户要求一些功能,涉及您编写一个接受字符串和数字的函数。你给了我一个模板化的解决方案,它采用任何两种类型,并为我的特定情况做正确的事情,以及在其余情况下可能正确或可能不正确的事情(由于没有要求),我将不胜感激。我会被打勾,除了付钱给你,我还必须付钱给某人来测试它,有人来记录它,如果将来发生更一般的情况,有人在你的限制范围内工作。

当然,并不是所有的概括都是过度概括。一切都应该尽可能简单,但不能更简单。必要时通用,但不再通用。尽我们所能承受的测试,但不再测试。等等。此外,“预测可能发生的变化并将其封装起来”。所有这些规则都很简单,但并不容易。这就是为什么智慧对开发人员和管理他们的人很重要。

于 2010-07-14T02:53:24.360 回答
11

如果你能同时做到这一点,而且代码至少一样清晰,泛化总是比专业化好。

XP 的人遵循一个叫做 YAGNI 的原则——你不需要它。

维基有这样的说法

即使您完全、完全、完全确定您以后需要某个功能,也不要现在实施它。通常,结果要么是 a) 你根本不需要它,要么 b) 你实际需要的与你之前预见到的完全不同。

这并不意味着您应该避免在代码中构建灵活性。这意味着您不应该根据您认为以后可能需要的东西过度设计某些东西。

于 2010-07-14T02:54:03.583 回答
6

太笼统了?我必须承认我是泛型编程的粉丝(作为一个原则),我真的很喜欢 Haskell 和 Go 在那里使用的想法。

然而,在使用 C++ 编程时,您可以使用两种方法来实现类似的目标:

  • 通用编程:通过模板的方式,即使存在编译时间、对实现的依赖等问题。
  • 面向对象编程:它的祖先在某种程度上将问题放在对象本身(类/结构)而不是函数上......

现在,什么时候使用?这肯定是一个难题。大多数时候,这只不过是一种直觉,我当然也看到过任何一种滥用。

根据经验,我会说函数/类越小,它的目标越基本,就越容易泛化。例如,在我的大多数宠物项目和工作中,我都会随身携带一个工具箱。那里的大多数函数/类都是通用的……在某种程度上有点像 Boost ;)

// No container implements this, it's easy... but better write it only once!
template <class Container, class Pred>
void erase_if(Container& c, Pred p)
{
  c.erase(std::remove_if(c.begin(), c.end(), p), c.end());
}

// Same as STL algo, but with precondition validation in debug mode
template <class Container, class Iterator = typename Container::iterator>
Iterator lower_bound(Container& c, typename Container::value_type const& v)
{
  ASSERT(is_sorted(c));
  return std::lower_bound(c.begin(), c.end(), v);
}

另一方面,您越接近特定业务的工作,您就越不可能成为普通人。

这就是为什么我自己欣赏最少努力的原则。当我想到一个类或方法时,我先退一步思考一下:

  • 让它更通用是否有意义?
  • 费用是多少?

根据回答者的不同,我调整了通用性的程度,并且我努力避免过早锁​​定,即当使用稍微更通用的方法成本不高时,我避免使用足够非通用的方法。

例子:

void Foo::print() { std::cout << /* some stuff */ << '\n'; }

// VS

std::ostream& operator<<(std::ostream& out, Foo const& foo)
{
  return out << /* some stuff */ << '\n';
}

它不仅更通用(我可以指定输出位置),而且也更惯用。

于 2010-07-14T09:22:28.223 回答
4

当你浪费时间概括某件事时,它就被过度概括了。如果您将来要使用通用功能,那么您可能不会浪费时间。这真的很简单[在我看来]。

需要注意的一件事是,使您的软件通用化并不一定是一种改进,如果它也使它变得更加混乱的话。经常有一个权衡。

于 2010-07-14T02:53:06.420 回答
2

我认为您应该考虑编程的两个基本原则:KISS(保持简单明了)和 DRY(不要重复自己)。大多数时候我从第一个开始:以最直接和最简单的方式实现所需的功能。很多时候就够了,因为它已经可以满足我的要求了。在这种情况下,它仍然很简单(而不是通用的)。

当第二次(或最多第三次)我需要类似的东西时,我会尝试根据具体的现实生活示例来概括问题(功能、类、设计等)-> 我不太可能只为自己进行概括。下一个类似的问题:如果它优雅地适合当前图片,很好,我可以轻松解决它。如果不是,我检查当前的解决方案是否可以进一步推广(不要让它太复杂/不那么优雅)。

我认为你应该做类似的事情,即使你事先知道你需要一个通用的解决方案:举一些具体的例子,并根据它们进行概括。否则很容易陷入死胡同,你有一个“不错”的通用解决方案,但它无法解决真正的问题。

但是,这可能有一些例外情况。
a) 当一个通用解决方案的工作量和复杂性几乎完全相同时。示例:使用泛型编写队列实现并不比仅对字符串执行相同操作复杂得多。
b) 如果用一般的方式解决问题更容易,而且解决方案也更容易理解。它不会经常发生,我目前无法提出一个简单的现实生活示例:-(。但即使在这种情况下,之前必须有/分析具体示例也是 IMO 必须的,因为只有它可以确认你在正确的轨道上。

可以说经验可以克服具体问题的先决条件,但我认为在这种情况下,经验意味着你已经看到并思考过具体的、类似的问题和解决方案。

如果你有时间,你可以看看计算机程序的结构和解释。它有很多有趣的东西,关于如何在通用性和复杂性之间找到正确的平衡,以及如何将复杂性保持在您的问题真正需要的最低限度。

当然,各种敏捷过程也推荐类似的东西:从简单的开始,在需要时重构。

于 2010-07-14T07:12:47.100 回答
2

对我来说,过度概括是,如果需要在任何进一步的步骤中打破抽象。项目中的示例,我住在:

Object saveOrUpdate(Object object)

这个方法太通用了,因为它是在一个 3-Tier-Architecture 中提供给客户端的,所以你必须在没有上下文的情况下检查服务器上保存的对象。

于 2011-07-27T12:02:44.220 回答
0

微软有两个过度概括的例子:
1.)CObject(MFC)
2.)Object(.Net)

它们都用于在 C++ 中“实现”大多数人不使用的泛型。事实上,每个人都对使用这些(CObject/Object)给出的参数进行了类型检查~

于 2010-07-14T03:05:35.967 回答