设计模式的意义

优秀的代码指具备

  • 可维护性,能否比较轻松应对需求的变更?
  • 可读性,在code review时别人读得懂吗?
  • 可扩展性,通过预留的设计不用大改就能应付需求。
  • 灵活性,在代码的设计中考虑到可扩展、模块化、可复用。
  • 简洁性,简单、清晰。
  • 可复用性,不要重复你自己。
  • 可测试性。

1.为何学习设计模式

学好设计模式可以应对写出更优秀的代码、代码设计更精妙、更好的理解源码、应付面试高频,在软件工程师这条道路上起飞。通过把握设计原则、深入理解设计模式、以及规范的编码,就能写出优秀的代码。优秀的代码意味着你是一个优秀的软件工程师。

2 设计原则

先谈设计原则,有SOLID原则、DRY 原则、KISS 原则、YAGNI 原则、LOD 法则。
这些原则挺抽象,在我的感觉,设计中有变和不变的部分,要把这两部分分离出来。对于变的部分,通过封装不必要信息、预留接口实现为以后的替换做准备。对于不变的部分主要是结构和模式不变。比如,封建王朝皇帝可以变,君臣模式不会变。

2.1 SOLID原则

每个字母代表一个原则。

  • SRP单一职责原则

SRP,Single Responsibility Principle。一个类或者模块只负责完成一个职责(或者功能)。不要设计大而全的类,要设计粒度小、功能单一的类。就像生物组织器官,各司其职,有特定的功能职责范围。

  • 如果类中的代码行数、函数或属性过多,影响代码的可读性和可维护性,或者依赖的其他类过多,不符合高内聚、低耦合的设计思想,我们也需要考虑对类进行拆分;
  • 如果类私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
  • 如果比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
  • 如果类中大量的方法都是集中操作类中的某几个属性,也表示可以进行类的拆分。
  • OCP开闭原则

OCP, Open Closed Principle。对扩展开放、修改关闭。要真正做到这一点很难,因为变化总在发生。 因此,提前预判变化点很重要。需要识别哪部分是核心需求,在设计上尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。为什么最好不去修改原有代码?因为修改往往意味着运维难度、测试难度的急剧增大。

  • LSP里式替换原则

LSP,Liskov Substitution Principle。按照协议来设计。子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。子类如果不满足,就换一个好了。如果父类的方法声明的功能,子类实现的结果是不一致的;如果子类对参数的检查比父类还严格;如果父类的方法只抛出某种异常,但子类却抛出了更多异常;如果子类对方法实现违背了父类的协议与设计,那么这些都是违反里氏替换原则的。说白了,子承父业,就要“无改于父道”。

  • ISP 接口隔离原则

ISP,Interface Segregation Principle。在做接口设计时应当按功能切面进行设计,不要设计一个臃肿接口导致实现类没必要却必须实现其方法,将臃肿的接口切分为多个接口,需要时进行实现使用即可。

  • DIP 依赖倒置原则

DIP,Dependence Inversion Principle。高层模块不要依赖低层模块。抽象不要依赖具体实现细节,也就是说先谋全局,建立框架,再逐步添加实现。IOC 就是一种DIP的实现方式,通过预先设定好框架然后调用框架预设的功能点实现控制反转。控制反转往往依托DI依赖注入实现。Google Guice、Java Spring、Pico Container、Butterfly Container是常见的控制反转容器。
举个例子,Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖 Sevlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。

2.2 DRY 、KISS 、YAGNI 、LOD

  • DRY
    Don’t Repeat Yourself.避免实现逻辑重复、功能语义重复、代码执行重复。不要重复定义相同语义的方法,避免为未来的功能升级修改埋雷。
  • KISS
    Keep It Simple and Stupid.尽量保持简单。不要用团队陌生的技术、不要重复实现底层、不要过度优化。
  • YAGNI
    You Ain’t Gonna Need It。不要去设计当前用不到的功能,不要做过度设计。可以预留,但是不必过度这部分功能。
  • LOD
    Law of Demeter,迪米特法则,最小知识原则。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,牵涉的类就会比较少。比如某个类可以同时实现序列化接口和反序列化接口,在大多数情况程序只需要了解序列化接口以及其实现类,在需要反序列化时才会用到反序列化接口。

3. 面向对象编程语言

3.1 面向对象语言的特性

面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并具有封装、抽象、继承、多态四个特性。大部分编程语言都是面向对象编程语言, 如Java、C++、Go、Python、C#、Ruby、JavaScript、Objective-C、Scala、PHP、Perl。

  • 封装(Encapsulation)
    保护类的字段不变随意修改,只暴露必要的调用方法,通过设计保证对类的使用的规范与安全。
  • 抽象(Abstraction)
    抽象是最有效的思维方式。抽象思维无处不在,即使是面向过程的语言也会使用到抽象。抽象提升了代码的可扩展性、维护性。
  • 继承(Inheritance)
    子类对父类的继承,可以通过继承复用父类的代码。C++ 、Python、Perl支持多继承模式。
    java支持单继承,可以实现多接口。一般java可以通过组合的模式实现多继承的效果,组合更加灵活,类的耦合程度降低了。
  • 多态
    通过继承(实现)+ 方法的重写,实现父类引用持有子类对象。Java的多态通过继承 或者 实现接口方式实现。多态是设计模式的基础。

Python等语言是动态语言,只要子类具有父类的方法,无需声明即可充当子类。这种模式被称为 duck - typing 。即只要能飞能游,我就认为它是鸭子。

3.2 面向对象与面向过程

