设计模式之美(⼀)——设计原则、规范与重构
  《》是极客时间上的⼀个代码学习系列,在学习之后特在此做记录和总结。
⼀、设计原则
1)SRP
  单⼀职责原则(Single Responsibility Principle,SRP)是指⼀个类或者模块只负责完成⼀个职责(或者功能),模块可看作⽐类更加粗粒度的代码块,模块中包含多个类,多个类组成⼀个模块。
  ⼀个类包含了两个或者两个以上业务不相⼲的功能,那就说它职责不够单⼀,应该将它拆分成多个功能更加单⼀、粒度更细的类。
  判断类的职责是否⾜够单⼀,需要根据具体的应⽤场景和阶段需求,例如。
  (1)如果在社交产品中,⽤户的地址信息只是单纯地⽤来展⽰,那 UserInfo 可包含地址信息。
  (2)如果社交产品中添加了电商模块,⽤户的地址信息还会⽤在电商物流中,那最好将地址信息从 UserInfo 中拆分出来。
  由此可知,评价⼀个类的职责是否⾜够单⼀,并没有⼀个⾮常明确的、可以量化的标准。
  下⾯这⼏条拆分判断原则,要更有指导意义、更具有可执⾏性:
  (1)类中的代码⾏数、函数或属性过多,会影响代码的可读性和可维护性,⾏数最好不超过 200 ⾏,函数个数及属性个数都最好不超过 10 个。
  (2)类依赖的其他类过多,或者依赖类的其他类过多,不符合⾼内聚、低耦合的设计思想。
  (3)私有⽅法过多,就要考虑能否将私有⽅法独⽴到新的类中,设置为 public ⽅法,提⾼代码的复⽤性。
  (4)⽐较难给类起⼀个合适名字,很难⽤⼀个业务名词概括,这就说明类的职责定义得可能不够清晰。
  (5)类中⼤量的⽅法都是集中操作类中的某⼏个属性,那就可以考虑将这⼏个属性和对应的⽅法拆分出来。
2)OCP
  开闭原则(Open Closed Principle,OCP)是指添加⼀个新的功能,在已有代码基础上扩展代码(新增模块、类、⽅法等),⽽⾮修改已有代码(修改模块、类、⽅法等)。
  注意,没必要纠结某个代码改动是“修改”还是“扩展”,更没必要太纠结它是否违反“开闭原则”。只要没有破坏原有代码和单元测试的正常运⾏,就可以说,这是⼀次合格的代码改动。
  不过,有些修改是在所难免的,是可以被接受的。尽量让修改操作更集中、更少、更上层,尽量让最核⼼、最复杂的那部分逻辑代码满⾜开闭原则。
  偏向顶层的指导思想:多花点时间思考,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不改动代码整体结构、做到最⼩代码改动的情况下,新的代码能够很灵活地插⼊到扩展点上,做到“对扩展开放、对修改关闭”。
  实际上,多态、依赖注⼊、基于接⼝⽽⾮实现编程,以及抽象意识,说的都是同⼀种设计思路:提升代码扩展性,只是从不同的⾓度、不同的层⾯来阐述⽽已。
  基于接⼝⽽⾮实现编程的设计初衷是,将接⼝和实现相分离,封装不稳定的实现,暴露稳定的接⼝。要遵从该原则,需要做到下⾯这 3点。
  (1)函数的命名不能暴露任何实现细节。⽐如, uploadToAliyun() 就不符合要求,改为更加抽象的
命名⽅式:upload()。
  (2)封装具体的实现细节。例如对上传(或下载)流程进⾏封装,对外提供⼀个包裹所有上传(或下载)细节的⽅法,给调⽤者使⽤。
  (3)为实现类定义抽象的接⼝。使⽤者依赖接⼝,⽽不是具体的实现类来编程。
  如何在项⽬中灵活运⽤ OCP:
  (1)对于⼀些⽐较确定的、短期内可能就会扩展,或者需求改动对代码结构影响⽐较⼤的情况,或者实现成本不⾼的扩展点,在编写代码的时候,就可以事先做些扩展性设计。
  (2)但对于⼀些不确定未来是否要⽀持的需求,或者实现起来⽐较复杂的扩展点,可以等到有需求驱动的时候,再通过重构代码的⽅式来⽀持扩展的需求。
