[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Dispatch table of methods with various return value types

On 17/11/2020 23:35, Loris Bennett wrote:
> dn <PythonList at> 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
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 

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, ... )

	function add( self, ... )


Thus, the functional task can be tested independently of any reporting 
follow-up (for example in "get"); whilst maintaining/multiplying SRP...

>> 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'.

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

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... ?
Regards =dn