views:

44

answers:

1

Suppose you had code like this:

_READERS = None
_WRITERS = None

def Init(num_readers, reader_params, num_writers, writer_params, ...args...):
  ...logic...
  _READERS = new ReaderPool(num_readers, reader_params)
  _WRITERS = new WriterPool(num_writers, writer_params)
  ...more logic...

class Doer:
  def __init__(...args...):
    ...
  def Read(self, ...args...):
    c = _READERS.get()
    try:
      ...work with conn
    finally:
      _READERS.put(c)
  def Writer(...):
    ...similar to Read()...

To me, this is a bad pattern to follow, some cons:

  1. Doers can be created without its preconditions being satisfied
  2. The code isn't easily testable because ConnPool can't be directly mocked out.
  3. Init has to be called right the first time. If its changed so it can be called multiple times, extra logic has to be added to check if variables are already defined, and lots of NULL values have to be passed around to skip re-initializing.
  4. In the event of threads, the above becomes more complicated by adding locking
  5. Globals aren't being used to communicate state (which isn't strictly bad, but a code smell)

On the other hand, some pros:

  1. its very convenient to call Init(5, "user/pass", 2, "user/pass")
  2. It simple and "clean"

Personally, I think the cons outweigh the pros, that is, testability and assured preconditions outweigh simplicity and convenience.

+2  A: 

It seems to me that the single problem with this example is the use of global state. So don't do that.

Seriously- the concerns you're worried about are solved by passing the appropriate context to the Doer when it comes up. In some cases, "appropriate context" may be a couple of plain arguments (e.g. a list of readers and a list of writers) or it might be a more complicated aggregate object (the "connection manager" which may have connections added and removed external to anyone who's referencing it).

To explicitly address your cons:

  1. if Doer has preconditions, validate them. If they aren't met, raise an exception.
  2. (Solved if ConnPool is passed as a parameter to ctor or worker function.)
  3. Make Init create the thing you'll pass to Doer, rather than creating global data. For mockery, pass the class to be built as well? Basically, use a factory of some sort.
  4. You only need to worry about thread safety in your state if it's shared. If each thread has its own connection manager (for instance), then there's nothing to lock at this level.
  5. (Solved if you don't use globals, natch.)

So- it's not particularly less convenient to do:

class ConnPool:
   def __init__(self, numReaders, readerParams, numWriters, writerParams):
      (your InitFunction with a bunch of self. prepending)

class Doer:
   def __init__(self, connPool, ...):
      if not preconditions:
         raise DoerPreconditionsNotMetError()
      self.connections = connPool

   def Read(self):
      readers, writers = self.connections._READERS, self.connections._WRITERS
      ...

So, I don't know. I don't see that as particularly less concise or less readable than your example. (Alternatively, you could pass the connection manager to the Read function, that'll be up to your requirements obviously.)

dash-tom-bang
Thanks! This validates my own thoughts about it.
Richard Levasseur