Implementing collections and allowing direct indexed access like this has lots of issues.
First, since arrays are not dynamic after instantiation, one has to either have logic for copying back and forth between two arrays to allow expansion or use a multiple array scheme such as I have described elsewhere. The multiple array approach I thnk is superior because I think there is going to be substantial overhead in copying when the collection gets large. But, the multiple array approach is not going to allow the direct addressing.
Second, allowing direct access to the array like this is exposing implementation details.
Third, there are lots of question about what happens if one adds and deletes to the array. One either leaves holes, in which case O:A[20} can return a null ... and one has to test for that .... or, one has to pack the array after every deletion. Try an array of 10,000 and delete the first item and see how long the copy down takes. You might think this is an odd case, but what about the very common case where one fills a collection and then sequentially processes the contents, deleting each as the processing is complete. Yes, you can avoid the performance penalty by processing from the end forward ... but then you are relying on the implementation again. Moreover, it leads to the potentially confusining possibility that O:A at one time is not the same as O:A at another time since it might have been packed in the meantime. Moreover, if one tries to put back an object previously taken ... perhaps morphed to a different subtype, for example, then how does one know that O:A is the right place to put it back?
Fourth, if you do pack, what is next after O:A? Maybe 21, maybe not. Maybe even something less than 20.
None of these issues exist with generic collections.
Consulting in Model-Based Development, Transformation, and Object-Oriented Best Practice http://www.cintegrity.com
There is a certain logical difficulty for an object checking to see if it exists ...
I'm not sure what your difficulty is here. If object A has a possible relationship to a set of object B, but one has decided that this relationship is not necessarily instantiated at the outset, then it seems prefectly logical to test to see if the collection exists or not whenever one needs to access something from the collection. Yeah, it is a tiny bit of overhead, one line, but one has to expect to pay some price for making the instantiation optional.
If you use your direct addressing of the array approach, that might be a lot of places in the code, but if you get the next object in the collection and then do all your processing on the current object, then you only need to put that code at the place where you get the next one. One place.
jmls wrote:One thing that's been bugging me about the lazy part of it - who does the checking for a valid collection ? The object using the model ? If so, does that not mean that your code for checking (and also for creating the collection) then appear in several places (each object that requests the collection) or do you put the code into the model itself , or indeed the collection ?The latter two options present their own problems in that how do they know *how* to populate the collection ? You may have a collection called Contacts beginning with A, or a collection of all Contacts of client B
One thing that's been bugging me about the lazy part of it - who does the checking for a valid collection ? The object using the model ? If so, does that not mean that your code for checking (and also for creating the collection) then appear in several places (each object that requests the collection) or do you put the code into the model itself , or indeed the collection ?
The latter two options present their own problems in that how do they know *how* to populate the collection ? You may have a collection called Contacts beginning with A, or a collection of all Contacts of client B
I tend to always make sure that the collection property is created (can be done by the ctor or passed in), and then check :Size (or :Count or what have you) to see whether there's anything in it. Basically, initialisation for the whole object happens at once. This way, once NEW() has run, the object is in a usable state.
This ties in with the fact that I am beginning to tend towards the school of thought that says that a null/invalid object is always a bad thing - and if you want to indicate the fact that an object is null use a NullObject. I think that this makes code a little cleaner (since now "all" we have to deal with are exceptional conditions). But I only say that to justify the above to a degree, and not to split this thread (again?)
One problem with initializing the collection, but leaving it empty is that one then needs a separate flag to indicate whether the collection has been filled since Size = 0 is a perfectly valid state.
Why do you care when it has been filled (or even if)? If a collection has zero items it should be processed the same way, regardless of whether it has 0 items because it is new or whether it has 0 items because it has been dealt with completely. If you really, really care whether the an object has been init'ed or not, add a (n explicit) flag to that effect; but deducing that an object has been init'ed because of the state of another seems hinky/shonky to me.
Given an order with 100 lines and a desire to lazy instantiate because the lines are not needed for all processing.
Given a second order which has no lines, e.g., it has just been created and the lines have not been processed yet.
Omit creating the collection and one can determine by a test of Collection = ? whether it has been instantiated or not. The result is that on first access one notices that it is not initialized and goes to initialize it producing collections with 100 and 0 members respectively.
Create the collection and not fill it and the two collections appear identical, i.e., Size = 0 so one needs a separate flag to determine that one must go fill the collection in the first case and probably try to fill it in the second case.
I agree that one should know that an object is valid before creating it and this implies creating only logically complete objects, but this does not mean that one has to initialize all possible relations. In particular, an object may have multiple relations, only one or some of which are even meaningful in a particular context.
I asked my OO mentor about type safe conventions... he is admittedly a purist who comes down hard on the way that a lot of OO code is done in practice, but given that this is a person whose background is creating highly performance critical applications by translation, i.e., by getting the UML right and then creating code from that by translation I tend to think that he has demonstrated and experienced that one can do things right and it works out best in the long run. His full response is a little long, so e-mail me if you want the details and I will only quote limited parts here.
By definition, if one navigates R1 from ObjectA one should only get ObjectB objects and that is what the OOPL type systems support. When one implements OO relationships correctly -- which I contend PSC has not done yet -- and navigates them properly one always gets type safety for the OOPL R1 collection because there must be a type declaration of the correct type in the method that navigates to the collection.
Note that the cast is fine because one is implementing standardized collections and looking at the OOA/D model; the cast must be to ObjectB* because that is what R1 says. The OO paradigm also demands that myObjectBCollection must be instantiated only for the ObjectA instance in hand. When anyone is adding objects to it, they necessarily must have the ObjectA in hand and they use the myObjectBCollection reference to access it.
So when someone adds an object to the collection, they would have to be remarkably dense to look at the Class Model and add the wrong object (whose type they must also have in hand). Note that the casts are symmetric around instantiation and navigation of R1. Even if they were that careless, they would have to explicitly define the wrong types statically in one or both of the methods so the problem would become clear very quickly when all the objects in the collection were always of the wrongtype. Thus, "type safe" collections are only useful if you have people coding in OOPLs who don't understand the OO paradigm, don't think in terms of relationship navigation for collaboration, don't isolate instantiation, and don't employ generic implementation and navigation techniques for relationship infrastructure. IOW, teach the developers how to use OOA/D properly and you won't need "type safe" collections.
Not surprising. .NET is, at best, object-based and that is charitable. MS has been implementing "OO" development environments solely based on their ease of development for decades. Looking at their stuff it is hard to imagine anyone at MS has ever read a book on OOA/D.
Historically MS has joined standards groups as a hardball marketing tool. Their goal is to get the standards group to adopt whatever MS does and that puts their competitors at a disadvantage. When a group like OMG refuses to go along with the plan, MS leaves the group. They essentially trashed OMG's Motif UI doing that because OMG wouldn't modify it to emulate Windows. Alas, Windows was so popular that Motif died, which is sad because it was a pretty good standard. Now we have to live with idiocy like moving the cursor in an 'L' path when trying to select submenus. They did the same thing with the OO paradigm. They were originally active in the OO committees, including UML. However, they left in a huff when they discovered nobody liked the way they did MFC, COM, DCOM, and ActiveX. They are back again trying the same crap with the MOF initiatives.
BTW, there are other ways to do such "type safe" collections if one insists on doing them. You can override Object and add an attribute for a type code that is set by the ctor for that class. Then the collection can have a similar attribute that is set when the collection itself is instantiated. The add(...) then checks for a match in the codes. [Note that this is effectively doing what a dynamically bound OOPL does to check types. The housekeeping just isn't visible.]
After a couple more outside discussions and a bit of thinking about this, I am going to shift my position a bit.
First, I am going to continue to say that I don't really think type safe collections are necessary because of the way that collections should be used. I.e., if a collection is supposed to contain type X then a component using that collection should know it has an object of type X before adding it. Any unexpected type mixing in a collection is indicative that one has a mess.
But, if one had generics, then I can see that one could have a single generic source module which performed the cast prior to the return and N object modules, one for each type actually used (presumably created as needed by the compiler), and that this would be acceptable from a code management point of view and advantageous in limiting the need for casts.
If one was sure of getting generics in ABL, then I can see that the lnclude approach could be a bandaid to produce a similar effect at the expense of source code mess because when we got generics, one could simply modify the base and delete all the definitions containing the includes.
But, at this point I am not so sure about getting generics ... despite multiple use cases ... and so I'm not sure whether the value offsets the mess.