Question
· Jan 2, 2019

Anyone have a technique for COS Class Polymorphism that should survive an upgrade?

Hi -

I'm wondering if anyone has coded up a means to create an extension for a %Persistent class from a base class to a sub-class without making a ton of assumptions about the Global structure. I'm trying to create a new "extension" record that would have the same ID as the Base Class 

Class BaseRecord Extends %Persistent

and

Class SubRecord Extends BaseRecord

where I would have an instance of a "BaseRecord" and I want to turn it into a "SubRecord" instance and have all of the existing references to the BaseRecord survive.

I know I could go after the globals themselves but this approach relies on an awful lot of low level knowledge that really shouldn't be poked around with and would only really work with a single storage definition.

Any suggestions? (besides 'don't do that')

Discussion (7)3
Log in or sign up to continue

With profound apologies, I feel the compulsion to add an "don't do that" comment.

Use UUIDs instead of database IDs.

Why? The reason is quite simple: With database IDs, you do not have an ID until after the object is saved into the database. Are you sure you won't need an ID before saving the object, ever? Also, saving the object just to get an ID is bad practice IMO, as it possibly encourages mixing of database layer logic with other layers.

Also, with UUIDs, implementing your desired functionality will become trivial. Just implement GetObjectByUUID using ..%ClassName() in base class.

"Why?" (a good question) and "Don't do that!" aside, here's a technical answer: override %OnDetermineClass to allow instances of the parent class to be treated as the subclass in question. For example:

Class DC.Demo.Extension.BaseRecord Extends %Persistent
{

Property Name As %String;

ClassMethod %OnDetermineClass(oid As %ObjectIdentity, ByRef class As %String) As %Status [ ServerOnly = 1 ]
{
  Set tSC = ##super(oid,.class)
  If (class = "DC.Demo.Extension.BaseRecord") {
    Set class = $classname()
  }
  Quit tSC
}

Storage Default
{
<Data name="BaseRecordDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Name</Value>
</Value>
</Data>
<DataLocation>^DC.Demo.Extension.BaseRecordD</DataLocation>
<DefaultData>BaseRecordDefaultData</DefaultData>
<IdLocation>^DC.Demo.Extension.BaseRecordD</IdLocation>
<IndexLocation>^DC.Demo.Extension.BaseRecordI</IndexLocation>
<StreamLocation>^DC.Demo.Extension.BaseRecordS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}

}

Class DC.Demo.Extension.SubRecord Extends BaseRecord
{

Property Foo As %String;

Storage Default
{
<Data name="SubRecordDefaultData">
<Subscript>"SubRecord"</Subscript>
<Value name="1">
<Value>Foo</Value>
</Value>
</Data>
<DefaultData>SubRecordDefaultData</DefaultData>
<Type>%Library.CacheStorage</Type>
}

}

Class DC.Demo.Extension.Driver
{

ClassMethod Run()
{
  Do ##class(DC.Demo.Extension.BaseRecord).%KillExtent()

  Set tBaseRecord = ##class(DC.Demo.Extension.BaseRecord).%New()
  Set tBaseRecord.Name = "Fred"
  Write !,"Save base record: ",tBaseRecord.%Save()

  Write !,"Contents of global ^DC.Demo.Extension.BaseRecordD:",!
  zw ^DC.Demo.Extension.BaseRecordD

  Set tSubRecord = ##class(DC.Demo.Extension.SubRecord).%OpenId(1)
  Set tSubRecord.Foo = "Bar"
  Write !,"Open as sub record, name = ",tSubRecord.Name
  Write !,"Save sub record (converts to sub record): ",tSubRecord.%Save()

  Write !,"Contents of global ^DC.Demo.Extension.BaseRecordD:",!
  zw ^DC.Demo.Extension.BaseRecordD
}

}

Output is:

USER>d ##class(DC.Demo.Extension.Driver).Run()
 
Save base record: 1
Contents of global ^DC.Demo.Extension.BaseRecordD:
^DC.Demo.Extension.BaseRecordD=1
^DC.Demo.Extension.BaseRecordD(1)=$lb("","Fred")
 
Open as sub record, name = Fred
Save sub record (converts to sub record): 1
Contents of global ^DC.Demo.Extension.BaseRecordD:
^DC.Demo.Extension.BaseRecordD=1
^DC.Demo.Extension.BaseRecordD(1)=$lb("~DC.Demo.Extension.SubRecord~","Fred")
^DC.Demo.Extension.BaseRecordD(1,"SubRecord")=$lb("Bar")

In answer to the "Why?" questions...

Assume you have a generic "Person" record, and now you want to treat this "Person" as a "Doctor" record where Doctor is an Extension of Person. When the Person record was created, it was not known that this represented person was a going to be doctor record at some point in the future. The person record was created, properties set to values, and saved, linked referenced (i.e. "used" as a Person record). Now at some point later, there is a need to make a "Doctor" record out of this "Person" record. Since anything you could do with/to a Person record can be done with a Doctor record and Doctor only adds functionality (and possibly overriding methods), creating a "new" Doctor record and then replicating data values will not update any references to the "Person" record (and may not even be identifiable from just the Person record to begin with), but morphing that instance of a Person into an instance of a Doctor will preserve all of the references that might exist while enabling the new properties and methods of a Doctor.

Hi Chip.

In general, there is a danger of the tail (OOP) wagging the dog (ORM).

Polymorphism itself is a funny old thing. When it comes to true abstract things, its more easy to understand where its benefits come from. I always like to use IDataReader as a strong example of explaining Polymorphism. Where it becomes a bit murky is with the daft examples that try and teach Polymorphism, such as Cat and Horse implement Animal. It never really makes any real world sense, and as soon as you mix database storage it can go from murky to bizarre.

If we simplify it down to just shared data attributes then we can see how Person might make sense, but this type of design will have a trade off somewhere else in the architecture. You could have a Person table, and many concrete role looking implementations of Person, but then other areas such as SQL reporting can become really complex.

From the perspective of the example provided there are well established design patterns, we would have a Patient class and a separate Staff and Role class. Staff might share similarities to Patient, but we would try and solve this through composition over inheritance (and polymorphism), so for instance Address could be a shared thing. In this sense there is no problem that a Doctor can also be a Patient. There is duplication of storage here, but its a small trade off from other aspects of the architecture. There is also the aspect that something like Address has a different context anyway, a Patient Address would be home, whilst a Doctors Address would be work, and here the two database pools don't mix that well, hence why they tend to be two separate data entities.

That all said, its still an interesting question, and perhaps it just needs a better real world use case to explore it further...