Dispatch table of methods with various return value types

Loris Bennett loris.bennett at fu-berlin.de
Wed Nov 18 08:13:01 EST 2020


dn <PythonList at DancesWithMice.info> writes:

Firsty, thanks for taking the time to write such a detailed reply.

> On 17/11/2020 23:35, Loris Bennett wrote:
>> dn <PythonList at DancesWithMice.info> writes:
>>
>>> On 17/11/2020 22:01, Loris Bennett wrote:
>>>> Hi,
>>>>
>>>> I have a method for manipulating the membership of groups such as:
>>>>
>>>>       def execute(self, operation, users, group):
>>>>           """
>>>>           Perform the given operation on the users with respect to the
>>>>           group
>>>>           """
>>>>
>>>>           action = {
>>>>               'get': self.get,
>>>>               'add': self.add,
>>>>               'delete': self.delete,
>>>>           }
>>>>
>>>>           return action.get(operation)(users, group)
>>>>
>>>> The 'get' action would return, say, a dict of users attribute, whereas
>>>> the 'add/delete' actions would return, say, nothing, and all actions
>>>> could raise an exception if something goes wrong.
>>>>
>>>> The method which calls 'execute' has to print something to the terminal,
>>>> such as the attributes in the case of 'get' and 'OK' in the cases of
>>>> 'add/delete' (assuming no exception occurred).
>>>>
>>>> Is there a canonical way of dealing with a method which returns different
>>>> types of data, or should I just make all actions return the same data
>>>> structure so that I can generate a generic response?
>>>
>>>
>>> Is the problem caused by coding the first step before thinking of the overall
>>> task? Try diagramming or pseudo-coding the complete solution (with multiple
>>> approaches), ie the operations AND the printing and exception-handling.
>>
>> You could have a point, although I do have a reasonable idea of what the
>> task is and coming from a Perl background, Python always feels a bit
>> like pseudocode anyway (which is one of the things I like about Python).
>
> +1 the ease of Python, but can this be seductive?
>
> Per the comment about Perl/Python experience, the operative part is the
> "thinking", not the tool - as revealed in responses below...
>
> Sometimes we design one 'solution' to a problem, and forget (or 'brainwash'
> ourselves into thinking) that there might be 'another way'.
>
> It may/not apply in this case, but adjusting from a diagram-first methodology,
> to the habit of 'jumping straight into code' exhibited by many colleagues,
> before readjusting back to (hopefully) a better balance; I felt that
> coding-first often caused me to 'paint myself into a corner' with some
> 'solutions, by being too-close to the code and not 'stepping back' to take a
> wider view of the design - but enough about me...
>
>
>>> Might it be more appropriate to complete not only the get but also its
>>> reporting, as a unit. Similarly the add and whatever happens after that; and the
>>> delete, likewise.
>>
>> Currently I am already obtaining the result and doing the reporting in
>> one method, but that makes it difficult to write tests, since it
>> violates the idea that one method should, in general, just do one thing.
>> That separation would seem appropriate here, since testing whether a
>> data set is correctly retrieved from a database seems to be
>> significantly different to  testing whether the
>> reporting of an action is correctly laid out and free of typos.
>
> SRP = design thinking! +1

I knew the idea, but I didn't now the TLA for it ;-)

> TDD = early testing! +1
>
> Agreed: The tasks are definitely separate. The first is data-related. The second
> is about presentation.
>
> In keeping with the SRP philosophy, keep the split of execution-flow into the
> three (or more) functional-tasks by data-process, but turn each of those tasks
> into two steps/routines. (once the reporting routine following "add" has been
> coded, and it comes time to implement "delete", it may become possible to repeat
> the pattern, and thus 're-use' the second-half...)
>
> Putting it more formally: as the second-half is effectively 'chosen' at the same
> time as the first, is the reporting-routine "dependent" upon the data-processor?
>
> 	function get( self, ... )
> 		self.get_data()
> 		self.present_data()
>
> 	function add( self, ... )
> 		self.add_data()
> 		self.report_success_fail()
>
> 	...
>
> Thus, the functional task can be tested independently of any reporting follow-up
> (for example in "get"); whilst maintaining/multiplying SRP...

