It is good practice not to expose contained collections directly to clients. There is a list of "standard" methods that you typically provide on the containing class to manage the relationship (get all, add, remove, etc.).
In theory, it's advised that you don't ever return the actual collection to the client, rather an immutable copy of it containing the same objects. This way the client can't modify the collection directly, but has to go through your delegate methods.
Bi-directional relationships are not a problem, but slightly more tricky; use them where needed, but avoid the additional complexity if you don't. You have to make sure that the bi-directionality is maintained correctly in the model, and not rely on client code to do it. The complexity will depend on which mutations are possible after construction, but generally:
- If you add a Message to a Command, that Command must always be the one set on the Message (and vice versa)
- If you remove a Message from a Command, the Command reference of the Message must be set to null (and vice versa)
Your problem is that you link method calls before the program is fully type-checked. In a complicated language with subtype polymorphism and parametric polymorphism, you cannot do any kind of linking before you know the type (or in the case of subtyping, the type bounds) of an expression. The solution is to introduce type variables for expressions of (initially) unknown type, and properly unify type variables with concrete types. If you cannot resolve a type variable, that's an error and not an invitation to deduce the most general type.
A proper type inferer would look at the code
var list2 = list.mapped { s => s.toUpperCase }
in the static environment
list : List[String]
List[E]#mapped[U] : (E => U) => List[U]
String#toUpperCase : () => String
like this:
- infer
var list2 = list.mapped { s => s.toUpperCase }
- infer
list.mapped { s => s.toUpperCase }
- infer
list
- known to be
List[String]
- save
E = String
to the static environment for this expression.
- resolves to
List[E]#mapped[U]
- introduce variable
$U
for U
.
- known to be
(E => $U) => List[$U]
- unify arguments:
(E => $U) >: typeof({ s => s.toUpperCase })
- infer
{ s => s.toUpperCase }
- introduce variables
$a, $b
- known to be
$a => $b
- unify
(E => $U) >: ($a => $b)
: E <: $a
and $b <: $U
. Technically, these are just type bounds, but let's consider them equal.
- save
E = $a
, $b = $U
to the static environment for this expression
- save
s : $a
to the static environment for this expression
- infer
s.toUpperCase
- infer
s
- infer
.toUpperCase
.
- matches
String#toUpperCase
- known to be
() => String
- is
String
- unify
String <: $b
, but lets consider them equal
- save
$b = String
to the static environment for this expression
- is
($a => $b) = (String => String)
- already unified
(E => $U) >: ($a => $b)
.
- is
List[$U] = List[String]
- infer
var list2
- introduce variable
$c
- save
list2 : $c
into the static environment for this scope
- is
$c
- unify
$c >: List[String]
, but let's consider them equal
- save
$c = List[String]
into the static environment
After this round of type inference, asking the static environment for the type of list2
gives us List[String]
. The above steps roughly follow along the type inference steps for a Hindley-Milner type system, but note that you are probably dealing with subtyping, which makes type inference far more complicated: unifying a type only provides us bounds on the type and very rarely a concrete type. In the above example, I ignored these details and always unified the types as equals.
The second major problem is infering the type of the object on which to dispatch for a method call. In this example, I always tried to infer the type of the object before resolving the method call. However, this is not necessary. A method List[E]#mapper[U] : (E => U) => List[U]
could also be viewed as a free function .mapper[E, U]: (List[E], E => U) => List[U]
. In other words, the implicit this
argument is transformed to an explicit argument. This allows us use type variables for the invocant variable, and makes it easier to deal with a set of overloads. Consider the following example:
static env:
.add[E] : (List[E], E) => void
.add[E] : (List[E], List[E]) => void
.add[K, V] : (Map[K, V], K, V) => void
.add : (Integer, Integer) => Integer
a : $a
b : $b
expression:
a.add(b)
when type-infering that expression, we initially only know that this is some kind of .add
call, but we do not know which one – we cannot resolve it at this point. We have to unify each of the possible types with the given types. If one unification fails, we remove that possibility from the set of overloads.
- infer
a.add(b)
- candidate
(List[E], E) => void
- variable
$c
for E
- unify arguments
- is
void
- candidate
(List[E], List[E]) => void
- variable
$c
for E
- unify arguments
$a = List[$c]
$b = List[$c]
- is void
- candidate
(Map[K, V], K, V) => void
- variable
$c
for K
- variable
$d
for V
- unify arguments
$a = Map[$c, $d]
$b = $c
- ERROR: no argument for
V
-parameter
- discard candidate
- candidate
(Integer, Integer) => Integer
- unifiy arguments
$a = Integer
$b = Integer
- is
Integer
So after doing a round of type inference, we are still left with a number of choices. Some languages employ some ranking mechanism, e.g. methods on subtypes are preferred over methods on supertypes. In our example, there is no sensible ranking. If you want your type-checker to be moderately well-performing, you should issue a compiler exception here, e.g.
foo.sourcefile:42:3 error: could not resolve call to method `add`
a.add(b)
^
candidates are:
List[E]#add(E elem)
List[E]#add(List[E] elems)
Integer#add(Integer rhs)
Likewise, you should display an error when the set of possible methods is empty after attempting unification.
Note that for a different static environment, the the set of possible solutions would only contain one method which you could bind to. E.g.:
a : List[$c]
b : $c
Since the call can be resolved unambiguously in that case, we can continue doing type inference for the rest of the compilation unit in hopes of resolving the $c
type variable.
Depending on how you implement generics, you could link the method call directly after successfully infering the type for the call. If you need to know the value of the type variable $c
, you need to wait until type inference for the compilation unit has completed, and can do linking in a second pass. This is now easy to do since all expressions are already annotated with their inferred type.
Best Answer
Why do you need to extend this class? And why do you need to name your own method the same as
showDialog
?In reality your method does something entirely different than what
showDialog
does. A better name for your method would beshowDialogAtLocationAndReturnSelectedFile
as your method does more or less these things. Naming itshowDialog
will only confuse your code users.Also, without knowing anything else, I'd say you're trying to shove too much in a single method. How do you react on a cancel press? How about an error? Do you return null? If so, you're forcing the user of the code to check the return value yet again. This has the potential of being just another "Leaky Abstraction", and Java APIs already have enough of these.
An important part of API design is making sure that the name of a function/class/method matches what it really does. And that is why in
JFileChooser
the method's name isshowDialog
. It just shows the dialog. It doesn't open the file for reading, it doesn't perform a check whether the filename is valid, and honestly, why would it? The user of the code just asked the class to show the dialog.The creator of Ruby calls this the 'Principle of Least Surprise'*, and while I don't really know Ruby, this is a great line to learn from. Your code should be in the service of its user, and a part of this service is embedding the contract of the method/class in its name.
You might think you're not designing an API, but I doubt you work alone: there's probably someone else in the team, and they will appreciate this. Also, I heartily recommend this lecture on API Design: How To Design A Good API and Why it Matters. It was presented to Googlers by a Java designer, so it kinda matters.
Maybe this is more than you asked for, but I feel you seem to be somewhat missing the point of naming methods.
UPDATE: * I seem to be mistaken, the creator of Ruby has actually stated that he designed Ruby with the "Principle of Least Astonishment", not "Principle of Least Surprise". In any case, what I said still holds.