7

I'm working on a project with others and we have a discussion about information hiding and static type safety. Our scenario is described below.

Language: C++11

Scenario: We want to create a tree-like structure. Each tree nodes has its own class, which all are sub-types of base class NodeType. Each sub-type has its own rule to link other nodes. For example, NodeTypeA, NodeTypeB and NodeTypeC are sub-classes of NodeType, and

  • NodeTypeA can only has NodeTypeB as its first child, NodeTypeA as its second child.
  • NodeTypeB can only has NodeTypeB as its childron. (It may have any number of childron)
  • NodeTypeC can only has NodeTypeA as its first child, NodeTypeB as its second child.

The example may have recursion problem, but for illustration is fine.

Now there is a Factory class to create each node:

  • createNodeTypeA
  • createNodeTypeB
  • createNodeTypeC

There is a class, Builder, that wants to convert user text input to the tree.

Builder need not know information about each node's type. It just carries pointers got from Factory, and passes them to another Factory's method. In Builder's perspective, all node's type can be the base class NodeType.

Dilemma:

If we value Information Hiding more, the Factory's method should be:

  • NodeType *createNodeTypeA(NodeType *first, NodeType *second)
  • NodeType *createNodeTypeB(std::vector<NodeType *> children)
  • NodeType *createNodeTypeC(NodeType *first, NodeType *second)

and to provide correctness, we have to provide run-time check on each parameter in Factory's method.

If we value Static Type Safety more, the Factory's method should be:

  • NodeTypeA *createNodeTypeA(NodeTypeB *first, NodeTypeA *second)
  • NodeTypeB *createNodeTypeB(std::vector<NodeTypeB *> childron)
  • NodeTypeC *createNodeTypeC(NodeTypeA *first, NodeTypeB *second)

and Builder must know sub-types of each node. What we gain is type safety.

The figure below illustrates the two ways:

Information Hiding v.s. Static Type Safety

Discussion:

Favor Information Hiding:

  • Information hiding itself is important. Developers of Builder can work without knowledge of sub-types of nodes.
  • We can compensate the lose of static type safety by code review, testing and automatic analyzing tool. A qualified team is expected to complete those pretty well.

Favor Static Type Safety:

Question:

