【问题标题】:Data isolation between pure abstract class against its implmentation and the true meaning of pure abstract class?纯抽象类与其实现之间的数据隔离与纯抽象类的真正含义?
【发布时间】:2011-12-29 14:22:46
【问题描述】:

在 Bjarne Stroustrup 的《C++ 编程语言》一书中,声明了从类层次结构中的超类派生的类,在许多情况下,可以访问超类的数据。这本书认为这是一个问题,因为“两个相关但不同的数据集之间的共享是自找麻烦。迟早有人会让它们不同步。而且,经验表明新手程序员倾向于以不必要的方式弄乱受保护的数据并导致维护问题”。这里的问题是,从祖先类访问私有数据怎么会是自找麻烦?听起来从你的父类继承资产是一件坏事。

因此,纯抽象类机制允许抽象类与其实现之间的分离。注意实现中包含数据成员,因为如果数据成员定义在抽象类中,抽象类包含实现细节(定义的成员),因此抽象和实现之间没有很好的解耦。为什么需要这样的分离?如here 所述,其中一个原因是“一种强制类设计者和该类用户之间的契约的方式。该类的用户必须声明一个匹配的成员函数才能编译该类.” 另一个重要的原因,如书中所述,是为了防止实施更改。拥有抽象类层次结构,您可以防止需要编译整个类层次结构的更改。

例如,如果你有一个普通的类,该类包含10个数据成员和20个成员函数,并被根类以下几层的十几个类继承。对根类的成员函数的实现进行了一些更改(函数签名保持不变),您必须重新编译整个类层次结构以应用更改其终端类,否则程序会中断。在抽象类层次结构中,除非函数签名发生变化,否则如果终端类的实现发生变化,则只需要重新编译该终端类。因此,大部分代码都得到了最大限度的保护。 我的理解正确吗?

我认为他的示例(Ival_box 示例)与桥接设计模式相结合尝试并且很强大,因为桥接模式完全离开了抽象树,并且有另一个类层次结构来实现。 纯抽象类可以认为等价于Java中的接口,除了Java中的解释通常是抽象类可以被继承一次,而接口允许多重继承,因此具有共享行为封装在多个类之间的共享接口中。我认为这个答案在这种情况下是无效的,因为 C++ 允许多重继承。

最后一个问题是,派生类是如何编译并存在于二进制映像中的?编译? (类似于预处理)

tl;dr:

从祖先类访问私有数据怎么会自找麻烦?

纯抽象类(Java 中的接口)是一种保护源代码免受更改的方法吗,抽象树和实现树的分离?

抽象类/超类在C++中是如何编译的?是否通过填写超类的信息将派生类变成单个类,然后编译

【问题讨论】:

  • 我被文字墙吓到了。有什么办法可以减少一点(很多)?这将增加您获得答案的机会。
  • 好了,我给你总结了一下。

标签: c++ abstract-class


【解决方案1】:

元注释:您的问题可以作为多个单独的问题提出。

从祖先类访问私有数据怎么会自找麻烦?

非正交性。当祖先类的受保护部分更改时,所有派生类也必须更改。这很容易出错,因为更改祖先的程序员可能在他的思维范围内没有派生类。

对于抽象类层次结构,除非函数签名发生变化,否则如果终端类的实现发生变化,则只需重新编译该终端类。

技术上是正确的,但你错过了要点。重新编译不是很好,但也不是很昂贵,因为计算机会这样做。真正的成本是由非正交性引起的人工工作。当一个类有一个公共或受保护的数据成员时,人们会使用它,而当它发生变化时,事情就会中断,必须加以修复。

顺便说一句,“被十几个在根类之下有几层的类继承”通常是糟糕设计的标志。保持继承层次结构简洁明了。优先使用成员而不是基类:当您决定在 Bar 类中使用类 Foo 的功能时,并且没有令人信服的理由让 Bar 继承 Foo,而是使用 Foo 类型的成员。

我认为这个答案在这种情况下是无效的,因为 C++ 允许多重继承。

在 C++ 中,接口和类之间的区别没有被具体化(变成一个东西),但仍然被使用。在许多情况下构建接口非常有用,例如由抽象虚函数组成的类,因为它们避免了许多可怕的多重继承的麻烦。

派生类如何编译并存在于二进制映像中?

这是特定于编译器的。暂时不要担心 - 首先了解语言,然后是实现。

【讨论】:

  • +1。多继承应该多用为单继承+接口+mixins。
  • 感谢您的详细回答。这让我想起了当我用 Java 学习设计模式时,我想知道为什么继承被认为是脆弱的。我想数据成员应该与私有财产隔离。而且,“优先考虑成员而不是基类”是什么意思?
  • @Amumu:编辑了一个澄清句。
  • @thiton 啊,你的意思是更喜欢组合而不是继承,对吗?
