Re: Convenience Methods
Re: Convenience Methods
- Subject: Re: Convenience Methods
- From: Greg Titus <email@hidden>
- Date: Wed, 26 Sep 2007 21:46:09 -0700
On Sep 26, 2007, at 7:35 PM, Jeff Laing wrote:
From: Greg Titus [mailto:email@hidden]
This is just my opinion, but I think that YES, absolutely,
subclasses should know and depend upon the implementation details
of their
superclasses.
At this point, we part ways on the theory but thats fine.
To assume that you don't have to know how your own
superclass works is taking object-oriented theory too far, and just
Um, this strikes me as really hard to defend. Encapsulation is all
about
being able to wrap up "how" a class does its thing inside that
class, and
just worry about "what" it does.
Yes, but encapsulation is just for _using_ a class. If you are
subclassing you have (at least by default) access to all of the
superclass's instance variables and non-public methods. There's no
encapsulation there. If you ever touch any of them, you need to know
the superclass's implementation. If you are overriding existing
methods to have new behavior, you need to know the superclass's
implementation.
If you don't ever touch the superclass's internal state, why aren't
you using a Composite pattern instead? That's the only way to let its
implementation change out from under you (as long as its public API
remains the same) and still be sure that you won't be broken.
In this particular case, we've ended up with what I'd call "overly
defensive
programming" in the SUPERCLASS because it has no idea whether
someone will
come along and subclass it, so it complicates up its calls to
alloc. Thats
just pushing the problem from the guy who knew he wanted to
subclass into
the guy who didn't know.
Well, in the particular example given (+myClassWithFoo:) this is an
extremely common pattern to alloc/init/autorelease a new instance. I
don't think it complicates anything. If I was the superclass
implementor, I would habitually use [self alloc], which would then
work fine with subclasses. If I was the subclass implementor and
wanted to provide support for creation of my subclass with
+myClassWithFoo:, I would need to go look at the superclass
implementation, because I need to know that -initWithFoo: is the
initializer (I would probably guess that was the case, but I'd verify
that). If the superclass author _hadn't_ used [self alloc] and had
hardcoded the class name instead, I would change the superclass code
to use [self alloc] while I was there.
This brings up the point in another way that I'd previously alluded
to: I tend to see subclass/superclass relationships as parts of the
same "chunk" of code and modify inheritance subtrees all together. I
don't expect to be able to implement a subclass without also possibly
modifying the superclass or vice versa. You should always be willing
to refactor as you go along.
The exception is with a very few "hub" classes that are usually meant
to be subclassed across framework boundaries, like NSView. And there
you are explicitly writing your superclass with the expectation that
it'll be subclassed by perhaps many people in the future. And you
write defensively, and you document. But those examples are quite
rare. Most inheritance relationships (especially deeper trees,
especially model code) are all within the same domain and chunk of
code, and it makes much more sense to just be aware of the
implementation of that tree as a whole rather than to try to
artificially limit code changes to single classes.
Thats no big deal, except that it becomes mandatory to do "best-
practices"
when defining your classes since you can never tell whether you'll be
subclassed a year from now. And the original poster wanted to know
what was
"best practice", returning id or myclass*.
I'd always return MyClass * for exactly the reasons you already gave
when you originally answered this question. And no, it isn't
mandatory to do "best-practices" if you don't think you'll be
subclassed. That's exactly the same sort of over defensiveness in the
other direction. When someone wants to come along and subclass you,
they _make_ you cleanly subclassable _then_. In my world that
programmer needs to understand your implementation before they try to
subclass you, so they ought to be in your code checking it out.
(Again, the exception being those few classes you 'export' as planned
for subclassability.)
code. Not to mention the belief that you can just ignore how your
superclass is implemented leads to all sorts of bad code and
complacency. If you know what your superclass is doing, you write
I dont understand how relying on my superclass to honor its interface
contracts results in complacency or bad code. I'd be interested in
more
faulty examples here.
My argument is that if all you need is for it to honor its interface
contract then you generally don't need to subclass. You just need to
instantiate an instance of the superclass and message it using that
interface contract.
(By a framework boundary I mean where your superclass is
in a different framework than the subclass. A subclass of NSView in
your own application, for instance.)
which is, of course, why Apple recommend against subclassing
NSString, etc.
Right. I would never see the need to subclass NSString. It isn't
really meant for that.
But certainly in your own "Animals" framework, you better know how
your Animal superclass works, and you ought to take advantage
of that fact to write as little code as possible.
You are saying that if Dog is part of the Animals framework, I can
code it
one way, but if I move it to another framework (which happens
solely because
of organisational differences on my development team), I should
code it
differently?
Yes, absolutely. Organizational differences matter. Either there is
an organizational wall between the two classes or there isn't. And if
there is no programmer organizational wall between a super/subclass
then I don't think you should bother constructing an artificial wall
in your mind. If both are within your domain, you should look at both
when you modify either. Continually refactor what code is in which of
the classes as you work.
Apple advises using this pattern
rather than subclassing for class clusters:
I don't see Animal as a class cluster here.
No, me either. I was just using that as a pointer to an explanation
of the Composite pattern.
Finally, the design of Objective-C and Cocoa tends to lead to fairly
flat and broad inheritance hierarchies. There aren't that many deep
subclasses of subclasses of subclasses like you tend to find in some
other frameworks.
I think I understand what you are saying here, and the conclusion is
primarily that I still need to work on removing the C++isms from my
thinking
about Objective-C.
Possibly. One of the reasons C++ hierarchies tend to be deeper is
because C++ lacks things like categories. If I need some particular
functionality on NSString I don't have to subclass or drop out of OO-
land and use functions, I can add a category to add those methods to
NSString. I can call all of the existing NSString public API from
them, and don't need to be aware of the internal implementation.
That's another way where if all you need is to access the existing
public contract and don't need access to internals, you don't need to
subclass in Objective-C.
I have another pet project which had developed a class hierarchy
something
like:
basic_plugin
file_format_plugin
format_1_plugin
format_2_plugin
processing_plugin
process_1_plugin
process_2_plugin
The 'basic_plugin' is a class that I use across all my
applications, but it
isn't in a framework, the source code is just copied into each
application
(along with the basic_plugin_manager that handles plugin folders
etc at
application startup). file_format_plugin and processing_plugin are
both
specific to the current application, but neither of them want to
concern
themselves with the implementation of the basic_plugin logic, they
just want
to populate the standard entry methods and know that the level
above them do
the right thing. Similiarly, some instances of format_1_plugin,
etc are not
built as part of the application at all, since they expect to be
something
developable by a 3rd party. I can definitely see where adding in
common
superclasses under 'processing_plugin' for plugins that do similiar
things
is also quite likely.
Do those lowest-level plugins really need to know about the
internals all
the way up the tree, or just up one level? Or can they rely on the
theory
that says "my superclass knows what he's doing and honors his
interface
contracts". Which would imply that "if the method says it returns
basic_plugin*, then thats all it returns". Clumsy, not quite what
you'd
like, but 100% explicit and predictable.
Okay, the lowest level plugins only need to know about
file_format_plugin and processing_plugin. You are exposing those
classes to 3rd parties and expect them to be subclassed, therefore
you know to write them defensively and document them, there's an
organizational wall there. As part of that defensiveness I would
expect your file_format_plugin and processing_plugin to make clear
what it returns via documentation in each method.
I would argue that when you work on this chunk of code that you
should consider basic_plugin, file_format_plugin, and
processing_plugin all together, and it should be fine for the two
subclasses to know of and take advantage of the basic_plugin internal
implementation, and that you should always be refactoring the three
together (moving common code up, adding new virtual methods on the
superclass to call when the subclasses need to do something
differently from each other in some shared situation, et cetera).
In short, lavish your attention on careful interface contracts at
organizational boundaries, but not at all on class boundaries within
the same organization.
Patterns like delegation keeps the number of
subclasses you need to write yourself to a minimum.
... but are irrelevant to the sorts of problems I'm thinking about.
Sure, ok. But another way that you could have designed your plugin
architecture would be to have a file_format_plugin_protocol and a
processing_plugin_protocol, and the actual plugins wouldn't have any
mandated superclass at all, they'd just need to respond to particular
methods. If there was shared behavior or ways of handling the plugins
in your application, you'd have a file_format_plugin_helper and
processing_plugin_helper which would have the shared code for
managing those types of plugins and would just have a pointer to a
plugin instance. That would be a composite pattern where the
combination of your helper class and the plugin with unknown
superclass would together act in the same role as your current plugin
class. The helper does the management and flow of control and such
and delegates the specific file format work (or whatever) to the plugin.
That kind of structure has its advantages in not forcing the
inheritance hierarchy on your 3rd parties. What if there was a 3rd
party that had a file_format_plugin and a processing_plugin that
shared a bunch of the same code? They would have liked to have used a
common superclass for the two, but can't with your inheritance-based
design. (At least not in Obj-C, which is only single inheritance. But
don't get my ranting on the evils of multiple inheritance.) :-)
of your own code ought to be a single step away from one of a few
"hub" superclasses: NSObject, NSView, NSWindowController. That means
I'm somewhat surprised that most examples that people wheel out to
discuss
why Cocoa class hierarchies are shallow tend to be user-interface
related.
There are other domains of problems that *do* have deeply nested
hierarchies
for application specific reasons that a shallow hierarchy+nesting
do not
fit.
The examples are wheeled out in the user-interface domain because
that's the only somewhat complicated domain that the Cocoa frameworks
themselves cover. :-) To talk about other domains requires a whole
lot more explanation because we can't assume that we have classes/
code/domain-knowledge in common.
But yes, object models, for example, tend to have more deeply nested
hierarchies. I think in general you're better off trying to use
composition and delegation and similar tricks that Objective-C's
dynamicism is well-suited for in order to end up with less depth than
you might find in a C++ hierarchy in the same problem domain, but
still, they'll generally be deeper.
If he reimplements a superclass and doesn't check to see whether any
subclasses were depending upon the previous implementation,
I'd bitch at "the other guy" until he fixes it.
No, he didn't reimplement the superclass. He looked at a class
that had a
method of the form
+ fooFromBar:(Bar*)b
{
Foo *foo = [[[self class] alloc] initWithBar:b];
}
and because some other part of the system told him that we was
spending a
significant amount of time doing that particular function (lets not
get
distracted onto algorithm design, he's making a mistake, remember),
he sees
that that [self class] seems a bit wasteful.
Well, it should just be [[self alloc] initWithBar:b], but yes, he's
making a mistake, right...
So he changed it to
+ (Foo*)fooFromBar:(Bar*)b
{
Foo *foo = [[Foo alloc] initWithBar:b];
}
and then he tested his changes to death in his own test harnesses,
none of
which exercised "subclassing Foo". How does he tell that someone else
depended on that fairly subtle behaviour?
If Foo is a class that is 'exposed' at an organizational boundary and
is expected and documented to be subclassed, it's his fault for
subclassing not being part of his test harness.
If the subclass(es) is/are within the same framework, he should be
examining them as he is doing profiling and making code changes and
they should be tested at the same time as the superclass.
If the subclass(es) is/are external to his organization, and Foo
isn't documented and intended to be subclassed, at that point it
isn't his fault. It is the programmer who wrote the subclass's fault.
That is the time when the subclass-er needs to be very careful about
the contracted API, and avoid subclassing entirely, if possible. This
is the situation in which I agree with your original comment on this
topic, that the subclass-er shouldn't have counted on +fooFromBar:
returning the subclass, and should have written their own
+subclassFromBar: method instead, even if it ends up doing exactly
the same thing.
The last of those 3 situations is hopefully fairly rare. It means
that there's either a mistake in the design of the class hierarchy
(that something needed subclassing which wasn't expected to be), or
there's a mistake in the management of the development team (that
these two classes were split into two different organizations).
Hope this explains more where I'm coming from,
-- Greg
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden