【问题标题】:Isn't there a point where encapsulation gets ridiculous?封装不是很可笑吗?
【发布时间】:2009-11-11 02:12:26
【问题描述】:

对于我的软件开发编程课程,我们应该为 RSS 提要制作一个“提要管理器”类型的程序。以下是我如何处理 FeedItems 的实现。

又好又简单:

struct FeedItem {
    string title;
    string description;
    string url;
}

为此我被打分了,“正确”的示例答案如下:

class FeedItem
{
public:
    FeedItem(string title, string description, string url);

    inline string getTitle() const { return this->title; }
    inline string getDescription() const { return this->description; }
    inline string getURL() const { return this->url; }

    inline void setTitle(string title) { this->title = title; }
    inline void setDescription(string description){ this->description = description; }
    inline void setURL(string url) { this->url = url; }

private:
    string title;
    string description;
    string url;
};

现在对我来说,这似乎很愚蠢。老实说,我不敢相信我被降价了,因为这与我的完全相同,但开销更大。


这让我想起在 C# 中人们总是这样做的:

public class Example
{
    private int _myint;

    public int MyInt
    {
        get
        {
            return this._myint;
        }
        set
        {
            this._myint = value;
        }
    }
}

我的意思是我 GET 他们为什么这样做,也许稍后他们想验证 setter 中的数据或在 getter 中增加数据。但是为什么你们不这样做直到这种情况出现?

public class Example
{
    public int MyInt;
}

抱歉,这是一种咆哮,并不是真正的问题,但冗余让我抓狂。为什么 getter 和 setter 在不需要时如此受欢迎?

【问题讨论】:

  • 如果您认为这很荒谬,您还没有使用带有单例工厂类的库包含 SQL 语句的字符串。哦,每个中间类都有一个接口。
  • 就我个人而言,我讨厌 geters/setter(它类似于 Java,而不是 C++)。你为什么要摆弄对象的内部结构。创建后更改这些成员似乎没有意义(但这取决于使用要求)。我会将所有成员设为私有,构造函数来创建它们并查看对象的使用方式。添加不必要的 getter/setter 几乎和使用公共变量一样糟糕。
  • @Martin - 我认为“API 已经公开,所以为时已晚”的论点是错误的,并且使用得太频繁了。当然,如果您要更改的all 是内部实现机制,那么getter/setter 可以保护客户端免受更改。但是如果你想改变行为,假装 getter/setter 保护用户免受改变是一个谎言。如果 setX() 现在可以在以前从未失败的情况下失败(例如,由于访问控制),那么类的接口已经改变,用户无论如何都会中断。更糟糕的是,它会在运行时而不是编译时中断。
  • 在 C# 中,我会编写 public class Example{ public int MyInt{get;set;} },这不会比字段长多少。
  • 关于 C# 的注释:public int MyInt; 是一个类成员,与 public int MyInt{get;set;} 是一个属性不同。如果您以后需要用属性替换成员,您将更改类的签名。先前指向该成员的代码将不知道相同名称的属性是有意的。不同之处在于,在 IL 中更改成员是更改变量,而更改属性是调用方法。这是这种原本美丽的语法糖可以咬你的少数几次之一。

标签: c# c++ encapsulation yagni


【解决方案1】:

这是“最佳实践”和风格的问题。

  • 您永远不想直接公开您的数据成员。您总是希望能够控制它们的访问方式。我同意,在这种情况下,这似乎有点荒谬,但它旨在教你这种风格,让你习惯它。
  • 它有助于为类定义一致的接口。你总是知道如何到达某物 --> 调用它的 get 方法。

还有可重用性问题。说,在路上,你需要改变当有人访问数据成员时发生的事情。您可以在不强制客户端重新编译代码的情况下做到这一点。您可以简单地更改类中的方法并保证使用新的逻辑。