【解决方案2】:

如果根类的暴露成员发生更改,您对需要重新编译所有内容的理解是正确的。

此外,您对如何编译派生类的理解是正确的。事实上,第一个 C++ 编译器只是一个花哨的 C 预处理器。

从祖先类访问私有数据是自找麻烦,因为这样子的构建依赖于祖先的实现细节。如果祖先需要改变它的工作方式,(即使它的接口和功能保持不变),所有后代都会中断。

【讨论】:

  • 我明白了。不仅抽象和实现分离,类层次结构中各层之间的类之间的解耦也是保护类的另一种方式。很有趣。
  • 对。这就是为什么数据成员几乎总是被声明为私有的。 (此外,只打算由后代调用的方法被声明为受保护。)
【解决方案3】:

如何从祖先类访问私有数据? 自找麻烦?

想象一下,如果一个祖先类具有它所维护的某些特性、状态和方法。假设成员数据的某些组合是无效的(也就是违反类不变量)。然后想象一个子类可以在不维护不变量的情况下更改祖先数据。您最终会遇到无效状态的情况,不仅如此,父母甚至不知道发生了什么,因此它没有机会抛出异常或以任何方式恢复。不仅如此,即使它最终处于有效状态,如果祖先类在类(公共/受保护)接口之外发生变异,它仍可能对其所处的状态“感到困惑”。

纯抽象类(Java 中的接口)是一种保护源代码的方法吗? 针对更改的代码,分离抽象树和 实现树?

纯抽象类用于表示接口概念。它本身并不能保护源代码免受更改。通常只要接口合适并且可以轻松地将实现写入接口,客户端代码就不必更改,尽管可能需要重新构建。接口和实现的分离绝对是可取的,并且可以通过使用委托给虚拟 pimpl 实现的非虚拟接口(或使用非虚拟接口模式)进一步改进。

抽象类/超类如何在 C++ 中编译?它是否转动 通过填写来自的信息将派生类转换为单个类 超类,然后编译

超类被视为派生类的一部分。具体细节因实施而异。

【讨论】:

  • 我回复晚了,但感谢您的回答。它与其他人一起澄清了此事。
【解决方案4】:

曾与多个 O.O.语言,我学到了一些东西,有助于用 C++ 编程。 这适用于您的问题。

这是一个冗长而无聊的问题,但是,它可能值得花时间。

具体类,首先

我通常从具体的类开始,当几个类共享共同的特性,如方法或字段时,我会设计一个共同的超类。许多开发人员(使用任何语言)似乎直接跳到做超类,然后是子具体类。

O.O.的设计程序,无论是否抽象,似乎都是首先设计的超类。但是,在现实世界中,大多数类一开始都是具体的,而超类是后来出现的。即使在设计上,U.M.L.或代码超类首先出现。

所以这个例子,包括数据和方法:


//  ....

#include <iostream>

class Employee {
  public:
    Employee();                         // constructor;

    void SayHello();
    void Work();
};

class Student {
  public:
    Student();                          // constructor;

    void SayHello();
    void Study();
};

//  ....

变成这样:


//  ....

#include <iostream>

class Person {
  public:
    Person();                         // constructor;

    virtual void SayHello() = 0;
};

class Employee {
  public:
    Employee();                         // constructor;

    virtual void SayHello();
    void Work();
};

class Student {
  public:
    Student();                         // constructor;

    virtual void SayHello();
    void Study();
};

//  ....

这些前面的例子只显示了方法,而不是数据,但同样的原则也适用。

避免私有字段或方法

我通常会做的事情是避免“私人”部分。如果我需要隐藏,我会使用“protected”来代替。

所以,这个:


//  ....

#include <iostream>
#include <cstring>

class Person {
  private:
    char[512] Name;

  public:
    Person();                         // constructor;

    virtual void SayHello() = 0;    
};

//  ....

变成这样:


//  ....

#include <iostream>
#include <cstring>

class Person {
  protected:
    char[512] Name;

  public:
    Person();                         // constructor;

    char[512] Name;

    virtual void SayHello() = 0;    
};

//  ....

如果您正在使用一个类,无论是否抽象,您可能会认为,您或其他开发人员可以编写子类并访问父类字段或方法。

使用属性(使用访问器方法)代替普通数据字段

真正困扰我的一点是,“C++”和“Java”中缺少“真实属性”。

从对象访问、修改、控制或更新数据的一种方法,它的直通属性,即使用称为访问器的方法访问的字段。

属性可以在 C++ 中实现,使用模板,或者只是手动编码方法。

所以这个,直接非隔离字段示例:


//  ....

#include <iostream>
#include <cstring>

class Person {
  public:
    Person();                         // constructor;

    char[512] Name;

    virtual void SayHello() = 0;    
};

