前言
在面向对象的软件设计中,只有尽量降低各个模块之间的耦合度,才能提高代码的复用率,系统的可维护性、可扩展性才能提高。面向对象的软件设计中,有23种经典的设计模式,是一套前人代码设计经验的总结,如果把设计模式比作武功招式,那么设计原则就好比是内功心法。常用的设计原则有七个,本文将具体介绍单一职责原则。
设计原则简介
- 单一职责原则:专注降低类的复杂度,实现类要职责单一;
- 开放关闭原则:所有面向对象原则的核心,设计要对扩展开发,对修改关闭;
- 里式替换原则:实现开放关闭原则的重要方式之一,设计不要破坏继承关系; (只要有父类出现的地方,都可以用子类来替换)
- 依赖倒置原则:系统抽象化的具体实现,要求面向接口编程,是面向对象设计的主要实现机制之一;
- 接口隔离原则:要求接口的方法尽量少,接口尽量细化;
- 迪米特法则:降低系统的耦合度,使一个模块的修改尽量少的影响其他模块,扩展会相对容易;
- 组合复用原则:在软件设计中,尽量使用组合/聚合而不是继承达到代码复用的目的。
这些设计原则并不说我们一定要遵循他们来进行设计,而是根据我们的实际情况去怎么去选择使用他们,来让我们的程序做的更加的完善。
单一职责原则
就一个类而言,应该仅有一个引起它变化的原因,通俗的说,就是一个类只负责一项职责。此原则的核心就是解耦和增强内聚性。
如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计。
优点
:
(1)降低类的复杂度;
(2)提高类的可读性,提高系统的可维护性;
(3)降低变更引起的风险(降低对其他功能的影响)。
链接:https://juejin.cn/post/6960839149699989512
开放关闭原则
简介
开放封闭原则是这样表述的:
软件实体(类、模块、函数)应该对扩展开放,对修改封闭。
这个说法是 Bertrand Meyer 在其著作《面向对象软件构造》(Object-Oriented Software Construction)中提出来的,它给软件设计提出了一个极高的要求:不修改代码。
或许你想问,不修改原有代码,那我怎么实现新的需求呢?答案就是靠扩展。用更通俗的话来解释,就是新需求应该用新代码实现。
开放封闭原则向我们描述的是一个结果,就是我们可以不修改原有代码而仅凭扩展就完成新功能。但是,这个结果的前提是要在软件内部留好扩展点,而这正是需要我们去设计的地方。因为每一个扩展点都是一个需要设计的模型。
解释
举个例子,假如我们正在开发一个酒店预订系统,针对不同的用户,我们需要计算出不同的房价。比如,普通用户是全价,金卡是 8 折,银卡是 9 折,代码写出来可能是这样的:
class HotelService { public double getRoomPrice(final User user, final Room room) { double price = room.getPrice(); if (user.getLevel() == Level.GOLD) { return price * 0.8; } if (user.getLevel() == Level.SILVER) { return price * 0.9; } return price; } }
这时,新的需求来了,要增加白金卡会员,给出 75 折的优惠,如法炮制的写法应该是这样的:
class HotelService { public double getRoomPrice(final User user, final Room room) { double price = room.getPrice(); if (user.getLevel() == UserLevel.GOLD) { return price * 0.8; } if (user.getLevel() == UserLevel.SILVER) { return price * 0.9; } if (user.getLevel() == UserLevel.PLATINUM) { return price * 0.75; } return price; } }
显然,这种做法就是修改代码的做法,每增加一个新的类型就要修改一次代码。但是,一个有各种级别用户的酒店系统肯定不只是房价有区别,提供的服务也可能有区别。可想而知,每增加一个用户级别,我们要改的代码就漫山遍野。
那应该怎么办呢?我们应该考虑如何把它设计成一个可以扩展的模型。在这个例子里面,既然每次要增加的是用户级别,而且各种服务的差异都体现在用户级别上,我们就需要一个用户级别的模型。在前面的代码里,用户级别只是一个简单的枚举,我们可以给它丰富一下:
interface UserLevel { double getRoomPrice(Room room); } class GoldUserLevel implements UserLevel { public double getRoomPrice(final Room room) { return room.getPrice() * 0.8; } } class SilverUserLevel implements UserLevel { public double getRoomPrice(final Room room) { return room.getPrice() * 0.9; } }
我们原来的代码就可以变成这样:
class HotelService { public double getRoomPrice(final User user, final Room room) { return user.getRoomPrice(room); } } class User { private UserLevel level; ... public double getRoomPrice(final Room room) { return level.getRoomPrice(room); } }
这样一来,再增加白金用户,我们只要写一个新的类就好了:
class PlatinumUserLevel implements UserLevel { public double getRoomPrice(final Room room) { return room.getPrice() * 0.75; } }
之所以我们可以这么做,是因为我们在代码里留好了扩展点:UserLevel。在这里,我们把原来的只支持枚举值的 UserLevel 升级成了一个有行为的 UserLevel。
经过这番改造,HotelService 的 getRoomPrice 这个方法就稳定了下来,我们就不需要根据用户级别不断地调整这个方法了。至此,我们就拥有了一个稳定的构造块,可以在后期的工作中把它当做一个稳定的模块来使用。
当然,在这个例子里,这个方法是比较简单的。而在实际的项目中,业务方法都会比较复杂。
链接:https://juejin.cn/post/6960852120367005726
里氏替换原则
链接:https://www.jianshu.com/p/cf9f3c7c0df5
https://juejin.cn/post/6961203475640221704
在学习java类的继承时,我们知道继承有一些优点:
- 子类拥有父类的所有方法和属性,从而可以减少创建类的工作量。
- 提高了代码的重用性。
- 提高了代码的扩展性,子类不但拥有了父类的所有功能,还可以添加自己的功能。
但又有点也同样存在缺点:
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
- 降低了代码的灵活性。因为继承时,父类会对子类有一种约束。
- 增强了耦合性。当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。有时修改了一点点代码都有可能需要对打断程序进行重构。
如何扬长避短呢?方法是引入里氏替换原则。
定义
-
第一种定义,也是最正宗的定义:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。 -
第二种定义:Functions that use pointers or references to base classes must be able to useobjects of derived classes without knowing it.
所有引用基类的地方必须能透明地使用其子类的对象。
第二种定义比较通俗,容易理解:只要有父类出现的地方,都可以用子类来替代,而且不会出现任何错误和异常。但是反过来则不行,有子类出现的地方,不能用其父类替代。
四层含义
里氏替换原则对继承进行了规则上的约束,这种约束主要体现在四个方面:
- 子类必须实现父类的抽象方法,但不得重写(覆盖) 父类的非抽象(已实现)方法。
- 子类中可以增加自己特有的方法。
- 当子类重载父类的方法时(方法参数不一致),方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。 如——
- 若 A类 f(Map() map), AA类 f(HashMap() map) 且在代码里AA替换了A,此时参数调用传入 HashMap类型(具体类型)的参数 mapParam仅执行AA类的方法; (父类不可以被子类替换)
- 若 A类 f(HashMap() map),AA类 f(Map() map) 且在代码里AA替换了A,此时参数调用传入 HashMap类型(具体类型)的参数 mapParam仅执行A类的方法 (父类可以被子类替换)
- 当子类的方法实现 父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。 如——
- abstract A类:Map f(); AA类:HashMap f(); HashMap可以替换Map类型
- abstract A类:HashMap f(), AA类:Map f(); 编译报错
依赖倒置原则
https://juejin.cn/post/6961568751720333342
谁依赖谁
依赖倒置原则(Dependency inversion principle,简称 DIP)是这样表述的:
高层模块不应依赖于低层模块,二者应依赖于抽象。
抽象不应依赖于细节,细节应依赖于抽象。
学习这个原则,最重要的是要理解“倒置”,而要理解什么是“倒置”,就要先理解所谓的“正常依赖”是什么样的。
我们很自然地就会写出类似下面的这种代码:
class CriticalFeature { // 高层模块 private Step1 step1; private Step2 step2; ... void run() { // 执行第一步 step1.execute(); // 低层模块 // 执行第二步 step2.execute(); ... } }
但是,这种未经审视的结构天然就有一个问题:高层模块会依赖于低层模块。在上面这段代码里,CriticalFeature 类就是高层类,Step1 和 Step2 就是低层模块,而且 Step1 和 Step2 通常都是具体类。虽然这是一种自然而然的写法,但是这种写法确实是有问题的。
在实际的项目中,代码经常会直接耦合在具体的实现上。比如,我们用 Kafka 做消息传递,我们就在代码里直接创建了一个 KafkaProducer 去发送消息。我们就可能会写出这样的代码:
class Handler { private KafkaProducer producer; void send() { ... Message message = ...; producer.send(new KafkaRecord<>("topic", message); ... } }
也许你会问,我就是用了 Kafka 发消息,创建一个 KafkaProducer,这有什么问题吗?其实,我们需要站在长期的角度去看,什么东西是变的、什么东西是不变的。Kafka 虽然很好,但它并不是系统最核心的部分,我们在未来是可能把它换掉的。
你可能会想,这可是我实现的一个关键组件,我怎么可能会换掉它呢?软件设计需要关注长期、放眼长期,所有那些不在自己掌控之内的东西,都是有可能被替换的。其实,替换一个中间件是经常发生的。所以,依赖于一个可能会变的东西,从设计的角度看,并不是一个好的做法。那我们应该怎么做呢?这就轮到倒置登场了。
所谓倒置,就是把这种习惯性的做法倒过来,让高层模块不再依赖于低层模块。那要是这样的话,我们的功能又该如何完成呢?计算机行业中一句名言告诉了我们答案:
计算机科学中的所有问题都可以通过引入一个间接层得到解决。
是的,引入一个间接层。这个间接层指的就是 DIP 里所说的抽象。也就是说,这段代码里面缺少了一个模型,而这个模型就是这个低层模块在这个过程中所承担的角色。
依赖于抽象
抽象不应依赖于细节,细节应依赖于抽象。
其实,这个可以更简单地理解为一点:依赖于抽象,从这点出发,我们可以推导出一些更具体的指导编码的规则:
- 任何变量都不应该指向一个具体类;
- 任何类都不应继承自具体类;
- 任何方法都不应该改写父类中已经实现的方法。
举个List 声明的例子,其实背后遵循的就是这里的第一条规则:
List<String> list = new ArrayList<>();
标签:return,room,原则,代码,面向对象,模块,设计,父类 From: https://www.cnblogs.com/wxdlut/p/17657010.html