面向对象设计原则

面向对象设计原则

单一职责原则(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)

定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

其实这是一个很模糊地概念,十分空洞。

开闭原则一直在强调:用抽象构建框架,用实现扩展细节。用抽象构建框架,用实现扩展细节。

前面的五个原则就像实在告诉你如何达到”用抽象构建框架,用实现扩展细节。

开闭原则更像是综合前面五个原则,前面五个原则遵循的好,那么你开闭原则就遵循的好,反之则遵循的不好

(这里的好指的是对遵循原则的程度把控,一定要适当,过少和过多都是不好)

所以,对这六个原则的遵守并不是是与否,有和无的问题,而是多和少的问题,讲究的是程度!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值