面向过程是线性思维,通过执行过程的划分,实现对应的函数,再对函数进行调用即可。
面向对象是非线性思维。在大规模复杂程序中,整个程序的处理流程错综复杂,系统模块之间的关系众多,因此需要对业务进行抽象和建模,将业务需求设计为一个个类,并且定义类之间的关系,有了类的设计之后,然后再像搭积木一样,按照处理流程,将类组装起来形成整个程序。
许多代码虽然是用java等面向对象的语音编写,实际上还是属于面向过程的编码。

  • 滥用 getter、setter 方法
    get时,如果返回的对象是可变对象就要注意了,你真的希望把这个可变对象交出去吗?
    set时,考虑,直接赋值会不会导致类内部的状态不一致?
  • 滥用全局变量和全局方法
    比如Constant、Utils类,在实际中应用比较多,但是真的需要吗?为了减轻依赖,能不能细分为几个不同的的类?
  • 数据与方法分离的类
    无论是MVC的VO、BO、Entity等。

3.3 面向对象语言的一些细节

抽象类与接口的异同

抽象类不能被实例化,可以包含属性和方法,子类在继承时必须实现所有抽象方法。接口也不能被实例化,不能包含属性和方法(在新版JDK中已经可以了),实现类必须实现接口的所有方法。所以现在接口越来越像抽象类了。

普通类也可以被继承,为什么需要抽象类呢?

因为如果这个类不是抽象的,容易被误用。比如Log类,定义了log()方法,由子类实现。如果Log类不是抽象类,会导致。实例化Log类,但Log类功能并不全;子类不知道要重写哪些方法。

普通类模拟抽象类

普通类也可以模拟抽象类,比如这个方法必须由子类定义,那么该方法可以默认抛异常。

 public void method() {
   throw new UnSupportedException();
 } 

面向接口编程

基于抽象编程,接口实际上是一个契约,在定义接口时,需要考虑扩展性,与具体的实现无关。

多用组合少用继承

由于java是单继承缘故,对于复杂性适应不好,如果非要用继承,会导致继承层次深关系复杂,对代码的维护性有损。可以使用组合、接口、委托三个技术手段。即,从能力上定义多个接口以及实现类,在客户端类内持有这些能力,在调用方法时,释放能力。

interface Fly : fly()
class Flyablity implements Fly
Class Bird {
	Flyablity ability;
	fly(){
		ability.fly();
	}
}

继承的使用场景

如果类的继承层次比较浅,可以使用继承,又或者在某些情况下,方法的参数声明为一个类而不是接口,如果你想在传入参数时,可以是一个自定义的类,那么你只能使用继承。在设计模式中继承很重要的,比如模板模式。

贫血模型与充血模型哪个才是OOP

贫血模型和充血模型应用在web开发中,在web开发中一般分为controller,service,respository层,贫血模型和充血模型区别在service层。贫血模型:service+BO,充血模型:service+Domain。Domain相比BO提供了更丰富的业务方法。充血模型更符合面向对象设计理念。现在流行的DDD就是一种充血模型,将业务逻辑封装在Domain中。但是实际上我们在业务领域使用贫血模型比较多,因为核心业务逻辑用CRUD就能实现了,如果基于面向对象的方法设计,也会无谓增加设计复杂度。

题外话 DDD

基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,主要区别在 Service 层。在基于充血模型的开发模式下,我们将部分原来在 Service 类中的业务逻辑移动到了一个充血的 Domain 领域模型中,让 Service 类的实现依赖这个 Domain 类。在基于充血模型的 DDD 开发模式下,Service 类并不会完全移除,而是负责一些不适合放在 Domain 类中的功能。比如,负责与 Repository 层打交道、跨领域模型的业务聚合功能、幂等事务等非功能性的工作。可以参考一个虚拟钱包的项目,其中钱包类可以增加一些收入、支出的功能,这样的service层的复杂度下降了,逻辑能够进入业务领域。

3.3 UML

UML 用于建模,在面向对象分析设计时常用。可以用于类图、用例图、顺序图、活动图、状态图、组件图。可以表示类之间的泛化、实现、关联、聚合、组合、依赖等关系。可以参考一遍就能懂的UML
对于聚合、组合有很多人分不出区别。

  • 聚合,持有这个对象,这个对象可以暴露在外。
	class A{
		public A(B b){
			this.b = b;
		}
	}
  • 组合,这个对象就是我内部的。
	class A{
		public A(B b){
			this.b = new B();
		}
	}

3.4 面向对象的项目设计

设计流程

  1. 划分职责进而识别出有哪些类
  2. 定义类及其属性和方法
  3. 定义类与类之间的交互关系
  4. 将类组装起来并提供执行入口

举个例子。关于接口鉴权的示例。调用方进行接口请求的时候,将 URL、AppID、密码、时间戳拼接在一起,通过加密算法生成 token,并且将 token、AppID、时间戳拼接在 URL 中,一并发送到微服务端。微服务端在接收到调用方的接口请求之后,从请求中拆解出 token、AppID、时间戳。微服务端首先检查传递过来的时间戳跟当前时间,是否在 token 失效时间窗口内。如果已经超过失效时间,那就算接口调用鉴权失败,拒绝接口调用请求。如果 token 验证没有过期失效,微服务端再从自己的存储中,取出 AppID 对应的密码,通过同样的 token 生成算法,生成另外一个 token,与调用方传递过来的 token 进行匹配;如果一致,则鉴权成功,允许接口调用,否则就拒绝接口调用。

一个例子

很多项目或者模块其实内部也可以进行二次拆分形成上级业务模块和下级执行模块。MVC结构分层带来的好处是,易变的与稳定的部分按层分离开了,这样就能更好的适应变化。在MVC中有VO、BO、Entity等对象,可以通过继承来避免重复的代码,VO、BO、Entity之间可以通过BeanUtils等工具将其进行转化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟空学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值