3)LSP
  ⾥式替换原则(Liskov Substitution Principle,LSP)是指⼦类对象能够替换程序中⽗类对象出现的任何地⽅,并且保证原来程序的逻
辑⾏为不变及正确性不被破坏。
  ⾥式替换原则就是⼦类完美继承⽗类的设计初衷,并做了增强。
  与多态的区别:
  (1)多态是⾯向对象编程的⼀⼤特性,也是⾯向对象编程语⾔的⼀种语法和代码实现的思路。
  (2)⾥式替换是⼀种设计原则,⽤来指导继承关系中⼦类该如何设计的。
  按照协议来设计:
  (1)⼦类在设计的时候,要遵守⽗类的⾏为约定(或者叫协议)。
  (2)⽗类定义了函数的⾏为约定,那⼦类可以改变函数的内部实现逻辑,但不能改变函数原有的⾏为约定。这⾥的⾏为约定包括:
    a、函数声明要实现的功能;
    b、对输⼊、输出、异常的约定;
    c、甚⾄包括注释中所罗列的任何特殊说明。
  实际上,定义中⽗类和⼦类之间的关系,也可以替换成接⼝和实现类之间的关系。
4)ISP
  接⼝隔离原则(Interface Segregation Principle,ISP)是指接⼝的调⽤者或使⽤者不应该强迫依赖它不需要的接⼝。
  接⼝可理解为下⾯三种东西:
  (1)⼀组 API 接⼝集合。
  例如将删除接⼝单独放到另外⼀个接⼝ RestrictedUserService 中,⽽不是 UserService 中,只打包提供给后台管理系统来使⽤。
  (2)单个 API 接⼝或函数。
  函数的设计要功能单⼀,不要将多个不同的功能逻辑在⼀个函数中实现。
  (3)OOP 中的接⼝概念。
  例如设计⼀个功能单⼀的接⼝:Updater。ScheduledUpdater 只依赖 Updater 这个跟热更新相关的接⼝,不需要被强迫去依赖不需要的Viewer 接⼝。
  与单⼀职责原则的区别:
  (1)单⼀职责原则针对的是模块、类、接⼝的设计。
  (2)接⼝隔离原则相对于单⼀职责原则,⼀⽅⾯它更侧重于接⼝的设计,另⼀⽅⾯它的思考⾓度不同。
  它提供了⼀种判断接⼝是否职责单⼀的标准:通过调⽤者如何使⽤接⼝来间接地判定。如果调⽤者只使⽤部分接⼝或接⼝的部分功能,那接⼝的设计就不够职责单⼀。
