Perhaps one of the reasons that I've been struggling with picking DBFSpy
back up is that it has a lot of code in it that's only secondarily related to it's
core purpose. That may sound kind of strange on the surface (I know I struggled
with coming to the conclusion, and I'm not completely sure that it's correct,
but for now, that's my story and I'm sticking to it).
Consider, for example, that ultimately, it should be something configurable
(which I have accounted for with my last post), and that it has database-connectivity
functionality. Neither of those is directly related to the FUSE-based-filesystem
aspect of the project, though, to be fair, it couldn't do what it's intended to
do without at least having the database-connection capabilities. That does
not mean that the database-connectivity functionality needs to be a part
of the core DBFSpy
code, though. So, in the interests of both keeping
the code modular, and of simplifying my future efforts on DBFSpy
,
I'm going to break the database-functionality out into it's own module:
DataConnectors
.
Code-share: dl.dropbox.com/u/1917253/site-packages/DataConnectors.py
Let's start with some design first. By my reckoning, it needs to have the ability to connect to, query against, and return results from a back-end database. Since my database-focus (for now) is centered around MySQL, I'll stop when I get to a functional MySQL connection with querying capabilities, though I want to make sure that it's extensible enough to allow use with, say, ODBC data-sources, PostgreSQL, or whatever else is technically possible. Additionally, there needs to be some sort of common mechanism to allow objects to persist their state-data as needed, and to track that their state data has changed or has been created, and thus needs to be updated or created in the database. In my mind, then, the class-structure will look something like the class diagram below.
The Class Diagram
In today's post, I'm only going to cover one of the nominal interfaces of this module. There's a substantial amount of stuff going on even there, and I also wanted to share some of my thoughts (dare I call them insights?) on the creation of explicit interfaces.
In most languages that directly support explicit interfaces, those interface definitions do not allow the specification of anything other than abstract methods. They don't allow properties to be specified, nor do they allow any sort of true functionality to be defined for any methods - everything is an abstract method-stub.
That seems pretty straightforward.
But taking a step back, a question occurs to me. Let me lead into it with some other thoughts/questions, though. First off, what purposes are served by specifying an interface? One of them, certainly, is to provide a contract, right? When a class is defined that implements a particular interface, it's promising that it will provide the functionality specified by that interface. Somewhere along the line, whether as part of a compilation process, or at run-time, a failure of a class to make good on that promise should raise some kind of error (at run-time is, perhaps, less than optimal, but can be worked around with discipline, I believe). A secondary, but related purpose is to provide what I've been calling "type-identity" in my class' doc-strings. If a class implements an interface, and assuming that the contract associated with it is enforced in any fashion, the derivation from that interface allows our code to know that any given instance is of a given type.
My question, then, is "why not allow interfaces to specify properties?" Properties, even in a language like Python, where they have no set type, and can be defined on the fly, could just as easily be part of a contract, couldn't they?
The only arguments I've been able to find thus far to justify properties not being part of interface definitions all seem to be variations on "properties represent specific implementations of data representations, and would break encapsulation." I think I'd argue that, provided that properties didn't specify anything more than that they exist, and maybe what type they are (if applicable), that I'd disagree. But maybe I'm missing something.
The reason for most of the pondering above is that as I was thinking out some
of the details for the interfaces in the DataConnectors
module, it
occurred to me that some of my nominal interfaces could easily specify/require
that specific properties would at least exist for derived classes. It
would even have been possible to force specific types, perhaps, though I suspect
that it would've been more trouble than it would be worth (and may not have been
possible without a lot of nominal interface clutter).
As a case in point, consider the IsDataConnector
nominal interface.
IsDataConnector (Nominal interface):
class IsDataConnector( object ): """Provides interface definition and type-identity for objects that can represent connectors to a back-end data-source (typically a relational database engine).""" ################################## # Class Attributes # ################################## ################################## # Class Property-Getter Methods # ################################## ################################## # Class Property-Setter Methods # ################################## ################################## # Class Property-Deleter Methods # ################################## ################################## # Class Properties # ################################## Connection = property( None, None, None, None ) Database = property( None, None, None, None ) Host = property( None, None, None, None ) Password = property( None, None, None, None ) User = property( None, None, None, None ) ################################## # Object Constructor # ################################## @DocumentArgument( 'argument', 'self', None, SelfDocumentationString ) def __init__( self ): """Object constructor.""" # Nominally abstract: Don't allow instantiation of the class if self.__class__ == IsDataConnector: raise NotImplementedError( 'IsDataConnector is (nominally) an interface, and is not intended to be instantiated.' ) ################################## # Object Destructor # ################################## ################################## # Class Methods # ################################## @DocumentArgument( 'argument', 'self', None, SelfDocumentationString ) def Connect( self ): """Connects to the database specified by Database, residing on Host, using User and Password to connect.""" raise NotImplementedError( '%s.Connect is not implemented as required by IsDataConnector.' % ( self.__class__.__name__ ) ) @DocumentArgument( 'argument', 'self', None, SelfDocumentationString ) @DocumentArgument( 'argument', 'query', None, 'The IsQuery object that will be executed against the object\'s Connection.' ) def Query( self, query ): """Executes the provided IsQuery object's query against the object's connection, returning one or more results.""" raise NotImplementedError( '%s.Query is not implemented as required by IsDataConnector.' % ( self.__class__.__name__ ) ) __all__ += [ 'IsDataConnector' ]
Commentary
- Line(s)
- 24-28
- Here is where I've defined the "required" properties for classes implementing the interface.
- The practical upshot of these is that each property has been defined, but
has no implementation associated with them (they have no getter, setter
or deleter methods). The more I think about this approach, the more I
like it. Consider:
- Each property is now defined here, but has no implementation, and will raise errors when tested. In combination with the normal unit-testing processes (from previous posts) that requires a test-method for each property, this feels pretty safe.
- That effectively requires (provided that discipline is maintained at the unit-testing level) that all derived classes will have to implement the properties defined here.
property()
calls. This also feels like another justification for explicitly defining properties using theproperty()
function, rather than using the decorator-structure. - 24
- The
Connection
property will contain the actual database-connector object in a derived instance. - 25
Database
will contain the name of the database being connected to.- 26
Host
will contain the name or IP-address of the host machine that theDatabase
-database resides on.- 27
Password
will contain the password used to connect to the database.- 28
User
will contain the user-name used to connect to the database.- 49-52
- Requires a common method/mechanism to connect to the database that the derived object represents a connection to.
- 55-60
- Requires a common method/mechanism to execute a query against the database.
Looking at this definition, then, and applying it to BaseDataConnector
and MySQLConnector
(and any other connectors that may be derived from
BaseDataConector
), it should hopefully be obvious that they will have:
- A
Connection
property; - A
Database
property; - A
Host
property; - A
Password
property; - A
User
property; - A
Connect
method; - A
Query
method; and
AppendToQueue
method - will be implemented in
BaseDataConenctor
. That's the lowest common denominator for actual
implementations of various database-connector objects, at least, so that would
make the most sense, but we'll have to see how that works out later.
I'm going to stop here for today, while this post sinks in, but before I do, here are the unit-tests for the interface.
class IsDataConnectorDerived( IsDataConnector ): def __init__( self ): pass class testIsDataConnector( unittest.TestCase ): """Unit-tests the IsDataConnector class.""" def setUp( self ): pass def tearDown( self ): pass def testAbstract( self ): """Testing abstract nature of the IsDataConnector class.""" try: testObject = IsDataConnector() self.fail( 'IsDataConnector is a nominal interface, and should not be instantiable.' ) except NotImplementedError: pass except Exception, error: self.fail( 'Attempting to instantiate an IsDataConnector should raise a NotImplementedError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) testObject = IsDataConnectorDerived() self.assertTrue( isinstance( testObject, IsDataConnector ), 'Objects derived from IsDataConnector should be instances of it.' ) def testPropertyCountAndTests( self ): """Testing the properties of the IsDataConnector class.""" items = getMemberNames( IsDataConnector )[0] actual = len( items ) expected = 5 self.assertEquals( expected, actual, 'IsDataConnector is expected to have %d properties to test, but %d were dicovered by inspection.' % ( expected, actual ) ) for item in items: self.assertTrue( HasTestFor( self, item ), 'There should be a test for the %s property (test%s), but none was identifiable.' % ( item, item ) ) def testMethodCountAndTests( self ): """Testing the methods of the IsDataConnector class.""" items = getMemberNames( IsDataConnector )[1] actual = len( items ) expected = 2 self.assertEquals( expected, actual, 'IsDataConnector is expected to have %d methods to test, but %d were dicovered by inspection.' % ( expected, actual ) ) for item in items: self.assertTrue( HasTestFor( self, item ), 'There should be a test for the %s method (test%s), but none was identifiable.' % ( item, item ) ) # Unit-test properties def testConnection( self ): """Unit-tests the Connection property of the IsDataConnector interface.""" testObject = IsDataConnectorDerived() try: testObject.Connection = None self.fail( 'The Connection property of IsDataConnector is nominally abstract, and should not be settable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to set the Connection property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) try: testValue = testObject.Connection self.fail( 'The Connection property of IsDataConnector is nominally abstract, and should not be gettable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to get the Connection property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) try: del testObject.Connection self.fail( 'The Connection property of IsDataConnector is nominally abstract, and should not be deletable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to delete the Connection property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) def testDatabase( self ): """Unit-tests the Database property of the IsDataConnector interface.""" testObject = IsDataConnectorDerived() try: testObject.Database = None self.fail( 'The Database property of IsDataConnector is nominally abstract, and should not be settable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to set the Database property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) try: testValue = testObject.Database self.fail( 'The Database property of IsDataConnector is nominally abstract, and should not be gettable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to get the Database property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) try: del testObject.Database self.fail( 'The Database property of IsDataConnector is nominally abstract, and should not be deletable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to delete the Database property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) def testHost( self ): """Unit-tests the Host property of the IsDataConnector interface.""" testObject = IsDataConnectorDerived() try: testObject.Host = None self.fail( 'The Host property of IsDataConnector is nominally abstract, and should not be settable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to set the Host property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) try: testValue = testObject.Host self.fail( 'The Host property of IsDataConnector is nominally abstract, and should not be gettable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to get the Host property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) try: del testObject.Host self.fail( 'The Host property of IsDataConnector is nominally abstract, and should not be deletable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to delete the Host property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) def testPassword( self ): """Unit-tests the Password property of the IsDataConnector interface.""" testObject = IsDataConnectorDerived() try: testObject.Password = None self.fail( 'The Password property of IsDataConnector is nominally abstract, and should not be settable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to set the Password property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) try: testValue = testObject.Password self.fail( 'The Password property of IsDataConnector is nominally abstract, and should not be gettable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to get the Password property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) try: del testObject.Password self.fail( 'The Password property of IsDataConnector is nominally abstract, and should not be deletable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to delete the Password property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) def testUser( self ): """Unit-tests the User property of the IsDataConnector interface.""" testObject = IsDataConnectorDerived() try: testObject.User = None self.fail( 'The User property of IsDataConnector is nominally abstract, and should not be settable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to set the User property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) try: testValue = testObject.User self.fail( 'The User property of IsDataConnector is nominally abstract, and should not be gettable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to get the User property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) try: del testObject.User self.fail( 'The User property of IsDataConnector is nominally abstract, and should not be deletable' ) except AttributeError: pass except Exception, error: self.fail( 'Trying to delete the User property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) ) # Unit-test methods def testConnect( self ): """Unit-tests the Connect method of the IsDataConnector interface.""" testObject = IsDataConnectorDerived() try: testObject.Connect() self.fail( 'Connect should raise a NotImplementedError.' ) except NotImplementedError: pass except Exception, error: self.fail( 'Connect should raise a NotImplementedError, but %s was raised instead:\n %s' % ( error.__class__.__mame__, error ) ) def testQuery( self ): """Unit-tests the Query method of the IsDataConnector interface.""" testObject = IsDataConnectorDerived() try: testObject.Query( None ) self.fail( 'Query should raise a NotImplementedError.' ) except NotImplementedError: pass except Exception, error: self.fail( 'Query should raise a NotImplementedError, but %s was raised instead:\n %s' % ( error.__class__.__mame__, error ) ) testSuite.addTests( unittest.TestLoader().loadTestsFromTestCase( testIsDataConnector ) )
No comments:
Post a Comment