This discussion is archived
13 Replies Latest reply: May 24, 2012 10:30 AM by Zlatko Sirotic RSS

ObserverPattern

926088 Newbie
Currently Being Moderated
Hi,

i'm trying to implement some of the common oo-design patterns with pl/sql objects but ran into some strange issue.

I'm implementing the ObserverPattern, but the observable is not holding the correct ObserverObjects, after i registered them.
The observable only seems to holds local copies of the observers and therefore can not update the real observers.
declare     
     myDevice     DeviceClass; -- observer
     mySensor     SensorClass; -- observable
begin
     myDevice:=DeviceClass();
     mySensor:=SensorClass();
          
     mySensor.registerObserver(myDevice); -- internal value of myDevice is zero
        mySensor.setValue(30); -- this invokes the update method that sends the update to the observer
                                         -- myDevice should have the internal value 30
        myDevice.getValue(); -- should output the internal value updated by the sensor, in this case 30 - but outputs 0
end;
If it's true that i can not pass the instance of an object to another object to work with it, what is the whole OO-approach
in PL/SQL worth in the end? In this case most of the important patterns can not be implemented... And it's (imho) a very
bad workaround to store those objects in tables (even if it's just temporary) to use ref and deref, since those object are
there to process data and are not to be stored.

So, am i missing something? How could i get this to work?

Greetings
  • 1. Re: ObserverPattern
    BillyVerreynne Oracle ACE
    Currently Being Moderated
    The following code sample tries to explain how it works in the PL/SQL environment:
    SQL> create or replace type TSensor is object(
      2          sensorName      varchar2(10),
      3          sensorReading   number
      4  );
      5  /
    
    Type created.
    
    SQL> 
    SQL> create or replace type TSensorArray as table of TSensor;
      2  /
    
    Type created.
    
    SQL> 
    SQL> create or replace type TDevice is object(
      2          deviceName      varchar2(10),
      3          deviceSensors   TSensorArray,
      4  
      5          constructor function TDevice( name varchar2 ) return self as result,
      6          member function SensorCount return integer,
      7          member procedure AddSensor( sensor TSensor )
      8  );
      9  /
    
    Type created.
    
    SQL> 
    SQL> create or replace type body TDevice as
      2  
      3          constructor function TDevice( name varchar2 ) return self as result is
      4          begin
      5                  self.deviceName := name;
      6                  self.deviceSensors := new TSensorArray();
      7                  return;
      8          end;
      9  
     10          member function SensorCount return integer is
     11          begin
     12                  return( self.deviceSensors.Count );
     13          end;
     14  
     15          member procedure AddSensor( sensor TSensor ) is
     16          begin
     17                  --// add a new object
     18                  self.deviceSensors.Extend(1);
     19  
     20                  --// duplicate the object passed as parameter
     21                  --// into the array
     22                  self.deviceSensors( self.deviceSensors.Count ) := sensor;
     23          end;
     24  
     25  end;
     26  /
    
    Type body created.
    
    SQL> 
    SQL> declare
      2          sensor  TSensor;
      3          device  TDevice;
      4  begin
      5          device := new TDevice( '/dev/foo' );
      6          DBMS_OUTPUT.put_line( 'device has '||device.SensorCount||' sensor(s)' );
      7  
      8          --// instantiates a new object
      9          sensor := new TSensor( 'sensor1', 100 );
     10  
     11          --// copies (not move!) the object into
     12          --// the device object's sensor array
     13          device.AddSensor( sensor );
     14          DBMS_OUTPUT.put_line( 'device has '||device.SensorCount||' sensor(s)' );
     15  
     16          --// can also do it as follows
     17          device.AddSensor(
     18                  TSensor( 'sensor2', 50 )
     19          );
     20          DBMS_OUTPUT.put_line( 'device has '||device.SensorCount||' sensor(s)' );
     21  
     22          --// we can make a copy of a sensor from the device's array.
     23          --// but the copy is a brand new object and does not provide
     24          --// a direct reference to the sensor object in the array
     25          --// (there are no pointers in this case)
     26          sensor := device.deviceSensors(2);
     27  
     28          --// .. and this changes the copy of the sensor and not
     29          --// the actual target sensor in the device
     30          sensor.sensorReading := 200;
     31  
     32          --// access to the device's sensors need to be
     33          --// via the device object itself
     34          for i in 1..device.SensorCount loop
     35                  DBMS_OUTPUT.put_line(
     36                          device.deviceSensors(i).sensorName||'='||
     37                          device.deviceSensors(i).sensorReading
     38                  );
     39          end loop;
     40  end;
     41  /
    device has 0 sensor(s)
    device has 1 sensor(s)
    device has 2 sensor(s)
    sensor1=100
    sensor2=50
    
    PL/SQL procedure successfully completed.
    
    SQL>
  • 2. Re: ObserverPattern
    BillyVerreynne Oracle ACE
    Currently Being Moderated
    Oh yeah - you can also directly access the sensor objects in the device as follows:
    SQL> 
    SQL> declare
      2          device  TDevice;
      3  begin
      4          device := new TDevice( '/dev/foo' );
      5  
      6          --// manually add a sensor location
      7          device.deviceSensors.Extend(1);
      8  
      9          --// instantiate a sensor in that location
     10          device.deviceSensors( device.SensorCount ) := TSensor(  'sensor1', 100 );
     11  end;
     12  /
    
    PL/SQL procedure successfully completed.
    
    SQL> 
    No need for the device class to provide methods for access the sensor class methods.

    An issue to keep in mind that there is no private or protected properties or methods in PL/SQL o-o. But then Java is not that great either when it comes to properly supporting properties and getters and setters (Delphi was the first IMO to do this in a logical and sane fashion).

    So while there are limits to o-o supported features in PL/SQL, the basics certainly work well enough.
  • 3. Re: ObserverPattern
    926088 Newbie
    Currently Being Moderated
    Thank you for your quick reply!

    This seems to be a good workaround ( i mean, copying the sensors into a local instance),
    since there is obviously no call by reference for object types, but somehow this "feels" odd
    which might be caused by my Java background ;)

    Greetings

    P.S.: I guess i know, what's causing the odd feeling. You will always need access to all objects.
    The Java implementation is different in this point. Here you could create a device instance, which
    you can read out or maybe two or three. Then you could pass this instances to other processes/objects/methods
    which register those devices to one (or maybe different) sensors. The sensors update the devices
    and there is no need to know about the sensors or their internal implementation. One would simply
    read out/poll the devices.

    Or in other words, the workaround knows about the implementation, which can be considered as
    "bad" OO-coding style. It's a real pitty there is no call by reference :( maybe in Oracle 12.something

    Edited by: MiBoe on 21.05.2012 05:28
  • 4. Re: ObserverPattern
    926088 Newbie
    Currently Being Moderated
    Well, i have thought about the problem having to know each object and found a solution to this.

    I have created a pl/sql package to work as object registry which is unique per session (since a package can be seen
    as singleton pattern). This package is only used by the object super class.
    This registry stores the instances of the objects in a pl/sql table where the index of the corresponding object
    is used as unique id.

    The derived objects only call simple get, new and update procedures inherited by the super class and are filled
    with the actual (stored) state. Of cause one has to know when to update/get the used object, but this way
    the usage of the objects becomes more transparent.

    So, thank you for your help, it pushed me in the right direction :)
  • 5. Re: ObserverPattern
    BillyVerreynne Oracle ACE
    Currently Being Moderated
    A PL/SQL "+table+" is not a term I like.

    The term has a very specific meaning in a database - and the structure created in PL/SQL is not by any stretch of the imagination anything like a table. Correct terms are arrays and collections, or associative arrays (depending on how the structure is defined).

    The best place for data however, is inside real SQL tables. Not PL/SQL arrays. Tables provide far greater flexibility and usability. It provides performance and scalability. It supports parallel processing and a host of CRUD features.

    Throw a lot of data into a PL/SQL array structure and it can exhaust all server memory - and degrade server performance to such an extent that a sysadmin will not even be able to correct the problem via the console.

    Unfortunately, due to the term "+table"+, there is this notion that storing data in PL/SQL, instead of SQL tables, is an acceptable approach and do not need any consideration - when that is not the case.

    SQL tables can be static or temporary. A SQL global temporary table (GTT) is like an o-o class definition. You define it once, up front. When a session "touches" this GTT via SQL, that session instantiates its own private copy of that GTT. Which is destroyed when the session terminates.

    A GTT offers far better flexibility and scalability than PL/SQL arrays. Not to say that such arrays are not useful - but they are more often abused than correctly used IMO.

    A SQL table/GTT can also serve as an object container/persistence layer. It can be defined as a table of the abstract parent/base class. And any instantiated child class object can be inserted into it. And the SQL language supports filtering of just specific child class objects, calling their methods (as part of the SQL projection) for example, and so on.

    So if your PL/SQL array is more than a few KB in size (where there can be 100's of client sessions creating such arrays), or when you need to touch that array via the SQL engine using the TABLE() function - a PL/SQL array likely is the wrong choice.
  • 6. Re: ObserverPattern
    926088 Newbie
    Currently Being Moderated
    Billy  Verreynne  wrote:
    A SQL table/GTT can also serve as an object container/persistence layer. It can be defined as a table of the abstract parent/base class. And any instantiated child class object can be inserted into it. And the SQL language supports filtering of just specific child class objects, calling their methods (as part of the SQL projection) for example, and so on.
    That sounds in deed very interesting, thank you!

    Btw. is there a way to ask an instantiated object for it's class? I know about "IS OF" to check if an object is an instance of a class. But having a function that returns the actual class of the object would give me the opportunity to hash this class an use it as index, in addition to the ID of the object in the GTT.

    Edited by: MiBoe on 22.05.2012 00:08
  • 7. Re: ObserverPattern
    926088 Newbie
    Currently Being Moderated
    Well, i have changed the implementation to use a relational table instead of the temporary one.
    The GTT seems no to prohibit the usage of nested tables, which i am using when inserting the object.

    This is what the table def looks like
    create table SessionObjectRegistry(
         oid          number,
         transactid     varchar2(200),
         sessionid     number,
         hash          number,
         object          ObjectClass
    );
    The oid is generated when the object is instantiated, since i found the ref-deref-function not as transparent to use as this
    approach. The transactid is the id of the transaction that uses the object, session id is clear as well. Both ids are used
    to implement different levels of "garbage collection" since i can now control wether i want to "destroy" a single object,
    all objects that have been created during the transaction or all objects that have been created during the session.
    Of cause, i can keep the object's in the table an (re)use them later, if neccessary.

    The hash entry is planned to store the value of the hashed class name of the object (e.g. ora_hash(class_name))

    Once again, thank you :)
  • 8. Re: ObserverPattern
    BillyVerreynne Oracle ACE
    Currently Being Moderated
    MiBoe wrote:

    Btw. is there a way to ask an instantiated object for it's class? I know about "IS OF" to check if an object is an instance of a class. But having a function that returns the actual class of the object would give me the opportunity to hash this class an use it as index, in addition to the ID of the object in the GTT.
    No - have not done a lot of o-o stuff in 11g yet, but cannot recall that it was possible in 10g - and I doubt that it is in 11g either.

    One can implement it using a couple of approaches along the following lines. This one is generic (the class name is not hard coded), but each subclass need to implement a static class method and a normal method to allow one to identify the class and identify an instantiation of that class.

    Not really sure I like it, but it seems to work:
    SQL> --// defining the mother of all classes (ala Delphi style)
    SQL> create or replace type TCustomClass as object(
      2          objectID        integer,
      3
      4          static function GetChildClass return varchar2,
      5          static function Class return varchar2,
      6          member function ClassName return varchar2
      7  ) not final not instantiable;
      8  /
    
    Type created.
    
    SQL> create or replace type body TCustomClass as
      2
      3          --// utility class method - will be called by a subclass to determine
      4          --// its class name
      5          static function GetChildClass return varchar2 is
      6                  callStack       varchar2(32767);
      7                  startPos        integer;
      8                  endPos          integer;
      9          begin
     10                  --// get the current PL/SQL call stack
     11                  callStack := dbms_utility.format_call_stack;
     12
     13                  --// our caller is on the 4th line
     14                  startPos := InStr(callStack,chr(10),1,4)+1;     --// start of 4th line
     15                  EndPos := InStr(callStack,chr(10),1,5);         --// end of 4th line
     16
     17                  --// format of line is "handle line_no object_name"
     18                  callStack := substr(
     19                                  callStack,
     20                                  startPos,
     21                                  endPos-startPos
     22                          );
     23                  --// return 3rd token in line
     24                  return(
     25                          RegExp_SubStr( callStack, '[^ ]+',1,3)
     26                  );
     27          end;
     28
     29          static function Class return varchar2 is
     30          begin
     31                  null;
     32          end;
     33
     34          member function ClassName return varchar2 is
     35          begin
     36                  null;
     37          end;
     38
     39  end;
     40  /
    
    Type body created.
    
    SQL>
    SQL> --// subclass that implements a specific abstract class
    SQL> create or replace type TCustomDevice under TCustomClass(
      2          deviceHandle    integer,
      3          deviceName      varchar2(100),
      4
      5          static function Class return varchar2,
      6          overriding member function ClassName return varchar2
      7  ) not final;
      8  /
    
    Type created.
    
    SQL> create or replace type body TCustomDevice as
      2
      3          static function Class return varchar2 is
      4          begin
      5                  return( TCustomClass.GetChildClass() );
      6          end;
      7
      8          overriding member function ClassName return varchar2 is
      9          begin
     10                  return( TCustomDevice.Class() );
     11          end;
     12
     13  end;
     14  /
    
    Type body created.
    
    SQL>
    SQL> --// realisation of this abstract class
    SQL> create or replace type TFileDevice under TCustomDevice(
      2          fileMode        varchar2(1),
      3          dirObject       varchar2(100),
      4
      5          static function Class return varchar2,
      6          overriding member function ClassName return varchar2
      7  ) not final;
      8  /
    
    Type created.
    
    SQL> create or replace type body TFileDevice as
      2
      3          static function Class return varchar2 is
      4          begin
      5                  return( TCustomClass.GetChildClass() );
      6          end;
      7
      8          overriding member function ClassName return varchar2 is
      9          begin
     10                  return( TFileDevice.Class() );
     11          end;
     12  end;
     13  /
    
    Type body created.
    
    SQL>
    SQL> --// W = DBMS_OUTPUT.put_line()
    SQL> declare
      2          c       TCustomDevice;
      3  begin
      4          W( 'TCustomDevice class='||TCustomDevice.Class );
      5          W( 'TFileDevice class='||TFileDevice.Class );
      6
      7          c := new TCustomDevice(null,null,null);
      8          W( 'I am an object of class '||c.ClassName );
      9
     10          c := new TFileDevice(null,null,null,null,null);
     11          W( 'I am an object of class '||c.ClassName );
     12  end;
     13  /
    TCustomDevice class=BILLY.TCUSTOMDEVICE
    TFileDevice class=BILLY.TFILEDEVICE
    I am an object of class BILLY.TCUSTOMDEVICE
    I am an object of class BILLY.TFILEDEVICE
    
    PL/SQL procedure successfully completed.
    
    SQL>
  • 9. Re: ObserverPattern
    926088 Newbie
    Currently Being Moderated
    Wow, this looks quite powerfull! I didn't know about the call_stack_format utility. Hopefully Oracle never changes the layout
    of the output ;) But up to then this looks like a suitable way. To prevent the creation of new hard dependencies, the
    function themselves should be moved into a package, that can be updated. The base class only calls those package
    methods then.

    I'm going to have a closer look on this.

    Btw. if someone ever needs to check for the dependencies of an object type
    (especially in case of "ORA-02303: cannot drop or replace a type with type or table dependents") this might help:
    BEGIN
        dbms_utility.get_dependency( TYPE   => 'TYPE',
                                     SCHEMA => 'TEST',
                                     NAME   => 'OBSERVERINTERFACE');
    END;
    /
    gives in my case this result:
    -
    DEPENDENCIES ON TEST.OBSERVERINTERFACE
    ------------------------------------------------------------------
    *TYPE TEST.OBSERVERINTERFACE()
    *   TYPE TEST.T_OBSERVERLIST()
    *   TABLE TEST.SESSIONOBJECTREGISTRY()
    *      PACKAGE BODY TEST.PLSQL_OBJECT()
    which lists indeed all the dependencies of an object_type.

    and another place to have a look:
     SELECT * FROM DBA_DEPENDENCIES WHERE NAME='MY_TYPE_NAME'
    Greetings
  • 10. Re: ObserverPattern
    BillyVerreynne Oracle ACE
    Currently Being Moderated
    MiBoe wrote:
    didn't know about the call_stack_format utility. Hopefully Oracle never changes the layout of the output ;)
    Format has not changed in several versions now as far as I recall... But yeah, you do raise a valid concern.

    One can use compiler macros (#define like format as in C/C++) to create conditional compilation. So assuming there is a difference in a future release, one can still ship the same code and use conditional compilation to compile the appropriate code for that Oracle database version.

    So it is not that much different than writing code in C/C++ for different o/s versions. :-)
  • 11. Re: ObserverPattern
    Zlatko Sirotic Explorer
    Currently Being Moderated
    Btw. is there a way to ask an instantiated object for it's class?
    create or replace type PersonClass as object (
      ssn number,
      name varchar2 (30),
      address varchar(100),
      member function className return varchar2
    ) not final
    /

    create or replace type body PersonClass as
      member function className return varchar2 is
        l_any anydata;
      begin
        l_any := anydata.ConvertObject (self);
        return l_any.GetTypeName;
      end;
    end;
    /

    create or replace type StudentClass under PersonClass (
      deptid number,
      major varchar(30)
    ) not final
    /

    set serveroutput on

    declare
      person PersonClass;
      student StudentClass;
    begin
      person := new PersonClass (1243, 'Bob', '121 Front St');
      student := new StudentClass (3456, 'Joe', '34 View', 12, 'HISTORY');
      dbms_output.put_line (person.className);
      dbms_output.put_line (student.className);
      person := student; -- polymorphic assignment
      dbms_output.put_line (person.className);
    end;
    /

    SCHEMA.PERSONCLASS
    SCHEMA.STUDENTCLASS
    SCHEMA.STUDENTCLASS
    Regards
  • 12. Re: ObserverPattern
    BillyVerreynne Oracle ACE
    Currently Being Moderated
    Nice. Never had to use AnyData in any real development, besides the odd testing of an idea/concept - so completely forgot about it.

    One issue though with this approach. As I understand it, AnyDate is not an o-o interface in the "traditional" sense - so using it means constructing a copy of the current object as an AnyData object. Which can be a slow and expensive (memory wise) overhead.
  • 13. Re: ObserverPattern
    Zlatko Sirotic Explorer
    Currently Being Moderated
    Nice.
    Thanks.
    (Very) old thread:
    Finding the type of a sub-type
    :)


    Which can be a slow and expensive (memory wise) overhead.
    I agree with you.



    Maybe SYS_TYPEID is enough:
    create or replace type PersonClass as object (
      ssn number,
      name varchar2 (30),
      address varchar(100),
      member function classId return number
    ) not final
    /

    create or replace type body PersonClass as
      member function classId return number is
        l_type_id number;
      begin
        select to_number (sys_typeid (self)) into l_type_id from dual;
        return l_type_id;
      end;
    end;
    /

    create or replace type StudentClass under PersonClass (
      deptid number,
      major varchar(30)
    ) not final
    /

    set serveroutput on

    declare
      person PersonClass;
      student StudentClass;
    begin
      person := PersonClass (1243, 'Bob', '121 Front St');
      student := StudentClass (3456, 'Joe', '34 View', 12, 'HISTORY');
      dbms_output.put_line (person.classId);
      dbms_output.put_line (student.classId);
      person := student; -- polymorphic assignment
      dbms_output.put_line (person.classId);
    end;
    /
    1
    2
    2
    Regards

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points