This may be nothing more than a small mental tic that I carry over
from my file-based static typing background, but I thought I'd solicit people's opinions about it anyway. I first started programming in C, which had more strict rules about the order in which files could be included and complied. For example, suppose we have the following modules: a.h a.c and b.h b.c If some data structure defined in header file a.h referenced a data structure defined in b.h, but then conversely b.h also referenced a.h, then we have a cyclic dependency between the 2 modules. This is frowned upon in C, especially since C has used make to build its projects. The make utility would not know whether to compile a.h and a.c first or b.h and b.c first. When working in C, it is very much common practice to work out module dependencies so that they are acyclic, if merely for the sake for compilation ordering. When I moved on to Java, I was often faced with the same scenario when modeling bidirectional relationships between 2 objects (especially when implementing O-R relationships). For example, suppose we have 2 classes Customer.java and Reservation.java, which reference each other: public class Reservation { public Customer getCustomer(); } public class Customer { public Reservation getReservation(); } Java allows for cyclic dependencies by permitting both classes to be compiled simultaneously: javac Customer.java Reservation.java However, coming from my C heritage, I usually opt for acyclic object references by using interfaces. I would have something like an ICustomer.java interface which Reservation references, and which Customer implements: public class Reservation { public ICustomer getCustomer(); } public class Customer implements ICustomer { public Reservation getReservation(); } Now the compilation ordering is acyclic again, since the ordering would be: compile ICustomer.java first, then Reservation.java, and finally Customer.java. Even in Ruby, which is file-based and has a notion of module inclusion ('require' and 'include' commands), I take care to maintain acyclic references. So my question now that I'm making a foray in the Smalltalk world is this: Should I simply forget about trying to maintain acyclic object references? Is it common practice in Smalltalk for objects to have cyclic dependencies to each other? How about GNU Smalltalk which is file-based? Is this a non-issue in GNU Smalltalk as well? Many thanks, Damon |
On 30 Apr 2003 17:49:34 -0700,
Damon <[hidden email]> wrote: > > So my question now that I'm making a foray in the Smalltalk world is > this: Should I simply forget about trying to maintain acyclic object > references? Is it common practice in Smalltalk for objects to have > cyclic dependencies to each other? If we keep the notion of "compile time" and "run time", I'd say that Smalltalk objects are acyclic "by definition", because they can only reference ... objects! Only during "run time" the type of those objects is known. Imagine programming in C and referencing all of your data with (char *) or whatever "generic" pointer type you like. > > How about GNU Smalltalk which is file-based? Is this a non-issue in > GNU Smalltalk as well? I hope so. > > Many thanks, > > Damon |
Stefan Schmiedl <[hidden email]> wrote in message news:<b8qdbr$cjuok$[hidden email]>...
> On 30 Apr 2003 17:49:34 -0700, > Damon <[hidden email]> wrote: > > > > So my question now that I'm making a foray in the Smalltalk world is > > this: Should I simply forget about trying to maintain acyclic object > > references? Is it common practice in Smalltalk for objects to have > > cyclic dependencies to each other? > > If we keep the notion of "compile time" and "run time", > I'd say that Smalltalk objects are acyclic "by definition", > because they can only reference ... objects! Only during > "run time" the type of those objects is known. > > Imagine programming in C and referencing all of your data > with (char *) or whatever "generic" pointer type you like. > But that still wouldn't solve the cyclic dependency problem in C. For example suppose we have: a.h and a.c containing some structure A and b.h and b.c containing some structure B, where in a.c, we have void *some_ptr = malloc(sizeof (B)); but in b.c, we have void *some_other_ptr = malloc(sizeof (A)); By naming the structure B in a.c and A in b.c, we would still have a problem determining which module to compile first. The fact that the pointers are 'void *' rather than 'A *' and 'B *' only moves the cylic dependency problem from the header files to the source files, because the strucures B and A are named in the a.c and b.c source files. Similarly, in Smalltalk, we can have: a_instanceVar := B new. in class A, while in class B, we have: b_instanceVar := A new. Offhand I can't think of a case in which this might pose a problem in Smalltalk, so this may just be a hangover from C programming, but I'd like to check with more experienced people to see if this a non-issue or still a concern. Thanks, Damon |
In reply to this post by Damon
Damon wrote:
[re: Cyclic definitions] You can think of this question two ways. One is about the practicalities of cyclic definitions, will the compiler accept them, etc, and how to get around any problems; the other is about the logical relationships and whether they are well-formed. Smalltalk has *less* of a problem with the practicalities, but not none at all. The absence of declared types means that most code has no "compile-time" dependency on the classes of the objects that it will manipulate. However, explicit references to classes have to be compiled into references to the appropriate global, so there is still some dependency. That matters if you have the dependency cross package boundaries; say you have two packages A.pac and B.pac, which define classes A and B respectively -- if code in class A refers directly to B, and vice versa then you have a cyclic dependency between the packages. That's not a viable situation since Dolphin attempts to resolve package dependencies before loading, and in this case it would be unable to load *either* of the two packages since they both require the other to be loaded first. (IMO this is a weakness in the Dolphin package system, but it has never caused me any problems even so) The simple solution is to put both classes in the same package. That's probably the best solution too, since if the two classes are so intimately interdependent that you can't use one without the other, then where's the point in packaging them separately ? A more extreme solution (which you'll see sometimes used in the image to avoid unwanted package interdependencies) would be to refer to the "other" class via a runtime expression like: Smalltalk at: #AnotherClass ifAbsent: [...something...] The other question is about the design that gave rise the cyclic dependencies. I suspect that *most* (not all) such cycles are symptoms of poor factoring. If the two "modules" really are intended to be independent, but nevertheless refer to each other, then something odd is happening. Probably something wrong. So I think that the C habit of avoiding interdependencies among modules will probably continue to serve you well in the Smalltalk world *providing* that you don't overdo it and confuse a "module" (by which I mean a chunk of co-designed and co-packaged code) with a class. For instance if you really need a pattern like your example: > public class Reservation { > > public Customer getCustomer(); > } > > public class Customer { > > public Reservation getReservation(); > } then Customer and Reservation had better be in the same (conceptual) module. (They had also better be in the same physical package too -- but that's the practicality question). I doubt if this *particular* example really has that co-designed quality about it, though. Customers will know how to make reservations, but Reservations don't (I'd imagine) want to make Customers. Since the only routine reason for code to refer to another class is that it uses it as a factory object (sends it the #new message or similar) I don't expect that Reservation would refer to Customer at all, in which case (in Smalltalk) there is no physical dependency of Reservation on Customer. It may or may not be the case that there's a logical dependency, that depends on the rest of the design, but I'd be inclined to suspect not. If not, then Smalltalk doesn't create the artificial physical dependency that Java's declarative type system produces, and there would be no problem with cyclic dependencies. For instance, Reservation might be a very complicated object, that knows how to talk 3270 to some reservation system running on a mainframe, whereas Customer might know about your local database. In such cases there might be a valid pressure to put them in separate packages and think of them as logically independent. In such cases, in Java, you'd have to make *both* of them into interfaces, and use the factory object pattern (plus some runtime configuration) to ensure that neither of the concrete classes referred to the other, only to the interfaces. In Smalltalk most of that comes for free, you'd have to ensure that Customer knew how to find the Reservation factory at runtime (just as in Java) but the rest of the code would remain unchanged. In effect Smalltalk has the only-use-interfaces and Factory Object patterns built into the semantics of the language. -- chris |
In reply to this post by Damon
On 1 May 2003 01:36:54 -0700,
Damon <[hidden email]> wrote: > Stefan Schmiedl <[hidden email]> wrote in message news:<b8qdbr$cjuok$[hidden email]>... >> >> Imagine programming in C and referencing all of your data >> with (char *) or whatever "generic" pointer type you like. >> > > > But that still wouldn't solve the cyclic dependency problem in C. For > example suppose we have: > > a.h and a.c containing some structure A > > and > > b.h and b.c containing some structure B, > > where in a.c, we have > > void *some_ptr = malloc(sizeof (B)); > > but in b.c, we have > > void *some_other_ptr = malloc(sizeof (A)); ahhh... If you are a Real Programmer (like Mel:-) and do everything with (char *) then you will also be able to hardcode the struct sizes :-) s. |
> ahhh... If you are a Real Programmer (like Mel:-) and do everything
> with (char *) then you will also be able to hardcode the struct sizes (char*)??? STRUCTS??? What's with all of this high level stuff!! Real programmers write native instructions w/ none of those reference books that wimps use :) Actually, I didn't see it myself, but I heard from a reliable first hand witness about a consultant who patched the _object code_ for a main frame's serial communications driver. Got it right on the first try. Have a good one, Bill -- Wilhelm K. Schwab, Ph.D. [hidden email] |
In reply to this post by Chris Uppal-3
"Chris Uppal" <[hidden email]> wrote in message
news:3eb11903$0$45175$[hidden email]... > Damon wrote: > > [re: Cyclic definitions] > >.... However, > explicit references to classes have to be compiled into references to the > appropriate global, so there is still some dependency. That matters if you > have the dependency cross package boundaries; say you have two packages A.pac > and B.pac, which define classes A and B respectively -- if code in class A > refers directly to B, and vice versa then you have a cyclic dependency between > the packages. That's not a viable situation since Dolphin attempts to resolve > package dependencies before loading, and in this case it would be unable to > load *either* of the two packages since they both require the other to be > loaded first. (IMO this is a weakness in the Dolphin package system, but it > has never caused me any problems even so) It is a deliberate weakness (that would be relative easily overcome), because... >...The simple solution is to put both > classes in the same package. That's probably the best solution too, since if > the two classes are so intimately interdependent that you can't use one without > the other, then where's the point in packaging them separately ? ...of precisely that. It is not difficult to resolve cyclic references by one of a number of techniques, including (as you say) having just one package, or introducing a common base package. The advantage of this "discipline", is that one ends up with a clean hierarchy of packages that actually allows the re-use of base packages without bringing along loads of unwanted pieces. It is also key to successful image stripping, since the removal of entire packages is much more effective than eliminating unreferenced globals and methods at a finer granularity. >...A more > extreme solution (which you'll see sometimes used in the image to avoid > unwanted package interdependencies) would be to refer to the "other" class via > a runtime expression like: > > Smalltalk at: #AnotherClass ifAbsent: [...something...] Creating a symbolic reference is indeed one easy way to break a dependency, but as Chris implies this should be used with care since this reference is now not seen as implying a dependency in the referent, and so may result in issues at deployment time. > > The other question is about the design that gave rise the cyclic dependencies. > I suspect that *most* (not all) such cycles are symptoms of poor factoring. No disagreement with that here. >...If > the two "modules" really are intended to be independent, but nevertheless refer > to each other, then something odd is happening. Probably something wrong. So > I think that the C habit of avoiding interdependencies among modules will > probably continue to serve you well in the Smalltalk world *providing* that you > don't overdo it and confuse a "module" (by which I mean a chunk of co-designed > and co-packaged code) with a class. Precisely. At the risk of repeating myself, that is why we don't support cyclic dependencies between packages. Its also why we eschewed the traditional 'Undeclared' mechanism of Smalltalk (by which forward references to as yet undefined globals get entered into a special dictionary), because we don't think it is needed or helpful in the long run. YMMV. >.... Regards Blair |
Blair,
Ditto to pretty much everything you said. My one wish would be to have better visualization of the dependencies between packages (both "up and down"). Easier said than done, I know (which is why I haven't already done it). My slightly battered 5.1 image contains 222 packages. Speaking of stripping, congratualations are in order. Lagoon of 5.1 (quite correctly) removed ZLib from my deployed apps. It was easy to find, and easier to fix by adding a manual prerequisite that probably should have been required all along. Have a good one, Bill -- Wilhelm K. Schwab, Ph.D. [hidden email] |
In reply to this post by Chris Uppal-3
"Chris Uppal" <[hidden email]> wrote in message
>Customers will know how to make reservations, but Reservations > don't (I'd imagine) want to make Customers. Since the only routine reason for > code to refer to another class is that it uses it as a factory object (sends it > the #new message or similar) I don't expect that Reservation would refer to > Customer at all, in which case (in Smalltalk) there is no physical dependency > of Reservation on Customer. It may or may not be the case that there's a > logical dependency, that depends on the rest of the design, but I'd be inclined > to suspect not. If not, then Smalltalk doesn't create the artificial physical > dependency that Java's declarative type system produces, and there would be no > problem with cyclic dependencies. Very well put Chris. Thank you. > In such cases, in Java, you'd have to make *both* of them into > interfaces, and use the factory object pattern (plus some runtime > configuration) to ensure that neither of the concrete classes referred to the > other, only to the interfaces. In Smalltalk most of that comes for free, > you'd have to ensure that Customer knew how to find the Reservation factory at > runtime (just as in Java) but the rest of the code would remain unchanged. In > effect Smalltalk has the only-use-interfaces and Factory Object patterns built > into the semantics of the language. This really nails it down for me. I've been so mired in Algol-like languages in which even type declarations introduce dependencies, that I hadn't really noticed that there were 2 design issues at work: type reference dependencies and factory object dependencies. Your comments about how Smalltalk need only be concerned with the latter helps clarify the differences between the two. Having recently started learning Smalltalk, I'm beginning to regret all the years I've wasted on those other languages, where issues such as those described above (physical dependencies, type reference dependencies, and factory dependencies) all seem to creep up and blur together. The amount of energy I've spent sorting out classes and modules to keep them decoupled is mindnumbing. I could have been so much more productive much earlier. If I could only get all those years back. Damon |
In reply to this post by Bill Schwab-2
On Thu, 1 May 2003 07:20:01 -0500,
Bill Schwab <[hidden email]> wrote: >> ahhh... If you are a Real Programmer (like Mel:-) and do everything >> with (char *) then you will also be able to hardcode the struct sizes > > (char*)??? STRUCTS??? What's with all of this high level stuff!! Real > programmers write native instructions w/ none of those reference books that > wimps use :) > > Actually, I didn't see it myself, but I heard from a reliable first hand > witness about a consultant who patched the _object code_ for a main frame's > serial communications driver. Got it right on the first try. > > Have a good one, > http://ars.userfriendly.org/cartoons/?id=19990508 :-D having a good one, s. |
Free forum by Nabble | Edit this page |