Another newbie question

Mike Meyer mwm at mired.org
Sun Dec 11 01:16:45 EST 2005


Steven D'Aprano <steve at REMOVETHIScyber.com.au> writes:
> On Sat, 10 Dec 2005 13:33:25 -0500, Mike Meyer wrote:
>> Steven D'Aprano <steve at REMOVETHIScyber.com.au> writes:
>>>> 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:
>>>
>>> [quote]
>>> 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.

>>> Do you lie awake at nights worrying that in Python 2.6 sys.stdout will
>>> be renamed to sys.standard_output, and that it will no longer have a
>>> write() method? According to the "law" of Demeter, you should, and the
>>> writers of the sys module should have abstracted the fact that stdout
>>> is a file away by providing a sys.write_to_stdout() function. That is
>>> precisely the sort of behaviour which I maintain is unnecessary.
>> 
>> And that's not the kind of behavior I'm talking about here, nor is it
>> the kind of behavior that the LoD is designed to help you with (those
>> are two different things).
>
> 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.

>> I've
>> already pointed out one trivial way to deal with this, and there are
>> others.
> 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
-- 
Mike Meyer <mwm at mired.org>			http://www.mired.org/home/mwm/
Independent WWW/Perforce/FreeBSD/Unix consultant, email for more information.



More information about the Python-list mailing list