Which one may be a good practice? We value long-term impact more.

  • 4
    It doesn't sound like your nodes should have a common ancestor type. I'm also not sure the "first" / "second" distinction is necessary. – Caleth Oct 13 '18 at 23:45
  • @Caleth Actually, the common ancestor type may define a common interface to get some metainformation for output, such as the type's name, the rule of connecting its children. The "first" / "second" have some meaning, and are fields of the sub-type. Maybe I should name them as "red" / "blue" to avoid ambiguity. – Leo Jacob Oct 14 '18 at 5:40
  • 2
    If you are dealing with dynamically typed Nodes, at some point you will have to cast them to a concrete type. Here, you merely have the choice between performing this cast within the builder, or in the code that calls the builder. Typically, such conversions/parsing should occur at the boundary of your data model, possibly as part of your user interface where errors can be handled more easily. The inheritance from NodeType is a red herring – implementation inheritance (as here) should not be misconstrued as interface inheritance. Your Nodes are not objects in the OOP sense. – amon Oct 14 '18 at 10:31
  • 1
    To clarify my points: You can have a static interface (e.g. the various iterator types have an interface, but don't share any ancestor type). Your Bs form trees, but A's are just lists (with B trees hanging off each element) and C is only available at the root – Caleth Oct 15 '18 at 8:44
4
NodeType *createNodeTypeA(NodeType *first, NodeType *second)
NodeType *createNodeTypeB(std::vector<NodeType *> children)
NodeType *createNodeTypeC(NodeType *first, NodeType *second)

This does not "hide information", Builder still knows which factory it calls, and which data it should pass to it. So using dynamic type does not seem to give any benefit in this case

What could be done to actually make Builder to be unavare of node type, you couls use a single factory:

NodeType *createNode(std::vector<NodeType*> children)

But this does not allow to specify the node type. This is often the case with constructing, and in general puts a question: is it in principle possible to remove the node type information from the Builder. Maybe the decoupling should rather be on consiming/preocessing side.

4

Favor Information Hiding:

  • Information hiding itself is important. Developers of Builder can work without knowledge of sub-types of nodes.

This is incorrect, because the arguments passed to the factory methods need to be of a certain (run-time) type. Therefore, your program needs to be aware of subtypes when building the tree, regardless whether the relations are checked at compile time or run time.

  • We can compensate the loss of static type safety by code review, testing and automatic analyzing tool. A qualified team is expected to complete those pretty well.

Or you can save yourself lots of work by letting the compiler do its work.

The real problem seems to be that your interface is poorly designed. Different subtypes require knowledge of each other, so they are not independent and not interchangeable. Ask yourself how much extensibility your design actually offers and how much you actually need (if you cannot come up with a use case that could realistically occur in the near future, you don't need it).

I suspect that the cause of this design failure is the fact that you are representing your tree data structure intrusively, i.e. storing the relation between nodes as data of the nodes themselves. The only distinction between the subtypes you mention is the different relations they have to other nodes. Therefore, if you separate these relations from the representation of your nodes, you may end up without the need for subtyping nodes - or, at least, be able to provide a better abstraction.

The difficulty of such a change is that functionality that uses the relations between nodes needs to be reformulated as a graph (search) algorithm. This often requires a change of mindset. Using the Boost Graph Library will help you with this and provides data structure and algorithms in a way that makes it easy to separate the tree representation, node/edge data representation and algorithms.

  • 1
    The tree structure is an Abstract Syntax Tree (AST) of a simple script language. Because each node has its own meaning, the relationship itself is important. Indeed, each node just carry a little of information, you can treat them as a std::string. Most of information encoded in their relationship. – Leo Jacob Oct 15 '18 at 8:34
  • Then maybe the node just needs to store a string and a type enum. You can use information hiding to enforce a correct tree structure - i.e. correct number/type of children. – D Drmmr Oct 16 '18 at 11:32
1

I think a little more context would have been appreciated :).

Let me rephrase your dilemma :

  • You want to be able to see a tree in the most abstract way possible. You want to be able to say : "A tree is just a non empty collection of Nodes that have one and only one parent (except the root node) that can do whatever a general tree can do".
  • You want to be able to check for the invalidity of the Tree quickly.

An algorithm that builds up the tree must be aware of the different node types and build a valid tree. If the algorithm is being supplied a stream of nodes, it should be able to tell, after checking, which node should go where. If it sees an invalid input or is forced to create an invalid tree, it simply halts and prints error.

So, define a Tree containing only methods that take a NodeType as the parameter such as findChildren, getRoot, getSize, getDepth etc. You can call your Parser(algorithm) after giving it a method buildTree which returns a NodeType(which is equivalent to a Tree !!!). We will decide on whether a node is NodeTypeA, NodeTypeB, and NodeTypeC by looking at the payload of the NodeType which can contain an enum such as TypeA, TypeB etc. All the logic that distinguishes between the node types will be in the Parser!

In conclusion, we used something called the Parser(or Algorithm) which is aware of the different node types to separate concerns from the Tree abstraction which can do very general Tree things.

1

It is a faulty comparison

"Information hiding" and "static data typing" are not opposing concepts and are not mutually exclusive neither absolutely nor relatively.

This is a false dichotomy that skews away from the true issue which @DDmmr answer touches - design. Then a related comment gives a key clue: The tree structure is an Abstract Syntax Tree (AST)] of a simple script


The design is missing a grammar for the 'simple script'

There is a class, Builder, that wants to convert user text input to the tree.

I suggest that this design statement conflates parsing with object construction leading directly to the OP's question. Parsing requires a defined structure of the "simple script." This definition is called a grammar.

A grammar describes the fundamental bits of the "simple script" as well as the allowable combinations, and combinations of combinations, and so on. Inevitably certain (complex) bits will map directly as Nodes and Node subtypes (and potentially related properties).

Importantly, all these distinct bits from fundamental to most complex have names, e.g. "number", "keyword", "digit", "letter", "operator", etc.