5)DIP
  (1)依赖倒置原则(Dependency Inversion Principle,DIP)
  ⾼层模块不要依赖低层模块。⾼层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。
  调⽤者属于⾼层,被调⽤者属于低层。在平时的业务代码开发中,⾼层模块依赖底层模块是没有任何问题的。
  这条原则主要还是⽤来指导框架层⾯的设计。
  (2)控制反转(Inversion Of Control,IOC)
  “控制”指的是对程序执⾏流程的控制,⽽“反转”指的是在没有使⽤框架之前,程序员⾃⼰控制整个程序的执⾏。在使⽤框架之后,整个程序的执⾏流程可以通过框架来控制。流程的控制权从程序员“反转”到了框架。
  框架提供了⼀个可扩展的代码⾻架,⽤来组装对象、管理整个执⾏流程。程序员利⽤框架进⾏开发的时候,只需要往预留的扩展点上,添加跟⾃⼰业务相关的代码,就可以利⽤框架来驱动整个程序流程的执⾏。
  控制反转并不是⼀种具体的实现技巧,⽽是⼀个⽐较笼统的设计思想,⼀般⽤来指导框架层⾯的设计。
  (3)依赖注⼊(Dependency Injection,DI)
  不通过 new() 的⽅式在类内部创建依赖类对象,⽽是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等⽅式传递(或注
⼊)给类使⽤。
6)KISS和YAGNI
  KISS 原则的英⽂描述有好⼏个版本。
  (1)Keep It Simple and Stupid.
  (2)Keep It Short and Simple.
  (3)Keep It Simple and Straightforward.
  它们要表达的意思其实差不多,翻译成中⽂就是:尽量保持简单。代码⾜够简单,也就意味着很容易读懂,bug ⽐较难隐藏。即便出现bug,修复起来也⽐较简单。
  指导如何开发出 KISS 原则的⽅法论:
  (1)代码⾏数越少并不是就越“简单”。
  (2)代码逻辑复杂不违背 KISS 原则。
  如何写出满⾜ KISS 原则:
  (1)不要使⽤同事可能不懂的技术来实现代码。例如例⼦中的正则表达式,还有⼀些编程语⾔中过于⾼级的语法等。
  (2)不要重复造轮⼦,要善于使⽤已经有的⼯具类库。经验证明,⾃⼰去实现这些类库,出 bug 的概率会更⾼,维护的成本也⽐较⾼。
  (3)不要过度优化。不要过度使⽤⼀些奇技淫巧(⽐如,位运算代替算术运算、复杂的条件语句代替 if-else、使⽤⼀些过于底层的函数等)来优化代码,牺牲代码的可读性。
  注意,在做开发的时候,⼀定不要过度设计,不要觉得简单的东西就没有技术含量。实际上,越是能⽤简单的⽅法解决复杂的问题,越能体现⼀个⼈的能⼒。
  YAGNI(You Ain’t Gonna Need It)是指不要去设计当前⽤不到的功能;不要去编写当前⽤不到的代码。其核⼼思想就是:不要做过度设计。
  例如不要在项⽬中提前引⼊不需要依赖的开发包,在未⽤到 ZooKeeper 之前没必要提前编写这部分代码。
  KISS 原则讲的是“如何做”的问题(尽量保持简单),⽽ YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做)。
7)DRY
  DRY(Don’t Repeat Yourself)的定义⾮常简单,三种典型的代码重复情况:
  (1)实现逻辑重复
  将 isValidUserName() 和 isValidPassword() 两个函数中的重复代码合并到 isValidUserNameOrPassword() 函数,负责两件事情,违反了“单⼀职责原则”和“接⼝隔离原则”。
  虽然从代码实现逻辑上看起来它们是重复的,但是从语义上并不重复。
  所谓“语义不重复”指的是:从功能上来看,这两个函数⼲的是完全不重复的两件事情,⼀个是校验⽤户名,另⼀个是校验密码。
  尽管在⽬前的设计中,两个校验逻辑是完全⼀样的,但如果按照第⼆种写法,将两个函数的合并,那就会存在潜在的问题。
  (2)功能语义重复
  在同⼀个项⽬代码中有两个函数:isValidIp() 和 checkIfIpValid()。尽管两个函数的命名不同,实现逻辑不同,但功能是相同的,都是⽤来判定 IP 地址是否合法的。
  在这个例⼦中,尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,可以认为它违反了 DRY 原则。
  (3)代码执⾏重复
  在这个例⼦中,既没有逻辑重复,也没有语义重复,但仍然违反了 DRY 原则。这是因为代码中存在“执⾏重复”。
  在 login() 函数中,email 的校验逻辑被执⾏了两次。⼀次是在调⽤ checkIfUserExisted() 函数的时候,另⼀次是调⽤ getUserByEmail()函数的时候。
  除此之外,代码中还有⼀处⽐较隐蔽的执⾏重复,login() 函数并不需要调⽤ checkIfUserExisted() 函数,只需要调⽤⼀次getUserByEmail() 函数,从数据库中获取到⽤户的 email、password 等信息,然后跟⽤户输⼊的 email、password 信息做对⽐,依次判断是否登录成功。
  三个概念:
  (1)代码复⽤(Code Resue)表⽰⼀种⾏为:在开发新功能的时候,尽量复⽤已经存在的代码。
  (2)代码复⽤性(Code Reusability)表⽰⼀段代码可被复⽤的特性或能⼒:在编写代码的时候,让代码尽量可复⽤。
  (3)DRY 原则是⼀条原则:不要写重复的代码。
  区分:
  (1)⾸先,“不重复”并不代表“可复⽤”。
  在⼀个项⽬代码中,可能不存在任何重复的代码,但也并不表⽰⾥⾯有可复⽤的代码,不重复和可复⽤完全是两个概念。
  (2)其次,“复⽤”和“可复⽤性”关注⾓度不同。
  代码“可复⽤性”是从代码开发者的⾓度来讲的,“复⽤”是从代码使⽤者的⾓度来讲的。
  ⽐如,A 同事编写了⼀个 UrlUtils 类,代码的“可复⽤性”很好。B 同事在开发新功能的时候,直接“复⽤”A 同事编写的 UrlUtils 类。
  尽管复⽤、可复⽤性、DRY 原则这三者从理解上有所区别,但实际上要达到的⽬的都是类似的,都是为了减少代码量,提⾼代码的可读性、可维护性。
  “复⽤”这个概念不仅可以指导细粒度的模块、类、函数的设计开发,实际上,⼀些框架、类库、组件等的产⽣也都是为了达到复⽤的⽬的。⽐如,Spring 框架、Google Guava 类库、UI 组件等等。
  提⾼代码复⽤性 7 个⽅法:
程序员培训机构选极客时间
  (1)减少代码耦合。
  对于⾼度耦合的代码,当希望复⽤其中的⼀个功能,想把这个功能的代码抽取出来成为⼀个独⽴的模块、类或者函数的时候,往往会发现牵⼀发⽽动全⾝。
  (2)满⾜单⼀职责原则。
  越细粒度的代码,代码的通⽤性会越好,越容易被复⽤。
  (3)模块化。
  可将模块理解为单个类、函数。独⽴的模块就像⼀块⼀块的积⽊,更加容易复⽤,可以直接拿来搭建更加复杂的系统。
  (4)业务与⾮业务逻辑分离。
  越是跟业务⽆关的代码越是容易复⽤,越是针对特定业务的代码越难复⽤。
  (5)通⽤代码下沉。
  从分层的⾓度来看,越底层的代码越通⽤、会被越多的模块调⽤,越应该设计得⾜够可复⽤。为了避
免交叉调⽤导致调⽤关系混乱,只允许上层代码调⽤下层代码及同层代码之间的调⽤,杜绝下层代码调⽤上层代码。
  (6)继承、多态、抽象、封装。
  利⽤继承,可以将公共的代码抽取到⽗类,⼦类复⽤⽗类的属性和⽅法。利⽤多态,可以动态地替换⼀段代码的部分逻辑,让这段代码可复⽤。越抽象、越不依赖具体的实现,越容易复⽤。代码封装成模块,隐藏可变的细节、暴露不变的接⼝,就越容易复⽤。
  (7)应⽤模板等设计模式。
  模板模式利⽤了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复⽤。
  实际上,除⾮有⾮常明确的复⽤需求,否则,为了暂时⽤不到的复⽤需求,花费太多的时间、精⼒,投⼊太多的开发成本,并不是⼀个值得推荐的做法。这也违反之前讲到的 YAGNI 原则。
  除此之外,有⼀个著名的原则,叫作“Rule of Three”。第⼀次编写代码的时候,不考虑复⽤性;第⼆次遇到复⽤场景的时候,再进⾏重构使其复⽤。
8)LOD
  迪⽶特法则(Law of Demeter,LOD)也叫最⼩知识原则(The Least Knowledge Principle),是指每个模块只应该了解那些与它关系密切的模块的有限知识。或者说,每个模块只和⾃⼰的朋友“说话”,不和陌⽣⼈“说话”。
  换句话说,就是不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接⼝(也就是定义中的“有限知识”)。
  迪⽶特法则是希望减少类之间的耦合,让类越独⽴越好。每个类都应该少了解系统的其他部分。⼀旦发⽣变化,需要了解这⼀变化的类就会⽐较少。
  单⼀职责原则、接⼝隔离原则、基于接⼝⽽⾮实现编程和迪⽶特法则,⽬的都是实现⾼内聚低耦合,但是出发的⾓度不⼀样,单⼀职责
是从⾃⾝提供的功能出发,迪⽶特法则是从关系出发,针对接⼝⽽⾮实现编程是使⽤者的⾓度,殊途同归。
  (1)⾼内聚,就是指相近的功能应该放到同⼀个类中,不相近的功能不要放到同⼀个类中。放到同⼀个类中,修改会⽐较集中,代码容易维护。
  (2)松耦合,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,⼀个类的代码
