BaseDataObject is a nominal abstract class that is intended to provide a common mechanism for object state-persistence to and from a back-end database, where the object itself is responsible for keeping track of whether it's state needs to be saved. Like the BaseDatabaseConnection nominal abstract class, it is intended to be a building-block for other, more complicated objects, not an instantiable class in it's own right.
In and of itself, it will add the following members to derived objects:
Datasource
(Property):- The BaseDatabaseConnection-derived object that the object will use to save it's state to the database.
Deleted
(Property):- A flag indicating that the object's state-record has been flagged for deletion.
Dirty
(Property):- A flag indicating that the object's state-data has been modified since it's initial creation or retrieval, and needs to be saved.
Id
(Property):- An abstract property that provides the unique identifier in the database of the object's state-data record.
New
(Property):- A flag indicating that the object's state-data has been created outside of the dataase, and needs to be saved.
_Create
(Method):- An abstract method that inserts the object's state-data into the database. Not intended to be called directly (see
Save
, below). _Delete
(Method):- An abstract method that flags the object's existing state-data in the database as deleted or deletable. It may also perform an actual deletion if necessary at an object-by-object level. Not intended to be called directly (see
Save
, below). Fetch
(Static Method):- A method that allows easy retrieval of a single, identified object of the type from the database.
FetchAll
(Static Method):- A method that allows easy retrieval of all objects of the type from the database.
Save
(Method):- Saves the object's state-data according to it's
Dirty
,Deleted
orNew
status _Update
(Method):- An abstract method that updates the object's existing state-data in the database. Not intended to be called directly (see
Save
, above).
Ultimately,
BaseDataObject
functionality is more or less intended to provide CRUD mechanisms on an object-by-object basis, as well as a way of retrieving all available instances.It feels to me like the approach that I'm taking - specifically of providing a number of abstract classes to define common functionality or functional intents, at least, teeters on the edge of acceptability from a Single Responsibility Principle standpoint. "Responsibility" in this context feels a bit nebulous to me. For example, objects derived from BaseDataObject (and other definitions to come) may have a lot of functionality that originates from any number of external definitions, but all of that functionality still feels like it's part of that object's responsibilities: To represent [whatever], and be able to persist state-data for [whatever] makes the "responsibility" of a given object broader, but not... diffuse, maybe? It doesn't feel bad, at least not yet, so I'm going to continue down this path.
class BaseDataObject( object ): """Nominal abstract class, provides baseline functionality and interface requirements for objects whose state data is persisted in a back-end data-store.""" ########################### # Class Attributes # ########################### ########################### # Class Property Getters # ########################### def _GetDatasource( self ): """Gets or sets the BaseDatabaseConnection object that will handle this object's database interaction. Raises ConnectionChangeError if an attempt is made to change an already-set value. Raises TypeError if the set value is not an object implementing BaseDatabaseConnection.""" return self._datasource def _GetDeleted( self ): """Gets or sets the object's "deleted" flag, indicating that the object's state has been or should be flagged as deleted when it is next saved.""" return self._deleted def _GetDirty( self ): """Gets or sets the object's "dirty" flag, indicating that the object's state has been changed since it's initial load.""" return self._dirty def _GetId( self ): """Abstract property - Gets or sets the unique identifier of the record where the object's state is stored. Implementations should not allow the value, once set, to be changed.""" raise NotImplementedError( '%s.Id error: Id has not been implemented as defined by BaseDataObject.' % ( self.__class__.__name__ ) ) def _GetNew( self ): """Gets or sets the object's "new" flag, indicating that the object's state has been created outside the database, and needs to be saved.""" return self._new ########################### # Class Property Setters # ########################### def _SetDatasource( self, value ): if self._datasource != None: raise ConnectionChangeError( '%s.Datasource error: Changes to the Datasource property after it has been set are not allowed.' % ( self.__class__.__name__ ) ) if not isinstance( value, BaseDatabaseConnection ): raise TypeError( '%s.Datasource error: %s is not an object implementing BaseDatabaseConnection' % ( self.__class__.__name__, value ) ) def _SetDeleted( self, value ): if type( value ) != types.BooleanType: raise TypeError( '%s.Dirty error: %s is not a boolean value' % ( self.__class__.__name__, value ) ) self._deleted = value def _SetDirty( self, value ): if type( value ) != types.BooleanType: raise TypeError( '%s.Dirty error: %s is not a boolean value' % ( self.__class__.__name__, value ) ) self._dirty = value def _SetId( self, value ): raise NotImplementedError( '%s.Id error: Id has not been implemented as defined by BaseDataObject.' % ( self.__class__.__name__ ) ) def _SetNew( self, value ): if type( value ) != types.BooleanType: raise TypeError( '%s.New error: %s is not a boolean value' % ( self.__class__.__name__, value ) ) self._new = value ########################### # Class Property Deleters # ########################### def _DelDatasource( self ): raise NotImplementedError( '%s.Datasource error: the Datasource property cannot be deleted.' % ( self.__class__.__name__ ) ) def _DelDeleted( self ): raise NotImplementedError( '%s.Deleted error: the Deleted property cannot be deleted.' % ( self.__class__.__name__ ) ) def _DelDirty( self ): raise NotImplementedError( '%s.Dirty error: the Dirty property cannot be deleted.' % ( self.__class__.__name__ ) ) def __DelId( self ): raise NotImplementedError( '%s.Id error: the Id property cannot be deleted.' % ( self.__class__.__name__ ) ) def __DelNew( self ): raise NotImplementedError( '%s.New error: the New property cannot be deleted.' % ( self.__class__.__name__ ) ) ########################### # Class Properties # ########################### Datasource = property( _GetDatasource, _SetDatasource, _DelDatasource, _GetDatasource.__doc__ ) Deleted = property( _GetDeleted, _SetDeleted, _DelDeleted, _GetDeleted.__doc__ ) Dirty = property( _GetDirty, _SetDirty, _DelDirty, _GetDirty.__doc__ ) Id = property( _GetId, _SetId, _DelId, _GetId.__doc__ ) New = property( _GetNew, _SetNew, _DelNew, _GetNew.__doc__ ) ########################### # Object Constructor # ########################### def __init__( self, datasource=None ): """Object constructor. datasource ... [BaseDatabaseConnection instance, optional, default None] The datasource (a BaseDatabaseConnection instance) that the object will use to save it's state-data. Raises NotImplementedError if an attempt is made to instantiate the class. Raises TypeError if a datasource is supplied but does not implement BaseDatabaseConnection. Raises RuntimeError if any other exception is raised by the creation process. """ if self.__class__ == BaseDataObject: raise NotImplementedError( 'BaseDataObject is nominally an abstract class, and cannot be instantiated' ) self._datasource = None self._deleted = False self._dirty = False self._new = True if datasource != None: try: self._SetDatasource( datasource ) except TypeError, error: raise TypeError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) ) except Exception, error: raise RuntimeError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) ) ########################### # Object Destructor # ########################### def __del__( self ): """Object destructor. Assures that any changes to an object's state-data are saved before the object is destroyed.""" if self._dirty or self._new: self.Save() ########################### # Class Methods # ########################### def _Create( self ): """Abstract method - Inserts a state-record into the database, using whatever mechanism(s) are needed.""" raise NotImplementedError( '%s._Create Error: _Create has not been overridden in from it\'s definition in BaseDataObject' % ( self.__class__.__name__ ) ) def _Delete( self ): """Abstract method - Deletes the object's state-record from the database, using whatever mechanism(s) are needed.""" raise NotImplementedError( '%s._Delete Error: _Delete has not been overridden in from it\'s definition in BaseDataObject' % ( self.__class__.__name__ ) ) def Save( self ): """Saves the object's state to the database.""" if self._deleted: self._Delete() if self._new: self._Create() if self._dirty: self._Update() def _Update( self ): """Abstract method - Updates the object's state-record in the database, using whatever mechanism(s) are needed.""" raise NotImplementedError( '%s._Update Error: _Update has not been overridden in from it\'s definition in BaseDataObject' % ( self.__class__.__name__ ) ) ########################### # Static Class Methods # ########################### @staticmethod def Fetch( self, datasource, id ): """Returns a single instance of the object-type from the database. datasource ... [BaseDatabaseConnection instance] The BaseDatabase- Connection-derived object that the method will use to retrieve the specific instance. id ........... The unique identifier for the instance to retrieve.""" raise NotImplementedError( '%s.Fetch Error: Fetch has not been overridden from it\'s definition in BaseDataObject' % ( self.__class__.__name__ ) ) @staticmethod def FetchAll( self, datasource ): """Returns a list of all available instances of the object-type from the database. datasource ... [BaseDatabaseConnection instance] The BaseDatabase- Connection-derived object that the method will use to retrieve all available objects.""" raise NotImplementedError( '%s.FetchAll Error: FetchAll has not been overridden from it\'s definition in BaseDataObject' % ( self.__class__.__name__ ) ) __all__ += [ 'BaseDataObject' ]
Commentary
The provision of properties by an abstract class feels... risky, in the sense that it would not be difficult for someone (even myself) to, say, define their own
Datasource
property on a derived object that would break the functionality implied or provided. I don't know that I'm completely comfortable with trying to come up with ways to mitigate that risk in the code, however. Most of the approaches that I can think of feel like kludges, at best, and would likely require a lot more maintenance/upkeep as changes are inevitably needed and made.At the same time, setting that concern aside, the approach feels fairly elegant to me - it keeps the implementation of specific interfaces restricted to as few places as I can manage (a good thing), and allows a lot of re-use (potentially - this project is pretty small).
Edited: Made Fetch and FetchAll into static methods - 8/22/11
No comments:
Post a Comment