Separation of concerns

The AST "named nodes" are a metadata, an abstraction of concrete Node objects yet to be built. This metadata is the "seam" separating the AST from Node tree implementation.

The design then is three general pieces: a defined grammar, parsing, and concrete Node object construction.


Node implementation Design

Parsing's output is the AST. But the eventual Node tree is derived from the AST, it is not the AST. But I can certainly see the confusion. This feels like an artificial abstraction but it is an important design point.

Now the Builder will take the AST. Note that the Node relation rules are already baked into the AST's structure. Thus the Builder constructs a Node tree based solely on the AST structure without explicit knowledge of these rules.

I'm guessing that the "simple script" is not full blown general purpose scripting language so parsing is truly simple but do not let this lull you into violating Single Responsibility a.k.a. Separation of Concerns.

Perhaps the AST does not need to be utterly parsed out into elements/nodes of script fundamental particles. Maybe there are NodeMetaData objects, let's say, in the AST that correspond to Node super class properties.


The OP quandary is designed away

"Information Hiding" is inherently appropriate given the separation of concerns and a thoughtfully designed Builder. Static type parameters give way to an AST object with well defined metadata.

Builder and factories do not take Node nor subtype objects as arguments. Builder controls stepping through the AST and factories are instantiated and called based on the AST metadata. Builder assembles the NodeTree from factory-created NodeTree parts.

Node tree construction is potentially complex so study construction design patterns such as "Builder" and "Factory". Note the factory pattern varies based on complexity.

0

Information hiding is an external benefit. Given class A, A has zero benefit in hiding it’s information. It doesn’t help the class do it’s job and it doesn’t help the developer reason about the code. It helps external developers who don’t have to be exposed to useless knowledge. Hiding information is a favor your class does for other classes.

Static type checking is a benefit to your class and the class developers. It benefits the class because it needs to do less validation. Give class A and method Foo, that takes an integer and returns a decimal. Method Foo doesn’t have to check to see if the integer null or an array or something else, that check is either done at compile time or at runtime elsewhere inside that method it WILL be an integer. If Foo is private it will return a decimal. This is a benefit for your developers.

Now, whether you really need to make this trade off I can’t say, but information hiding can be dispensed with. The negative’s are external and thus not directly your problem.

Update in response to a comment: imagine that you have a procedural implementation of a sorting algorithm, it uses several global variables and a couple of functions to return intermediate results. It is absolutely rock solid, fast, efficient, great big O. Not the best target for rewriting using object oriented design, but you are doing the rest of the application. If you just wrap this in a class, it’s still a rock solid implementation that is fast and efficient and bug free. Hiding it’s globals (now class level fields or properties) and private methods doesn’t make it any faster or eliminate any bugs within the class. What it does is prevent anyone else from needing to learn the details of how it operates internally in order to safely use it. Zero benefit for our hypothetical sorting class, huge win for our application.

  • "class A has zero benefit in hiding it’s information." Information hiding is a huge benefit, because it allows to restrict the possible values that data members can have. – D Drmmr Oct 16 '18 at 11:41
  • @DDrmmr: no, that doesn’t help the class. The class can already restrict what values they can have during correct usage of the class. Suppose that your class has a property whose value is supposed to be restricted to 1,2 or 3. If the class never sets the value to anything else, it has restricted the possible values. What making that property private does is remove the burden of knowing to restrict the value from USERS of the class. This is a benefit to them. They won’t accidentally break it. But that doesn’t help the original class, it’s not responsible for bad input by others. – jmoreno Oct 17 '18 at 1:29
  • @DDrmmr: I am not saying that information hiding isn’t beneficial, it is, it makes the entire system both more reliable and easier to develop. Imagine you have a class/library and where everything is public and if used correctly it is perfect, does absolutely everything you could want, nothing you don’t, and has zero bugs. Hiding private information won’t help the class do whatever it is supposed to do, it can do it’s job perfectly without that. But if it exposes it’s private information it puts a burden on consumers not to screw it up. – jmoreno Oct 17 '18 at 1:47

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.