前沿:
随着程序规模的增大,程序功能的越发复杂,如何组织程序,如何实现具体的功能成为了程序设计中的一些重要问题,下面介绍程序设计模式中的七大原则与软件工程的核心原则。
注:本文只是对七大原则进行介绍,并没有针对具体的设计模式进行详细的阐述。
1. 开闭原则OCP(Open-Closed Principle)
- **定义:对扩展开放,对修改关闭。**在程序需要扩展的时候,不要去修改原来的代码,而是通过定义接口来扩展功能,需要扩展新功能的时候,实现新的接口,而不是修改原来的代码。
- 例子:例如实现图形面积的计算类,不要为每个具体的图形定义特定的计算方法,而是定义一个抽象的Shape类(接口最好,但感觉Shape当基类也挺合适),在内部自己实现面积的计算,需要添加对新图形的支持时,只需要实现新的Shape类,而不需要修改原来其他图形计算的代码。
2. 单一职责原则SRP(Single Responsibility Principle)
- **定义:一个类只负责一项职责,即只做一件事。**如果一个类承担的职责过多,如果修改这类的一个功能,可能导致其他的功能损坏。
- 例子:如果一个UserService类,他既承担用户的登录功能,又承担用户的信息管理功能,那么当需要修改登录功能时,可能会影响到用户信息管理功能。应该将登录功能和用户信息管理功能分离到不同的类中。
3.里氏替换原则LSP(Liskov Substitution Principle)
- 定义:所有用到父类的方法都能被子类替换并保持正确性即子类必须完全实现基类的方法,才能替换基类对象。只要父类出现的地方子类都可以出现,而且替换为子类不会引起任何错误。
- 例子:假如一个父类有一个方法,但子类对这个方法加了新的约束,这样子类就无法在使用这个方法的地方替换父类,应该把这个方法变为独立的方法,而不是重写原方法。
4. 接口隔离原则ISP(Interface Segregation Principle)
- 定义:使用多个专门的接口,而不使用单一的总接口。即接口应该被细分为更具体的接口,减少接口中的依赖性。
- 例子:例如,一个电商系统,应该设计一个购物车接口,一个订单接口,一个支付接口,而不是设计一个通用的接口。
5. 依赖倒置原则DIP(Dependency Inversion Principle)
- **定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。**即要依赖抽象,不要依赖具体实现,具体实现应该依赖抽象。
- 例子:例如,一个电商系统,应该设计一个购物车接口,一个订单接口,一个支付接口,而不是设计一个具体的购物车类,具体的订单类,具体的支付类。
6. 迪米特法则LoD(Law of Demeter)
- **定义:一个对象应该对其他对象有最少的了解。**即一个对象应该尽可能少的与其他对象通信,只与必要的对象通信。
- 例子:例如一个电商系统,只与购物车对话,购物车再与商品对话,把购物车作为中间层,从而减少与多个对象的通信。
7. 合成复用原则CRP(Composite Reuse Principle)
- **定义:尽量使用对象组合,而不是继承来达到复用的目的。**在面向对象设计中,组合比继承更加灵活,它可以在运行时动态地改变对象的行为(即动态的功能扩展)将一整个类变为多个小的模块,而继承则是在编译时就已经确定了对象的行为。
- 使用组合既可以避免继承层次过深过宽的问题,同时也可以提高代码的可维护性和可扩展性。
- 用组合的功能一般用接口也可以实现,但组合更加灵活,适用于需要动态行为、能够避免继承层次过深、提高可扩展性;而接口被继承就是写死的了,不能向组合一样随意地替换被组合的类。
- 例子:假如有一个智能模块类和一个智能电器类,就可以把智能模块类组合进智能电器类中,就可以获得智能电器类的功能,同时可以随意替换具体的智能模块(如果有需要替换的场景,比如适配不同种类的智能模块或不同厂商的)。
软件工程核心原则:
这七大基本原则能够让代码结构更加清晰,但同样会带来一些问题,教条的遵守上面的原则可能会使项目变得更加糟糕,在构建程序的时候也要注意下面的原则:
KISS原则(Keep It Simple and Stupid):
- **定义:即保持简单愚蠢原则。在设计和实现过程中,尽量保持简单。**不要过度设计,不要过度复杂化,不要过度工程化,会导致代码难以理解、维护。通过保持代码简单,可以提高开发效率,减少错误。对于轻量的小代码,强硬的按照上面的原则构建项目拆分文件只会增加复杂度,降低阅读效率,是代码更加难理解。
DRY原则(Don’t Repeat Yourself):
- **定义:即不要重复自己原则。在设计和实现过程中,不要重复代码,保持代码的一致性和可维护性。**应将重复的代码提取到函数和类中,实现复用。通过减少代码冗余,可以提高代码的可维护性,并减少错误。
YAGNI原则(You Ain’t Gonna Need It):
- 定义:即不会需要它原则,如果你不需要这个功能,先不要实现,有的时候为了满足上面的原则,强行抽象,只是为了写代码而写代码。**设计模式是为了解决复杂性,而不是制造复杂性,**有句话是“手里拿着锤子,看什么都是钉子”,不能为了使用而去使用。
SOLID原则:
- **定义:**就是上面前5条,省略
写者感悟
注:最后一部分纯属我再发牢骚,路人不用看
上述所讲的技巧是为了解决复杂性,而不是制造复杂性,封装能够使得代码更加将各个清晰,但盲目的封装会导致代码过于冗杂教条,需要自己掌握好度,这个是比较抽象的一个任务。学习完设计原则后不能把手段当成目的,不能脱离实际坚持某种特定形式,不能为了封装而封装,不能迷信于文件分割,不能脱离语境用架构。
我是这么理解的,假如只是下楼丢个垃圾 (简单的课程作业),还要确定一份详细的《出行计划书》(定义严格的文件结构);准备一个专业的登山包,虽然只装一把钥匙(封装类,但是类里基本没东西,而且是单例类),穿上全套的登山装(复杂的设计模式),反而会让事情变得更加复杂,浪费时间和精力。如果这个人要去爬山,他的做法是正确的,但他只是去简单下个楼,他就是荒谬的。“杀鸡焉用牛刀”,大概就是这个道理。
C++之父Bjarne Stroustrup说过:如果一个函数就能解决问题,就不要写一个类。就如STL内的方法都是独立函数,而不是类.方法,把所有东西塞进class内,只是用面向对象的壳子写面向过程的代码,这样实际上是违背KISS原则的。形式主义不可取,简单易懂的才是好代码,能灵活选择架构模式的程序员才是好程序员。
此外,盲目的采取分文件编写还会降低代码的可读性,阅读的时候需要跳跃式阅读,,信噪比极低,在一些紧密耦合的逻辑中强行拆分可能会使自然而然的高内聚破坏,还增加了额外的接口调用、指针传递。真正好的代码数据流向明确,状态变化方便追踪;而不是文件列表看起来整齐,每个文件都很短。
糟糕的代码是面条,逻辑混为一团,还存在着很多的“上帝类”(即杂糅了全部功能的类);教条的代码是饺子,成百上千个单体,但不知道如何组合,还不知道关键在哪里;好的代码是千层面,层次分明(头部定义,工具函数,主逻辑),结构紧凑。不要盲目的在小项目中追求大型软件工程的标准,不要用空间复杂度换取所谓的结构美感,还是那句话:原则是解决问题的,不是制造问题的。
软件工程始终在复杂性与灵活性中权衡,SOLID可以提高代码的可维护性,可扩展性,可复用性;但会增加代码量,增加文件数量,降低代码的直观性。在不考虑维护性、扩展性、复用性的情况下(例如练手的小项目与课程小作业之类),严格按照SOLID的收益为零(写完就结束不要维护、不需要进一步扩展、不需要拿着个模块给别的项目用),就可以适当放宽这些要求。为了搭一个狗窝,画摩天大楼般的图纸,使用钢筋搭建,反而得不偿失。
著名的计算机领域开创者Donald Knuth说过:“Premature optimization is the root of all evil.” (过早优化是万恶之源),对这句话正是对KISS原则最好的诠释,对于不需要的东西不要去特意去干,不要为了不必要的事去牺牲简洁性。同理,过早抽象也是万恶之源:Scott Meyers说过:“如果一个函数不需要访问类的私有成员,那就不要把它变成成员函数。”,在代码规模很小的时候,函数就是最优秀的抽象工具,命名空间就是最简洁的封装方法,只使用一次的单例类有时候不需要单独建立。只有当函数多到管理不来的时候,或者难以维护的时候才需要升级到类;只有当类多到需要解耦的时候,或是支持多种实现方式的时候才需要抽象到接口,跳过这些步骤只是为了设计而设计。设计原则本身是管理复杂度的,当复杂度很低的时候引入设计模式本身就是一种复杂度。对于一次性、需求固定的小作业上,过程化设计客观上是优于之,前提到的复杂设计的,是能够避免样板代码的。
此外在一些多范式语言中,能够使用多种编程范式(例如面向对象,面向过程,函数式编程等),要灵活的根据不同的环境使用不同的范式,不要拘泥于某一种范式。例如Python,JavaScript等语言,函数式编程的风格在一些环境中往往更加简洁高效,而不是一味地使用面向对象的设计模式。
