Python is wierd!

Alex Martelli alex at magenta.com
Wed Jul 26 19:18:52 EDT 2000


This discussion may appear out of place in this group, but I do not
think it is, because its central theme is VERY germane to recent
Python discussions: what IS the correct unit/granularity for
encapsulation.  Some people appear to believe that the class holds
that role, and worry about Python's "weirdness" in making it so
explicitly not-so.  I claim that, not just (e.g.) in Java with its
explicit package concept and default package-accessibility, but
also in C++ (through the proper use of 'friend'), the component,
NOT the class, holds that role.  So, here we go again...:

"Mark Baker" <mbaker at 0x7a69.net> wrote in message
news:SQGf5.5281$c04.627774 at nntp2.onemain.com...
    [snip]
> >> friend is simply an evil way of violating encapsulation. It's not
> > analogous to
> >
> > Absolutely false, although people with a superficial understanding of
> > C++ do tend to think that way.  The proper use of 'friend' is to split
> > an encapsulated, cohesive functionality among more than one class,
> > *because the class is NOT the natural unit of encapsulation*.  friend
    [snip]
> I've been writing in C++ for many years now, but this isn't a matter of
> who has a larger penis...

Clearly not: it is a matter of who is right, and who is wrong, about
a specific technical issue: is "friend", in C++, "simply an evil way
of violating encapsulation", as you say, or is it rather a way to
correctly partition the functionality of one component among more
than one class, as I say?

I think that Lakos, in his "Large-Scale C++ Software Design", puts
it best (p. 136 ff in the edition I have).

I can't use bold fonts and pretty display presentation in a post,
but that's what he uses in his book to highlight this...:

"""
Principle:
Friendship within a component is an implementation detail of that
component.
"""

"By treating the component and not the class as the fundamental
unit of design, we gain an entirely different perspective" -- one
without which, Lakos explains (and I fully agree), effective
design of large-scale C++ systems is simply unfeasible.

The C++ class is *not* the correct unit of encapsulation: the
*component*, a coordinated set of classes (and, possibly, free
standing functions), that are designed, tested, and released
together.  (Martin also makes, and richly exemplifies, this
crucial concept, in his book and in his articles, of course).

Therefore, back to quoting Lakos...:

"""
Principle:
Granting (local) friendship to classes defined within the same
component does _not_ violate encapsulation.
"""
(Here, the _not_ is in italics in Lakos' text).


An example that Lakos gives is the Container/Iterator pattern:
two classes, that are part of one, single component, and need
intimate access to each other's private parts *to the exclusion
of other classes that lie outside the component*.

"Locally, within a single component, friendship should be
granted where necessary to achieve proper encapsulation for
the component as a whole".


As it happens, one B. Stroustrup appears to share this opinion,
in as much as he wrote, for example, in the Annotated C++
Reference Manual, "A friend is as much a part of the interface
of a class as a member is".  Saying that friends are evil and
break encapsulation is therefore just about as silly as saying
that member functions do so; they are BOTH parts of the class's
interface, so why single out one over the other as "evil" &c?


> If an object's internals are to be accessed externally, then methods
should
> be provided to this inside of the object itself. It should not indicate
that
> it's alright for "some" people to violate its encapsulation.

It's not an issue of *people*.  The key issue is that "certain parts
need to be accessed from code that is syntactically not part of
the class" (but IS "part of the interface of the class", because
a friend IS that -- or do you think the ARM does not describe C++,
but rather some different language?), but that code is part of the
same COMPONENT -- the same unit of encapsulation.  No other code
needs such unrestrained access: providing that access, now THAT
*would* violate the component's encapsulation.

> If another object needs access to the internals of another object, then
this
> should solely be provided by the accessor/mutator pattern.

There is no reason to say that, if some classes within one
component need to be closely coupled, and interact strongly
(which is quite frequent: a component IS well designed as
a cohesive unit, after all!), then the close-coupling must
be exposed so it can be accessed from *outside* the boundary
of the component!

