DEV Community

Cover image for A Pythonic Guide to SOLID Design Principles

A Pythonic Guide to SOLID Design Principles

Derek D. on March 20, 2020

People that know me will tell you I am a big fan of the SOLID Design Principles championed by Robert C. Martin (Uncle Bob). Over the years I've use...
Collapse
 
joolius profile image
Julius

I am not sure if I understand the example used for LSP.

You may have noticed all of the FTP client classes so far have the same function signatures. That was done purposefully so they would follow Liskov's Substituitability Principle. An SFTPClient object can replace an FTPClient object and whatever code is calling upload, or download, is blissfully unaware.

SFTPClient requires username and password, whereas FTPClient does not. In the calling code I cannot replace FTPClient(host=host, port=port) with SFTPClient(host=host, port=port), so I do not see how SFTPClient can be a substitute for FTPClient. Could you please explain?

Collapse
 
ezzy1337 profile image
Derek D.

Great question! I had to look up Liskov's again to make sure this example was still valid. Fortunately for me, and quite by accident, I think it still works. Let me try to explain.

Liskov's as it originally appeared states, "Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T." So I took this as the principle applies to objects, not classes. In general, I think most people agree. So a class has a constructor which returns an object and as long as the object returned by the constructor can replace an object of the parent type you are following Liskov's.

All that being said with higher-level languages like Python it's probably a better practice to follow Liskov's even with the constructor since we can pass a Class name to a function and then construct an object of that class inside the function. In that scenario what I've written in this article would absolutely blow up.

Collapse
 
irunatbullets profile image
irunatbullets

I had to look up Liskov's again to make sure this example was still valid.

This is actually my biggest issue with SOLID. Having to look it up. After reading the Zen of Python at the top of the article, it feels easier because it's a mindset.

Collapse
 
joolius profile image
Julius

Thank you for the explanation!

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
ezzy1337 profile image
Derek D.

Thanks for pointing it out. I've fixed it.

Collapse
 
unclebob profile image
Robert C. Martin

Nicely done!

Collapse
 
lostmsu profile image
Victor • Edited

In the generateReport function signature you either have to specify the concrete FTPClient class as the parameter type

void generateReport<TTransferClient>(TTransferClient client)
  where TTransferClient: ICanUpload, ICanDownload
{
  ...
}

Also, I think IFTPClient is a violation of SRP in this case. You should simply take in both separately (ICanUpload uploader, ICanDownload downloader).

Collapse
 
paddy3118 profile image
Paddy3118

On single responsibility:
If one class/function/method does X on L or X on M or X on N then it seems the single responsibility could be "do X" rather than splitting it to 3 separate places of L. M. and N handling.
If a lead uses the principle to insist others change code then it could cause friction if it is not convincingly argued why a view on responsibility is to be preferred.

Collapse
 
ezzy1337 profile image
Derek D.

You've hit the nail on the head. I tried to enforce SOLID design in a code base where I was not a team lead. It caused a lot of friction because I didn't explain the principle or benefits well. I hope this article helps with insufficient explanations, but I don't think friction is inherently bad. As a team lead you should be pushing your developers to be even better, but sometimes that means letting them make a mistake and learn from it. The really good team leads will be able to find that balance.

I'm curious about the X on L or X or M, or X on N explanations you offered though. I don't quite follow what you are getting at, but if you have a more concrete example I'd love to discuss it further.

Collapse
 
chordmemory profile image
Jordan Cahill

I believe that what Paddy3118 meant with the X on L or X or M, or X on N explanations is that sometimes developers may have different opinions on what a single responsibility constitutes. In the end ...

There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.

I typically find that after discussing the various possibilities with my team and figuring out the pros and cons of different implementations, the obvious best way will arise.

Thread Thread
 
paddy3118 profile image
Paddy3118

Ay, someone else may have thought through wider implications, or not 🤔

Collapse
 
brycepg profile image
Bryce Guinta • Edited

What do you think about dependency injecting the FTPDriver itself so that the IO (establishing connection) isn't in the constructor? Example:

class FTPClient:
    def __init__(self, driver):
        self._driver = driver

    @classmethod
    def from_connection(cls, host, port):
        driver = FTPDriver(host, port)
        return cls(driver)
    ...
