In looking at John Sadd's latest Overview of the OpenEdge Reference Architecture, one of the things I noticed in the discussion is that a beOrder "object" has a "fetchOrder" method. This struck me as curious because it amounts to suggesting that a domain object should include a method corresponding to "instantiate yourself". This leads me to think that we have a difference in the concept of a business entity between the .p version and what I would expect in a .cls version.
In the .p version, the beXXX object is not really a wrapper for the domain data ... the PDS is that wrapper and is the thing which is passed between data access and business logic layers. Instead, the beXXXX object is a kind of controller for the domain data and has methods such as fetchXXXX which one would not expect to find in a domain object itself.
In a .cls version, I would expect the domain object to encapsulate both the data and some of the logic found in beXXXX, but for other parts of that logic to be moved to other places, such as a Finder object and for those methods to be called, not by the domain object itself, but by other task or service objects which needed one of the domain objects. I.e., in John's discussion, the request for the order wouldn't go to beOrder, but either some service or to the task which was managing the user session.
Thoughts?
In looking at John Sadd's latest Overview of the
OpenEdge Reference Architecture, one of the things I
noticed in the discussion is that a beOrder "object"
has a "fetchOrder" method. This struck me as curious
because it amounts to suggesting that a domain object
should include a method corresponding to "instantiate
yourself".
When you browse the internet you will find more articles/samples using this approach. It's some syntactic sugar to make it easier to use objects while hiding persistence. You will also find samples with Save/Delete/IsDirty/IsNew in their API. Now this doesn't have to be bad, when the "Load" simply delegates the call to a persistence class...
One of the problems with this approach is the "state of the object". You can't put validation constraints in the getter/setter of properties (you might want to load the object, or it can be in loaded state and you want to override the key, etc). You can ask yourself if its wise to put validation constraints in the getter/setter other than checking string length and other basic stuff...
When you don't delegate the persistence logic, but implement it in your domain object, things might become messy. Your domain object might start extending from some persistence base class that deals with database access. Soon it will become a very fat class with lots of responsibilities.
I must be missing something ... I see that
one can instantiate a .p which contains an empty PDS and the run a
method that invokes a lower layer to fill the PDS, but it seems
screwy to me to instantiate an empty domain object and call a
method that invokes another object which then returns the value,
rather than returning the object itself.
I must be missing something ... I see that one can
instantiate a .p which contains an empty PDS and the
run a method that invokes a lower layer to fill the
PDS, but it seems screwy to me to instantiate an
empty domain object and call a method that invokes
another object which then returns the value, rather
than returning the object itself.
Now you're touching an interesting topic: when you look at Data Access Layers out there you will see that these use simple value types in their API. So things like "Customer.Save(custName, birthDay, ..., out custId)". This is done to decouple the lower layer from the upper layer. You will get a tight coupling between the two when you let the data access layer return domain objects.
An Object Relation Mapping framework uses a different approach. This uses generics to load domain objects. It doesn't have knowledge about a specific "Customer"-entity, it only knows how to load generic database data and how to transfer it to something like an entity.
but it seems screwy to me to instantiate an
empty domain object and call a method that invokes
another object which then returns the value, rather
than returning the object itself.
If you're using the "query by example" mechanism, you don't have an empty domain object. You will populate the key values of the domain object and call Load() on it. The domain object can now prepare a data access request and delegate it to the proper data access class, whic will return a temp-table or query object. The domein object will use that data to rehydrate itself.
When you compare it so serializing and deserializing data, you find it not so bad when a ProDataSet has a SaveToXml or LoadFromXml method. Actually it's pretty convenient. That the ProDataSet uses lots of stuff internally, you don't care..
Nice that, after
all these hundreds of posts, I finally managed one! I guess I'm going to need some
more info to know what you are talking about here. Seems to me that
to do an OR mapping implies: 1) Knowledge of the way the data is
stored; 2) Knowledge of the object to be constructed; and 3) A
mechanism for mapping between one and the other. There might be
parts of this process where on can deal with instances of
Progress.Lang.Object, as my collection classes do, but ultimately
the objects need type to do any real work.
I'm not
exactly sure what we are comparing here, but I will say that, while
the XML methods on temp-tables and PDS are certainly attractive,
thus far I'm not sure that they are actually compelling reasons to
use PDS or TT in places where they are not needed, e.g., in a
domain object which is a single instance. Creating these methods
based on isolated properties is very easy and easily automated and
they execute very fast as well. Indeed, Greg was saying a while
back on PEG that his experiments suggested that one could serialize
a TT to XML, transmit it, and deserialize it faster end to end than
sending the temp-table itself. Frankly, I am more concerned about
the Before-Table features, but I still can't see using a PDS for a
single instance.
Now you're touching an interesting topic
Nice that, after all these hundreds of posts, I
finally managed one!
Sorry, I'm Dutch, so English is not my native language....
when you look at Data Access Layers out there you
will see that these use simple value types in their
API. So things like "Customer.Save(custName,
birthDay, ..., out custId)".
Perhaps you and I have different notions of what we
consider good models
As far as I can see, I didn't show my preference in this thread. I was merely pointing to other practical examples using this approach. Maybe I should try to think more in ABL and try to forget my practical experience with C#/.Net, since I feel that stands in our way. In .Net tightly coupling of layers becomes very obvious due to circular referencing of projects*: when you have a business logic project and a data access project and you want to share the business entity between the two, you have a limited set of options. The compiler is the restriction here...
Where are
these properties coming from?
The Customer object can provide them (it manages the mapping in this case).
What are they
contained in when they are above the DA layer. What
function does this Customer object have if it
doesn't contain data about the customer?
Sure it contains data about the customer. The Customer object is a domain object with logical properties like "CustomerName", "CustomerId", etc. But it could use a data access instance to do the persistence for it.
This is done to decouple the lower layer from the
upper layer.
Lower and upper within the DA layer, as in DA vs DS
objects?
No, it's about the domain object (i.e. Customer) and the data access layer (persistence).
You will get a tight coupling between the two when
you let the data access layer return domain
objects.
Now you have me really confused. Why does having the
DA layer return a domain object to the BL layer
couple the two ...
A data access layer has limited responsibilities. It takes care of persistence and it does it efficiently. On top of persistence you will have business rules. When you start thinking in application layers and projects*, you have to be aware how projects reference each other. Like I said, this is obvious when you do some .Net development: things are determined by their compilation sequence. You need the data access project to be compiled before you can compile the business logic project. When the two reference a "customer entity", it means you will need a third project that has no dependencies to either or the data access project provides it. In the latter case the functionality of the entity will be limited.
*) in .Net there are projects as logical units (DLL's) and the projects are grouped in a solution. A project has to reference another project when it wants to use classes/interfaces from the other project.
An Object Relation Mapping framework uses a
different approach.
Different from what? Both of the above are forms of
OR mapping.
Please have a look at hibernate for instance or the O-R mapping in DataXtend (http://www.progress.com/dataxtend/or_mapping/index.ssp). These are isolated frameworks that have no application specific knowledge. A mapping file, the target classes and a target database is enough information to broker class instances for you. This is different from a application specific data access layer, that uses "Customer" in it's API. This type of data access layer is very application specific.
I guess I am missing the implication here. To be
sure, there are different levels of tool support for handling the
DA layer, including both ones that work based on generalized
components driven by maps and tools which generate the layer.
Frankly, I think the DA layer is the easiest and should be the
first thing that one generates and one shouldn't be writing that
code by hand except when one is still experimenting. To be sure, if
one is using a generalized tool, then there is a very limited
amount of logic that one can put into the map, but then I thought
that we agreed that this is what we should be doing anyway ...
dealing only with the persistence, not with any logic. Again, I
think this is a case in which the tool is merely enforcing what we
want to do anyway.
After all, how many
examples of ABL code that you have ever seen
represent models on which you would like to base
future work?
I like the work of John Sadd, but as soon as things translate to actual ABL I'm less pleased. And that's due to the ABL...
But, they are being supplied to a method on the
Customer object ... what is the point in passing it
properties that it already has?
I.e., if these are properties of Customer, then
Customer.Save(custName, birthDay, ..., out custId)
is really
Customer.Save(Customer:custName,Customer:birthDay,
... out custID)
Well no, the flow would be like this:
Customer cus = new Customer();
cus.CustomerId = 1000;
cus.Load();
The "Customer.Load()" would do the following:
public void Load()
{
CustomerFinder finder = new CustomerFinder();
if (finder.FindByKey( _customerId ))
{
_customerName = finder.CustomerName;
....
}
}
Now this "Finder" can be a wrapper around an internal temp-table, but it could return a temp-table as well. Anything generic would be OK. It could also return a generated "CustomerRow" object. But that CustomerRow is not the same as the Customer in the domain logic.
The Customer-class could implement a lazy loaded "Address"-collection like this as well, so you can say "cus.Addresses" and internally the customer could use an AddressFinder.
Now we have separated some concerns:
- the Customer is responsible for the customer integrity
- the Customer chooses a default persistence mechanism via the Finder
- the Customer decides how the primary key will be generated
- the Finder is responsible to persisting properties and loading them back
Frankly, I think the
DA layer is the easiest and should be the first thing
that one generates and one shouldn't be writing that
code by hand except when one is still experimenting.
Or you can use a generic and dynamic framework instead of generating code. But one way or the other: the fun starts once the objects are loaded into memory. You will have to make sure that the in memory data is consistent. But I think we had that discussion earlier...
And, what added value have we
accomplished with a second object with the same properties? I don't
think that we differ in goals here, only in mechanism. Let me ask
this, then ... what happens to the Customer:Load() when the desired
criterion is Customer:Name = "A*"? How is that implemented using
this mechanism. By putting this in the DA layer, I can distinguish
between an expected return of a single object or an expected return
of a collection by the method I use. What do you do with your
approach.
Except that the method I was referring to was your
previous reference to Customer:Save(), not
Customer.Load().
Sorry, Save() would go the other way around, so the Customer would pass in it's private members...
I don't see
the benefit to this over having the Finder return a
domain object to the BL layer component that needs
the Customer.
Now assume the following chicken-and-egg dilemma:
- you want to generate Finders
- you want them to broker full blown Customer domain objects
What code do you create first, the Customer or the CustomerFinder? And when you don't create the Finder first, how do you want to test the Customer? What you could do is creating a more stable Customer interface or a base class and let the Finder use that definition. So basically you start with defining all the Finder needs.
I don't think that we differ in goals here, only in
mechanism.
And I still didn't say I liked the Customer.Save/Load approach For me it's some syntactic sugar: it doesn't really matter much whether you say:
customer.Save()
Or
customerPersister.Save(customer)
The latter one is slightly more verbose to the "user" of your Customer class.
Let me ask this, then ... what happens to the
Customer:Load() when the desired criterion is
Customer:Name = "A*"? How is that implemented using
this mechanism.
In that case you would treat the "Customer" as a "query-by-example", so you would have a "Customer.LoadQueryByExample()" which would return a list of customers. The Customer providing the list would not be populated in that case...
By putting this in the DA layer, I
can distinguish between an expected return of a
single object or an expected return of a collection
by the method I use. What do you do with your
approach.
Well that's only a matter of creating an unambigious contract. I can define a "Finder.Get(..)" that isn't very clear either: it could return a list or a single object.
I don't see
the benefit to this over having the Finder return a
domain object to the BL layer component that needs
the Customer.
Another thing to keep in mind when moving the Save() from the Customer away is the "trust"-part. When you have some data access class method "CustomerPersister.Save(Customer)", what do you expect it to do? Accept any "Customer"-instance or only "Customer"-instances that find themselve "valid for saving"? You don't want the data access class to validate the Customer-instance, do you, else what's the purpose of the "Customer"-class other than being a data transfer object?
I would understand if
this was the Save() within the Customer object passing its
properties back to the DA layer object, but your example was
Customer.Save( .... ) which indicates that the properties of
Customer are being supplied to Customer from outside of Customer.
As one could if the return were a
Progress.Lang.Object which was then cast according to the nature of
the result. Not that I think I would do it that way. The contrast
here, in pseudo code, seems to be new a Finder ... decide whether
one wants an individual or a set for an individual call finder with
key and return domain object use it give it back to finder to save
... or for a set set target values in finder call finder to return
set use it possibly select one for update, etc. versus new an empty
domain object ... decide whether one wants an individual or a set
for an individual call load with key and populate domain object
domain object calls finder to do load use it call save on domain
object domain object calls finder to do save ... or for a set set
target values in domain object call method on domain object for get
domain object calls finder to get set domain object method returns
set, leaving domain object unpopulated use set possibly select one
for update, etc. Is that a fair comparison?
As discussed elsewhere, there are really two possible levels of validation. One of these is appropriate in the domain object itself ... and I see nothing wrong with the DA layer calling that Validate method again, just to make sure that the BL layer isn't trying to fool it by passing in bad data. I.e., that method can be used regardless of context. Yes, I know there are some issues about look up values and such, but those exist regardless.
But, there is another part of the validation which can only occur in the presence of the database, e.g., I pass in a perfectly valid Customer object which happens to have a duplicate unique key to one that is already in the database ... quite possibly from some other source. I can't detect that until I am connected to and aware of the database.
Domain objects can have lots of behavior that has nothing to do with how they get stored and validated.
and I see nothing wrong
with the DA layer calling that Validate method again,
But is it the DA-layer's responsibility to validate again? And how do you prevent developers putting in complex logic in the Validate, something the DA-class isn't expecting? How is the Customer-object going to validate anything sensible without having access to the DA? Yes, it could ask another entity... but since that one has no access to it's DA-layer, we wouldn't get far...
just to make sure that the BL layer isn't trying to
fool it by passing in bad data. I.e., that method
can be used regardless of context.
So when is it valid enough for the DA-class?
But, there is another part of the validation which
can only occur in the presence of the database, e.g.,
I pass in a perfectly valid Customer object which
happens to have a duplicate unique key to one that is
already in the database ... quite possibly from some
other source. I can't detect that until I am
connected to and aware of the database.
So is this any different from the case that Customer calls the Finder in it's Save()-method? Here the Finder can throw an exception on duplicate key, which can be caught by the Customer and reported back...
Domain objects can have lots of behavior that has
nothing to do with how they get stored and validated.
Sure they can and in the ideal case we fully abstract persistence.
So, I create a Customer object, but it never gets
populated and a method call on it returns a
collection? This seems bizarre to me.
That is not necessary a problem. A more fundamental problem is the fact that I can instantiate 3 Customer instances with the same primary key. So at a certain point in time you can have multiple versions of the same Customer during a single transaction. When you have a context that manages Customer instances, it can return an existing version. It's a similar concept with 4GL buffers: when you do a subsequent FIND, the runtime will go to the database and drop the buffer when it's already in memory. This way you will always read your own changes and consistently.
It is only a
difference in packaging, i.e., Create customer Set value Call Load
on customer Oops load failed and this customer isn't what I was
hoping for versus Call Get One on Finder Oops method returned an
error
Many kinds of validation don't require access to the
database and many other kinds relate to something
that can be validated against a local cache.
Well that's very abstract, can you be more concrete?
When it is valid enough to store. Is this a trick
question?
Hehe... no, I just found myself defending the Customer.Save/Load, while I didn't want to.