dn <pythonl...@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 <pythonl...@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. -- https://mail.python.org/mailman/listinfo/python-list