360 likes | 499 Vues
SYNERGY 2007. Design Patterns and the Object Factory. Peter Bragg, Care Data Systems. Miami l Thursday, May 17th. The Company (Background). Care Data Systems Limited Based in Birmingham, UK Dataflex users since 1985
E N D
SYNERGY 2007 Design Patterns and the Object Factory Peter Bragg, Care Data Systems Miami l Thursday, May 17th
The Company (Background) • Care Data Systems Limited • Based in Birmingham, UK • Dataflex users since 1985 • Employed that handsome, young fellow Peter Bragg on September 18th 2001 • Senior Product Developer
The Product (Background) • Donorflex • Comprehensive system designed to support the ‘Not for Profit’ sector • donations management (financial control, gift aid, gifts in kind, merchandise sales) • fundraising and strategic development (campaign management & control) • relationship management (profiling, segmentation, targeting) • operational support (acknowledgements, communications, action plans) • events management (charity events and third party fundraising and sponsorship)
Typical Clients (Background) • Healthcare • Education • Arts • Community Social Services • Overseas Aid • Disability Services • Political Reform • Religious Organisations • Service Veterans • Sport
The Story Begins… • January 17th 2007
Design Patterns & the Object Factory • I answered the question so precisely that no further comments were required! (Erm…) • No one is interested in Factories • No one is using Factories • Might make an interesting topic?
Design Patterns & the Object Factory • I am not a Design Pattern Expert • ..at all! • Knowledge limited to ‘what I do’
Design Patterns & the Object Factory Creational Design Patterns and the Object Factory …in donorflex
Design Patterns & the Object Factory • Our Design Need: • One Application (donorflex) • Different backends (Dataflex, MSSQL, ?) • Intelligent code optimisation/execution
Design Patterns & the Object Factory • The need: • Call a process (e.g. a ‘search’) and execute code appropriate for the backend. • Possible design solutions: • If, Else or Case Statements • Deferred creation of ‘Process’ Objects
Design Patterns & the Object Factory View Object (i.e. Enquiry) Process Object (i.e. Enquiry Engine) Procedure OnProcess If (Backend=Dataflex) … end else if (Backend=MSSQL)… end etc. End_Procedure
Design Patterns & the Object Factory View Object (i.e. Enquiry) Procedure DoRunEnquiry Handle hoEnquiryEngine If (Backend=Dataflex) Get Create U_ … to hoEnquiryEngine else if (Backend=MSSQL) Get Create U_ ……. Send DoProcess of hoEnquiryEngine send Destroy of hoEnquiryEngine End_Procedure
Design Patterns & the Object Factory View Object (i.e. Enquiry) Procedure DoRunEnquiry Send DoProcess of (Instance(oFactory(self))) End_Procedure Factory Object (oFactory) Responsible for creating appropriate Process Object
So how does it all hang together? • Write different versions of a (business) process as classes: • cAbstractPowerSearch (defines interface) • cPowerSearch_df is a cAbstractPowerSearch • cPowerSearch_mssql is a cAbstractPowerSearch • Classes are then registered with a “Factory”:
Design Patterns & the Object Factory Class cPowerSearchEnq_Factory is a cFactory Procedure Construct_Object Forward Send Construct_Object Send RegisterType Factory_Type_Dataflex U_cPowerSearch_DF Send RegisterType Factory_Type_MSSQL U_cPowerSearch_MSSQL End_Procedure End_Class
So how does it all hang together? • An instance of the factory is then added to a view Object oPowerSearchEnq Is A cPowerSearchEnq_Factory End_Object • A call is then made to an ‘Instance’ of this Factory So instead of: Send DoProcess of oPowerSearchEnq We would code: Send DoProcess of (Instance(oPowerSearchEnq(self))) • A ‘singleton’ Factory Controller instructs the Factory which version of the process to create
Factory Controller Class Enum_List Define Factory_Type_Dataflex Define Factory_Type_MSSQL Define Factory_Type_Pervasive Define Factory_Type_MySQL Define All_Factory_Types //need to be last – gives number of defined types End_Enum_List Class cFactoryController is a cObject Procedure Construct_Object String sEngine Forward Send Construct_Object Property Integer piCurrentType Factory_Type_Dataflex // Default type. // Check the application object: Get psFactoryEngine of ghoApplication to sEngine //set during OnCreate of Application object Case Begin Case (sEngine = "mssql") Set piCurrentType to Factory_Type_MSSQL Case Break Case End End_Procedure// Construct_Object Function CurrentType Returns Integer Function_Return (piCurrentType(Self)) End_Function End_Class
Factory Controller Object Global_Variable Handle ghFactoryController Get Create of Desktop U_cFactoryController to ghFactoryController
The Factory Class Class cFactory is a cObject Procedure Construct_Object integer[] iTypeMappings Forward Send Construct_Object // Holds the type of the current factory object: Property Integer piInstanceType (CurrentType(ghFactoryController)) // Holds the object instance that this factory object represents: Property Handle phInstance 0 // Initially zero. // Array that holds class identifiers to be constructed dependant on the required type: Property Integer[] piTypeMappings //initialise piTypeMappings Array to avoid index out of range errors move 0 to iTypeMappings[All_Factory_Types] Set piTypeMappings to iTypeMappings End_Procedure // Construct_Object // Provides a means by which to 'register' what class should be used against a particular type of connection: Procedure RegisterType Integer iType Integer iClass integer[] iTypeMappings Get piTypeMappings to iTypeMappings move iClass to iTypeMappings[iType] Set piTypeMappings to iTypeMappings End_Procedure
The Factory Class… // Returns the class associated by a previous call to RegisterType: Function TypeClass Integer iType ReturnsInteger Integer iClass integer[] iTypeMappings Get piTypeMappings to iTypeMappings move iTypeMappings[iType] to iClass // if we don't find a registered class of the type we are looking for, look for and return the native dataflex // factory type.. if (iClass = 0) move iTypeMappings[Factory_Type_Dataflex] to iClass // Support drop through the default // type. // if we still have no registered class, we must give an error if (iClass = 0) Error 4113 ("Programmatic Error - No registered type for factory "+name(self)) Function_Return iClass End_Function // Returns the current 'type' associated with this factory: Function InstanceType Returns Integer Function_Return (piInstanceType(Self)) End_Function // Allows a new type to be set against this object: Procedure Set InstanceType Integer iType Set piInstanceType to iType End_Procedure
The Factory Class… // Returns or creates the correct instance of a type for this factory: Function Instance ReturnsHandle Handle hObj Integer iType iNewType Get phInstance to hObj Get InstanceType to iType Get CurrentType of ghFactoryController to iNewType // If the type doesn't match what we are currently using then destroy the object and create a new one: If ((iType <> iNewType) And (hObj <> 0)) Begin Send Destroy of hObj Move 0 to hObj End If (Not(hObj)) Begin Get Create (TypeClass(Self, iNewType)) to hObj Set phInstance to hObj Set InstanceType to iNewType End Function_Return hObj End_Function // Boolean function that tells the caller if the factory instance has been created yet: Function IsCreated ReturnsBoolean Function_Return (phInstance(self) <> 0) End_Function End_Class
A Simple Example • User logs in and we look to see if they have any communications to make. • Code we execute will depend on the backend • So we will use a factory for this…
A Simple Example Use cFactory.pkg Define the interface: class cAbstractLoginPrompts is a cObject Function DoFindLoginPrompts string sUser date dDate integer iDays returns integer[] end_function end_class Or sometimes.. class cAbstractLoginPrompts is a cObject Function DoFindLoginPrompts string sUser date dDate integer iDays returns integer[] Error DFErr_Program “Method not defined for concrete class” end_function end_class
A Simple Example Write the classes: class cLoginPrompts_df is a cAbstractLoginPrompts Function DoFindLoginPrompts string sUser date dDate integer iDays returns integer[] integer[3] iRetVal //outstanding/today/future integer iOverdue iToday iFuture open doncomms For_All DONCOMMS BY Index.3 Constrain Doncomms.On_behalf_of EQ sUser Constrain Doncomms.Login_prompt EQ "Y" DO if (doncomms.date < dDate) increment iOverdue else if (doncomms.date = dDate) increment iToday else if (doncomms.date <= (dDate+iDays-1)) increment iFuture End_For_All move iOverdue to iRetVal[0] move iToday to iRetVal[1] move iFuture to iRetVal[2] function_return iRetVal end_function end_class
A Simple Example class cLoginPrompts_sql is a cAbstractLoginPrompts Function DoFindLoginPrompts string sUser date dDate integer iDays returns integer[] integer[3] iRetVal //outstanding/today/future integer iFetchResult iCount iData handle hSQLInstance hConn hSQL string sSQL sDate sDateFuture sBase boolean bNoMoreResultSets move (Instance(ghSQLConnectionFactory)) to hSQLInstance move ("Select count(*) from doncomms where doncomms.On_behalf_of = '"+sUser+"' and doncomms.Entity = 'R' and doncomms.Login_prompt = 'Y' and ") to sBase Get DateToSQL of hSQLInstance dDate to sDate move (sSQL+sBase+"doncomms.Date < '"+sDate+"';") to sSQL move (sSQL+sBase+"doncomms.Date = '"+sDate+"';") to sSQL Get DateToSQL of hSQLInstance (dDate+1) to sDate Get DateToSQL of hSQLInstance (dDate+iDays-1) to sDateFuture move (sSQL+sBase+"doncomms.date between '"+sDate+"' and '"+sDateFuture+"';") to sSQL // Open Connection Get Connection of hSQLInstance to hConn Get SQLOpen of hConn to hSQL // Send the Statement .. Send SQLExecDirect of hSQL sSQL move False to bNoMoreResultSets
A Simple Example Repeat Get SQLFetch Of hSQL To iFetchResult If (iFetchResult <> 0) begin move (trim(SQLColumnValue(hSQL,1))) to iData increment iCount move iData to iRetVal[iCount-1] end Get SQLNextResultSet of hSQL to iFetchResult if (not(iFetchResult)) move True to bNoMoreResultSets Until (bNoMoreResultSets) // Release the Statement: Send SQLClose of hSQL function_return iRetVal end_function end_class
A Simple Example Write a factory class and register both classes: class cLoginPrompts_Factory is a cFactory Procedure Construct_Object Forward Send Construct_Object // Register the default class that will be used as a drop through if other types aren't // registered: Send RegisterType Factory_Type_Dataflex U_cLoginPrompts_df Send RegisterType Factory_Type_MSSQL U_cLoginPrompts_sql End_Procedure end_Class Create an Object instance of the Factory Object oLoginPromptsFactory is a cLoginPrompts_Factory End_object
A Simple Example Then call the function and let the factory do the rest Integer[] iRetVal Get DoFindLoginPrompts of (instance(oLoginPromptsFactory(Self))) sUser dDate iDays to iRetVal • Under the hood: • The function “Instance” asks “have I already created an object for you?” • If so, it simply returns a handle to the object it created earlier • If not: • It looks to see what the “Factory Controller” says it should be creating • It creates an object of the required class • It sets a property with the handle of the object • It returns a handle to the object • “DoFindLoginPrompts” is then executed
How it works… Use dnTreeView.pkg Use cProfileFactory.pkg class cProfileTree is a dnTreeView Import_Class_Protocol Server_Mixin Procedure Construct_Object forward send Construct_Object Property Integer piPrivate.CurrentProfile 0 Property Integer piPrivate.HelpArray_ID 0 Property Handle phProfileTreeFactory (Create(self,U_cProfileTree_Factory)) Send Define_Server // Set tree properties: Set TreeLinesState to False Set TreeRootLinesState to False Object PopupMenu is a cProfilePopUpMenu DelegateSet PopupMenu_ID toSelf End_Object end_procedure
Design Patterns & the Object Factory • Using this ‘Factory’ approach as a model: • “The need to know” - objects only get involved with what they need to. Views simply make a call to a common interface and don’t care about what lies behind this. • Scalability – new classes can be written and registered with a Factory very easily. • Easy to code and implement new factory instances.
SYNERGY 2007 Thank You Miami l Thursday, May 17th