前言
身为程序员我们每天都与代码打交道,而编程思想则是程序员在编写程序时所遵循的一种思维方式和方法论。它涵盖了程序员在面对问题时的思考方式、解决问题的方法以及编写代码的技巧和规范,下面简单说一下
一、编程范式
我们编程的过程中常见以下三种范式,分别是结构化编程**(Structured Programming)、 面向对象编程 (Object-Oriented Programming)以及函数式编程(Functional Programming)**
结构化编程(Structured Programming)
结构化编程是一种编程范式,它强调在编写程序时应该使用结构化的、清晰、易于理解的控制结构,以提高代码的可读性和可维护性。结构化编程的核心思想是将程序的流程控制结构化为顺序、选择和循环,同时避免使用不受限制的分支和跳转语句。 结构化编程的主要特征包括以下几点:
- 顺序结构:程序由一系列按顺序执行的语句组成,每个语句都在前一个语句执行完毕后才执行。这使得代码的执行流程清晰明了。
- 选择结构 :结构化编程支持条件语句,例如 if 和 switch,以根据条件执行不同的代码块。条件语句可以嵌套,但应保持简洁。
- 循环结构 :结构化编程允许使用循环语句,如 for 、while 和 do-while,来多次执行相同的代码块,从而避免重复的代码。
- 模块化:结构化编程鼓励将代码划分为小的模块或函数,每个模块执行特定的任务。这种模块化的方式使得代码更易于理解、测试和维护。
- 递归:结构化编程也支持递归,但要求递归调用是有限的,避免无限递归。
- 禁止不受限制的跳转和分支 :结构化编程通常禁止使用不受限制的 goto 语句,因为它可以导致难以理解和维护的代码。
结构化编程的目标是降低程序的复杂性,减少错误和调试的难度,使代码更具可读性和可维护性。
面向对象编程(Object-Oriented Programming,OOP)
面向对象编程是一种常用的编程范式,它的核心思想是将程序中的数据(对象)和操作数据的方法(函数)组织成对象,以模拟现实世界中的实体和它们之间的关系。面向对象编程强调数据封装、继承和多态等概念,以提高代码的可重用性、可维护性和可扩展性。 面向对象编程的主要特征包括以下几点:
- 类与对象:面向对象编程通过定义类(Class)来描述对象的结构和行为。类是对象的模板,它定义了对象具有的属性(成员变量)和方法(成员函数)。对象是类的实例化,每个对象都具有类定义的属性和方法。
- 封装:封装是将数据和方法打包在一个对象中,并对外部隐藏对象内部的细节。通过访问修饰符(如私有、公共、受保护等),可以控制对象的哪些部分可以被外部访问,从而提高数据的安全性和代码的可维护性。
- 继承:继承是一种机制,允许创建一个新类(子类或派生类),该类继承了一个或多个现有类(父类或基类)的属性和方法。继承可以促进代码的重用,减少代码的重复编写。
- 多态:多态性允许不同的对象对相同的消息作出不同的响应。它通过方法的重写(覆盖)和接口的实现来实现。多态性提高了代码的灵活性和可扩展性。
- 抽象类与接口:抽象类是一种不能被实例化的类,它可以包含抽象方法,需要子类实现。接口是一种纯抽象类,定义了一组方法的签名,需要实现这些方法的具体细节。抽象类和接口用于定义规范和约定,以确保对象的一致性。
- 消息传递:面向对象编程通过对象之间的消息传递来实现操作。对象可以向其他对象发送消息,请求执行某个方法,这种消息传递是面向对象编程的核心概念。
面向对象编程通常用于构建复杂的软件系统,它将问题分解为对象的组合,每个对象负责特定的任务。这种编程范式有助于提高代码的可维护性、可理解性和可扩展性,因此在现代软件开发中得到广泛应用。许多编程语言,如**Java、C++、Python、C#**等,都支持面向对象编程,提供了类和对象的概念以及相应的语法和工具。
函数式编程(Functional Programming,FP)
函数式编程是一种编程范式,它将计算视为数学函数的求值过程,强调使用纯函数和避免可变状态和可变数据。函数式编程的核心思想是将计算过程分解为一系列函数的组合和应用,这些函数不会修改状态或产生副作用,而只是根据输入生成输出。函数式编程可以帮助程序员编写更简洁、可维护、并发安全的代码,并提供一种不同于传统命令式编程的思考方式。 函数式编程的主要特征包括:
- 纯函数 :纯函数是指具有以下性质的函数:
- 对于相同的输入,总是产生相同的输出。
- 不会修改外部状态或变量。
- 不依赖于外部状态或变量。
- 不可变性:函数式编程鼓励使用不可变数据结构,这意味着一旦数据被创建,就不能被修改。如果需要修改数据,必须创建一个新的数据副本。不可变性有助于避免竞态条件和并发问题,使代码更加稳定。
- 高阶函数:高阶函数是可以接受一个或多个函数作为参数,并/或返回一个函数作为结果的函数。高阶函数允许将函数作为一等公民来处理,从而支持函数的组合和抽象。
- 递归:函数式编程通常使用递归代替循环来实现迭代过程。递归是一种自引用的技术,它可以用于处理列表、树等数据结构。
- 惰性计算:函数式编程中的某些操作(如映射、过滤、折叠)可能是惰性的,也就是说它们不会立即执行,而是在需要时才执行。这可以提高性能和资源利用率。
- 不可变性:函数式编程鼓励使用不可变数据结构,这意味着一旦数据被创建,就不能被修改。如果需要修改数据,必须创建一个新的数据副本。不可变性有助于避免竞态条件和并发问题,使代码更加稳定。
- 引用透明性:引用透明性是指一个函数的调用结果只取决于其输入参数,而不依赖于任何外部状态或变量。这使得函数在不同的上下文中可以安全地替换和重用。
函数式编程通常用于解决复杂的问题,如并发编程、数据处理、事件驱动编程等。一些编程语言,如Haskell、Scala、Clojure、Elixir 等,被设计为纯函数式编程语言,而其他语言,如JavaScript、Python、Java等,则提供了函数式编程的特性和库。函数式编程的思想也在现代编程中得到了广泛的应用,尤其是在大数据处理、分布式系统和前端开发领域。
二、设计原则
设计原则和架构之间有着紧密的联系。设计原则是我们进行架构设计的指导思想,它指导我们如何将数据和函数组织成类,以及如何将类链接起来成为组件和程序。而架构的主要工作则是将软件拆解为组件,设计原则则指导我们如何进行拆解、拆解的粒度、组件间依赖的方向以及组件解耦的方式等。
主导原则:OCP(开闭原则) 类和代码的层级上:SRP(单一职责原则)、LSP(里氏替换原则)、ISP(接口隔离原则)、DIP(依赖反转原则) 在组件的层级上:REP(复用、发布等同原则)、CCP(共同闭包原则)、CRP(共同复用原则) 。 处理组件依赖问题的三个原则:无依赖环原则、稳定依赖原则以及稳定抽象原则。
这些设计原则和架构的原则都是为了提高软件的可维护性、可扩展性和可重用性。它们帮助我们构建出更加健壮、灵活和高效的系统。
为什么要遵循设计原则 ?
设计原则是做设计时所要依据的准则,它为我们的设计提供向导,体现我们的设计价值观。遵循设计原则可以使我们的代码更加可读性、可维护性和可扩展性。同时,它也可以帮助团队成员更好地理解和维护代码,从而提高团队的协作效率。
1.OCP(开闭原则)
- 对扩展开放(Open for Extension):这意味着在不修改现有代码的情况下,应该能够添加新功能或扩展现有功能。这可以通过创建新的类、接口、模块等方式来实现。
- 对修改关闭(Closed for Modification):这意味着一旦一个软件实体(如类)被创建并投入使用,就不应该对其进行修改。任何变化应该通过扩展来实现,而不是直接修改原有代码。
开闭原则的目标是提高软件系统的可维护性、可扩展性和可复用性。它鼓励使用抽象、接口、多态等面向对象编程的特性来实现扩展,同时避免破坏现有代码,从而降低了引入新功能时引入错误的风险。
开闭原则通常与设计模式、依赖注入、接口抽象等编程概念和技术一起使用,以创建灵活且易于维护的软件系统。遵循这一原则有助于减少代码的耦合性,提高代码的可测试性,并支持持续集成和持续交付等开发实践。
2.SRP(单一职责原则)
- 一个类应该只有一个原因引起变化:这意味着一个类应该只有一个职责或功能。如果一个类有多个不同的原因可能会导致它发生变化,那么这个类就违反了单一职责原则。
- 高内聚性:单一职责原则鼓励将与一个类的职责相关的数据和方法封装在一起,以确保类的内部结构具有高内聚性。高内聚性有助于提高代码的可读性、可维护性和可测试性。
单一职责原则的目标是确保每个类都专注于执行一项明确定义的任务,从而降低了类的复杂性,提高了代码的可理解性和可维护性。它有助于避免类的膨胀和混乱,减少了因修改一个职责而引起的潜在问题。
遵循单一职责原则通常需要进行类的拆分和重构,将大型、复杂的类拆分成多个小而精确的类,每个类负责一个明确的职责。这样的设计有助于降低耦合性,提高代码的可重用性,并支持面向对象编程的其他原则和设计模式。
3.LSP(里氏替换原则)
- 子类型必须完全实现基类的接口:子类型应该继承或实现基类的所有属性和方法,以确保能够无缝地替换基类的实例。子类型不应该删除基类的属性或方法,也不应该引入与基类不兼容的新属性或方法。
- 子类型可以扩展基类的功能:虽然子类型必须保持对基类的兼容性,但它们可以添加额外的功能或修改基类的实现。这允许子类型在不破坏替换性的前提下,提供自己的特定行为。
- 保持不变性:子类型不应该破坏基类的不变性条件。这意味着子类型的操作不应该导致基类的不变性条件变为假。这是确保程序的正确性的关键。
LSP的目标是确保在使用多态性时,代码可以安全地替换基类的实例为子类的实例,而不会引发错误或破坏程序的正确性。遵循LSP有助于提高代码的可维护性、可扩展性和可重用性,因为它允许开发人员在不改变现有代码的前提下,引入新的子类型。
这一原则的实际应用通常涉及到合理的继承和多态设计,以确保子类型可以无缝替换基类,从而实现面向对象编程的核心特性之一。
4.ISP(接口隔离原则)
- 接口应该小而专一:接口应该只包含客户端需要的方法,而不应该包含大量不相关的方法。这意味着接口应该尽可能小,不应该过度臃肿。
- 类不应该被强迫实现不需要的接口:如果一个类实现了一个包含多个方法的接口,但只使用了其中的一部分方法,那么这个类就被强迫实现了它不需要的方法,这是不合理的。
- 客户端不应该依赖于它们不使用的接口:客户端类应该只依赖于它们真正需要的接口,而不应该依赖于那些它们不使用的接口。这可以通过将接口细分成更小的、更专一的接口来实现。
ISP原则的目标是减少类之间的耦合性,提高代码的可维护性和灵活性。遵循ISP原则有助于防止类变得庞大而复杂,减少不必要的依赖关系,使代码更易于理解、测试和修改。
一个常见的实践是将一个大型接口分解成多个小型接口,每个接口只包含一组相关的方法。这样,类可以选择性地实现它们真正需要的接口,而不需要强制性地实现不相关的方法。这有助于遵循ISP原则,使接口更加灵活和可定制。
5.DIP(依赖反转原则)
- 高层模块与低层模块都应该依赖于抽象:高层模块(如应用程序的业务逻辑)和低层模块(如数据访问层或底层设备驱动)都应该依赖于抽象,而不应该直接依赖于具体的实现细节。
- 抽象不应该依赖于细节:抽象接口或类不应该依赖于具体实现的细节。这意味着抽象应该定义通用的行为和契约,而不关心底层细节的实现方式。
- 细节应该依赖于抽象:具体的实现细节(如具体的数据库访问类或底层硬件驱动)应该依赖于抽象定义的接口或类。这确保了细节能够适应不同的抽象实现,而不需要修改高层模块的代码。
DIP原则的目标是降低模块之间的耦合性,提高代码的可扩展性和可维护性。通过遵循DIP原则,可以实现以下优点:
- 容易替换底层实现:由于高层模块依赖于抽象,因此可以轻松地替换底层实现细节,而不会影响高层模块的代码。
- 容易进行单元测试:通过将高层模块与抽象接口进行解耦,可以更容易地编写单元测试,使用模拟对象**(Mock)**替代底层实现。
- 降低代码修改的风险:当需要修改底层实现时,不必修改高层模块的代码,只需确保新的实现符合抽象接口的契约即可。
DIP 原则通常与依赖注入**(Dependency Injection)一起使用,依赖注入是一种实现 DIP的方式,通过将依赖关系从高层模块外部注入,以确保高层模块不需要自己创建或直接依赖于底层模块的实例。这有助于更好地符合DIP**原则。
6.REP(复用、发布等同原则)
REP 原则是指组件中的类与模块必须是彼此紧密相关的,一个组件不能由一组毫无关联的类和组件组成,它们之间应该有一个共同的主题或大致方向。同时,一个组件中包含的类和模块还应该是可以同时发布的,这意味着它们共享相同的版本号与版本跟踪,并且包含在相同的发行文档中。 需要注意的是,REP原则并不适用于所有情况。有时候,将相关的类和模块分离到不同的组件中可能更有利于代码的组织和维护。因此,在实际开发中,我们需要根据具体的需求和项目特点来灵活运用这个原则。
7.CCP(共同闭包原则)
对于大部分应用程序来说,可维护性的重要性要远高于可复用性。如果某个程序中的代码必须要进行某些变更,那么这些变更最好都体现在同一个组件中,而不是分布于很多个组件中。因为如果这些变更都集中在同一个组件中,我们就只需要重新部署该组件,其他组件则不需要被重新验证、重新部署了。 CCP的主要作用就是提示我们要将所有有可能会被一起修改的类集中在一处。也就是说,如果两个类紧密相关,不管是源代码层面还是抽象理念层面,永远都会一起被修改,那么它们就应该被归属为同一个组件。通过遵守这个原则,我们就可以有效地降低因软件发布、验证及部署所带来的工作压力。
8.CRP(共同复用原则)
通常情况下,类很少会被单独复用。更常见的情况是多个类同时作为某个可复用抽象定义被共同复用。CRP 原则指导我们将这些类放在同一个组件中,而在这样的组件中,我们应该预见到会存在着许多相互依赖的类。 CRP原则的作用不仅是告诉我们应该将那些类放在一起,更重要的是要告诉我们应该将哪些类分开。因为每当一个组件应用了另一个组件时,就等于增加了一条依赖关系。虽然这个引用关系仅涉及被引用组件中的一个类,但它所带来的依赖关系丝毫没有减弱。也就是说,引用组件已然依赖于被引用的组件了。 由于这种依赖关系的存在,每当被引用组件发送变更时,引用它的组件一般也需要做出相应的变更。即使它们不需要进行代码级的变更,一般也免不了需要被重新编译、验证和部署。哪怕引用组件根本不关心被引用组件中的变更,也要如此。 因此,当我们决定要依赖某个组件时,最好是实际需要依赖该组件中的每个类。换句话说,我们希望组件中的所有类是不能拆分的,即不应该出现别人只需依赖它的某几个类而不需要其他类的情况。否则,我们后续就好浪费不少时间与精力来做不必要的组件部署。
8.1 REP、CCP、CRP三者关系
REP、CCP、CRP 三个原则之间存在彼此竞争的关系,REP 和 CCP 是黏合性原则,它们会让组件变得更大,而 CRP 原则是排除性原则,它会让组件变小。遵守**REP、CCP **而忽略 CRP ,就会依赖了太多没有用到的组件和类,而这些组件或类的变动会导致你自己的组件进行太多不必要的发布;遵守 REP 、CRP 而忽略 CCP,因为组件拆分的太细了,一个需求变更可能要改n个组件,带来的成本也是巨大的。
9.无依赖环原则(Acyclic Dependencies Principle,ADP)
- 无循环依赖:ADP 要求在软件系统中不应该存在循环依赖关系。这意味着一个组件不应该直接或间接地依赖于自己,也不应该依赖于依赖于它的组件。
- 松耦合:ADP 有助于实现松耦合,因为循环依赖会导致高度耦合的组件。通过确保依赖关系是无环的,可以降低组件之间的耦合度,使系统更加灵活和可维护。
- 依赖倒置:ADP 通常与依赖倒置原则**(DIP)**一起使用。DIP 要求高级组件不应该依赖于低级组件,而是两者都应该依赖于抽象。ADP 可以帮助确保这种抽象关系不会导致循环依赖。
无依赖环原则有助于减少代码复杂性和提高系统的可维护性。它鼓励开发人员在设计和组织组件时考虑依赖关系,并避免不必要的复杂性和错误。这个原则在大型软件系统的设计和架构中特别有用,可以帮助避免潜在的设计问题。
10.稳定依赖原则(Stable Dependencies Principle,SDP)
- 稳定性 :SDP 强调高层模块或组件应该更加稳定,而低层模块或组件应该更加不稳定。稳定性指的是组件的变更频率,稳定的组件不容易发生变化,而不稳定的组件可能会频繁变化。
- 依赖方向 :SDP 鼓励依赖关系的方向应该从不稳定的组件指向稳定的组件。也就是说,稳定的组件不应该依赖于不稳定的组件,因为不稳定的组件可能会频繁变化,导致对稳定组件的影响。
- 稳定性级别 :SDP 将组件分为不同的稳定性级别,通常使用数字来表示。例如,级别 1 表示最不稳定的组件,级别 10 表示最稳定的组件。稳定的组件应该依赖于比它更不稳定的组件。
- 可维护性:通过遵循稳定依赖原则,可以提高系统的可维护性。稳定的组件变化较少,因此不会频繁影响其他组件,使系统更容易维护和扩展。
- 逆向依赖关系 :SDP 还提到了逆向依赖关系,即反向依赖。它表示低层次的不稳定组件依赖于高层次的稳定组件,这种情况应该尽量避免,因为它可能导致设计的混乱和不稳定。
稳定依赖原则有助于设计和组织软件系统,以确保高层次的模块或组件不容易受到底层模块的变化影响。这有助于降低系统中的意外副作用和错误,提高代码的可维护性和稳定性。在软件架构和设计中考虑稳定依赖原则可以帮助开发人员制定合理的依赖策略。
11.稳定抽象原则
- 稳定性:与稳定依赖原则类似,稳定抽象原则也关注稳定性,但侧重于抽象层次。稳定性指的是抽象的变化频率,稳定的抽象不容易发生变化,而不稳定的抽象可能会频繁变化。
- 抽象级别:软件系统通常由多个抽象层次组成,从高级别的抽象到低级别的具体实现。稳定抽象原则建议高级别的抽象应该更加稳定,而低级别的抽象应该更加不稳定。这意味着高级别的抽象不应该频繁变化,以确保系统的稳定性。
- 依赖方向:与稳定依赖原则类似,稳定抽象原则也强调依赖关系的方向。稳定的抽象应该被不稳定的抽象依赖,而不应该依赖于不稳定的抽象。这有助于确保高级别的抽象不容易受到低级别实现的变化影响。
- 稳定性级别:类似于稳定依赖原则,稳定抽象原则也使用数字来表示抽象的稳定性级别。高级别的抽象应该拥有较高的稳定性级别,而低级别的抽象应该拥有较低的稳定性级别。
- 可维护性:通过遵循稳定抽象原则,可以提高系统的可维护性。稳定的抽象不容易发生变化,因此高级别的抽象不会频繁影响到依赖它的其他部分,从而使系统更易于维护和扩展。
稳定抽象原则与稳定依赖原则一起帮助开发人员设计稳定且易于维护的软件系统。它强调了抽象的重要性,高级别的抽象应该是系统的核心,并且应该是稳定的,以确保系统的稳定性和可维护性。通过合理设计抽象层次和依赖关系,可以更好地应对变化和需求的演化。
三、总结
编程范式与设计原则是软件开发中的两个关键概念,它们对于构建高质量、可维护和可扩展的软件系统至关重要。编程范式提供了一种编码和思考的方法,而设计原则则为我们提供了一组指导原则,以确保代码的质量和可维护性。 总的来说,编程范式和设计原则是软件开发的基石,它们有助于创建高质量、可维护和可扩展的软件系统。了解它们并将它们应用到实际项目中将有助于提高代码的质量和可维护性,从而为用户提供更好的体验。