Monday, November 28, 2011

The DataConnectors module (part 1), and some ruminations on interfaces

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.
I also didn't specify a documentation-string for them, though I'm toying with the idea of putting the documentation string in at this level in the object/interface tree, which is why they aren't just property() calls. This also feels like another justification for explicitly defining properties using the property() 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 the Database-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
There are no implementation details, and it's not possible to determine (yet) where these properties and methods will actually be realized. I expect that the items that aren't specific to a given database-engine - most of the properties, possibly all of them, and the 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