The above approach appeals to me a lot.  Slight downsides are that
such 'metafunctions' by necessity non-SRP functions and that, as there
would be no point writing tests for such functions, some tools which try
to determine test coverage might moan.

>>> Otherwise the code must first decide which action-handler, and later,
>>> which result-handler - but aren't they effectively the same decision?
>>> Thus, is the reporting integral to the get (even if they are in
>>> separate routines)?
>>
>> I think you are right here.  Perhaps I should just ditch the dispatch
>> table.  Maybe that only really makes sense if the methods being
>> dispatched are indeed more similar.  Since I don't anticipate having
>> more than half a dozen actions, if that, so an if-elif-else chain
>> wouldn't be too clunky.
>
> An if...elif...else 'ladder' is logically-easy to read, but with many choices it
> may become logistically-complicated - even too long to display at-once on a
> single screen.
>
> Whereas, the table is a more complex solution (see 'Zen of Python') that only
> becomes 'simple' with practice.
>
> So, now we must balance the 'level(s)' of the team likely to maintain the
> program(me) against the evaluation of simple~complex. Someone with a ComSc
> background will have no trouble coping with the table - and once Python's
> concepts of dictionaries and functions as 'first-class objects' are understood,
> will take to it like the proverbial "duck to water". Whereas, someone else may
> end-up scratching his/her head trying to cope with 'all the above'.

The team?  L'équipe, c'est moi :-) Having said that I do try to program
not only with my fictitious replacement in mind, should I be hit by the
proverbial bus, but also my future self, and so tend to err on the side
of 'simple'.

> Given that Python does not (yet) have a switch/case construct, does the table
> idea assume a greater importance? Could it be (reasonably) expected that
> pythonista will understand such more readily?
>
>
> IMHO the table is easier to maintain - particularly 'six months later', but
> likely 'appears' as a 'natural effect' of re-factoring*, once I've implemented
> the beginnings of an if-ladder and 'found' some of those common follow-up
> functions.
> * although, like you, I may well 'see' it at the design-stage, particularly if
> there are a number (more) cases to implement!
>
> Is functional "similar"[ity] (as above) the most-appropriate metric? What about
> the number of decision-points in the code? (ie please re-consider "effectively
> the same decision")
>
> 	# which data-function to execute?
> 	if action==get
> 		do get_data
> 	elif action == add
> 		do add_data
> 	elif ...
>
> 	...
>
> 	# now 'the work' has been done, what is the follow-through?
> 	if action=get
> 		do present_data
> 	elif action == add
> 		report success/fail
> 	...

In my current case this is there is a one-to-one relationship between
the 'work' and the 'follow-through', so this approach doesn't seem that
appealing to me.  However I have other cases in which the data to be
displayed comes from multiple sources where the structure above might
be a good fit.

Having said that, I do prefer the idea of having a single jumping off
point, be it a dispatch table or a single if-then-else ladder, which
reflects the actions which the user can take and where the unpleasant
details of, say, how the data are gathered are deferred to a lower level
of the code.

> Back to the comment about maintainability - is there a risk that an extension
> requested in six months' time will tempt the coding of a new "do" function AND
> induce failure to notice that there must be a corresponding additional function
> in the second 'ladder'?
>
> This becomes worse if we re-factor to re-use/share some of the follow-throughs,
> eg
>
> 	...
> 	elif action in [ add, delete, update]
> 		report success/fail
> 	...
>
> because, at first glance, the second 'ladder' appears to be quite dissimilar -
> is a different length, doesn't have the condition-clause symmetry of the first,
> etc! So, our fictional maintainer can ignore the second, correct???
>
> Consider SRP again, and add DRY: should the "despatch" decision be made once, or
> twice, or... ?

With my non-fictional-maintainer-cum-six-month-older-self hat on I think
you have made a good case for the dispatch table, which is my latent
preference anyway, especially in connection with the 'do/display'
metafunctions and the fact that in my current case DRY implies that the
dispatch decision should only be made once.

Thanks again for the input!

Cheers,

Loris

-- 
This signature is currently under construction.


More information about the Python-list mailing list