Dealing with class extension is very subtle. Approaches taken by Selector Namespace, Classboxes and Virtual Classes rely on bounding the visibility of class extension: the one who defines a class extension is the only one to see it. Conflicts are easily avoided in that way...
Cheers, Alexandre
On Sun, Feb 27, 2005 at 01:11:31AM -0500, Colin Putney wrote:
On Feb 26, 2005, at 11:20 PM, Florin Mateoc wrote:
We have two versions of a method, both with complete version history. Because we have the version history, it doesn't really matter that the two versions come from different packages, it's exactly the same as merging two versions of the same package. So instead of one version overriding the other, we do a merge. By comparing the method histories we can decide if one version supersedes the other. That would mean that it's an updated version of the other, which means we can rely on the user's wisdom again. If the user changed the method from one of the versions we have to the other one, he must know what he's doing. Therefore we use which ever version the user has already chosen.
I am sorry, but this is simply not true. A developer may choose, in a newer version of a class, to ignore some unrelated development, and stick to an older protocol, by including some older versions for some of the methods. This is not a made up example, I have encountered the situation quite often. You can easily have, as a simplistic example, PackageA>ClassB>methodC(version1),methodD(version2) and PackageE>ClassB>methodC(version2),methodD(version1). The automatic resolution will do the wrong thing, and it won't even inform the user.
Ok, let's get into this in excruciating detail, because it's not clear to me why you think the above example cannot be resolved correctly. Let's say I have the following program elements, drawn from your example, with history.
PackageA
ClassB>>methodC.version1 (ancestors: version0) ClassB>>methodD.version2 (ancestors: version0, version1) ClassB>>MethodE.version1 (ancestors: version0)
PackageZ
ClassB>>methodC.version2 (ancestors: version0, version1) ClassB>>methodD.version1 (ancestors: version0) ClassB>>methodE.version2 (ancestors: version0)
Ok, so let's see what happens if we load both packages into the same image. PackageA and PackageZ both define methodC, and they have different definitions. So we've got to decide which version, if any, will be in the image. The version in PackageA is called version1, and it was derived from version0. The version in PackageZ is called version2, and lists version1 as its ancestor. Therefore, version2 was created by modifying version1. So we'll choose version2, from PackageZ.
MethodD has the reverse situation. PackageA's version is a descendent of PackageZ's, so we'll choose version2 again, but this time from PackageA.
MethodE presents a conflict. Both versions descend from a common ancestor, but neither descends from the other. So we pop up a conflict resolution window, and let the user decide what methodE should look like when both packages are present. This results in a new version, called version3. When we're done, the image looks like this:
ClassB>>methodC.version2 (ancestors: version0, version1) ClassB>>methodD.version2 (ancestors: version0, version1) ClassB>>methodE.version3 (ancestors: version0, version1, version2)
Now, you are correct to point out that, say, methodC.version2 might have been developed in PackageZ without PackageA loaded, and so might not work as PackageA expects. Perhaps we should indeed log to the Transcript when a merge automatically resolves overlapping packages. There is no *guarentee* that version2 will work right. But our chances of success are better if we follow the intention of the developer of version2, which was to replace version1. Following the order that packages are loaded is little different than choosing at random.
Making independently developed packages work together means (intelligent) work, and if there's any overlap, the chances of solving the issues automatically are, I believe, very slim, and versioning does not help. Even if all the common methods in one of the packages are newer versions (and descendants) of the same methods in the other packages, it still doesn't mean that they are made to work with the older package, it may simply mean that the newer package is supposed to work with a newer version of the older package. I think the only situation where you can say that there is no conflict is when the common methods are all identical, and for this you don't need versions. This is why, to my mind, overrides have nothing to do with versioning, they are simply a different kind of extension.
I agree that it takes intelligent work to make packages work together, and I'm not suggesting that the computer can do that. I am suggesting that, having done the work, we record the results so that we don't have to do it again everytime we load those packages.
[snip]
This is probably just the memory of a frustration with Envy: because it stores all these method editions (inluding every time you put a "halt" in a method), the noise level is pretty high, so I always wished that I could see at a glance, when looking at the list of editions for a method, which editions are "real". But even if we have explicit method versioning, so the noise is reduced, the most "real" ones are the ones associated with the holder's version, because there is an implicit minimal testing expectation for versions.For the method version I would expect something like a unit-test, for a class, the beginning of some functional testing. The expectation is even higher for the package, because it usually groups together classes working in tight coupling, so the testing done for a package version is more of a functional test, so now those methods "really" work. I guess it would be fun to disallow versioning if we detect that testing was not performed :) Seriously though, it might be interesting if we could link somehow versions to the tests performed.
Ok, I see. You just want to define a group of program elements that should be versioned together. With Monticello you do this explicitly, so there's a lot less noise. Everything is a "real" version, and they correspond to a bunch of other "real" versions that were current at the same time.
Even if you do it explicitely, not all versioning happens at the same time. I develop a method, it looks good, I test it a little (workspace, unit-test, whatever), I am happy with it and I want to keep it. I version it (separately, because this is what method-level granularity means).
Interesting, because that's not what I mean by "method-level granularity."
In MC1 (and Store, as far as I can tell), only packages have ancestry. The ancestry of a method has to be reconstructed by examing all the versions of the package it appears in and noting how it changes. In MC2 (and Envy, as far as I can tell), methods have individually recorded ancestry. This is why I say that MC2 versions at method-level granularity.
But that doesn't mean that you have to version new methods in isolation, whether explicitly or with every accept as in Envy. If you do that, you loose the spacial context I mentioned in my last post. That version is just noise, so why bother? It's not like the method is going anywhere. You can save your image without versioning it, and even if you manage to crash the VM you can always pull it out of the change log.
In MC1 you always version whole packages at a time. MC2 is more flexible, in that you can specify other ways of separating the code you are interested in from the rest of the image. But whatever your method of segregation, you always version all of it at once. So in this sense, Monticello versions at "project-level granularity," the project being whatever you're working on, be it a package, change set or whatever. It's important to do that so as to get the spacial context needed to merge snapshots correctly.
Colin