For example: in my component I can expose a class X, that I
do NOT want other components to instantiate and delete at
will; every request-for-an-instance of X, and every return-
of-an-used-instance of X, *must* go through the appropriate
method of class Factory.  I hope you can see that this is
a frequent and important case; for example, class X may
encapsulate costly/scarce resources, and Factory embeds
the recycling strategy for those resources (as well as the
other uses of those resources encapsulated in other classes
Y, Z, and T, of the same component).  Or, X might be a
semantically-rich Iterator, not creatable "in vacuo" but
only in the context of a given Tree (which is the class
that would play Factory's role then), and a Tree must keep
track of all the Iterators extant upon it.  Etc, etc.

How do you propose that the component's semantics, "every
'creation' of X MUST occur through a call to Factory's
methods, every 'destruction' of X MUST occur through yet
another set of such methods", be enforced "by the
accessor/mutator pattern"?  I'll tell you: *YOU DON'T*.

With 'friend', of course, this is trivially easy -- X's
constructors and destructor are private, and X declares
Factory as its friend.  Compile-time checking of correct
encapsulation is then as guaranteed as it ever can be
in the C++ language.

C++'s approach to encapsulation has its limits, but
"friend" is DEFINITELY not such a "limit": it is a
crucial ingredient of good large-scale C++ software
design.  Have you never met such issues in your "many
years" of C++ experience?!  Or have you let your fetish
for letting the class play the inappropriate role of
unit of encapsulation stand in the way of good design,
when faced with such issues?  Perhaps not noticing the
fact that friends *are* a part of a class's interface...?


> Friends, if attempted for use as "encapsulation" suffer from issues
regarding
> protecting implementation details, in a paradoxial and limitedly
extendable way.
> It is rare to be used in this manner.

Well-designed software is rare.  In well-designed C++ software,
however, the proper use of 'friend' is QUITE frequent, and
suffers from none of the ills you claim for it.

When I want to constrain the creation/disposal of X objects
to happen through Factory, this is NOT an "implementation
detail" I'm "protecting" -- it IS a crucial part of the
interface of my component, with absolutely no paradox to
it.  In Python, I would rely on convention, as it does not
enforce encapsulation.  In Java, I would rely on package
level accessibility for the constructors.  In C++, I will
rely on "friend".  Accessors and mutators have nothing to
do with the case.


> They are used quite often to violate real encapsulation principles, for
the sake
> of performance. Methods that are declared inline may or may not end up as
> such with any given compiler. Plus depending on the complexity of
accessors,
> one may not wish to have this code inlined in every case. In the end, the

In which case, one provides two otherwise-identical accessors -- an
inline one, and a non-inline one.  Hardly rocket science, after all.
But if the accessor is complex enough to make inlining it a problem,
then the performance 'boost' of the inlining will normally be risible
anyway -- and so will be that gained by 'manually inlining' all of
this complex code.  Worst case, a good designer, faced with a horrible
performance bottleneck, may provide lower-level accessors (to expose
the specific details that are though to be so crucial to performance,
without uselessly exposing all the rest).

And if your compiler won't inline "int getX() const { return this->x; }",
you have worse problems than any amount of 'friend' can cure.

> compiler and not the programmer should be concerned with these, in any
event.
> But now I'm diverging...
>
> In the real world, people do use friend to violate encapsulation.

In the real world, there's a lot of horribly designed code, and a
lot of people who do not understand a too-complex tool well even if
they have been using it for years.  C++ is arguably too complex
for most programmers who now use it, and for most uses to which
it is put; Python's simplicity and straightforwardness are a deep
breath of fresh air, in comparison, even to somebody who's easily
in the top centile in terms of knowledge of C++'s arcana.

But that does not justify insulting those language features one
does not understand, rather than either trying to understand them
better, or giving up the language altogether.


> >> modules, packages, or anything else. It simply provides a means of
> > violating
> >> C++'s encapsulation (usually for speed purposes, since you should be
> >> able to use the accessor/mutator pattern to work with object
> >> properties)
> >
> > Again, absolutely false, and, again, suggesting rather superficial
> > understanding of C++'s working.  A public accessor/mutator can be, and
> > generally is, written in-line, with no speed penalty whatsoever over
> > access to an instance variable.
>
> Unless of course you're not simply returning a primitive instance
variable.
> And then perhaps, as I said, you aren't inclined to want it inlined, or
have
> it inlined automatically by the compiler.
>
> Friend, though, is also used rather often to execute private methods.

