Steven D'Aprano said:
In particular,
you can get most of your meaningless methods out of a properly
designed Coordinate API. For example, add/sub_x/y_ord can all be
handled with move(delta_x = 0, delta_y = 0).
Here is my example again:
Then, somewhere in my application, I need twice the
value of the y ordinate. I would simply say:
value = 2*pt.y
[end quote]
I didn't say I wanted a coordinate pair where the y ordinate was double
that of the original coordinate pair. I wanted twice the y ordinate, which
is a single real number, not a coordinate pair.
Here you're not manipulating the attribute to change the class -
you're just using the value of the attribute. That's what they're
there for.
[bites tongue to avoid saying a rude word]
That's what I've been saying all along!
But according to the "Law" of Demeter, if you take it seriously, I
mustn't/shouldn't do that, because I'm assuming pt.y will always have a
__mul__ method, which is "too much coupling". My Coordinate class
must/should create wrapper functions like this:
I think you've misunderstood the LoD. In particular, 2 * pt.y doesn't
necessarily involve violating the LOD, if it's (2).__add__(pt.y). If
it's pt.y.__add__(2), then it would. But more on that later.
How are they different? Because one is a class and the other is a module?
That's a meaningless distinction: you are still coupled to a particular
behaviour of something two levels away. If the so-called Law of Demeter
makes sense for classes, it makes sense for modules too.
And here's where I get to avoid saying a rude word. I'm not going to
chase down my original quote, but it was something along the lines of
"You shouldn't reach through multiple levels of attribute to change
things like that, it's generally considered a bad design". You asked
why, and I responded by pointing to the LoD, because it covers that,
and the underlying reasons are mostly right. I was being lazy, and
took an easy out - and got punished for it by winding up in the
position of defending the LoD.
My problem with the original code wasn't that it violated the LoD; it
was that it was reaching into the implementation in the process, and
manipulating attributes to do things that a well-designed API would do
via methods of the object.
The LoD forces you to uncouple your code from your clients, and
provide interfaces for manipulating your object other than by mucking
around with your attribute. I consider this a good thing. However, it
also prevents perfectly reasonable behavior, and there we part
company.
And of course, it doesn't ensure good design. As you demonstrated, you
can translate the API "manipulate my guts by manipulating my
attributes" into an LoD compliant API by creating a collection
meaningless methods. If the API design was bad to begin with, changing
the syntax doesn't make it good. What's a bad idea hefre is exposing
parts of your implementation to clients so they can control your
state. Whether you do that with a slew of methods for mangling the
implementation, or just grab the attribute and use it is
immaterial. The LoD tries to deal with this by outlawing such
manipulation. People respond by mechanically translating the design
into a form that follows the law. Mechanically translating a bad
design into compliance with a design law doesn't make it a good
design.
Instead of using a vague, simple example with a design we don't agree
on, let's try taking a solid example that we both (I hope) agree is
good, and changing it to violate encapsulation.
Start with dict. Dict has an argumentless method, which means we could
easily express it as an attribute: keys. I'm going to treat it as an
attribute for this discussion, because it's really immaterial to the
point (and would work in some languages), but not to people's
perceptions.
Given that, what should mydict.keys.append('foobar') do? Given the
current implementation, it appends 'foobar' to a list that started
life as a list of the keys of mydict. It doesn't do anything to
mydict; in particular, the next time you reference mydict.keys, you
won't get back the list. This is a good design. If
mydict.keys.append('foobar') were the same as "mydict['foobar'] =
None", that would be a bad design.
Now suppose you want the keys in sorted order? That's a common enough
thing to want. The obvious way to get it is to get the list of keys
and to sort them. The LoD isn't clear on that (or maybe I failed to
read it properly), as you're allowed to call methods on objects that
you created. Did you create the list of keys? Did mydict? Which is
allowed? I dunno.
On the other hand, I don't have a problem with it. The keys feature
gives you a picture of part of the dictionary. What you do with the
picture after you get it is up to you - it isn't going to change
mydict. Once you've got the list, it's no longer part of mydict, so
invoking methods on it don't violate encapsulation, so there's no
problem with it.
Back to your question about sys.stdout. I said the LoD says it's ok
because I think a module is a collection, meaning sys.stdout is an
element of a collection, and it's ok to call methods on them. Others
may disagree about modules being collections. I say the call is ok
because sys.stdout is a value from sys, and manipulating it doesn't
change the internal state of the module sys.
I think I've explained the difference between what I'm saying and the
what LoD says. I think there's a relationship between the two; I'm
just not sure what it is.
[snip]
Again, this is *your* API, not mine. You're forcing an ugly, obvious API
instead of assuming the designer has some smidgen of ability.
But isn't that the whole question? Should programmers follow slavishly the
so-called Law of Demeter to the extremes it implies, even at the cost of
writing ugly, unnecessary, silly code, or should they treat it as a
guideline, to be obeyed or not as appropriate?
I believe the correct answer is "practicality beats purity". On the
other hand, I'll continue to argue that following the LoD - or at
least parts of it - only leads to ugly, unnecessary, silly code if
your design was bad in the first place. Not following the LoD doesn't
make the design good - it just means you write a lot less code in
creating your bad design.
Doesn't Python encourage the LoD to be treated as a guideline, by allowing
class designers to use public attributes instead of forcing them to write
tons of boilerplate code like some other languages?
Python encourages damn near everything to be treated as a
guideline. It's one of the things I like about the language - if I
need a hack *now* that fixes a problem, I don' have to fight the
language, I can just do it. I argue with people who try and create
classes that break that because they think "it enforces good style".
The thing about the tons of boilerplate code is that it's enforcing an
arbitrary rule in the name of enforcing "good design". But it doesn't
make the design good. In particular, if letting someone write
"obj.foo.mutate(value)" to manipulate obj is bad design, then making
them write "obj.mutate_foo(value)" doesn't mean the design is good.
Mike, the only "trivial way to deal with this" that you have pointed out
was this:
"For example, add/sub_x/y_ord can all be handled with move(delta_x = 0,
delta_y = 0)."
That's a wonderful answer *for the wrong question*. I thought I explained
that already.
If you did, I must have missed it. But maybe I've been answering the
wrong question all along.
<mike