beXXXX.p vs beXXXX.cls

Posted by Thomas Mercer-Hursh on 02-Feb-2007 16:25

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?

All Replies

Posted by Admin on 03-Feb-2007 03:40

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.

Posted by Thomas Mercer-Hursh on 03-Feb-2007 13:16

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.

Posted by Admin on 04-Feb-2007 05:48

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.

Posted by Admin on 04-Feb-2007 05:53

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..

Posted by Thomas Mercer-Hursh on 04-Feb-2007 14:17

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.

Posted by Thomas Mercer-Hursh on 04-Feb-2007 14:31

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.

Posted by Admin on 05-Feb-2007 02:16

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.

Posted by Thomas Mercer-Hursh on 05-Feb-2007 11:50

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.

Posted by Admin on 05-Feb-2007 12:23

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

Posted by Admin on 05-Feb-2007 12:30

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...

Posted by Thomas Mercer-Hursh on 05-Feb-2007 13:00

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.

Posted by Admin on 05-Feb-2007 13:44

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.

Posted by Admin on 05-Feb-2007 13:48

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?

Posted by Thomas Mercer-Hursh on 05-Feb-2007 14:18

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?

Posted by Thomas Mercer-Hursh on 05-Feb-2007 14:24

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.

Posted by Admin on 05-Feb-2007 14:45

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.

Posted by Admin on 05-Feb-2007 14:51

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.

Posted by Thomas Mercer-Hursh on 05-Feb-2007 14:52

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

Posted by Admin on 05-Feb-2007 15:00

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.

Posted by Thomas Mercer-Hursh on 05-Feb-2007 15:06

This thread is closed