设计模式

面向对象

面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。

面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。

面向对象编程是以类为思考对象。在进行面向对象编程的时候,我们并不是一上来就去思考,如何将复杂的流程拆解为一个一个方法,先去思考如何给业务建模,如何将需求翻译为类,如何给类之间建立交互关系,而完成这些工作完全不需要考虑错综复杂的处理流程。当我们有了类的设计之后,然后再像搭积木一样,按照处理流程,将类组装起来形成整个程序。这种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候,思路更加清晰。

面向对象的四大特征

这些特征提供了设计模式实现的基础,在设计模式种大量的使用了这些特征

封装

类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(函数)来访问内部信息或者数据,来达到访问权限控制的效果。主要是实现如何隐藏信息、保护数据。

如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另一方面来说,过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。

类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多。

在C#中一共有4种访问控制

  public

  private

  interval

  protected

抽象

抽象用于如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。

抽象这个概念是一个非常通用的设计思想。而且这个特性也并不需要编程语言提供特殊的语法机制来支持,只需要提供“函数”这一非常基础的语法机制,就可以实现抽象特性、所以,它没有很强的“特异性”,有时候并不被看作面向对象编程的特性之一。

继承

继承是用来表示类之间的 is-a 关系

继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。

过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。(多用组合少用继承)

多态

多态是指,子类可以替换父类

多态特性能提高代码的可扩展性和复用性。

 关于面向过程

面向过程是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。

面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。

我们很容易会造成使用面向对象的语言写出面向过程的代码,现在大部分都是使用的贫血模型(一个数据模型类种不不包含方法,仅仅作为数据载体,而充血模型就是包含方法)

基于贫血模型的传统的开发模式,比较适合业务比较简单的系统开发。相对应的,基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发。

SQL 驱动的开发模式,面向过程也比面向对象要更加符合人的思维习惯,面向对象更加需要抽象思维

开发原则

基于接口而非实现编程

封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。

越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。

多用组合少用继承

如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。

面向对象分析与设计

面向对象分析的产出是详细的需求描述,那面向对象设计的产出就是类。在面向对象设计环节,我们将需求描述转化为具体的类的设计

  划分职责进而识别出有哪些类;

  定义类及其属性和方法;

  定义类与类之间的交互关系;

  将类组装起来并提供执行入口。

设计模式

设计原则和思想其实比设计模式更加普适和重要,掌握了代码的设计原则和思想,我们甚至可以自己创造出来新的设计模式

设计模式要干的事情就是解耦

类的代码越多,就越难读懂,越难修改,维护的成本也就越高。很多设计模式都是试图将庞大的类拆分成更细小的类,然后再通过某种更合理的结构组装在一起。

区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。有时候从代码实现上来看,模式和模式之间有很多相似之处,但从设计意图上来看,它们是完全不同的。

设计原则

单一职责

一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

最开始可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是持续重构

开闭原则

添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。

最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程。很多设计原则、设计思想、设计模式,也都是以提高代码的扩展性为最终目的的。

里氏替换

子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。这就要求子类的实现不能违背父类的约定和实现

多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

接口隔离原则

接口的定义

  一组 API 接口集合单个

   API 接口或函数

  OOP 中的接口概念

客户端不应该被强迫依赖它不需要的接口。

函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。

控制反转、依赖反转、依赖注入

控制反转

框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。(是一种思想并不涉及到实现)

依赖注入

依赖注入是控制反转思想的一种实现,控制反转还有其他实现如模板模式

不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

依赖反转原则(依赖倒置原则)

高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。

KISS 原则(尽量保持简单)

KISS 原则是保持代码可读和可维护的重要手段。代码足够简单,也就意味着很容易读懂,bug 比较难隐藏。即便出现 bug,修复起来也比较简单。

不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。

不要过度优化,牺牲代码的可读性。

不要做过度设计和不要编写当前用不到的代码

DRY 原则(不要写重复的代码)

对于一个不同行为的两个方法中存在重复代码做法应该是对于包含重复代码的问题,我们可以通过抽象成更细粒度函数的方式来解决。

在开发新功能的时候,尽量复用已经存在的代码。

