立体设计原则的Pythonic指南-Dev

2020-06-05 04:03:04

认识我的人会告诉你,我是罗伯特·C·马丁(鲍勃叔叔)倡导的可靠设计原则的忠实粉丝。多年来,我在C#、PHP、Node.js和Python中使用过这些原则。无论我把它们带到哪里,它们通常都很受欢迎……除了我开始用Python工作的时候。在代码审查期间,我不断收到这样的评论:这不是一种非常Python式的做事方式。当时我还是Python的新手,所以我真的不知道如何回应。我不知道Pythonic的代码是什么意思,也不知道它看起来是什么样子,而且没有一个解释是非常令人满意的。老实说,这让我很生气。我感觉人们正在使用Python的方式来逃避编写更有纪律的代码。从那时起,我就肩负起一项使命,要证明可靠的代码是Python式的。这就是现在的结局,这就是投球。

迈克尔·费瑟斯基于罗伯特·C·马丁的论文“设计原则和设计模式”中的原则创作了助记符实体。原则是。

我们稍后将更详细地介绍这些内容。关于可靠的设计原则,最需要注意的是它们应该是整体使用的。选择一个并且只选择一个对你不会有太大帮助。只有当它们一起使用时,你才会开始看到这些原则的真正价值。

虽然没有关于Pythonic方式的官方定义,但稍微用谷歌搜索一下,就会给出几个答案,而这些答案通常都是徒劳的。

上面的正确语法Pythonic代码遵循Python社区通常接受的约定,并以遵循创始哲学的方式使用语言。";-Derek D.。

蒂姆·彼得斯的“蟒蛇之禅”,美丽胜过丑陋。显式比隐式好。简单总比复杂好。复杂总比复杂好。平面比嵌套好。稀疏比密集好。可读性很重要。特殊情况不够特殊,不足以违反规则。尽管实用胜过纯洁。错误永远不应该悄无声息地过去。除非明确沉默。面对模棱两可的情况,拒绝猜测的诱惑。应该有1个,最好只有一个--显然可以做到这一点。尽管这种方式一开始可能并不明显,除非你是荷兰人。现在总比没有好。虽然从来没有比现在更好的。如果实现很难解释,那就不是个好主意。如果实现很容易解释,这可能是一个好主意。名称空间是一个非常棒的主意--让我们做更多这样的事情吧!

在我直接跳到这些原则以及它们与Python禅宗的关系之前,有一件事我想做,这是其他可靠的教程都不会做的。我们不是为每个原则使用不同的代码片段,而是使用单个代码库,并在涵盖每个原则时使其更加稳固。这是我们要开始的代码。

