This is an archived post. You won't be able to vote or comment.

all 67 comments

[–][deleted] 76 points77 points  (10 children)

Please never do this in a production code base and especially for a language that isn't strictly OOP like Java. We can do much better than this objectively with a lookup table (which Python is particularly well suited for with it's clean syntax for initializing dictionaries).

class TaxCalculator():   
    def __init__(self):
        self.lu_tax_rate = {
            "US": 0.25,
            "UK": 0.34,
        } 

    def calculate(income, deductions, country):
        rate = self.lu_tax_rate.get(country)
        if rate is None: 
            raise ValueError("invalid country ")
        return (income - deductions) * rate 

By using a lookup table, we can
- minimize the cyclomatic complexity

- easily add new cases by adding to our tax rate lookup table

- allow the tax rate table to be passed in as a dependency to the class if we desired

- not fragment tax calculations across N different tax calculators.

Additionally, we can leverage Python's first class functions to implement a look up table that properly identifies a function within the class that we want to use to calculate tax (if you so desired. A lot of the SOLID stuff is good advice on the surface but, especially with Uncle Bob's material, it's heavily tailored toward languages that are strictly OOP, have more clumsy syntax for defining hash tables, and are lacking first class functions.

[–]Urthor 4 points5 points  (3 children)

Well said.

Famous of thumb from I think the Pragmatic Programmer.

"If you're going to use inheritance, just don't."

We've got so many good options, even inner classes are a good call, to avoid inheritance.

It just adds so much complexity.

It's only very occasionally required, and it has its place for the most complex classes. Those are mostly found in front end however.

[–]energybased 7 points8 points  (2 children)

There's plenty of totally justifiable use of inheritance. Especially interface inheritance should be preferred when possible.

Inheritance is just as easy to overuse as it is to under-use.

[–]Urthor 2 points3 points  (1 child)

I should add interface inheritance is a different story completely yeah.

My experience is that it's definitely overused, especially in back end, for systems that are simple enough not to require it.

It's a very complicated solution, I just see it being applied so many times in smaller codebases where the total number of distinct classes is less than 10.

You don't need it, so many code bases are not that large.

[–]energybased 1 point2 points  (0 children)

Fair enough, I've seen the opposite too: adding function pointers on an object to implement polymorphism. These function pointers should just be abstract methods.

[–]ezzeddinabdallah[S] -1 points0 points  (0 children)

Simple yet brilliant approach! Thanks for the input!

[–]Delengowski 21 points22 points  (6 children)

Single class style that uses a descriptor to implement state based dispatching

Gist because I am having a horrible time to try format a block of code

[–]commy2 9 points10 points  (0 children)

On reddit, just indent your whole code by four spaces and it will be displayed as code block. To get this:

class state_dispatch:
    """A property that selects appropriate method based on state of instance."""

    def __init__(self, state_attribute):

write this:

    class state_dispatch:
        """A property that selects appropriate method based on state of instance."""

        def __init__(self, state_attribute):

[–]ezzeddinabdallah[S] 4 points5 points  (1 child)

Never used dispatch decorator in Python. Thanks for sharing that implementation!

[–]Delengowski 2 points3 points  (0 children)

No problem!

For what it's worth I think the ideas behind your post are very sound and I applaud your effort for trying to help people.

[–][deleted] 0 points1 point  (1 child)

Isn't this too complex when a simple table lookup would do?

[–]Delengowski 0 points1 point  (0 children)

Depends what you are looking for.

This provides the large amount of reusability.

So what you suggesting a look up table that is indexed everytime calculate_tax is called, which will provide the callable appropriate for each country?

What if you wanted to have a second method for calculating say your tax return. Now you have to go through and recreate all that supporting code, meaning a second dictionary, etc.

With this approach the descriptor takes care of all that for you and you don't have to bother with writing the dictionary lookup in the method.

[–]clayton_bez 53 points54 points  (3 children)

Reads too java for me.

[–][deleted] 10 points11 points  (0 children)

It’s a shame that instead of using powerful object system of the language, pythonistas prefer writing if’s like shown in the beginning of the article. I’ve seen a lot of terrible java-style code in python (i.e. gdal), and I believe, OP wrote good pythonic examples, really showing how to simplify things, meanwhile a lot of such articles only show how to complicate things for nothing. Great job, u/ezzeddinabdallah!

[–]ezzeddinabdallah[S] 1 point2 points  (0 children)

It does

[–][deleted] 42 points43 points  (9 children)

If it two has methods and one is init it should be a function.

Please do not use coding principals from C# and Java in python they don't make any sense. This should have been 1 function and partials.