Yes, of course, and of course your "usually for speed purposes" becomes
laughable here.  What "speed purposes" do you claim are being served,
by making a method private and using friend, rather than making the
method public?!  Clearly, and in exact rebuttal to your "usually", in
these cases which now you claim occur "rather often", the purpose here
is letting a certain strong coupling happen *selectively*: only between
those classes (of the same component) that specifically NEED it, keeping
the component's encapsulation INTACT with regard to classes OUTSIDE of
the component itself.

Note that my example, above, does concern itself with private methods
(specifically, private constructors and destructor).  I suspect that
(partly due to convenience issues) this may be a more frequent case
than access to private instance-variables (or private static class
variables and functions, probably a rarer need).


> > The problem is, this lets ANYBODY read/write that property (through the
> > accessor and mutator); what if you only want ONE OTHER, SPECIFIC CLASS,
> > to have that ability?  Answer: you use friend, which lets you ENFORCE
> > encapsulation
> > (and keep the accessor and/or mutator private, so that only the friend
> > class
> > can make use of them -- if it still makes sense to have them, which it
> > often does, essentially for purposes of convenience).
>
> This allows you to partially violate encapsulation, in order to maintain
it
> elsewhere. This doesn't really increase it, it decreases the violation of
it.
> To claim otherwise is amusing, at best.

Only to someone who clings to the idea that the class is the
right unit/granularity for encapsulation, and does not accept
the fact (proclaimed in the ARM) that friends are a part of
a class's interface.

To anybody who understands, there is no violation at all in
letting any part of a class's interface access the class's
internals, of course, and, so, no occasion for merriment.


> More often than not, if you need access to some part of an object's
> information, there are chances that someone else will. If you need

Nope.  I need strong coupling between the classes that make
up my single, coherent component; for example, I need my
Factory class to strictly control creation and disposal of
other object-instances provided by the component.  This
affords no inference that anybody else needs equally strict
control.

> access to someone's private implementation, then their framework
> lacks much to be desired. Encapsulation is meant to protect mostly the
> latter.

I suspect you mean "leaves", rather than "lacks", but this
is still rather unparsable even with this substitution.

"Someone's private implementation" is not the issue.  "friend"
is never correct _across_ component boundaries; it IS a crucial
implementation detail *within* a component.


> The Design and Evolution of C++ and The C++ Programming Language are
> also good in that Bjarne is more than willing to say where C++ could have
been
> better, or why a "bad" decision was made.

Yes, and never does he say that "friend" is one such bad decision.
Much less "evil", as you assert.  Stroustrup does not defend bad
design decisions (such as the existence of protected data members);
you should be thereby readier to listen when he does explain crucial
issues, such as the fact that a friend is part of a class's interface
just as much as any member is.

> Multi-Paradigm Designs for C++, Generic Programming and the STL, and other
> such books are also interesting as far as C++ goes.

And Lakos' "Large Scale C++ Software Design".  I would not recommend
it to a C++ beginner, but if you are still using C++ as well as having
all those years of experience, you would be well advised to have a
go at it: it's VERY instructive.


> Even better than strictly C++ books are things such as Design Patterns,
> Analysis Patterns: Reusable Object Models, Smalltalk 80: The Language,
> Understanding CLOS, and some others. It's much more interesting to look
> at language, framework, and pattern designs for a multitude of things, and
> then look at any one language's specific features, instead of accepting
the
> decisions or beliefs of one faction of programmers.

Learning and practising many languages is a wonderful thing, but it
does not substitute for a deep and complete understanding of one
language -- not if one wants to use that language for the best, and
not if one wants to freely use words as strong as "evil" to condemn
very specific features of that language.

It's not an issue of "factions".  If you consider C++ too complex,
and want to eschew using it wherever avoidable, you will hear no
disagreement from me -- it IS too complex for optimal human use,
and I believe an ideal situation would see it used only for a small
minority of the tasks in which it's so pervasive today, those for
which it really _can_ be optimal (mostly, IMHO, for performance
reasons).  But as and when you use it, or discuss it, you really
should base your usage, and your discussion, on a deeper and more
complete understanding -- exactly because it IS so rich & complex,
facilely branding specific features of it as "evil", without a full
understanding of their role in the overall scheme of things, is a
temptation a wise man would, I believe, carefully avoid.  (But then,
I'm not half as good in knowledge about what wise men do, than I am
about how C++ is exactly defined and best used in various cases:-).


Alex






More information about the Python-list mailing list