While playing around with some ideas for a fairly complex class-library for a project at work, I hit on an idea that I wanted to explore and share. The back-story is that the class-library contains a number of abstract base classes that define interface contracts, and most of those ABCs get used in defining concrete classes that are focused on integration with one of several third-party system APIs. Right now, there's four or possibly five such third-party APIs, and between them there are at least two different underlying connection- and usage-styles — implementing the actions through a local Python library for one, and messages to and from a REST/JSON web-service in another. I fully expect that the remaining 2-3 APIs that I'll eventually have to integrate my code with will surface another Python library or a REST/JSON process that has different data-structures I'll have to contend with.
My task, with respect to all that, is to write our own API, one that can act as an adapter or wrapper around those other APIs, so that our code doesn't have to speak all those APIs' languages natively, as it were. There are (so far) 11 concrete classes that exist in parallel across those 4-5 APIs that I'll be contending with, and at a minimum, they have several common properties and methods, even if the implementations of those vary wildly from one API's object-instance to another's.
That's exactly the sort of thing that abstraction, whether in the form of an ABC or a nominal interface, is intended to help manage.
Where it started feeling sketchy was when I started thinking about how to
implement the common methods. In many cases, they required arguments that were
instances of subclasses of one of the ABCs. In others, arguments were expected
to be the same type or set of types across all the implementations. An instance's
name
, for example, is going to be a str
or
unicode
type pretty much everywhere, with the same kinds of
constraints (no line-breaks or tabs, perhaps, for example).
That started me thinking: I didn't want to duplicate the type- and value-checking code for all those arguments across all of those classes — a few methods I've surfaced so far have as many as 9-10 args in their signatures. So how could I keep those in one place in order to minimize maintenance efforts in the future, while still having them accessible across all the concrete classes?
Where I eventually landed was putting concrete
code into the abstract
methods of the ABCs, then calling those original
abstract methods from
their concrete-class implementations.
By way of example, consider this simple abstract class:
class MyAbstractClass(object):
__metaclass__ = abc.ABCMeta
def __init__(self):
pass
@abc.abstractmethod
def do_something(self, arg):
"""
Does something with arg"""
if type(arg) not in (str, unicode):
raise TypeError(
'%s.do_something expects a str or '
'unicode value for its arg, but '
'was passed "%s" (%s)' %
(self.__class__.__name__, arg,
type(arg).__name__)
)
The do_something
method is still abstract, in that you cannot
create an instance of a derived class without that class defining its own
do_something
method. Doing so:
class MyClass2(MyAbstractClass, object):
pass
print 'MyAbstractClass is still abstract:'
try:
my_object = MyClass2()
except Exception as error:
print '%s: %s' % (error.__class__.__name__, error)
yields an error when executed:
MyAbstractClass is still abstract: TypeError: Can't instantiate abstract class MyClass2 with abstract methods do_something
If, on the other hand, an implementation in a derived class calls
the original do_something
method from MyAbstractClass
,
it executes:
class MyClass(MyAbstractClass, object):
def do_something(self, arg):
MyAbstractClass.do_something(self, arg)
print(
'The argument "%s" is a %s of length %d' %
(arg, type(arg).__name__, len(arg))
)
my_object = MyClass()
print 'Valid call:'
my_object.do_something('me, myself and eye')
print
print 'Raises error:'
try:
my_object.do_something(2)
except Exception as error:
print '%s: %s' % (error.__class__.__name__, error)
That code yields:
Valid call: The argument "me, myself and eye" is a str of length 18 Raises error: TypeError: MyClass.do_something expects a str or unicode value for its arg, but was passed "2" (int)
Problem solved, it feels like... Though it raises the question of how to keep the documentation-decoration of the original abstract method associated with the implemented concrete methods. That's something I'll have to consider.
No comments:
Post a Comment