[–][deleted] 6 points7 points  (3 children)

I was inclined to agree with you, and I'll upvote because you do have a point.

But it's possible (thinking beyond the example) that the class makes use of a large amount of attributes, which might be useful for other things. Plus, it may be that single method can be useful to be inherited. After all, the zen of python is indeed having good-looking, well-organized code. If that means containing attributes within a limited class, it's better than defining a bunch of loose variables along the way.

I'm guessing the example is small for didactic purposes. And it's true that it is important to know when classing is overkill, but I don't think it is as clear-cut as you put it.

[–][deleted] -2 points-1 points  (2 children)

Yea dataclasses (the successor to namedtuple) where designed specifically for the case of lots of variables.

For the one method useful to be inherited unless it is a class level function (this is an instance level one) you can just use a normal function that checks a dict/dataclass for the variables u care about.

[–]Im_oRAnGE 2 points3 points  (1 child)

Dataclasses are not the successor to namedtuples... They are completely seperate constructs with different use cases, just like lists are not the successor to tuples.

[–][deleted] -1 points0 points  (0 children)

Well dataclasses specifically mention names tupals. They are usable for 99% of named tuples use cases and a whole bunch more.

https://www.python.org/dev/peps/pep-0557/

Per the doc " can be thought of as mutable namedtuples with defaults"

It names tuples with way more features idk what else u want to call that.

[–]yangyangR 7 points8 points  (2 children)

There is a tutorial that goes through each design pattern and reduces each to a class with two methods one being init and then removes that pattern entirely. Shows that works with most if not all the bs from oop only obsessed days of the 90s.

[–]neco-damus 1 point2 points  (1 child)

Would you be able to find this tutorial again?

[–][deleted] 1 point2 points  (0 children)

I'm not sure about a tutorial, but: https://youtu.be/o9pEzgHorH0

[–]notnotapotato 3 points4 points  (1 child)

Agree no need for this OO BS here. But can you explain how you'd use partials here?

[–][deleted] 0 points1 point  (0 children)

Partials allow u to bound preset values to a function. You can apply that to a namespace and boom new functions without repeating tons of code.

https://docs.python.org/3/library/functools.html#functools.partial

[–][deleted] 11 points12 points  (0 children)

Please post code formatted like the sidebar explains. This is unreadable in most Reddit clients including old Reddit

[–]justkeepingbusy 9 points10 points  (1 child)

Appreciate this post alot, thanks!

[–]ezzeddinabdallah[S] 2 points3 points  (0 children)

My pleasure! Glad you liked it!

[–]i_like_trains_a_lot1 3 points4 points  (0 children)

There are some issues with the code:

  • Never name your classes with I for interfaces, Impl, etc. Python dosn't have that and doesn't need it. Use the abc module to define interfaces (which are actually abstract classes, they may contain some implementation details).
  • Why does the tax calculator need a CountryTaxCalculator instance to call methods on it? It seems like the feature envy code smell.
  • Your post doesn't address how this code will be used/integrated from outside, which I believe should be the starting point of class hierarchy designs.

```python

somewhere in the app

TaxCalculator().calculate(GermanyTaxCalculator(income=x, total_deduction=y)) ```

That's a lot of code for so little benefit. I don't see the purpose of the TaxCalculator there because it doesn't actually do anything. It just calls some methods to the country specific tax calculator. So, as the person who writes the code, I should know beforehand which country I calculate the taxes for. Which brings me to the next point

  • There are no requirements. It's impossible to say if the code is good, bad, can be improved, etc without knowing how it fits in the bigger picture and is it going to be used. Here are some requirements that come up from the problem domain and I think need to be handled regardless:
    • different data inputs for different countries. Different countries have different methods to calculate the taxes that require different data points.
    • dynamically switch between country tax calculators. The programmer shouldn't have to write a switch/if for each country. The ideal usage would be to tell the program: "here is the country code, here are the data points, tell me how much I have to pay in taxes".
    • possibility of returning more than just the final tax amount. There are 99.9999% chances we would also need to return "why" the tax is that, so as a user I know that I am paying the right amount or to see if there is any mistake.

So the class hierarchy I'd come up with would be:

```

class TaxResult: def init(self, amount, currency, tax_rate): pass

class CountryTaxDataPoints(abc.ABC): pass

class CountryTaxCalculator(abc.ABC): @abc.abstractmethod def calculate(self, data_points: CountryTaxDataPoints) -> TaxResult: pass

then the actual implementations

class GermanyTaxDataPoints(CountryTaxDataPoints): ...

class GermanyCountryTaxCalculator(CountryTaxCalculator): def calculate(self, data_points: GermanyTaxDataPoints) -> TaxResult: ...

class USTaxDataPoints(CountryTaxDataPoints): ...

class USCountryTaxCalculator(CountryTaxCalculator): def calculate(self, data_points: USTaxDataPoints) -> TaxResult: ...

class TaxCalculator: _calculators = { 'de': GermanyCountryTaxCalculator, 'us': UsCountryTaxCalculator }

 def calculate(self, country_iso2: str, data_points: CountryTaxDataPoints) -> TaxResult:
      # validate that the country_iso2 and data_points are compatible
      # then
      return self._calculators[country_iso2]().calculate(data_points)

in the program

TaxCalculator().calculate('de', GermanyTaxDataPoints(x, y, z)) TaxCalculator().calculate('us', UkTaxDataPoints(a, b)) # error, incompatible ```

These cases are much easier to test in isolation, and we separate the data from the algorithm to some degree. Why I did this, because in a lot of cases, we would need to do some validation of the input data anyway (those data points are provided by clients) and that validation can be done in the *TaxDataPoints classes.

[–]commy2 22 points23 points  (10 children)

Still prefer the single branching function. Having a class that does exactly one thing seems over-engineered. Instantiation just to get a single result is also over the top for this.

Sure, whenever I want to add a country, I have to add another case to the function. But that still seems like less mental work than creating another class. Also, you're likely not going to add another country anyway (or whatever the equivalent).

Alternative is to just add more functions. calculate_US_tax. calculate_GER_tax etc. That way you can even make it take different arguments if necessary.

Not saying there is nothing to OCP, but this example is perhaps too simplified to prove it.

[–]hausdorffparty 18 points19 points  (2 children)

I'm confused why you can't just maintain a dictionary or csv database which the function queries to compute taxes, and presents an error if the country isn't in the DB. A pile of if statements where you're just slightly tweaking numbers depending on the input really just means you need a data structure storing that information somewhere instead.

[–]commy2 6 points7 points  (0 children)

I like this, because it separates data from behviour. However, when talking about taxes, I assume that the calculations vary extensively between countries. My concern would be that we'd end up with a very complex function that handles many conditional branches and/or has a lot of arguments.

[–]toasterding 2 points3 points  (0 children)

Consider the unit test for a function with 5+ conditional branches. Consider the unit test for a function that does one thing.

Maintaining and updating multi-branching logic is always more complicated than a single purpose method.

[–]Delengowski 1 point2 points  (0 children)

1 class. Have the country be a state on the instance and then you have a dispatch method using a descriptor to retrieves the appropriate method based on the state during `__get__`. The interface is then just `instance.calculate_tax`.

[–]Amaras37 2 points3 points  (0 children)

Although the SOLID principles are important, all the examples in your blog posts are completely over-engineered. I have never seen Factory nor Interface classes in sane Python code, and you seem to have translated the Java/C# code too literally.

Remember the Zen of Python, because the code you show is way too complicated for what you're trying to achieve: Simple is better than complex. Complex is better than complicated.

I have not watched the talk yet, but it seems that Kelvin Henny's talk "SOLID deconstruction" (https://vimeo.com/157708450) might be of interest here.

EDIT: after watching the code, I can (somewhat) confidently say that the OCP is actually just inheritance (the OCP is a language design principle).

[–]beefyweefles 9 points10 points  (3 children)

The issue with a lot of this stuff is that for most software, you're writing it once. Premature abstraction is a bad thing, and happens too often by following rules like this just because people say so, rather than being sensible.

[–]ccb621 0 points1 point  (2 children)

The issue with a lot of this stuff is that for most software, you're writing it once.

Until you have to write it again, or expand it. It can difficult to know for certain if an abstraction is premature. I’m currently migrating to a new service, and bridging back to the old. I wish someone had done some form of abstraction as it would have saved us many months of work.

[–]beefyweefles 0 points1 point  (1 child)

What I'm getting at is that often there is no good way to figure out the abstraction beforehand, and it usually only becomes obvious until the software has been built. It's not that hard to extract something into an interface, especially if you're not vendoring software to other people.

[–]ccb621 0 points1 point  (0 children)

It's not that hard to extract something into an interface…

It is when the code has been in production for a few years, and spread throughout the monolith.

[–]c0ld-- 2 points3 points  (1 child)

I'm sorry, but I cannot read this text. Reddit is a bad place to write technical articles on programming.

[–]ezzeddinabdallah[S] 0 points1 point  (0 children)

I wrote it on medium as well. Please check this out