Enter fullscreen mode Exit fullscreen mode

This allows one to install a TestDriver for testing FTPClient. and bubbles up IO to an outer layer which uncle bob is big on (blog.cleancoder.com/uncle-bob/2012...), but we get to do it in one class because Python supports classmethods.

Collapse
 
ezzy1337 profile image
Derek D.

It's an excellent practice. I left it out so the examples were more concrete and demonstrated the principles as simply as possible.

Collapse
 
paddy3118 profile image
Paddy3118

On the open closed principle
It seems useful on an established large project. But they are only a small subset of Python projects. There is also "plan to throw one away; you will, anyhow", which I use as Python is very good at exploring the solution space, in which OCP may be suppressed.

Collapse
 
ezzy1337 profile image
Derek D.

I love that, "plan to throw one away; you will, anyhow". When prototyping you should absolutely be ready to throw code away. I've been caught in the sunk cost fallacy plenty of times when I would have been better throwing things away.

Writing SOLID code is a discipline and the more often you practice it the easier it becomes. The entire FTP example I used for this article was born from just practicing writing SOLID code. I didn't intend for every example to build of the previous but going through the act of writing the code to adhere to each principle revealed an elegant solution I didn't even know was there.

Collapse
 
ram_murthy5 profile image
Ram

Appreciate your time, and efforts to teach what you have learnt!
I just have a quick question, how would you approach SOLID design principle for this following problem..

I will send a directory path to your class, your class should perform following steps:
1, connect to the remote path (can be sftp, ftp, local path, or anything else in the future)
2, Reach the directory & read directory contents
3, for each file, detect the format and apply appropriate extraction method for texts.
4, update the DB
5, index in the ES
Finally, send me back the bool signal (success/ fail)

If you could, please share your abstract-level idea, that will be greatly helpful for me easily pick up this SOLID principle stuff. Thanks!

Collapse
 
yonyonita profile image
YonYonita

Hi Derek and thanks for this article!
You mentioned changing parameter names in the function signature adheres to the OCP. I was wondering how it works when the users of the API which we want to keep 'Closed' for change, explicitly state the parameter names when calling the APIs (for example: calling ftpCilent.upload(file=file_to_be_uploaded). I would assume that in that kind of coding style (which IMHO adds a lot to code readability) OCP may also require that parameter names are kept intact. What is your take on this?

Collapse
 
ezzy1337 profile image
Derek D.

This is a good point. It should state changing positional parameter names is still valid within OCP. Keyword args (kwargs) require the caller knowing the internal variable names of a function so changing those is not valid within OCP. I don't like my calling code knowing what's going on in my functions so I don't use them often which is probably why I missed this scenario.

Collapse
 
alexgarel profile image
Alex Garel

Hi, thanks, this is a good article. It is interesting to see how SOLID can integrate into the python philosophy.

Collapse
 
jcoelho profile image
José Coelho

Great article! 👏 It was super easy to follow with the FTP example.
I recently moved from working with Java to Python and it's great to know that you can still apply SOLID principles with Python.

Collapse
 
lwgray profile image
Larry Gray

Great article!

Collapse
 
bshorrosh profile image
bshorrosh

How about adding @abc.abstractmethod to the abstract methods, which will enforce the implementation in the concrete class.

Collapse
 
ezzy1337 profile image
Derek D.

I debated using abc to enforce the implementations and went as far as to explain the Dreaded Diamond problem in C# and Java and how interfaces solve the problem in my first iteration of this article. In the end, I chose not to use abc so it wouldn't confuse readers users into thinking @abc.abstractmethod were required for ISP.

I personally do use them and would recommend them, especially while teaching the principle to others.

Collapse
 
ezzy1337 profile image
Derek D.

I didn't know about that module. Thanks for the tip. I look forward to playing around with it.

Collapse
 
jamieuka profile image
Jamie Collins

Typo in intro to ISP: "complicated is better than complex" - the Zen of Python has it the other way around. Doesn't detract from a great article :)

Collapse
 
ezzy1337 profile image
Derek D.

Definitely, something I want to fix though. Thanks for pointing it out.