Difficulty: 5
Take a few minutes to consider some implications of the interface principle on the way we reason about Program Design. We'll revisit a classic design problem: "What's the best way to writeOperator <()? "
There are two main ways to writeOperator <()For a class: As a free function that uses only the usual interface of the class (and possibly accessing nonpublic parts directly asFriend), Or as a free function that callaVirtual print ()Helper function in the class.
Which method is better? What are the tradeoffs?
Solution
Are you wondering why a question like this gets a title like "Name Lookup? A class = "doclink" href = "0201615622_part03.html # part03"> Part 3 "? If so, you'll soon see why, as we consider an application of the interface principle discussed in the previous item.
What does a class depend on?
"What's in a class? "Isn't just a philosophical question. It's a fundamentally practical question, because without the correct answer, we can't properly analyze class dependencies.
To demonstrate this, consider a seemingly unrelated problem: What's the best way to writeOperator <For a class? There are two main ways, both of which involve tradeoffs. i'll analyze both. in the end we'll find that we're re back to the interface principle and that it has given us important guidance to analyze the tradeoffs correctly.
Here's the first way:
//*** Example 5 (a) -- nonvirtual streaming class X{ /*...ostream is never mentioned here...*/};ostream& operator<<( ostream& o, const X& x ){ /* code to output an X to a stream */ return o;}
Here's the second:
//*** Example 5 (b) -- virtual streaming class X{ /*...*/public: virtual ostream& print( ostream& ) const;};ostream& X::print( ostream& o ) const{ /* code to output an X to a stream */ return o;}ostream& operator<<( ostream& o, const X& x ){ return x.print( o );}
Assume that in both cases the class and the function Declaration appear in the same header and/or namespace. Which one wocould you choose? What are the tradeoffs? Historically, experienced C ++ programmers have analyzed these options this way:
Option (a)'s advantage (we 've said until now) is thatXHas fewer dependencies. Because no member functionXMentionsOstream,XDoes not (appear to) depend onOstream. Option (a) also avoids the overhead of an extra virtual function call.
Option (B)'s advantage is that anyDerivedxWill also print correctly, even whenX &Is passedOperator <.
This is the traditional analysis. alas, this analysis is flawed. armed with the interface principle, we can see why: the First Advantage in option (a) is a phantom, as indicated by the comments in italics.
According to the interface principle, as longOperator <Both "mentions" X (true in both cases) and is "supplied with" X (true in both cases), it is logically partX.
In both cases,Operator <MentionsOstream, SoOperator <Depends onOstream.
Because, in both cases,Operator <Is logically partXAndOperator <Depends onOstream, Therefore in both cases,XDepends onOstream.
So what we 've traditionally thought of as option (a)'s main advantage is not an advantage at all. In both cases,XStill in fact depends onOstreamAnyway. If, as is typical,Operator <AndXAppear in the same headerX. h, Then bothX'S own implementation module and all client modules that useXPhysically depend onOstreamAnd require at least its forward declaration in order to compile.
With option (a)'s First Advantage exposed as a phantom, the choice really boils down to just the virtual function call overhead. without applying the interface principle, though, we wocould not have been able to as easily analyze the true dependencies (and therefore the true tradeoffs) in this common real-world example.
Bottom line, it's not always useful to distinguish between members and nonmembers, especially when it comes to analyzing dependencies, and that's exactly what the interface principle implies.
Some interesting (and even surprising) Results
In general, ifAAndBAre classes andF (a, B)Is a free function:
IfAAndFAre supplied together, thenFIs partA, SoADepends onB.
IfBAndFAre supplied together, thenFIs partB, SoBDepends onA.
IfA,B, AndFAre supplied together, thenFIs part of bothAAndB, SoAAndBAre interdependent. this has long made sense on an instinctive level when f the library author supplies two classes and an operation that uses both, the three are probably intended to be used together. now, however, the interface principle has given us a way to more clearly state this interdependency.
Finally, we get to the really interesting case. In general, ifAAndBAre classes andA: G (B)Is a member functionA:
BecauseA: G (B)Exists, clearlyAAlways depends onB. No surprises so far.
IfAAndBAre supplied together, then of courseA: G (B)AndBAre supplied together. Therefore, becauseA: G (B)Both "mentions"BAnd is "supplied"B, Then according to the interface principle, it follows (Perhaps surprisingly, at first) thatA: G (B)Is partB, And becauseA: G (B)Uses an (implicit)A *Parameter,BDepends onA. BecauseAAlso depends onB, This means thatAAndBAre interdependent.
At first, it might seem like a stretch to consider a member function of one class as also part of another class, But this is true only ifAAndBAre also supplied together. Consider: IfAAndBAre supplied together (say, in the same header file) andAMentionsBIn a member function like this, "gut feel" already usually tells usAAndBAre probably interdependent. they are certainly strongly coupled and cohesive, and the fact that they are supplied together and interact means that: (a) they are intended to be used together, and (B) changes to one affect the other.
The problem is that, until now, it's been hard to proveAAndB'S interdependence with anything more substantial than gut feel. Now their interdependence can be demonstrated as a direct consequence of the interface principle.
Note that, unlike classes, namespaces don't need to be declared all at once, and what's "supplied together" depends on what parts of the namespace are visible.
//*** Example 6 (a) //---file a.h---namespace N { class B; }// forward declnamespace N { class A; }// forward declclass N::A { public: void g(B); };//---file b.h---namespace N { class B { /*...*/ }; }
ClientsAIncludeA.h, So for themAAndBAre supplied together and are interdependent. ClientsBIncludeB. H, So for themAAndBAre not supplied together.
In summary, I 'd like you to take away three thoughts from this miniseries.
The interface Principle: For a class X, all functions, including free functions, that both "Mention" X and are "supplied with" X are logically parts of X, because they form part of the interface of X.
Therefore, both member and nonmember functions can be logically "part of" A class. A member function is still more stronugly related to a class than is a nonmember, however.
In the interface principle, a useful way to interpret "supplied with" is "appears in the same header and/or namespace. "If the function appears in the same header as the class, it is" part of "the class in terms of dependencies. if the function appears in the same namespace as the class, it is "part of" the class in terms of object use and Name Lookup.