【讨论】:

  • 是的,我明白这一点,标准接口是一个很好的论据。但是考虑一下 C# 示例,如果需要,在两者之间进行更改一点也不难,并且界面保持不变。在我的 C++ 示例中,FeedItem 将不再需要任何开销,它只是保存一些数据的便捷方式,根本不需要对其进行操作。
  • 也许关于您的 C# 示例,但是对于您的 C++ 示例,您正在做出假设。好的软件工程的关键是设计你的代码,这样当假设发生变化时你可以很容易地改变它。尽管这看起来有点荒谬,但老实说,我同意你的看法,在这种情况下,可能没有必要,关于你的代码的哪些假设可能会改变?也许这些 url 是以相对的方式提供给您的,但您的代码假定它们是绝对的,更改此类比更改使用它的 3k 程序容易得多:-)
  • 通常问题不大。如果您确实需要更改某人访问数据成员时发生的情况,您仍然可以更改类接口以使其成为私有成员并提供 getter 方法。然后编译器会指出您需要修改其余代码的位置。工作量不大。
  • 不,C# 示例中的界面保持不变。您将字段更改为属性。所有客户端都需要重新编译。在通常将代码编译并链接到一个二进制文件的 C++ 中,这可能不是问题,但在程序集在运行时“链接”的 .NET 环境中,这非常重要。
  • @StackedCrooked:当然,如果所有代码都是你的。但是使用您的界面的 13000 个应用程序会发生什么情况。你打算改变其他人的代码吗?即使您可以,其他团体也可能会反对,因为代码处于锁定状态或需要大量的测试发布周期(出于法律或其他原因)。
【解决方案2】:

这是一个关于这个主题的很长的 SO 讨论:Why use getters and setters

您想问自己的问题是“3 个月后,当您意识到 FeedItem.url 确实需要验证但它已经直接从 287 个其他类中引用时会发生什么?”

【讨论】:

  • 好吧,在那种特殊情况下,我在存储之前验证了 URL。结构只是一种保存数据的便捷方式,而不是真正从内部对其进行操作。顺便说一句,谢谢你的链接。
  • 您应该考虑(以面向对象的方式)谁(哪个类)负责验证 URL。我认为 FeedItem 类验证 URL 是有意义的,因为它是唯一可以保证 URL 始终有效的类。
  • 每个说要验证 URL 的人都错过了 RSS 提要的要点。无需验证 URL!!!!!!!!!!您只需显示提要所说的内容,如果提要发布商上的 URL 无效,而不是我。
  • 但是,这样一个根本性的改变不会从本质上改变函数的契约吗? 287 个其他类仍然可以编译,但现在 FeedItem 为它们做了一些完全不同的事情?例如,是否应该添加任何错误处理?
【解决方案3】:

在需要之前这样做的主要原因是版本控制。

