13 Replies Latest reply: May 24, 2012 12:30 PM by Zlatko Sirotic RSS

    ObserverPattern

    926088
      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
          Billy~Verreynne
          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
            Billy~Verreynne
            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
              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
                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
                  Billy~Verreynne
                  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
                    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
                      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
                        Billy~Verreynne
                        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
                          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
                            Billy~Verreynne
                            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
                              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
                                Billy~Verreynne
                                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
                                  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