改动不会或者很少导致依赖类的代码改动。
9)积分系统
  (1)合理地将功能划分到不同模块。
  为了避免业务知识的耦合,让下层系统更加通⽤,⼀般来讲,不希望下层系统(也就是被调⽤的系统)包含太多上层系统(也就是调⽤系统)的业务信息,但是,可以接受上层系统包含下层系统的业务信息。
  ⽐如,订单系统、优惠券系统、换购商城等作为调⽤积分系统的上层系统,可以包含⼀些积分相关的业务信息。但是,反过来,积分系统中最好不要包含太多跟订单、优惠券、换购等相关的信息。
  (2)设计模块与模块之间的交互关系。
  交互⽅式有两种,⼀种是同步接⼝调⽤,另⼀种是利⽤消息中间件异步调⽤。第⼀种⽅式简单直接,第⼆种⽅式的解耦效果更好。
  ⽐如,⽤户下订单成功之后,订单系统推送⼀条消息到消息中间件,营销系统订阅订单成功消息,触发执⾏相应的积分兑换逻辑。这样订单系统就跟营销系统完全解耦,订单系统不需要知道任何跟积分相关的逻辑,⽽营销系统也不需要直接跟订单系统交互。
  (3)设计模块的接⼝、数据库、业务模型。
  数据库和接⼝的设计⾮常重要,⼀旦设计好并投⼊使⽤之后,这两部分都不能轻易改动。改动数据库表结构,需要涉及数据的迁移和适配;改动接⼝,需要推动接⼝的使⽤者作相应的代码修改。
    a、数据库的设计⽐较简单。实际上,只需要⼀张记录积分流⽔明细的表就可以了。
    b、为了兼顾易⽤性和性能,可以借鉴 facade(外观)设计模式,在职责单⼀的细粒度接⼝之上,再封装⼀层粗粒度的接⼝给外部使⽤。
    c、将它跟营销系统放到⼀个项⽬中开发部署,只要做好代码的模块化和解耦即可。
  为什么要分 MVC 三层开发?
  (1)分层能起到代码复⽤的作⽤。
  同⼀个 Repository 可能会被多个 Service 来调⽤,同⼀个 Service 可能会被多个 Controller 调⽤。
  (2)分层能起到隔离变化的作⽤。
  基于接⼝⽽⾮实现编程的设计思想,Service 层使⽤ Repository 层提供的接⼝,并不关⼼其底层依赖
的是哪种具体的数据库。当需要替换数据库的时候,只需要改动 Repository 层的代码。
  (3)分层能起到隔离关注点的作⽤。
  Repository 层只关注数据的读写。Service 层只关注业务逻辑,不关注数据的来源。Controller 层只关注与外界打交道,数据校验、封装、格式转换,并不关⼼业务逻辑。
  (4)分层能提⾼代码的可测试性。
  Repsitory 层的代码通过依赖注⼊的⽅式供 Service 层使⽤,当要测试包含核⼼业务逻辑的 Service 层代码的时候,可以⽤ mock 的数据源替代真实的数据库,注⼊到 Service 层代码中。
  (5)分层能应对系统的复杂性。
  拆分有垂直和⽔平两个⽅向。⽔平⽅向基于业务来做拆分,就是模块化;垂直⽅向基于流程来做拆分,就是这⾥说的分层。
10)统计系统
  对于这样⼀个通⽤的框架的开发,还需要考虑很多⾮功能性的需求。
  (1)易⽤性,框架是否易集成、易插拔、跟业务代码是否松耦合、提供的接⼝是否够灵活等等。
  (2)性能,⼀⽅⾯,希望它低延迟,即统计代码不影响或很少影响接⼝本⾝的响应时间;另⼀⽅⾯,希望框架本⾝对内存的消耗不能太⼤。
  (3)扩展性,从框架使⽤者的⾓度来说,可以在不修改框架源码的情况下,为框架扩展新的功能,类似给框架开发插件。
  (4)容错性,不能因为框架本⾝的异常导致接⼝请求出错。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。