都2020了 还不知道Java面向对象思想

182 阅读7分钟

三大特性

封装

利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外的接口使其与外部发生联系。用户无需关心对象内部的细节,但可以通过对象对外提供的接口来访问该对象。

优点:

  • 减少耦合:可以独立地开发、测试、优化、使用、理解和修改。
  • 减轻维护的负担:可以更容易被理解,并且在调试的时候可以不影响其他模块。
  • 有效地调节性能:可以通过剖析来确定哪些模块影响了系统的性能。
  • 提高软件的可重用性。
  • 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的。

以下 Person 类封装 name、age、work 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 age 属性,而无法获取 work 属性,但是 work 属性可以供 work() 方法使用。

class Person {

    private String name;
    private int age;
    private String work;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void work(String name) {
        if (this.name.equals(name)) {
            System.out.println(work);
        }
    }
}

继承

继承实现了 IS-A 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。(HAS-A 对应接口;IS-LIKE-A 对应继承后新增方法)

继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。

Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 向上转型

Animal animal = new Cat();

多态

多态分为编译时多态和运行时多态:

  • 编译时多态主要指方法的重载
  • 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定

运行时多态有三个条件:

  • 继承
  • 覆盖(重写)
  • 向上转型

下面的代码中,乐器类(Instrument)有两个子类:Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。

public class Instrument {

    public void play() {
        System.out.println("Instument is playing...");
    }
}
public class Wind extends Instrument {

    public void play() {
        System.out.println("Wind is playing...");
    }
}
public class Percussion extends Instrument {

    public void play() {
        System.out.println("Percussion is playing...");
    }
}
public class Music {

    public static void main(String[] args) {
        List<Instrument> instruments = new ArrayList<>();
        instruments.add(new Wind());
        instruments.add(new Percussion());
        for(Instrument instrument : instruments) {
            instrument.play();
        }
    }
}

类图

类图图片取自类图

泛化关系

用来描述继承关系,在 Java 中使用 extends 关键字。

实现关系

用来实现一个接口,在 Java 中使用 implements 关键字。

聚合关系

表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。

组合关系

和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如头部和嘴部,头部没了嘴部就不存在了。但是汽车和工程师就属于聚合关系了,因为工程师没了汽车还在。

关联关系

表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如登陆表单和按钮就是一种关联关系,一个登陆表单可以有很多按钮,但是一个按钮只属于一个登陆表单,因此这是一种多对一的关系,在运行开始之前就可以确定。

依赖关系

和关联关系不同的是,依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:

  • A 类是 B 类方法的局部变量;
  • A 类是 B 类方法的参数;
  • A 类向 B 类发送消息,从而影响 B 类发生变化。

设计原则

S.O.L.I.D

单一责任原则

修改一个类的原因应该只有一个。

换句话说就是让一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类。

如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力。

开放封闭原则

类应该对扩展开放,对修改关闭。

扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码。

符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将责任附加到对象上,而不用去修改类的代码。

里氏替换原则

子类对象必须能够替换掉所有父类对象。

继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。

如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。

接口分离原则

不应该强迫客户依赖于它们不用的方法。

因此使用多个专门的接口比使用单一的总接口要好。

依赖倒置原则

高层模块不应该依赖于低层模块,二者都应该依赖于抽象; 抽象不应该依赖于细节,细节应该依赖于抽象。

高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于低层模块,那么低层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。

举个例子,现在你需要实现一个比萨店,你第一件想到的事情是什么?我想到的是一个比萨店,里面有很多具体的比萨,如:芝士比萨、素食比萨、海鲜比萨…… 比萨店是上层模块,比萨是下层模块,如果把比萨店和它依赖的对象画成一张图,看起来是这样:

没错!先从顶端开始,然后往下到具体类,但是,正如你看到的你不想让比萨店理会这些具体类,要不然比萨店将全都依赖这些具体类。现在“倒置”你的想法……别从上层模块比萨店开始思考,而是从下层模块比萨开始,然后想想看能抽象化些什么。你可能会想到,芝士比萨、素食比萨、海鲜比萨都是比萨,所以它们应该共享一个Pizza接口。对了,你想要抽象化一个Pizza。好,现在回头重新思考如何设计比萨店。

图一的依赖箭头都是从上往下的,图二的箭头出现了从下往上,依赖关系确实“倒置”了

另外,此例子也很好的解释了“高层模块不应该依赖于低层模块,二者都应该依赖于抽象。”,在最开始的设计中,高层模块PizzaStroe直接依赖低层模块(各种具体的Pizaa),调整设计后,高层模块和低层模块都依赖于抽象(Pizza)。

其他常见原则

迪米特法则

迪米特法则又叫作最少知识原则(Least Knowledge Principle,简写 LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。

合成复用原则

尽量使用对象组合,而不是通过继承来达到复用的目的。

共同封闭原则

一起修改的类,应该组合在一起(同一个包里)。如果必须修改应用程序里的代码,我们希望所有的修改都发生在一个包里(修改关闭),而不是遍布在很多包里。

稳定抽象原则

最稳定的包应该是最抽象的包,不稳定的包应该是具体的包,即包的抽象程度跟它的稳定性成正比。

稳定依赖原则

包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性。

本文参考六大设计原则之依赖倒置原则(DIP)