来源:NightTeam-韦世东
程序设计的好与坏,早在我们青葱岁月时就接触过了,只是那是并不知道这竟如此重要。能够立即改善程序设计、写出“好”代码的知识有以下几点:
面向对象五个基本原则;常见的三种架构;绘图;起一个好名字;优化嵌套的ifelse代码;
当然,其他技术知识的丰富程度也决定了程序设计的好坏。例如通过引入消息队列解决双端性能差异问题、通过增加缓存层提高查询效率等。下面我们一起来看看,上面列出的知识点包含哪些内容,这些内容对代码和程序设计的改善有何帮助。
面向对象五个基本原则本书作者是级学生,面向对象是作者青葱时期发展火热的编程范式。它的五个基本原则是:
单一职责原则;开放封闭原则;依赖倒置原则;接口隔离原则;合成复用原则;
下面我们将通过对比和场景假设的方式了解五个基本原则对代码质量的影响。
立竿见影的单一职责原则没错,立竿见影、效果卓越。对于我们这些自学编程无师自通的人来说,能把功能实现就可以了,根本没有时间考虑代码优化和维护成本问题。时光流逝,竟在接触编程很长一段时间后才发现它竟如此重要。
俗话说只要代码写的够烂,提升就足够明显。以一个从文件内容中匹配关键数据并根据匹配结果发出网络请求的案例,看看大部分程序员的写法:
importreimportrequestsFILE="./information.fet"defextract(file):fil=open(file,"r")content=fil.read()fil.close()find_object=re.search(r"url=\d+",content)find=find_object.group(1)text=requests.get(find)returntextif__name__=="__main__":text=extract(FILE)print(text)
需求已经实现,这点毋庸置疑,但是问题来了:
如果读取文件的时候发生异常了怎么办?如果数据源发生变化该如何处理?如果网络请求返回的数据不符合最终要求怎么办?
如果你心里的第一个反应是改代码,那你就要注意了。完成一件事中间的某个环节发生变化,改代码是在所难免的,但是如果按照上面这种写法,不仅代码越改越乱,连逻辑也会越来越乱。单一职责原则表达的是让一个函数尽量只做一件事,不要将多件事混杂在一个函数中。
上面的代码如果重新设计,我认为至少应该是这样的:
defget_source():"""获取数据源"""returndefextract_(val):"""匹配关键数据"""returndeffetch(val):"""发出网络请求"""returndeftrim(val):"""修剪数据"""returndefextract(file):"""提取目标数据"""source=get_source()content=extract_(source)text=trim(fetch(content))returntextif__name__=="__main__":text=extract(FILE)print(text)
把原来放在一个函数中实现的多个步骤拆分成为多个更小的函数,每个函数只做一件事。当数据源发生变化时,只需要改动get_source相关的代码即可;如果网络请求返回的数据不符合最终要求,我们可以在trim函数中对它进行修剪。这样一来,代码应对变化的能力提高了许多,整个流程也变得更清晰易懂。改动前后的变化如下图所示:
单一职责原则的核心是解耦和增强内聚力,如果一个函数承担的职责过多,等于把这些职责耦合在一起,这种耦合会导致脆弱的设计。当发生变化时,原本的设计会遭受到意想不到的破坏。单一职责原则实际上是把一件事拆分成多个步骤,代码修改造成的影响范围很小。
让代码稳定性飞升的开放封闭原则和依赖倒置原则开放封闭原则中的开放指的是对扩展开放,封闭指的是对修改封闭。需求总是变化的,业务方这个月让你把数据存储到MySQL数据库中,下个月就有可能让你导出到Excel表格里,这时候你就得改代码了。这个场景和上面的单一职责原则很相似,同样面临代码改动,单一职责原则示例主要表达的是通过解耦降低改动的影响,这里主要表达的是通过对扩展开放、对修改封闭提高程序应对变化的能力和提高程序稳定性。
稳定这个词如何理解呢?
较少的改动或者不改动即视为稳定,稳定意味着调用这个对象的其它代码拿到的结果是可以确定的,整体是稳定的。
按照一般程序员的写法,数据存储的代码大概是这样的:
classMySQLSave:def__init__(self):passdefinsert(self):passdefupdate(self):passclassBusiness:def__init__(self):passdefsave(self):saver=MySQLSave()saver.insert()
功能是能够实现的,这点毋庸置疑。来看看它如何应对变化,如果要更换存储,那么就意味着需要改代码。按照上面的代码示例,有两个选择:
重新写一个存储到ExcelSave的类;对MySQLSave类进行改动;
上面的两种选择,无论怎么选都会改动2个类。因为不仅存储的类需要改动,调用处的代码也需要更改。这样一来,它们整体都是不稳定的。如果换一种实现方式,根据依赖倒置的设计指导可以轻松应对这个问题。边看代码边理解:
importabcclassSave(metaclass=abc.ABCMeta):
abc.abstractmethoddefinsert(self):passabc.abstractmethoddefupdate(self):passclassMySQLSave(Save):def__init__(self):self.classify="mysql"passdefinsert(self):passdefupdate(self):passclassExcel(Save):def__init__(self):self.classify="excel"definsert(self):passdefupdate(self):passclassBusiness:def__init__(self,saver):self.saver=saverdefinsert(self):self.saver.insert()defupdate(self):self.saver.update()if__name__=="__main__":mysql_saver=MySQLSave()excel_saver=Excel()business=Business(mysql_saver)这里通过内置的abc实现了一个抽象基类,这个基类的目的是强制子类实现要求的方法,以达到子类功能统一。子类功能统一后,无论调用它的哪个子类,都是稳定的,不会出现调用方还需要修改方法名或者修改传入参数的情况。
依赖倒置中的倒置,指的是依赖关系的倒置。之前的代码是调用方Business依赖对象MySQLSave,一旦对象MySQLSave需要被替换,Business就需要改动。依赖倒置中的依赖指的是对象的依赖关系,之前依赖的是实体,如果改为后面这种依赖抽象的方式,情况就会扭转过来:
实体Business依赖抽象有一个好处:抽象稳定。相对于多变的实体来说,抽象更稳定。代码改动前后的依赖关系发生了重大变化,之前调用方Business直接依赖于实体MySQLSave,通过依赖倒置改造后Busines和ExcelSave、MySQLSave全都依赖抽象。
这样做的好处是如果需要更换存储,只需要创建一个新的存储实体,然后调用Business时传递进去即可,这样可以不用改动Business的代码,符合面向修改封闭、面向扩展开放的开放封闭原则;
依赖倒置的具体实现方式使用了一种叫做依赖注入的手段,实际上单纯使用依赖注入、不使用依赖倒置也可以满足开闭原则要求,感兴趣的读者不妨试一试。
挑肥拣瘦的接口隔离原则接口隔离原则中的接口指的是Interface,而不是Web应用里面的Restful接口,但是在实际应用中可以将其抽象理解为相同的对象。接口隔离原则在设计层面看,跟单一职责原则的目的是一致的。接口隔离原则的指导思想是:
调用方不应该依赖它不需要的接口;依赖关系应当建立在最小接口上;
这实际上是告诉我们要给接口减肥,过多功能的接口可以选用拆分的方式优化。举个例子,现在为图书馆设计一个图书的抽象类:
importabcclassBook(metaclass=abc.ABCMeta):
abc.abstractmethoddefbuy(self):passabc.abstractmethoddefborrow(self):passabc.abstractmethoddefshelf_off(self):passabc.abstractmethoddefshelf_on(self):pass图可以被购买、可以被借阅、可以下架、可以上架,这看起来并没有什么问题。但这样一来这个抽象只能提供给管理人员使用,用户操作时需要再设定一个新的抽象类,因为你不可能让用户可以操纵图书上下架。接口隔离原则推荐的做法是把图书的上下架和图书购买、借阅分成2个抽象类,管理端的图书类继承2个抽象类,用户端的图书类继承1个抽象类。这么看起来是有点绕,不要慌,我们看图理解:
这样是不是一下就看懂了。这个指导思想很重要,不仅能够指导我们设计抽象接口,也能够指导我们设计Restful接口,还能够帮助我们发现现有接口存在的问题,从而设计出更合理的程序。
轻装上阵的合成复用原则合成复用原则的指导思想是:尽量使用对象组合,而不是继承来达到复用的目的。合成复用的作用是降低对象之间的依赖,因为继承是强依赖关系,无论子类使用到父类的哪几个属性,子类都需要完全拥有父类。合成采用另一种方式实现对象之间的关联,降低依赖关系。
为什么推荐优先使用合成复用,而后考虑继承呢?
搜索