面向对象设计原则(附带代码样例和JDK中的例子)

1.单一职责(SRP)

定义

单一职责原则(Single Responsibility Principle)中的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。简单来说:一个类只负责一项职责。

优点

单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点:

  1. 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多。
  2. 提高类的可读性。复杂性降低,自然其可读性会提高。
  3. 提高系统的可维护性。可读性高了,那自然更容易维护了。
  4. 变更引起的风险降低。变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。

出现的原因

比如一个类T负责两个不同的职责:职责P1、职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。这种问题的出现就是因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。
因此在设计一个类 的时候,可以先从粗粒度的类开始设计,等到业务发展到一定规模,我们发现这个粗粒度的类方法和属性太多,且经常修改的时候,我们就可以对这个类进行重构了,将这个类拆分为粒度更细的类,这就是所谓的持续重构。
比如:类T只负责一个职责P,这样的设计时符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1、P2,这时候如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩展为P1、P2、P3、P4…Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。

例子

1.类单一职责和方法单一职责

一个类描述动物呼吸这个场景。

Class Animal{
    public void breathe(String animal){
        System.out.println(animal + "呼吸空气");
    }
}
public class Client {
    public static void main(String[] args){
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("猪");
    }
}

运行结果:

牛呼吸空气
羊呼吸空气
猪呼吸空气

程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrrestrial,水生动物Aquatic,代码如下:

class Terrestrial{
    public void breathe(String animal){
        System.out.println(animal + "呼吸空气");
    }
}
class Aquatic{
    public void breathe(String animal){
        System.out.println(animal + "呼吸水");
    }
}
public class Client {
    public static void main(String[] args){
        Terrestrial terrestrial = new Terrestrial();
        terrestrial.breathe("牛");
        terrestrial.breathe("羊");
        terrestrial.breathe("猪");

        Aquatic aquatic = new Aquatic();
        aquatic.breathe("鱼");
    }
}

运行结果:

牛呼吸空气
羊呼吸空气
猪呼吸空气
鱼呼吸水

此时发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:

public class Animal {
    public void breathe(String animal){
        if("鱼".equals(animal)){
            System.out.println(animal + "呼吸水");
        }else{
            System.out.println(animal + "呼吸空气");
        }
    }
}

可以看到,这种修改方式要简单的多。但是却存在这隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”、“牛”、“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。这种修改方式指直接在方法级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。
还有一种修改方式:

public class Animal {
    public void breathe(String animal){
        System.out.println(animal + "呼吸空气");
    }

    public void breathe2(String animal){
        System.out.println(animal + "呼吸水");
    }
}

可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为他并没有动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一种呢?其实这个比较难说,需要根据实际情况来确定。如果逻辑足够简单,可以在方法内部违反单一职责原则,如果类中方法数量足够少,可以在方法毕节上违反单一职责原则。

2.接口单一职责

初始接口里面既有获取信息行为,又有对课程的操作行为。

public interface ICourse {
    String getCourseName();

    byte[] getCourseVideo();

    void studyCourse();

    void refundCourse();
}

我们就可以对接口中的行为进行分类,使得每个接口都具有单一职责。实现类可以实现多个接口就行了。

public interface ICourseContent {
    String getCourseName();

    byte[] getCourseVideo();
}
public interface ICourseManager {
    void studyCourse();

    void refundCourse();
}
public class CourseImpl implements ICourseContent,ICourseManager {
    @Override
    public String getCourseName() {
        return null;
    }
    @Override
    public byte[] getCourseVideo() {
        return new byte[0];
    }
    @Override
    public void studyCourse() {
    }
    @Override
    public void refundCourse() {
    }
}
在JDK中的应用

接口单一职责:
在JDK1.8中的新特性CompletableFuture就实现了两个接口

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
	......
}

CompletableFuture功能是进行异步操作与获取返回结果。
其中CompletionStage接口定义的是异步操作的触发与操作函数,future接口定义的是返回结果相关的函数。

类单一职责:
atomic相关类,包装类,都是类单一职责的体现。

2.里氏替换原则(LSP)

定义

里氏替换原则(Liskov Substitution Principle)是面向对象设计的基本原则之一。里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能在基类的基础上增加新的行为。
可以解刨为以下描述:

  1. 如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
  2. 所有引用基类的地方必须能透明地使用其子类的对象。
    通俗来讲:子类可以扩展父类的功能,但不能改变父类原有的功能。

优点

  1. 克服了继承中重写父类造成的可复用性变差的缺点。
  2. 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
  3. 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展新,降低需求变更时引入的风险。

出现的原因

有一功能P1,由类A完成,现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方法:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法,父类可以使用final的手段强制子类来遵守。
总结来说:
1.子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
2.子类中可以增加自己特有的方法
3.当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
4.当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类的方法更严格或相等。

例子

1.子类实现时可以增加自己特有的方法,但是不能覆盖父类的非抽象方法

类A完成两数相减的功能

class A{
    public int func1(int a, int b){
        return a - b;
    }
}
public class Client {
    public static void main(String[] args){
        A a = new A();
        System.out.println("100 - 50 = " + a.func1(100, 50));
        System.out.println("100 - 80 = " + a.func1(100, 80));
    }
}

运行结果:

100 - 50 = 50
100 - 80 = 20

后来,我们需要增加一个新功能:完成两数相加,然后再与100求和,由类B来负责。
即类B需要完成两个功能:
1.两数相减。
2.两数相加,然后再加100。
由于类A已经实现了第一个功能【两数相减】,所以类B继承类A后,只需要再完成第二个功能【两数相加,然后再加100】就可以了。

class B extends A{
    public int func1(int a, int b){
        return a + b;
    }

    public int func2(int a, int b){
        return func1(a, b) + 100;
    }
}
public class ClientB {
    public static void main(String[] args){
        B b = new B();
        System.out.println("100 - 50 = " + b.func1(100, 50));
        System.out.println("100 - 80 = " +b.func1(100, 80));
        System.out.println("100 + 20 + 100 = " + b.func2(100, 20));
    }
}

类B完成后,运行结果:

100 - 50 = 150
100 
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值