代码的可复用性表示一段代码可被复用的特性或能力:我们在编写代码的时候,让代码尽量可复用。

减少代码耦合、满足单一职责原则、模块化、业务与非业务逻辑分离、通用代码下沉、继承、多态、抽象、封装、应用模板等设计模式

除非有非常明确的复用需求,否则,为了暂时用不到的复用需求,花费太多的时间、精力,投入太多的开发成本,并不是一个值得推荐的做法。

迪米特法则

又叫作最少知识原则,一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。(不该有直接依赖关系的类之间,不要有依赖)

“高内聚、松耦合”是一个比较通用的设计思想,可以用来指导不同粒度代码的设计与开发,比如系统、模块、类,甚至是函数,也可以应用到不同的开发场景中,比如微服务、框架、组件、类库等。

“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计

在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。

依赖注入、接口隔离、基于接口而非实现编程,以及今天讲的迪米特法则,都是为了实现代码的松耦合。

项目开发设计思路和流程

完成接口设计,就相当于完成了一半的开发任务。只要接口设计得好,那代码就差不到哪里去。尽量保持接口的可复用性(接口粒度大小设计要平衡),但针对特殊情况,允许提供冗余的门面接口,来提供更易用的接口。

对于一个工程师来说,如果要追求长远发展,你就不能一直只把自己放在执行者的角色,不能只是一个代码实现者,你还要有独立负责一个系统的能力,能端到端(end to end)开发一个完整的系统。这其中的工作就包括:前期的需求沟通分析、中期的代码设计实现、后期的系统上线维护等。

业务逻辑工程师翻译业务逻辑,很很难用到设计原则、思想、模式。

面向对象设计聚焦在代码层面(主要是针对类),那系统设计就是聚焦在架构层面(主要是针对模块),两者有很多相似之处。

如果一个功能的修改或添加,经常要跨团队、跨项目、跨系统才能完成,那说明模块划分的不够合理,职责不够清晰,耦合过于严重。

一般来讲我们不希望下层系统(也就是被调用的系统)包含太多上层系统(也就是调用系统)的业务信息,但是,可以接受上层系统包含下层系统的业务信息。

数据库和接口的设计非常重要,一旦设计好并投入使用之后,这两部分都不能轻易改动。改动数据库表结构,需要涉及数据的迁移和适配;改动接口,需要推动接口的使用者作相应的代码修改。

接口设计要符合单一职责原则,粒度越小通用性就越好。但是,接口粒度太小也会带来一些问题。比如,一个功能的实现要调用多个小接口,一方面如果接口调用走网络(特别是公网),多次远程接口调用会影响性能;另一方面,本该在一个接口中完成的原子操作,现在分拆成多个小接口来完成,就可能会涉及分布式事务的数据一致性问题(一个接口执行成功了,但另一个接口执行失败了)。所以,为了兼顾易用性和性能,我们可以借鉴 facade(外观)设计模式,在职责单一的细粒度接口之上,再封装一层粗粒度的接口给外部使用。

对于非业务通用框架的开发,我们在做需求分析的时候,除了功能性需求分析之外,还需要考虑框架的非功能性需求。比如,框架的易用性、性能、扩展性、容错性、通用性等。

对于复杂的框架设计很多时候觉得无从下手可以采用最小原型法,让问题简化、具体、明确,提供一个可迭代的基础(小步快跑敏捷开发)。

一般是先有一个粗糙的设计,然后着手实现,实现的过程发现问题,再回过头来补充修改设计。

  1.划分职责识别有哪些类

  2.定义类及类之间的关系

  3.将类组装起来并提供执行入口

系统分层的作用

代码复用、隔离变化、隔离关注点、提高代码的可测试性、能使系统应对系统的复杂问题

层与层之间都有对应的数据对象,这样做的目的是尽量减少每层之间的耦合,吧职责边界划分明确,每层都只维护当前层对应的数据对象,对于非常大的项目来说,结构清晰才是第一位(层与层之间的对象转换有一些框架)

