面向对象设计原则
单一职责原则(Single Responsibility Principle)
定义:一个类只负责一项职责
原则核心:模块功能抽象化,扩展功能交给实现层,代码只增不改
即便是经验丰富的程序员,也会有违背这一原则的代码存在。为什么呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)
举个栗子:
用一个类描述歌手唱歌
class Singer{
public void sing(String singer){
System.out.println(singer+"唱歌");
}
}
public class Client{
public static void main(String[] args){
Singer singer = new Singer();
singer.sing("张三");
singer.sing("李四");
singer.sing("王五");
}
}
运行结果是
张三唱歌
李四唱歌
王五唱歌
程序上线后,发现问题了,并不是所有的歌手唱的类型都一样,比如张三是男低音,李四是男中音,王五是男高音,修改时如果遵循单一职责原则,需要将Singer细分为男低音类Bass,男中音类Baritone,男高音类Tenor,代码如下:
class Bass{
public void sing(String singer){
System.out.println(singer+"唱低音");
}
}
class Baritone{
public void sing(String singer){
System.out.println(singer+"唱中音");
}
}
class Tenor{
public void sing(String singer){
System.out.println(singer+"唱高音");
}
}
public class Client{
public static void main(String[] args){
Bass bass = new Bass();
bass.sing("张三");
Baritone baritone = new Baritone();
baritone.sing("李四");
Tener tener = new Tener();
tener.sing("王五");
}
}
运行结果是
张三唱低音
李四唱中音
王五唱高音
遵循单一职责原的优点有:
- 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
- 提高类的可读性,提高系统的可维护性;
- 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
里氏替换原则(Liskov Substitution Principle)
定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
定义2:所有引用基类的地方必须能透明地使用其子类的对象。
**子类可以扩展父类的功能,但不能改变父类原有的功能。**它包含以下4层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
依赖倒置原则(Dependency Inversion Principle)
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
依赖倒置原则的核心思想是面向接口编程
再举个栗子:
家里炒菜时会放盐对吧:
class Salt {
public String getContent(){
return "炒菜时我放了盐";
}
}
class Mother {
void add(Salt salt){
System.out.println(salt.getContent());
}
}
public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.add(new Salt());
}
}
运行结果:
炒菜时我放了盐
那如果炒菜时还要放味精等其他调味料呢?总不可能再new
一个会放味精的妈妈吧🙄🙄🙄
所以我们将盐Salt
和味精Msg
提取出一个接口调味料Seasoning
修改代码如下:
interface Seasoning {
public String getContent(){
}
}
class Salt implements Seasoning {
public String getContent(){
return "炒菜时我放了盐";
}
}
class Msg implements Seasoning {
public String getContent(){
return "炒菜时我放了味精";
}
}
class Mother {
void add(Seasoning seasoning){
System.out.println(seasoning.getContent());
}
}
public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.add(new Salt());
mother,add(new Msg);
}
}
运行结果是:
炒菜时我放了盐
炒菜时我放了味精
在实际编程中,我们一般需要做到如下3点:
- 低层模块尽量都要有抽象类或接口,或者两者都有。
- 变量的声明类型尽量是抽象类或接口。
- 使用继承时遵循里氏替换原则。
接口隔离原则(Interface Segregation Principle)
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
我们有一个接口叫动物,其中包含了动物的部分行为:
interface Animal {
public void eatMeat();
public void eatGrass();
public void sleep();
// .....
}
当一个养动物的人需要通过这个接口依赖来养一些明确的动物:
interface Animal {
public void eatMeat();
public void eatGrass();
public void sleep();
// ......
}
class Wolf implements Animal {
public void eatMeat() {
System.out.println("狼吃下了肉");
}
public void eatGrass() {
System.out.println("狼吃了一口草,骂骂咧咧地吐了出来");
}
public void sleep() {
System.out.println("狼开始睡觉");
}
// ......
}
class Hunter {
public void feedMeat(Animal animal) {
System.out.println("猎人喂肉");
animal.eatMeat();
}
public void sleep(Animal animal) {
System.out.println("猎人睡觉");
animal.sleep();
}
}
class Sheep implements Animal {
public void eatMeat() {
System.out.println("羊吃了一点肉,骂骂咧咧地吐了出来");
}
public void eatGrass() {
System.out.println("羊吃下了草");
}
public void sleep() {
System.out.println("羊开始睡觉");
}
// ......
}
class Shepherd {
public void feedGrass(Animal animal) {
System.out.println("牧羊人喂草");
animal.eatGrass();
}
public void sleep(Animal animal) {
System.out.println("牧羊人睡觉");
animal.sleep();
}
}
public class Client{
public static void main(String[] args){
Hunter hunter = new Hunter();
Wolf wolf = new Wolf;
hunter.feedMeat(wolf);
hunter.sleep(wolf);
Shepherd shepherd = new Shepherd();
Sheep sheep = new Sheep();
shepherd.feedGrass(sheep);
shepherd.sleep(sheep);
}
}
运行结果:
猎人喂肉
狼吃下了肉
猎人睡觉
狼开始睡觉
牧羊人喂食
羊吃下了草
牧羊人睡觉
羊开始睡觉
但是实际现实中并不是所有动物既吃肉也吃草,比如狼吃肉,羊吃草,虽然它们都会睡觉。
我们构建狼这个类的时候如果实现Animal
这个接口,那么狼就要去实现吃草的方法,吃了再骂骂咧咧地吐出来?完全多此一举,大可不必。
所以依据接口隔离原则,我们应将吃肉,吃草,睡觉隔离开来,分别构建一个接口。
代码如下:
interface EatMeat {
public void eatMeat();
}
interface EatGrass {
public void eatGrass();
}
interface Sleep {
public void sleep();
}
// ......
class Wolf implements EatMeat implements Sleep {
public void eatMeat() {
System.out.println("狼吃下了肉");
}
public void sleep() {
System.out.println("狼开始睡觉");
}
}
class Hunter {
public void feedMeat(EatMeat eatMeat) {
System.out.println("猎人喂肉");
eatMeat.eatMeat();
}
public void sleepT(Sleep sleep) {
System.out.println("猎人睡觉");
sleep.sleep();
}
}
class Sheep implements EatGrass implements Sleep {
public void eatGrass() {
System.out.println("羊吃下了草");
}
public void sleep() {
System.out.println("羊开始睡觉");
}
}
class Shepherd {
public void feedGrass(EatGrass eatGrass) {
System.out.println("牧羊人喂草");
eatGrass.eatGrass();
}
public void sleepT(Sleep sleep) {
System.out.println("牧羊人睡觉");
sleep.sleep();
}
}
public class Client{
public static void main(String[] args){
Hunter hunter = new Hunter();
Wolf wolf = new Wolf;
hunter.feedMeat(wolf);
hunter.sleepT(wolf);
Shepherd shepherd = new Shepherd();
Sheep sheep = new Sheep();
shepherd.feedGrass(sheep);
shepherd.sleepT(sheep);
}
}
运行结果:
猎人喂肉
狼吃下了肉
猎人睡觉
狼开始睡觉
牧羊人喂食
羊吃下了草
牧羊人睡觉
羊开始睡觉
要注意以下几点:
- 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
- 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
迪米特法则(Law of Demeter)
定义:一个对象应该对其他对象保持最少的了解。
原则核心:减少对象之间的交互,可以引入第三者降低耦合
降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“第三者”来发生联系。过分地遵循迪米特法则,会产生大量这样的第三者和传递类,导致系统复杂度增加。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
开闭原则(Open-Closed Principle)
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
其实这是一个很模糊地概念,十分空洞。
开闭原则一直在强调:用抽象构建框架,用实现扩展细节。用抽象构建框架,用实现扩展细节。
前面的五个原则就像实在告诉你如何达到”用抽象构建框架,用实现扩展细节。“
开闭原则更像是综合前面五个原则,前面五个原则遵循的好,那么你开闭原则就遵循的好,反之则遵循的不好
(这里的好指的是对遵循原则的程度把控,一定要适当,过少和过多都是不好)
所以,对这六个原则的遵守并不是是与否,有和无的问题,而是多和少的问题,讲究的是程度!