FTPClient类:def__init__(self,**kwargs):self。_ftp_client=FTPDriver(kwargs[';host&39;],kwargs[';port&39;])self。_sftp_client=SFTPDriver(kwargs[';sftp_host&39;],kwargs[';user&39;],kwargs[';pw&39;])def Upload(self,file:bytes,**kwargs):is_sftp=kwargs[';sftp';]if is_sftp:with self。_sftp_client。Connection()as sftp:sftp。PUT(文件)ELSE:SELF。_ftp_client。Upload(File)def download(self,target:str,**kwargs)->;字节:is_sftp=kwargs[';sftp;]if is_sftp:with self。_sftp_client。connection()as sftp:返回sftp。GET(目标)ELSE:返回自我。_ftp_client。下载(目标)

定义:每个模块/类应该只有一个职责,因此只有一个更改原因。

相关禅宗:应该有1个,最好只有一个--显而易见的做事方式。

单一责任原则(SRP)完全是关于通过围绕责任组织代码来增加内聚力和减少耦合。要明白为什么会发生这种情况,这并不是一个很大的飞跃。如果任何给定职责的所有代码都在一个地方,并且这些职责可能是相似的,但它们通常不会重叠。考虑这个非代码示例。如果扫地是你的责任,而我是拖地的责任,那我就没有理由记录地板是否扫过了。我只能问你,地板扫过了吗?并根据你的反应来决定我的行动。

我发现把责任看作用例是很有用的,这就是我们的禅宗发挥作用的方式。每个用例都应该只在一个地方处理,进而创建一种明显的做事方式。这也满足了SRP定义中更改部分的一个原因。这个类应该更改的唯一原因是用例已经更改。

检查我们的原始代码,我们可以看到该类没有单一的职责,因为它必须管理FTP和SFTP服务器的连接详细信息。此外,这些方法甚至没有单独的责任,因为它们都必须选择要使用的协议。这可以通过将FTPClient类分成2个类来解决,每个类都有一个职责。

FTPClient类:def__init__(自身、主机、端口):自身。_CLIENT=FTPDriver(主机,端口)def Upload(自身,文件:字节):自身。_客户端。Upload(File)def download(self,target:str)->;字节:返回自身。_客户端。下载(目标)类SFTPClient(FTPClient):def__init__(self,host,user,password):self。_CLIENT=SFTPDriver(host,username=user,password=password)def Upload(self,file:bytes):WITH SELF。_客户端。Connection()as sftp:sftp。put(File)def download(self,target:str)->;bytes:with self。_客户端。connection()as sftp:返回sftp。获取(目标)。

一次快速的更改,我们的代码就已经感觉到了更多的Python风格。代码是稀疏的,不密集的,简单的不复杂的,扁平的,不嵌套的。如果您还没有加入到SRP中,那么考虑一下与SRP之后的代码相比,原始代码在错误处理方面会是什么样子。

定义:软件实体(类、函数、模块)对于扩展应该是开放的,对更改应该是封闭的。

由于变化和扩展的定义是如此相似,很容易被开闭原则淹没。我已经找到了最直观的方法来决定我是否要进行更改或扩展是考虑函数签名。更改是强制更新调用代码的任何内容。这可能是更改函数名称、交换参数顺序或添加非默认参数。调用该函数的任何代码都将被强制根据新签名进行更改。另一方面,扩展允许新功能,而不必更改调用代码。这可以是重命名参数、添加具有默认值的新参数,或者添加*arg或**kwargs参数。调用该函数的任何代码仍将按最初编写的方式工作。同样的规则也适用于班级。

您的本能反应可能是向FTPClient类添加一个UPLOAD_BULK和DOWNLOAD_BULK函数。幸运的是,这也是处理此用例的可靠方法。

类FTPClient:def__init__(self,host,port):.#对于此示例,__init__实现不是重要的def Upload(self,file:bytes):.#对于此示例,上传实现不是重要的def download(self,target:str)->;bytes:.#对于此示例,下载实现不是重要的def UPLOAD_BULK(SELF,FILES:LIST[STR]):对于文件中的文件:SELF。Upload(File)def download_Bulk(self,Targets:List[str])->;对于Targets:Files中的目标,List[Bytes]:files=[]。附加(自我。下载(目标))返回文件。

在这种情况下,最好使用函数来扩展类,而不是通过继承来扩展,因为BulkFTPClient子类在下载时必须更改函数签名,以反映它返回的是字节列表,而不仅仅是字节,这违反了Open-Closed原则和Liskov的Substituability原则。

定义:如果S是T的子类型,则可以用S类型的对象替换T类型的对象。

利斯科夫的替代性原则是我学到的第一个可靠的设计原则,也是我在大学里学到的唯一一个。也许这就是为什么这个对我来说是如此直观的原因。一种简单的英语说法是,任何子类都可以在不破坏功能的情况下替换其父类。

您可能已经注意到,到目前为止,所有FTP客户端类都具有相同的函数签名。这样做是有目的的,这样他们就会遵循利斯科夫的替代性原则。SFTPClient对象可以替换FTPClient对象,并且无论是什么代码调用UPLOAD或Download,都是幸福的不知不觉。

FTP文件传输的另一个特殊情况是支持FTPS(是的,FTPS和SFTP是不同的)。解决这个问题可能很棘手,因为我们有选择。它们是:1.添加UPLOAD_SECURE和DOWNLOAD_SECURE函数。2.添加通过**kwargs的安全标志。3.新建一个扩展FTPClient的类FTPSClient。

出于我们将在接口隔离和依赖关系反转原则中遇到的原因,新的FTPSClient类是可行的。

FTPClient类:def__init__(自身、主机、端口):.。定义上传(自身,文件:字节):.。def download(自身,目标:str)->;字节:.。类FTPSClient(FTPClient):def__init__(self,host,port,username,password):self。_CLIENT=FTPSDriver(主机,端口,用户=用户名,密码=密码)。

这正是边缘情况继承的目的所在,遵循Liskov的定义可以实现有效的多态性。您将注意到,现在FTPClient可以替换为FTPSClient或SFTPClient。事实上,所有3个都是可互换的,这就带来了接口隔离问题。

与利斯科夫不同的是,界面隔离原则是我理解的最后一个也是最难理解的原则。我总是把它等同于界面关键字,而大多数对可靠设计的解释并不能很好地消除这种困惑。此外,我发现大多数指南都试图将所有东西拆分成小界面,通常每个界面只有一个功能,因为太多的界面比太少的界面要好。

这里有两个问题,第一,Python没有接口,第二种语言,如C#和Java,有接口,过多地拆分它们总是以实现接口的接口而告终,这些接口可能会变得非常复杂,这不是Python语言。

首先,我想通过查看一些C#代码来探索接口太小的问题,然后我们将介绍一种Pythonical的ISP方法。如果您同意或者只是选择相信我,超小的界面不是分离您的界面的最佳方式,请随意跳到下面的Pythonic解决方案。

#WARNING HERE BE C#代码公共接口ICanUpload{void Upload(Byte[]file);}公共接口ICanDownload{Byte[]Download();}类FTPClient:ICanUpload,ICanDownload{public void Upload(Byte[]file){.}public Byte[]Download(String Target){.}}。

当您需要指定同时实现ICanDownload和ICanUpload接口的参数类型时,问题就开始了。下面的代码片段演示了该问题。

类ReportGenerator{public Byte[]doStuff(Byte[]raw){.}public void GenerateReport(/*这里应该放什么类型?*/client){raw_data=client。下载(';client_rundown。csv';);报告=此。doStuff(Raw_Data);客户端。上传(报表);}}。

在GenerateReport函数签名中,您要么必须将具体的FTPClient类指定为参数类型(这违反了依赖关系反转原则),要么必须创建一个同时实现ICanUpload和ICanDownload接口的接口。否则,只实现ICanUpload的对象可能会传入,但下载调用会失败,反之亦然,因为对象只实现ICanDownload接口。通常的答案是创建一个IFTPClient接口,并让GenerateReport函数依赖于该接口。

这是可行的,除非我们仍然依赖FTP客户端。如果我们想要开始在S3中存储报告,该怎么办?

对我来说,ISP就是为其他开发人员如何与您的代码交互做出合理的选择。没错,它更多地与API和CLI中的i相关,而不是interface关键字。这也是Python禅宗的可读性很重要的原因。一个好的接口将遵循抽象的语义,并与术语相匹配,从而使代码更具可读性。

让我们看看如何添加S3Client,因为它与FTPClient具有相同的上传/下载语义。我们希望保持上传和下载的S3Clients签名一致,但是新的S3Client继承FTPClient将是无稽之谈。毕竟,S3不是FTP的特例。FTP和S3的共同点在于它们都是文件传输协议,并且这些协议通常共享一个类似的接口,如本例所示。因此,与其从FTPClient继承,不如将这些类与抽象基类绑定在一起,抽象基类是Python最接近接口的东西。

我们创建了一个FileTransferClient,它成为我们的接口,我们所有的现有客户端现在都继承自该接口,而不是继承自FTPClient。这强制使用公共接口,并允许我们将批量操作移到它们自己的接口中,因为并不是每个文件传输协议都支持它们。

从ABC导入ABC def类FileTransferClient(Abc):def Upload(self,file:bytes):pass def download(self,target:str)->;bytes:pass def cd(self,target_dir):pass类BulkFileTransferClient(Abc):def Upload_Bulk(self,files:list[bytes]):pass def download_Bulk(self,target:list[str]):pass类BulkFileTransferClient(Abc):pass def download_Bulk(self,target:list[str]):pass。

哦伙计!这是个好代码还是怎么的。我们甚至设法挤入SCPClient,并保留批量操作作为它们自己的混合。所有这些都与依赖注入很好地结合在一起,依赖注入是一种用于依赖反转原则的技术。

定义:高级模块不应该依赖于低级模块。它们应该依赖于抽象,而抽象不应该依赖于细节,而细节应该依赖于抽象。

这就是把它联系在一起的东西。我们用其他可靠的原则所做的一切,都是为了达到一个我们不再依赖于用来移动文件的细节(底层文件传输协议)的地方。我们现在可以围绕业务规则编写代码,而无需将它们绑定到特定实现。我们的代码同时满足依赖倒置的两个要求。

我们的高级模块不再需要依赖于FTPClient、SFTPClient或S3Client等低级模块,而是依赖于抽象FileTransferClient。我们依赖于移动文件的抽象,而不是这些文件如何移动的细节。

我们的抽象FileTransferClient不依赖于特定于协议的细节,相反,这些细节取决于如何通过抽象使用它们(即可以上传或下载文件)。

def exchange(client:FileTransferClient,To_Upload:Bytes,To_Download:Str)->;Bytes:Exchange。Upload(To_Upload)返回交换机。如果__name__==';__main__';:ftp=FTPClient(';ftp.host.com';)sftp=FTPSClient(';sftp.host.com';,22)ftps=SFTPClient(';ftps.host.com';,990,';ftps_user';,';P@s。)S3=S3Client(';ftp.host.com';)scp=SCPClient(';ftp.host.com';)用于[ftp,sftp,ftps,S3,scp]中的客户端:Exchange(Client,b';Hello';,';greeting.txt';)。

在这里,您有一个坚实的实现,也是非常Python式的。如果您以前没有学习过Python,我希望您至少已经热身到了Solid,对于那些正在学习Python但不确定如何继续编写Solid代码的人来说,这是很有帮助的。当然,这是一个精心策划的例子,我知道它会为我的论点提供支持,但在写这篇文章的过程中,我仍然感到惊讶,因为在写这篇文章的过程中,我的变化是如此之大。并不是每个问题都符合这个精确的分类,但是我已经尝试在我的决定中包含足够的推理,以便您可以在将来选择最可靠的Pythonic实现。

如果您不同意或需要任何澄清,请在Twitter上留言或@d3r3kdrumm0nd。