在阅读 clean architecture的过程中,会发现作者经常提到recompile
redeploy
,这些术语看起来都跟静态类型语言有关,比如Java、C++、C#。而在我经常使用的python语言中,是不存在这些概念的。于是,在阅读的时候就会有一个疑惑,《clean architecture》中提到的各种原则,比如SOLID,是否对动态类型语言 -- 如python -- 同样适用?
SOLID是OOP的指导原则,更具指导性的应该是各种设计模式,GOF经典的Design Patterns: Elements of Reusable Object-Oriented Software 也是用C++来举例的,那么这些经典设计模式有多少是适用于动态语言如python的呢?本文记录对这些问题浅薄的思考,如果有认知错误的地方,还请大家不吝指教。
本文地址:https://www.cnblogs.com/xybaby/p/11767100.html
SOLID
SOLID是模块(module)设计的指导原则,有以下五个原则组成
- SRP(Single responsibility principle):单一职责原则,一个module只有一个原因修改
- OCP(Open/closed principle):开放-关闭原则,开放扩展,关闭修改
- LSP(Liskov substitution principle):里氏替换原则,子类型必须能够替换它们的基类型
- ISP(Interface segregation principle):接口隔离原则,你所依赖的必须是真正使用到的
- DIP(Dependency inversion principle):依赖倒置原则,依赖接口而不是实现(高层不需要知道底层的实现)
ISP
首先来看ISP,接口隔离原则,《clean architecture》的作者承认这是一个语言相关的原则
This fact could lead you to conclude that the ISP is a language issue, rather than an architecture issue.
为什么呢, ISP主要是为了解决“胖接口”导致的不必要的 recompilation and redeployment, 如下所示:
Use1对op1
的使用导致OPS的修改,导致User2 User3也要重新编译。而在动态语言中是不存在重新编译这样的问题的:
In dynamically typed languages like Ruby and Python, such declarations don’t exist in source code. Instead, they are inferred at runtime. Thus there are no source code dependencies to force recompilation and redeployment
DIP
DIP(依赖倒置原则)是SOLID的核心,OCP其实就依赖于DIP。也可以说,DIP是“clean architecture”的核心。
“clean architecture”由两部分组成:
- well-isolated components
- dependency rule
什么是”Dependency rule"呢?让低层的detail去依赖高层的policy。比如,业务逻辑(business rule)就相比数据存储(database)出于更高层,虽然逻辑上是业务逻辑要使用数据库,但为了可维护性、可扩展性,架构设计上得让database去依赖business rule,如下所示
从上图可以看出,为了达到这个目的,在静态语言中,会声明一个接口,调用的双方都依赖这个接口。如上图中的database interface
,让business rule和database都去依赖这个接口,而这个database interface和business rule在一个component,这就实现了让低层的database去依赖高层的business rule。
在静态类型语言(如Java、C++)中,其实就是利用运行时多态这个特性,使得可以在运行时 -- 而不是编译时 -- 改变软件的行为,当然为了达到这个目的,需要预先声明一个虚基类 或者接口(Java Interface)。
而在python中,本来就是运行的时候去求值,而且因为ducking type,所以无需事先声明接口或者强迫继承接口
Dependency structures in these languages(dynamic typed languages) are much simpler because dependency inversion does not require either the declaration or the inheritance of interfaces.
从静态类型语言到动态类型语言,其实是省略了很多东西
- 省略了虚函数,如template method模式
- 省略了虚基类、接口,如DIP、strategy模式
python中的依赖与依赖倒置
在python中,怎么算依赖,怎么算依赖倒置?
'''my.py''' import other class My(object): def f(self): other.act()
这一段代码中通过import
让module my
依赖于module other
,
'''my.py''' class My(object): def __init__(self, actor): self._actor = actor def f(self): self._actor.act()
那么在这里,my和other有依赖关系吗?没有的,这里压根就没有出现过other。由于动态类型加上ducking type,根本无需显式的接口定义,只要遵循相关的协议(契约)即可。而这个契约,没办法通过代码强行约束,调用者需要什么样的接口,被调用者应该具备什么样的行为,都只能通过文档(或者单元测试)来描述。
为了表达契约,上述代码应该加上docstring
'''my.py''' class My(object): def __init__(self, actor): '''Param: actor,该对象需要具备接收0个参数的act方法 ''' self._actor = actor def f(self): self._actor.act()
python中大量使用类似的协议,如context management
, iterator protocol
。虽然很方便,同时也对程序员有更高要求,因为至少得有靠谱的docstring。如果需要强加约束,那是是可以考虑使用abc的。
设计模式
首先声明的是,在本文中提到的设计模式,一般指Design Patterns: Elements of Reusable Object-Oriented Software 这本书中所描述的经典设计模式。
很早之前看过一种说法,“++设计模式是对静态语言缺陷的弥补”++,当时没经思考就全盘接受了,窃认为这就是真理。最近才真正思考这个问题,发现这种说法存在偏见与不全面。
首先抛出一个问题:设计模式是语言相关吗(language-specific)?是某种类型的编程语言需要设计模式,而另外一些编程语言就不需要?或者说,不同的编程语言需要的设计模式是不一样的?
什么是设计模式呢,《Design Patterns》中描述为,针对软件设计中某一类特定问题的简单且优美的解决方案。
Describes simple and elegant solutions to specific problems in object-oriented software design
也就是说,设计模式是解决某类特定问题的套路,或者说方法论。套路是针对某个问题,经过理论或实践验证的、行之有效的方法与步骤。没有方法论也能解决问题,可能就需要去大量的尝试、试错,得到一种解决办法(大概率也不是最优解),这个求解的过程耗时且低效。因此可以说,方法论(模式)加速了问题求解的过程。
比如,程序员每天都很大量的事情要做:要开会、要写代码、要处理bug、要自己充电。如何安排呢?可能自己思考这个问题就得焦头烂额,但是已经有成熟的方法论 --艾森豪威尔矩阵-- 可供使用了啊。
我们常说,站在巨人的肩膀上,套路、方法论就是巨人的肩膀。
设计模式同样如此。
设计模式与动态语言
《Design Patterns》这本书,写于1994年,作者提到写这本数的目标,就是将这些行之有效的经验记录下来。前面提到,设计模式是针对一类问题的解决方案,那么在介绍一种设计模式的时候,就一定会涉及到以下内容(包括但不限于):
- 要解决的问题是什么
- 解决方案是什么样子的
- 解决方案的缺陷与适用场景
- 解决方案的详细步骤
- 针对同一个问题,有没有其他解决方案,各自的优劣
当然,首先得给这个模式取一个恰如其分的名字,命名的重要性不容质疑。至少保证程序员之间在沟通的时候所表达的是同一个问题,不管这个沟通是peer to peer,还是通过代码。名字(术语、定义)也就减轻了沟通的成本。
在《Design Patterns》写成的两年后,即1996年,Peter Norvig就做了一个分享 “Design Patterns in Dynamic Programming”, 指出由于动态语言存在更少的语言层面的限制,GOF中的大多数设计模式在Lisp或者Dylan有更简单的实现,有的甚至简单到根本无需注意
16 of 23 patterns have qualitatively simpler implementation in Lisp or Dylan than in C++ for at least some uses of each pattern
16 of 23 patterns are either invisible or simpler
那么哪些模式变得“invisible”,哪些是“simpler”了呢?
《Design Patterns》中讲设计模式大致分为三类
- Creational: ways and means of object instantiation
- Structural: mutual composition of classes or objects (the Facade DP is Structural)
- Behavioral: how classes or objects interactand distribute responsibilities among them
由于在动态类型语言中,类(class, type)和方法(function)都是一等公民,因此Creational patterns
在动态类型语言,如Python中就变得“invisible”。
由于动态类型、ducking type,一些Creational patterns
如“Observer”,“Visitor”就变得“simpler”。这里要强调的是,变得更简单,并不意味这个这个模式就没有存在的意义了,比如观察者模式,或者订阅-发布,代表了松耦合的设计原则,在各个层级的设计中都是需要的。
对于这种体现更高原则、思想的设计模式,我们应该用模式去帮助思考和沟通,而不要拘泥于样板代码、特定语言实现。StackExchange上的这个排比句很恰当:
- I might say that I have a visitor pattern, but in any language with first class functions it will be just a function taking a function. Instead of factory class I usually have just a factory function. - I might say I have an interface, but then it's just a couple of methods marked with comments, because there wouldn't be any other implementation (of course in python an interface is always just comments, because it's duck-typed). - I still speak of the code as using the pattern, because it's a useful way to think about it, but don't actually type in all the stuff until I really need it.
那么回到问题,设计模式是语言相关吗(language-specific)?
我的回答是,部分设计模式是语言相关的,部分设计模式不是语言相关的,具体到某一个特定的模式还可能是变化的。
为什么呢,严谨一点,我们只能说设计模式是问题相关的 -- 是关乎某个问题的。核心在于,这个问题在什么情况下确实是一个问题。而且,随着发展,一个老问题会消亡,新问题会出现。
具体到编程语言,则应该关心的是一个问题是不是语言相关的。在静态类型语言,如C++中,对象都有类型,类型决定了其行为,那么为了运行时多态,就得有一个虚基类,同时还要做到OCP,这就需要各式各样的Creational Patterns。但到了动态类型语言,这个就不再是一个问题,因此就不再有与之对应的模式。