Another newbie question

Steven D'Aprano steve at REMOVETHIScyber.com.au
Sat Dec 10 00:32:31 EST 2005


On Thu, 08 Dec 2005 20:46:33 -0500, Mike Meyer wrote:

> Steven D'Aprano <steve at REMOVEMEcyber.com.au> writes:
>> Paul Rubin wrote:
>>> Steven D'Aprano <steve at REMOVETHIScyber.com.au> writes:
>>>>> Yes. Reaching through objects to do things is usually a bad idea.
>>>>I don't necessarily disagree, but I don't understand why you say this. Why
>>>>it is bad?
>>> The traditional OOP spirit is to encapsulate the object's entire
>>> behavior in the class definition.
>> Uh huh. Say I have:
>>
>> class Coordinate:
>>      def __init__(self, x=0.0, y=0.0):
>>          self.x = x
>>          self.y = y
>>
>> pt = Coordinate(1.0, 2.5)
>> Presumably then I also need add_y_ord, sub_y_ord, rsub_y_ord,
>> div_y_ord, and so on for every method that floats understand, plus
>> *another* set of methods that do the same thing for the x
>> ordinate. And in every case, these Coordinate methods are trivial
>> one-liners.
>> Do people really do this?
> 
> Yes, but usually with better API design. For instance, your Coordinate
> class might have scale, translate, rotate and move methods.

Which I obviously left as exercises for the reader. Did I really need to
specify the entire API for an example like this?


> These are
> still relatively simple, but they aren't one-liners. The import thing
> is that these methods capture common, Coordinate-level operations and
> bundle them up so that clients don't have to, for instance, do the
> trig needed to rotate a point themselves. They can just use the rotate
> method.

Of course, if you have to do an operation on a *coordinate* then it makes
perfect sense to create coordinate methods. I'm not disputing that. But in
my example, I'm not doing things to a coordinate, I'm doing things to the
entities which make up a coordinate -- the x and y ordinates.


>> Yes, I could encapsulate the lot with a factory function that applied
>> a specified operator to a specified attribute, and populate the class
>> at runtime. But why would I want to?
> 
> You don't. You're thinking about things at the wrong level. You don't
> want to think about "things you do to a Coordinate's attribute". You
> want to think about "things you do to a Coordinate".

I've done my thinking about coordinates. That is why I wrote a Coordinate
class. But sometimes you don't want to do an operation on a coordinate, you
want to do something to the x or y ordinate alone. It is utter nonsense to
suggest that you should abstract coordinates to the point that you no
longer know -- or pretend that you don't -- that a coordinate is a pair of
ordinates. What, we're supposed to guard against the possibility that
coordinates might be implemented by a B-tree or something?

(Yes, I'm aware there are 3D coordinates, or even n-dimensional ones, and
all sorts of special purpose coordinates with, e.g. ordinates limited to
integral values. I'm not writing a general anything-coordinate class, I'm
writing a simple 2D coordinate pair class.)


>> Now, as I see it, the whole point of encapsulation is that you *don't*
>> need to fill your class definition with meaningless helper functions.
> 
> Correct. That's where you went wrong - your methods were essentially
> meaningless. They just manipulated the attributes, not the Coordinate.
> Your methods should be meaningful for the object, not just the
> attributes.

*Exactly* my point -- and demonstrating that you've missed that point.
Writing special purpose methods to manipulate object attributes when you
can just as easily manipulate the object attributes is a bad idea. Methods
should be meaningful for the object.

According to the Law of Demeter (more of a guideline really), each level
should only talk to the immediate level next to it. Fine: I have a name
"pt", which is bound to a Coordinate object -- so "pt" can call Coordinate
methods. But it shouldn't call float methods on the attributes of that
Coordinate object, because those float methods are two levels away.

The bad side of the Guideline of Demeter is that following it requires
you to fill your class with trivial, unnecessary getter and setter
methods, plus methods for arithmetic operations, and so on.

Or just say No to the "law" of Demeter.

As a guideline, to make you think about what your doing ("am I doing too
much work? should my class implement a helper function for this common
task?") it is perfectly fine. But when you find yourself writing trivial
methods that do nothing but call methods on a attribute, you are another
victim of a bad law.

 
>> If an attribute of a instance is a float, you can just call float
>> methods on the attribute and it should work. If the attribute is a
>> list, list methods will work. If the attribute is an instance of a
>> custom class, the same general technique will still work.
> 
> So, if we expand your Coordinate class to have attriutes r and theta
> (the same coordinate expressed in polar form), which are also floats,
> does it make sense to write: pt.r *= 2? 

If I expand the Coordinate class to allow both polar coordinates and
Cartesian coordinates, I need some mechanism for keeping the two views in
sync. If I can do that (say, with properties) then sure it makes sense to
double the length of the coordinate vector.

If I *can't* keep the two views in sync, then I have some choices to make.
For instance, I might have a Cartesian class and a Polar class, with
methods to convert from one to the other, and give up on the desire for
one object to encapsulate both. My Coordinate class doesn't encapsulate
writing a point in English "one point two, three point four five"
either -- you shouldn't expect a single class to encapsulate every
imaginable representation of that class ("but what if I want to write my
binary tree in Morse code?").


-- 
Steven.




More information about the Python-list mailing list