Re: Design Questions re Subclassing
From: Rhino (rhino1_at_NOSPAM.sympatico.ca)
Date: 02/23/05
- Next message: Thomas Fritsch: "Re: How to make a file "executable" at runtime?"
- Previous message: Anthony Borla: "Re: Java String comparison"
- In reply to: Chris Uppal: "Re: Design Questions re Subclassing"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Date: Wed, 23 Feb 2005 10:06:22 -0500
"Chris Uppal" <chris.uppal@metagnostic.REMOVE-THIS.org> wrote in message
news:421c751f$2$38042$bed64819@news.gradwell.net...
> Rhino wrote:
>
> >...
>
> [btw, I'm replying out-of-order because I want to start with some concrete
> stuff, and leave the more abstract waffle for later in the post]
>
No problem at all ;-)
>
> > I liked the
> > fact that I could supply my two values to the TreeSet subclass in any
> > order since TreeSet will store them in the proper order "automagically"
> > but I can achieve the same behaviour by simply comparing the two values
> > and storing the lower value as "low" and the higher value as "high".
>
> As a practical design point (however it's implemented) I would advise
against
> automatically re-arranging the end-points. If you do that then you have
> introduced an irregularity into the semantics for the sake of convenience,
and
> that is rarely a good idea. (Convenience stuff layered /on top/ of clean
> semantics is OK, though). For instance, if you want to introduce methods
like
> intersection() to compute the intersection of two ranges, or isEmpty() to
ask
> if a range has any contents, then you'll need a way to represent empty
ranges.
> if you don't rearrange the end-points then that will happen naturally,
> otherwise you'll have to create messy code (at least) to handle the case,
and
> may not be able to do it at all. (If a Range includes its end-points,
/and/
> you re-arrange the end-points automatically, then you /cannot/ represent
an
> empty Range).
>
Your points make sense if I were trying to model Ranges in a general way -
and as far as you know so far, that IS what I'm trying to do. But, in fact,
I am trying to model a subset of Ranges. Actually, to be more precise, I'm
trying to imitate the behaviour of the BETWEEN clause in SQL.
For example:
select * from employee
where hiredate between '2004-01-01' and '2004-12-31'
[Display all the information in the Employee table for each person who
was hired in 2004, including people who were hired on Jan 1 and Dec 31 that
year.]
FYI: A BETWEEN in SQL always matches when the date is equal to either
end-point. Also, BETWEEN is always interpreted as follows: first-date <=
search-value <= second-date. Therefore, if the first-date is NOT equal to or
lower than the second-date, no value will satisfy the BETWEEN. In other
words, 'where age between 10 and 20' is NOT going to give the same result as
'where age between 20 and 10': the latter will always give an empty result,
forcing the user to always specify the lower value first.
The Range class I am trying to create is really just needed to allow me to
do BETWEENs so I am making the behaviour of Range imitate what happens in
SQL.
>
> > That's a good point and one that I had considered. I'm essentially
trying
> > to imitate the behaviour of the 'between' operator in DB2 and it
includes
> > both end points, i.e. 'where age between 18 and 21' matches with 18, 19,
> > 20 and
> > 21. Perhaps I should make my Range class flexible and include some
> > booleans in my between() method. Something like this (uncompiled and
> > untested):
>
> Um, I'd advise against that too. Unless your requirements are quite
unusual, I
> think it would just make the objects harder for people to use. In
particular,
> if anyone ever had to write code like:
>
> for (
> i = myRange.first();
> myRange.includesLast() ? (i <= myRange.last()) : (i <
myRange.last());
> i++) { ....
>
> then I think you could fairly say that the design had failed ;-) (And
what the
> writer of such code would say...)
>
Actually, my code is a lot simpler than that. This is the entire AgeRange
class, which I created by subclassing my TreeSet-based Range class. (I'm not
likely to keep this class around permanently since it is too specific;
instead, I'd rename it IntRange and let it be for any pair of ints.)
/**
* Class AgeRange is used to stored a range of ages, e.g. 18 to 21.
*
* @author Rhino
*
*/
public class AgeRange extends Range {
/**
* This constructor creates a new range containing two discrete ints
(represented
* as Integers).
*
* @param int firstInt one of two integers representing a range of
integers
* @param int secondInt the other of two integers representing a range
of integers
*/
public AgeRange(int firstAge, int secondAge) {
super(new Integer(firstAge), new Integer(secondAge));
}
/**
* This constructor creates a new range containing the first two values
in an array
* of int values (represented as Integers).
*
* @param int[] ints an array of int values
*/
public AgeRange(int[] ints) {
super(new Integer[] {new Integer(ints[0]), new Integer(ints[1])});
}
/**
* This version of between() is a convenience method that determines if
a given int
* value is within a range of int values.
*
* @param int input the value which is being compared to the range
* @return boolean true if the input value is equal to either endpoint
or lies between them, otherwise false
*/
public boolean between(int input) {
return between(true, input, true);
}
/**
* This version of between() determines if a given int is within a range
of int values.
*
* <p>Since different users have different expectations about the
behaviour of a
* between method, this method provides two booleans that control which
behaviour
* will be experienced. For example, if the user desires between to be
true if the input
* value is strictly between the two endpoints but not equal to any of
them, both booleans
* should be set to false.</p>
*
* @param includesLow if true the low end of the range should include
the low value, otherwise the low end of the range starts just above the low
value
* @param input the int value that is being compared to the range
* @param includesHigh if true the high end of the range should include
the high value, otherwise the high end of the range ends just below the high
value
* @return true if the input value is in the range, otherwise false
*/
public boolean between(boolean includesLow, int input, boolean
includesHigh) {
/*
* If 'includesLow' and 'includesHigh' are both true, return true if
'input'
* satisfies this condition: lowValue <= input <= highValue.
*/
if (includesLow && includesHigh) {
if (input >= ((Integer) first()).intValue() && input <=
((Integer) last()).intValue()) {
return true;
} else {
return false;
}
}
else
/*
* If 'includesLow' and 'includesHigh' are both false, return true
if 'input'
* satisfies this condition: lowValue < input < highValue.
*/
if (!includesLow && !includesHigh) {
if (input > ((Integer) first()).intValue() && input < ((Integer)
last()).intValue()) {
return true;
} else {
return false;
}
}
else
/*
* If 'includesLow' is true and 'includesHigh' is false, return true
if 'input'
* satisfies this condition: lowValue <= input < highValue.
*/
if (includesLow && !includesHigh) {
if (input >= ((Integer) first()).intValue() && input <
((Integer) last()).intValue()) {
return true;
} else {
return false;
}
}
else
/*
* If 'includesLow' is false and 'includesHigh' is true, return true
if 'input'
* satisfies this condition: lowValue < input <= highValue.
*/
if (!includesLow && includesHigh) {
if (input > ((Integer) first()).intValue() && input <=
((Integer) last()).intValue()) {
return true;
} else {
return false;
}
}
else {
return false;
}
}
}
To invoke it:
AgeRange myRange = new AgeRange(18, 21);
/* See if low-age <= 19 <= high-age */
boolean result = myRange.between(true, 19, true);
/* See if low-age < 19 < high-age */
result = myRange.between(false, 19, false);
/* See if low-age <= 19 < high-age */
result = myRange.between(true, 19, false);
/* See if low-age < 19 <= high-age */
result = myRange.between(false, 19, true);
Simple enough? ;-)
By the way, I have a convenience method that takes only one input parameter
to minimize coding for people (like me) who want the between() to work just
like it does in SQL:
AgeRange myRange = new AgeRange(18, 21);
/* See if low-age <= 19 <= high-age */
boolean result = myRange.between(19);
> Keep it simple. Choose one way of doing things, and stick to it (even if
it's
> not ideal in all cases). If necessary then you can add another
HalfOpenRange
> class later.
>
>
>
> > I'm using Java 1.5 but I'm really not up on all the Generics stuff yet.
> > (Translation: I read the "what's new" article in the API but haven't
> > really digested the proper use of Generics yet. Also, I'm not completely
> > sure I want to use techniques that aren't backward compatible with
> > pre-1.5 versions of Java yet.)
>
> Yes, I think that's reasonable. In case you do decide to try to apply
generics
> after all (as practise, say -- which is what I did) then be warned that
this is
> a relatively difficult concept to make generic (mainly because
> java.lang.Comparable is itself generic).
>
That's okay; I don't mind a challenge. I just want to make sure I've got
this working in a way that is backward compatible with older versions of
Java first - mostly so that I am confident I understand it first! - but then
I'll see if I can make it work using generics.
>
> > > Another BTW, I wouldn't bother with all those class checks. You
> > > probably don't need them even in pre-generics code, and the generics
> > > stuff makes it even more unnecessary in J5.
> > >
> > But how else do I make sure that I get two Objects of the same kind as
the
> > end-points of my Range? Surely I don't want to have the end-points be
two
> > different types? (Except that a range that has two numbers of different
> > types, e.g. 4 to 5.8, would make sense but a range of "cat" to 42
almost
> > certainly wouldn't.)
>
> Two answers, or maybe three:
>
> One is that -- as a practical point -- people very rarely /do/ make that
kind
> of mistake. And even more rarely make it in ways that won't show up in
even
> the sketchiest testing (E.g. if a Range where bounded by "cat" and 42,
then it
> would instantly fail on the first attempt to check contains()). So all
that
> code and checking is almost certain to be wasted effort.
>
It really wasn't much of an effort; a couple of simple lines of code. Only
took me a few seconds to write. I certainly don't begrudge that minor effort
if it keeps my code from breaking when a user deliberately/inadvertently
attempts to throw two different kinds of values into the class. It just
seems like a good precaution; simple defensive coding.
> Secondly, you will almost certainly switch to a generical version at some
> point. So the question is: how many /actual/ errors will those tests
catch in
> the (say) 1 year before the compiler starts checking for you ? My guess
is
> that the answer is almost certainly zero.
>
Since I'm likely to be the only user of these classes for the first while,
that's a pretty good guess ;-) I'm just not as certain that other users will
use these classes correctly so it seems like a good precaution to
"idiot-proof" them as much as possible.
> A third reason is that those kinds of tests can themselves be buggy, or
> introduce unecessary restrictions if you've mis-designed them. In the
case in
> point, your class tests show both problems:
>
> > String firstObjectClass = range[0].getClass().getName();
> > String secondObjectClass = range[1].getClass().getName();
> > if (!firstObjectClass.equals(secondObjectClass)) {
>
> The bug is that if you want to test if two classes are the same, then you
> should test the class objects for equality (you can use ==), not compare
the
> names (it is possible for different classes to have the same name, and
anyway
> it's unnecessary work).
I had actually planned to change the code to this today:
if (range[0].getClass() != range[1].getClass()) {
// throw exception
}
The mis-design is that you are checking for them being
> the same class, when it might well be that subclasses would be acceptable.
>
Yeah, and that's the ugly part. I really don't want to have some immense if
statement that tests each conceivable pair of types to see if that
combination is okay; there are thousands of classes in the base API alone
and lots of new ones coming along all the time. To avoid that, I'd rather
make the class a little narrower than it really has to be just to keep it
all manageable in size: I'll force the two values in the range to be the
exact same type. If the user wants to cast two logically comparable types to
the same type OUTSIDE of my class, he can do that fairly easily. That seems
a lot better than trying to handle each possible combination in my class.
>
> > You raise a good point in questioning whether TreeSet should be extended
> > in the first place for my Range class. My thinking was that I had a
> > Collection on my hands, albeit a very specialized one containing exactly
> > two values.
>
> [this is where it gets a bit waffly...]
>
> I think your instincts were right, and that a range can be though of as a
sort
> of collection (lower-case 'c'). However one correction I'd make is that it
> /doesn't/ contain exactly two elements. E.g. the range 7 through 10
contains
> 7, 8, 9, and 10 -- 4 elements. The range is /defined/ by exactly two
values,
> but they aren't even necessarily members of the range (if the range is
open).
> It's that, perhaps more than anything, that makes a Range very different
from a
> TreeSet with two elements.
>
Actually, I thought of it as being a collection of exactly two elements
because I deliberately didn't want to have to include all the intervening
elements. Therefore, to me, the Range 7 to 10 consists ONLY of 7 and 10. The
between() method will look at the comparison value, assume that it too is an
integer and then see if the comparison value is an integer that lies between
the end-points. That way, the Range will only need to contain two values,
NOT all the other values that lie between them.
> Secondly, although I agree that a range can usefully be thought of as a
kind of
> collection, it is not a kind of /container/. What I'm trying to get at is
that
> a collection is a general concept where you can ask of any object whether
its a
> member of the collection, whereas a container is a much more specific idea
of a
> collection that is defined by the elements that have been /added/ to it.
E.g.
> you can talk of the collection of all 40+, bearded Java programmers, and
it's
> relatively easy to check if any given Java programmer is a member of that
> collection; but there is certainly no container that holds all (and only)
those
> programmers -- it'd be Hell! (This is the distinction commonly made by
> theorists between intentionally defined sets, and extensionally defined
ones.).
> Anyway, after all that, what I want to say is that Java's so-called
> "Collections" would be better called (if you accept the way I use words)
> "Containers", because that's what they all are. The way the Collections
> library has been structured leaves no room for more abstract collections
that
> are defined only by some predicate, rather than by a more-or-less concrete
list
> of elements.
>
That's a very valid point and I accept your suggestion that Collections
would be better called Containers; that seems quite sound to me. And in that
sense, my Range is certainly NOT a container since it doesn't contain all
the values between the end-points.
> So, it's not that you are wrong to want to see a range as a Collection,
it's
> that the Java people have over-constrained the design of the "Collections"
> hierarchy to exclude such ideas. A pity really...
>
I certainly see your point. But I'm increasingly satisfied that my Range
class will do its job very well, aside from that fact that is probably
significantly more complicated than it needs to be. As I said in my other
thread on this issue, I should probably be working with something more along
the lines of Point than TreeSet to minimize overhead ;-) I'm working in that
direction now but I just wanted to take a fairly thorough look at the
overall design, particularly the TreeSet sublass design, before replacing it
with something simpler.
I've really enjoyed this conversation; it isn't often that I get to talk
design at length with another Java developer. I have certainly benefited
from this talk and I expect my designs will get better as I apply the ideas
you've suggested.
Thanks a lot!
Rhino
- Next message: Thomas Fritsch: "Re: How to make a file "executable" at runtime?"
- Previous message: Anthony Borla: "Re: Java String comparison"
- In reply to: Chris Uppal: "Re: Design Questions re Subclassing"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Relevant Pages
|
|