int main (...) {
  Person thisPerson = new Person();

  strcpy(thisPerson.Name, "John Doe");

  cout << thisPerson.Name << "\n";

  delete thisPerson;
}

//  ....

变成,这个孤立的属性例子:


//  ....

#include <iostream>
#include <cstring>

class Person {
  protected:
    // isolated data field
    char[512] Name;

  public:
    Person();                         // constructor;

    virtual void SayHello() = 0;

    // public data field accesor reader method or "getter"
    const char* getName();

    // public data field accesor writer method or "setter"
    void setName(const char* AName);
};

const char Person::getName() {
  char char* Result = this.FName;

  return Result;
}   

void setName(const char* AName) {
  bool IsValid = false;

  // do some validation before actually modifing field
  // ...

  if (IsValid)
  {
    strcpy(this.FName, AName);
  }
}   

int main (...) {
  Person thisPerson = new Person();

      // should be read as "thisPerson.Name = "John Doe";"
  thisPerson.setName("John Doe");

      // should be read as "cout << thisPerson.Name;"
  cout << thisPerson.getName() << "\n";

  delete thisPerson;
}

//  ....

许多 C++ 开发人员不喜欢“属性”,因为缺乏控制。我认为您应该习惯于访问数据作为您的问题,但作为任何功能,都应该“正确使用”。

属性访问方法应该是虚拟的

在某些语言中,可以实现属性的概念,直接访问对象的数据字段,通过方法读取和写入,或混合两种方法。为了使事情更复杂,方法可以是“虚拟的”或“非虚拟的”。

当我使用属性时,我总是使用虚拟方法来访问它们。速度和记忆力会受到惩罚,但是,这是值得的。具有虚拟覆盖方法的属性允许控制和访问对象的数据。

总结

您的问题涉及如何实现数据、如何隔离数据、如何访问类中的数据以及如何在子类中实现这些功能。

我在 Stackoverflow 中阅读过类似的问题,但我得出的结论是,有几个问题与 C++ 中“真实属性”的缺失有关。

您可能希望通过使用“属性”来管理您的数据访问和隔离。

并且,可能想尝试学习其他 O.O.,其中实现了“属性”的概念,例如:VB (.NET)、C#、Delphi、D 或 Vala。

稍后,如果需要,请尝试在您的 C++ 程序中实现这个概念。我在 C++ 中的类具有普通数据字段、被视为“属性”的字段,就像 Java 对访问器方法所做的那样,或者在需要时混合两者。

这并不意味着你要重写你正在工作的应用程序,或者尝试探索哪种编程语言更好,只是从其他编程语言中学习一些可能对你的工作有帮助的东西。

【讨论】:

  • 哇。感谢您的巨大回答:)。但是,至于您的设计建议是自下而上开始设计,我真的认为这是开发人员的偏好。如果我不确定类层次结构,我会在纸上设计,或者更好的是,使用 UML 工具,稍后这些工具会为我生成代码。一只箭两只鸟,非常方便,尤其是如果我用 Java 这样的“高级”语言编程。设计应该在实施之前在纸上完成(或者你可以称之为计划)。设计可能不正确,但这就是我们进行迭代开发的原因。
  • 我以前用 C# 编程过。语言很好,但它不是可移植的,这非常令人沮丧。而且我不会尝试单声道实现,因为它无论如何都不会像微软那样好。至于属性,我以前用过,虽然只是用于 getter 和 setter 的许多变体。据我了解,属性只是访问内部数据之前的另一层抽象。如果我实现属性,我会创建另一个内部包装类来包装数据成员。但是,我认为 getter/setter 就足够了。我还没有看到 C# 中定义的属性的高级用法。
  • @Amumu:有时,我在纸上设计,有时,直接跳到编码,但类层次结构,它不会自动出现。代码更改,最终会出现新的子类和新的父类,并且类层次结构(和您的代码)会随着时间发生变化......
  • @Amumu:“属性”的主要思想是其封装的属性(根据需要验证、隔离或公开字段)。应该像 C++ 中的 C# 一样编写 "cout
  • 我的问题不是关于基本的 OO :( 。当我第一次学习基本的 OO 概念时,它曾经让我感到沮丧,但是在 Java 中。有人告诉我,如果过度使用继承是不好的因为它很脆弱。我并没有真正看到它,因为我很少使用抽象类来表示抽象概念,而且我认为很多人都这样做了,因为你可以拥有一个普通的类并且仍然可以与抽象类做同样的工作,并且对于多态性,接口来拯救。直到我读了《C++ 编程语言》,我之前对 C++ 有过不错的经验。
猜你喜欢
  • 2016-06-29
  • 1970-01-01
  • 1970-01-01
  • 2011-01-06
  • 1970-01-01
  • 2010-10-10
  • 1970-01-01
  • 2012-03-18
  • 2017-04-28
相关资源
最近更新 更多