VO、BO、Entity 都是基于贫血模型的,而且为了兼容框架或开发库,我们还需要定义每个字段的 set 方法。这些都违背 OOP 的封装特性,会导致数据被随意修改。Entity 和 VO 的生命周期是有限的,都仅限在本层范围内。而对应的 Repository 层和 Controller 层也都不包含太多业务逻辑,所以也不会有太多代码随意修改数据,即便设计成贫血、定义每个字段的 set 方法,相对来说也是安全的。Service 层包含比较多的业务逻辑代码,所以 BO 就存在被任意修改的风险了。但是,设计的问题本身就没有最优解,只有权衡。

重构与规范

重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。

在保持功能不变的前提下,利用设计思想、原则、模式、编程规范等理论来优化代码,修改设计上的不足,提高代码质量。

重构是时刻保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。项目在演进,代码不停地在堆砌。如果没有人为代码的质量负责任,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。

优秀的代码或架构不是一开始就能完全设计好的,就像优秀的公司和产品也都是迭代出来的。我们无法 100% 遇见未来的需求,也没有足够的精力、时间、资源为遥远的未来买单,所以,随着系统的演进,重构代码也是不可避免的。

重构是避免过度设计的有效手段。在我们维护代码的过程中,真正遇到问题的时候,再对代码进行重构,能有效避免前期投入太多时间做过度的设计,做到有的放矢。

初级工程师在维护代码,高级工程师在设计代码,资深工程师在重构代码

在进行大型重构的时候,我们要提前做好完善的重构计划,有条不紊地分阶段来进行。每个阶段完成一小部分代码的重构,然后提交、测试、运行,发现没有问题之后,再继续进行下一阶段的重构,保证代码仓库中的代码一直处于可运行、逻辑正确的状态。每个阶段,我们都要控制好重构影响到的代码范围,考虑好如何兼容老的代码逻辑,必要的时候还需要写一些兼容过渡代码。只有这样,我们才能让每一阶段的重构都不至于耗时太长(最好一天就能完成),不至于与新的功能开发相冲突。可以使用静态代码分析工具(CheckStyle、FindBugs、PMD)

对于重构这件事情,资深的工程师、项目 leader 要负起责任来,没事就重构一下代码,时刻保证代码质量处在一个良好的状态。

最可落地执行、最有效的保证重构不出错的手段应该就是单元测试。单元测试除了能有效地为重构保驾护航之外,也是保证代码质量最有效的两个手段之一(另一个是 Code Review)。

有很多现成的工具专门用来做单元测试覆盖率统计

很多公司也不是特别重视单元测试包括BAT的一些开源项目中,由于单元测试需要编写测试代码,很多公司在实际执行中会存在难落地的问题

关于单元测试编写一个可测试性的代码也很重要,让单元测试代码不依赖任何不可控的外部服务。依赖注入是实现代码可测试性的最有效的手段。

当一个代码可测试性比较差的时候,那代码本身的设计也不够好

常见的反模式(代码中包含未决行为逻辑,滥用可变全局变量,滥用静态方法,使用复杂的继承关系,高度耦合的代码)

如果内功不够深厚,理论知识不够扎实,那就很难参透一个优秀项目的代码到底优秀在哪里。

规范

命名

代码注释

控制类,函数的大小

要有模块化和抽象思维善于将大块的复杂逻辑提炼成类或函数,屏蔽掉细节,让阅读的人不至于迷失在细节中,极大的提高代码的可读性、

避免函数参数过多

当一个函数中需要通过传入的参数来控制逻辑的时候,建议将其拆分为两个函数

避免层次过多的嵌套

使用解释性变量,尽量避免魔法数字

一共有23中设计模式

创建型

创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。

单例模式

工厂方法
抽象工厂
建造者模式
原型模式

结构型

结构型设计模式主要解决类或对象的组合或组装问题

代理模式

桥接模式
装饰器模式
适配器模式
外观模式
组合模式
享元模式

行为型

行为型设计模式主要解决的就是类或对象之间的交互问题

观察者模式
模板模式
策略模式
职责链模式
状态模式
迭代器模式
访问者模式
备忘录模式
命令模式
解释器模式
中介者模式

posted @ 2021-11-24 23:01  .NET_CJL  阅读(74)  评论(0编辑  收藏  举报