字段的行为与属性不同,尤其是在将它们用作左值时(通常不允许这样做,尤其是在 C# 中)。此外,如果您以后需要添加属性获取/设置例程,您将破坏您的 API - 您的类的用户将需要重写他们的代码以使用新版本。

提前这样做会更安全。

C# 3,顺便说一句,让这更容易:

public class Example
{
    public int MyInt { get; set; }
}

【讨论】:

  • 确实如此。在 C# 中,现在创建一个属性就像创建一个公共字段一样容易,所以不妨把它做好。
  • 然而,在 C++ 中,您实际上可以 编写属性,完全可用作左值。请参阅here 示例。
【解决方案4】:

我完全同意你的看法。但在生活中,你或许应该做正确的事:在学校里,就是为了取得好成绩。在您的工作场所,它是为了满足规格。如果您想固执己见,那没关系,但请解释一下自己——用 cmets 覆盖您的基地,以尽量减少您可能受到的伤害。

在您上面的特定示例中,我可以看到您可能想要验证 URL。也许你甚至想要清理标题和描述,但无论哪种方式,我认为这是你可以在课程设计的早期告诉的那种事情。在 cmets 中说明您的意图和理由。如果您不需要验证,那么您就不需要 getter 和 setter,您是绝对正确的。

简单是有回报的,它是一项有价值的功能。永远不要虔诚地做任何事情。

【讨论】:

  • 有时这是决定类设计的一个重要因素。请注意它。有时它只是错误的,而这种罗马的做事方式是事情失控的原因。然后你简化事情并解释自己。有人可能会用球棒打你,你必须纠正自己,但我认为整个过程对系统来说仍然是健康的。
【解决方案5】:

如果某物是一个简单的结构,那么是的,这很荒谬,因为它只是 DATA。

这实际上只是回到 OOP 的开始,那时人们还没有完全理解类的概念。没有理由拥有数百个 get 和 set 方法,以防万一有一天您可能将 getId() 更改为对哈勃望远镜的远程调用。

您确实希望在 TOP 级别具有该功能,但在底部它毫无价值。 IE 你会有一个复杂的方法,它被发送一个纯虚拟类来处理,保证不管下面发生什么它仍然可以工作。只是将它随机放置在每个结构中是一个笑话,决不应该为 POD 这样做。

【讨论】:

  • 但是在正确的 OOP 方法中,您可能永远不会使用结构。您似乎认为很少需要额外的代码来访问成员,但事实并非如此。最重要的是,您的 IDE 可以自动生成默认的 getter/setter,编译器可以优化它们。写这条评论比在 10 个类上自动创建 getter/setter 花费的时间更长。
  • “正确的 OOP 方法”的问题在于,并非所有数据都是对象。有时它们只是……数据片段。一些信息,它本身没有行为,但被系统的其他部分使用。如果它不是一个标量值,它必须是某种聚合结构,将我们置于一个 POD 结构中。
【解决方案6】:

也许这两个选项都有点错误,因为这两个版本的类都没有任何行为。如果没有更多的上下文,很难进一步评论。

http://www.pragprog.com/articles/tell-dont-ask

现在让我们想象一下,您的 FeedItem 课程已经非常受欢迎,并且被各地的项目所使用。您决定需要(正如其他答案所建议的那样)验证已提供的 URL。

快乐的日子,你为 URL 写了一个 setter。您编辑它,验证 URL 并在它无效时抛出异常。您发布了新版本的课程,每个使用它的人都很高兴。 (让我们忽略已检查和未检查的异常以保持正常运行)。

除了,你接到一个愤怒的开发者的电话。当他们的应用程序启动时,他们正在从文件中读取提要项列表。现在,如果有人在配置文件中犯了一个小错误,你的新异常就会被抛出,整个系统也不会启动,只是因为一个 frigging feed 项错误!

您可能保持方法签名相同,但您更改了接口的语义,因此它破坏了相关代码。现在,您可以占据制高点并告诉他们重新编写他们的程序,或者您谦虚地添加setURLAndValidate

【讨论】:

  • 好点:如果这只是一个POD类,封装的价值要小很多。
  • 也许,URL 本身应该是一个验证其自身内容的类的实例,而不是作为字符串保存,这样 FeedItem 和该类的任何客户端都不需要验证它。如果他们已经获得了一个 URL 实例,那么他们就知道他们可以依赖它是有效的,因为在构造该实例时会检查它。我就是这样处理它的——URL/URI 不仅仅是一个 POD,它具有严格的语法,并且可能提供诸如 getScheme()、getHostName()、getQueryString() 等方法。
【解决方案7】:

请记住,编码“最佳实践”通常会因编程语言的进步而过时。

例如,在 C# 中,getter/setter 概念已以属性的形式融入语言。 C# 3.0 通过引入自动属性使这变得更容易,编译器会自动为您生成 getter/setter。 C# 3.0 还引入了对象初始化器,这意味着在大多数情况下,您不再需要声明简单地初始化属性的构造器。

因此,执行您正在做的事情的规范 C# 方法如下所示:

class FeedItem
{
    public string Title { get; set; } // automatic properties
    public string Description { get; set; }
    public string Url { get; set; }
};

用法如下(使用对象初始化器):

FeedItem fi = new FeedItem() { Title = "Some Title", Description = "Some Description", Url = "Some Url" };

关键是您应该尝试了解针对您所使用的特定语言的最佳实践或规范的做事方式,而不是简单地复制不再有意义的旧习惯。

【讨论】:

  • +1。遗憾的是,尽管 C++ 语言中没有属性语法(它们可以用一些模板来模拟,但仍然如此),而且 Bjarne 似乎完全拒绝了它的需要。
【解决方案8】:

作为一名 C++ 开发人员,我将我的成员设为私有只是为了保持一致。所以我一直都知道我需要输入 p.x(),而不是 p.x。

另外,我通常避免实现 setter 方法。我没有更改对象,而是创建了一个新对象:

p = Point(p.x(), p.y() + 1);

这也保留了封装性。

【讨论】:

    【解决方案9】:

    绝对有一点,封装变得荒谬。

    在代码中引入的抽象越多,您的前期教育和学习曲线成本就越高。

    每个了解 C 的人都可以调试一个仅使用基本语言 C 标准库的 1000 行代码的可怕编写函数。不是每个人都能调试你发明的框架。必须权衡每个引入的级别封装/抽象与成本。这并不是说它不值得,但一如既往,您必须找到适合您情况的最佳平衡点。

    【讨论】:

    • 避免不必要的复杂性是一个有趣的论点,但它并不能反驳 getter 和 setter 提供了一个非常通用的接口并且几乎所有程序员都熟悉它的反对意见。
    【解决方案10】:

    软件行业面临的问题之一是可重用代码问题。它是一个大问题。在硬件世界中,硬件组件设计一次,然后在您购买组件并将它们组合在一起以制造新事物时重复使用该设计。

    在软件世界中,每次我们需要一个组件时,我们都会一次又一次地设计它。它非常浪费。

    封装是作为一种确保创建的模块可重用的技术而提出的。也就是说,有一个明确定义的接口,将模块的细节抽象出来,方便以后使用该模块。该接口还可以防止滥用对象。

    您在类中构建的简单类并不能充分说明对定义良好的接口的需求。说“但是为什么你们不这样做,直到出现这种情况?”在现实生活中是行不通的。你在软件工程课程中学到的是设计其他程序员可以使用的软件。考虑到由 .net 框架和 Java API 提供的库的创建者绝对需要这个规则。如果他们认为封装太麻烦,这些环境几乎无法使用。

    遵循这些准则将在未来产生高质量的代码。为该领域增加价值的代码,因为从中受益的不仅仅是您自己。

    最后一点,封装还可以充分测试一个模块并合理地确保它可以工作。如果没有封装,您的代码的测试和验证将会更加困难。

    【讨论】:

    • 不过,这似乎是一种反原型观点。
    • @Tchalvak 不一定。目标是实现良好封装的类。没有理由不能以增量方式完成。
    【解决方案11】:

    当然,Getter/Setter 是很好的做法,但它们写起来很乏味,更糟糕的是,读起来很乏味。

    有多少次我们读过一个包含六个成员变量和伴随的 getter/setter 的类,每个都有完整的 @param/@return HTML 编码,著名的无用注释,比如'get the value of X','set X的值','获取Y的值','设置Y的值','获取Z的值','设置Zzzzzzzzzzzzz的值。砰!

    【讨论】:

    • 你不需要写它们。获得一个像样的 IDE 或插件。
    • @John:如果您的 getter 和 setter 非常愚蠢,以至于您的 IDE 可以为您编写它们,那么您只是在证明原始发帖人的观点。显式 getter 和 setter 的整个概念存在根本缺陷,需要尽快从软件工程中清除。
    【解决方案12】:

    这是一个非常常见的问题:“但是为什么你们不这样做,直到出现这种情况?”。 原因很简单:通常不稍后修复/重新测试/重新部署它会便宜得多,而是第一次就做好。 旧的估计说维护成本是 80%,而大部分维护正是您所建议的:只有在有人出现问题后才做正确的事。第一次做对可以让我们专注于更有趣的事情并提高工作效率。

    草率的编码通常是非常无利可图的 - 您的客户不满意,因为产品不可靠并且他们在使用它时没有生产力。开发人员也不高兴——他们将 80% 的时间花在打补丁上,这很无聊。最终,您可能会失去客户和优秀的开发人员。

    【讨论】:

      【解决方案13】:

      我同意你的观点,但在系统中生存很重要。在学校时,假装同意。换句话说,被降价对你是有害的,因为你的原则、观点或价值观而被降价是不值得的。

      此外,在团队或雇主处工作时,假装同意。以后,自己创业,按照自己的方式去做。当您尝试他人的方式时,要冷静地以开放的心态对待他们——您可能会发现这些经历会重新塑造您的观点。

      封装在理论上很有用,以防内部实现发生变化。例如,如果每个对象的 URL 成为计算结果而不是存储值,那么 getUrl() 封装将继续工作。但我怀疑你已经听说过它的这一面。

      【讨论】:

      • “在学校的时候,假装同意。工作的时候……假装同意。以后,自己创业,按照自己的方式去做”下一句应该一直是“......然后然后你会发现为什么其他人都这样做。”
      • 好吧,我对尖酸刻薄不感兴趣。 OP 可能有一点关于封装有时被过分的观点。
      • 完全不同意。如果你和一位教授一起工作,他会因为 - 讨论 - 你对简单的好处的看法而打分,那就找一个不同的教授。如果你的雇主不愿意享受简单的好处,那就找一个不会让你走上纠缠不清的调试之路的雇主。请确保您随时准备从讨论中发现 - 你错了。
      • 在这种情况下,学生提交的作业似乎与课堂教学内容不符。如果教授拒绝discussion,那是令人反感的。但如果是在特定课程的背景下上交的书面作业,但会影响你的未来,“顺应导师的喜好”可能是我说的更好的方式。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多