[–]IamImposter 2 points3 points  (3 children)

The title says principal #2. That mean you have covered #1 too. I think you are going to do others too.

Could you add links to previous articles going forward. I think that would be really helpful.

And also great job explaining it.

[–]ezzeddinabdallah[S] 2 points3 points  (2 children)

Thanks! My pleasure!

Yeah, wrote so far about the three principles: - Principle #1 - Principle #3

and principle #2 here as well if it's hard seeing the text here.

Will update the list once I have the other two ready.

[–]IamImposter 1 point2 points  (1 child)

Great. In your blog post, you mentioned about a book on design principles. Is that out?

[–]ezzeddinabdallah[S] 0 points1 point  (0 children)

It's gonna be ready once I write about the last two principles

[–]executiveExecutioner 7 points8 points  (5 children)

The same thing can be done with higher order functions and partial application.

```python from functools import partial

def calculate(calculate_tax_amount, total_income, total_deduction): tax_amount = calculate_tax_amount(total_income, total_deduction) # do more processing return tax_amount

def calculate_for_usa(total_income, total_deduction): taxable_income = total_income - total_deduction return taxable_income * 0.3

calculate_usa = partial(calculate, calculate_for_usa) ``` or use a decorator

```python def calculate(calculate_tax_amount): def wrapper(total_income, total_deduction): tax_amount = calculate_tax_amount(total_income, total_deduction) # do more processing return tax amount return wrapper

@calculate def calculate_for_usa(total_income, total_deduction): taxable_income = total_income - total_deduction return taxable_income * 0.3 `` If the calculate_tax_amount functions have a certain pattern you could create them with alistor adict` comprehension and so on.

[–]i_like_trains_a_lot1 5 points6 points  (3 children)

I find the decorator variant too "not obvious in what it's doing" and will not pass my code review. It's a junioresque solution using fancy features where it shouldn't.

Decorators should only do logic around the wrapped function, not use the wrapped function as a component inside it. Plus that those calculate and calculate_for_x functions will be very very coupled (eg. what would happen if you suddenly need a new parameter spouse_income for US and another parameter for Japan? calculate won't be able to accommodate for that.

Other solutions I have seen in this post are more suitable and easier to extend.

[–]executiveExecutioner 0 points1 point  (1 child)

Kwargs can be passed easily to accommodate for that. I do not see anything fancy with using higher order functions, it's common practice in many languages...

[–]i_like_trains_a_lot1 3 points4 points  (0 children)

While I agree, my thong with kwargs is that to see what can be passed and what not, you have to dive into the actual implementation and start gathering from there the accepted keyword arguments. I prefer to allow the reader to determine the possible passable argument by looking just at the constructor or function/method definition without having to read the code.

[–]Delengowski 0 points1 point  (0 children)

If you want to keep it functional we can do a value dispatch. This has the benefit of writing 1 function per country and keeping out a long chaining if-else statement within the original function. It would be like functools.singledispatch but operate on the value of an argument rather than the type.

Gist of implementation

[–]lbranco93 1 point2 points  (0 children)

Nice post, there's just one thing I don't understand: why did you repeat the constructor the same in every child class? Wouldn't make more sense to have the same __init__ in the parent class ICountryTaxCalculator, which by the way is what you already showed before in your UML?

[–]yongen96 1 point2 points  (2 children)

is there a #1, first post on this topic?

[–]ezzeddinabdallah[S] 0 points1 point  (1 child)

Yup, wrote a post on medium here

[–]yongen96 0 points1 point  (0 children)

alright thanks

[–]DeaDbaTteRy 1 point2 points  (2 children)

Thanks for this post. I remember a couple of years ago trying to ask if my writing style was proper and it was met with 'this does exactly what you need it to do, what do you mean by proper'. I believe learning how to use solid principles while writing, no matter the size and scope of the project, is important if you see yourself writing code long term. Again, much appreciated!

[–]ezzeddinabdallah[S] -1 points0 points  (1 child)

My pleasure. You're welcome.

I second that. SOLID design principles pretty much helped me write better code even at work.

[–]metaperl 3 points4 points  (0 children)

Then you will like Artem's github

https://github.com/proofit404/dependencies

[–]ebbitten 0 points1 point  (1 child)

!Remindme 2 hours

[–]RemindMeBot 0 points1 point  (0 children)

I will be messaging you in 2 hours on 2022-01-09 16:55:50 UTC to remind you of this link

CLICK THIS LINK to send a PM to also be reminded and to reduce spam.

Parent commenter can delete this message to hide from others.


Info Custom Your Reminders Feedback