Java设计模式--------面向对象七大设计原则

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

例如:随着计算机的不断发展,程序学习这门技术也越来越重要,很多人都开启了学习java编程学习,本文就介绍了Java编程学习的七大基本原则。

面向对象设计原则一共有七个:开闭原则、里氏替换原则、依赖倒转原则、单一职责原则、接口隔离原则、组合/聚合复用原则、迪米特法则。

下面就仔细来介绍一下这些原则


提示:以下是本篇文章正文内容,下面案例可供参考

一、单一职责原则

对类来说,就是一个类应该只负责一项职责,这就是单一职责原则。

如果你有多个动机去修改一个类,那么这个类就有多个职责。这可能比较难理解,因为我们通常把一组职责放在一起思考。下面来看一个具体的例子。下面是一个 bird(调制解调器或者叫猫)的接口,以愤怒的小鸟为例来

package com.oop.day12;

/**
 * 以愤怒的小鸟游戏为li
 */
public interface Bird {
    void attack1();
    void attack2();
    void cry1();
    void cry2();
}

上面这个猫的接口中存在两个职责:第一个是攻击方式(attack1和attack2);第二个是攻击时的叫声(cry1和cry2)。这两个职责应该被分开,因为 :

  1. 它们没有共同点,而且通常会因为不同的原因被修改;
  2. 调用它们的代码通常属于应用的不同部分,而这部分代码也会因为不同的原因被修改。

下面是优化之后的

通过拆分猫的接口,我们可以在应用的其他部分将猫的设计分开来对待。虽然我们又在猫的实现中(Bird Implementation)将这两部分职责重新耦合在一起,但是除了初始化猫的代码以外,在使用面向接口编程的原则后,其他代码并不需要依赖于猫的实现。

SRP 是最简单的一个面向对象设计原则,但也是最难做正确的一个,因为我们习惯于将职责合并,而不是将它们分开来。找到并且拆分这些职责正是软件设计真正需要做的事情。

单一职责原则例子

package com.oop.day12;

public class Animal {
        public static void main(String[] args) {
            RoadAnimal roadAnimal = new RoadAnimal();
            //输出: 老虎 在森林里跑
            roadAnimal.run("老虎");
            //输出: 狮子 在森林上跑
            roadAnimal.run("摩托车");
           SeaAnimal seaAnimal = new SeaAnimal();
            //输出: 鲨鱼 在海洋里游
            seaAnimal.run("鲨鱼");
        }
    }

    // 陆地动物
    class RoadAnimal {
        public void run(String animal){
            System.out.println(animal + " 在公路上跑");
        }
    }

    //海洋动物
    class SeaAnimal {
        public void run(String animal){
            System.out.println(animal + " 在天上跑");
        }
    }

小结

核心思想:应该有且仅有一个原因引起类的变更

好处:类的复杂度降低、可读性提高、可维护性提高、扩展性提高、降低了变更引起的风险。

需注意:单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可以度量的,因项目和环境而异。

二、开闭原则 

开闭原则的英文是 Open Closed Principle,缩写为 OCP。

开闭原则说的是:软件实体(模块、类、函数等等)应该对扩展是开放的,对修改是关闭的

  • 对扩展是开放的,意味着软件实体的行为是可扩展的,当需求变更的时候,可以对模块进行扩展,使其满足需求变更的要求。
  • 对修改是关闭的,意味着当对软件实体进行扩展的时候,不需要改动当前的软件实体;不需要修改代码;对于已经完成的类文件不需要重新编辑;对于已经编译打包好的模块,不需要再重新编译。

两者结合起来表述为:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

最简单的就是利用抽象类,将方法定义为抽象方法,然后在后代中重写方法,来进行单继承多实现。

示例:员工回家乘坐的交通方式

public abstract class Tools {
    /**
     * 抽象方法型行驶
     */
    public abstract void run();

}
package com.oop.day10;

public class Bike extends Tools {
    @Override
    public void run(){
        System.out.println("下班的人努力蹬三蹦子回家");
    }
}
package com.oop.day10;

public class Bus extends Tools{
    @Override
    public void run(){
        System.out.println("下班的人挤公交车回家");
    }
}
package com.oop.day10;
/**
 * @authorDesc 诸神在上,佑我程序,玄功护体,BUG皆去!
 * @author 
 * @date 2022-11-04 10:34:34
 * @version 1.0.0
 * @description 员工类
 */
public class Employee {
    private String name;

    public Employee() {
    }

    public Employee(String name) {
        this.name = name;
    }
    /**
     * 乘坐交通工具回家
     */
    public void goHome(Tools tools){
        System.out.println(this.name + "准备回家");
        tools.run();
    }
}
package com.oop.day10;

public class Test {
    public static void main(String[] args) {
        Employee emp1 = new Employee("马云");
        Employee emp2 = new Employee("马化腾");

        Tools tools1 = new Bike();
        Tools tools2 = new Bus();

        emp1.goHome(tools1);
        emp2.goHome(tools2);
    }
}

小结

核心思想:尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化。例如就同过抽象类来解决这个问题

通俗来讲:一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。

三.接口隔离原则

什么是接口隔离原则

接口对于Java开发者来说都不陌生,它几乎存在于每一个Java程序中,是抽象的代名词。在讲接口隔离原则之前,先说说接口,接口分为以下两种:

  • 实例接口(Object Interface))
    在 Java 中声明一个类,然后用 new 关键字产生一个实例,是对一个类型的事物的描述,这就是一种接口。或许我们乍一看会有点懵,怎么和我们原来学习的接口不一样呢,其实我们这样想,我们都知道,在 Java 中有一个 Class 类,表示正在运行的类和接口,换句话说每一个正在运行时的类或接口都是 Class 类的对象,这是一种向上的抽象。接口是一种更为抽象的定义,类是一类相同事物的描述集合,那为什么不可以抽象为一个接口呢?
  • 类接口(Class Interface)
    这就是我们经常使用的用 interface 定义的接口

这里插一句,接口隔离原则中所说的接口并不是狭意的在 Java 中用 interface 定义的接口,而是一种更为宽泛的概念,可以是接口,抽象类或者实体类。

接口隔离原则分析

接口隔离原则是指使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立角色,不多不少,不干不该干的事,该干的事都要干。

1.一个接口就只代表一个角色,每个角色都有它特定的一个接口,此时这个原则可以叫做“角色隔离原则”。

2.接口仅仅提供客户端需要的行为,即所需的方法,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口

3.使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下,接口中的方法越少越好。

4.可以在进行系统设计时采用定制服务的方式,即为不同的客户端提供宽窄不同的接口,只提供用户需要的行为,而隐藏用户不需要的行为。

一旦一个接口太大,则需要将它,分割成一些更细小的接口,使用该接口的客户端需知道与之相关的方法即可。

 设计存在问题:接口承担了太多职责

导致该接口的实现类很庞大,实现类中都需要实现接口所有方法,灵活性较差,如果出现大量的空方法,将导致系统中产生大量的无用代码,影响代码质量;

由于客户端针对大接口编程,将在一定程度上破坏程序的封装性,客户端看到了不应该看到的方法,没有为客户端定制接口。

因此需要按照接口隔离原则和单一职责原则进行重构。将其中的一些方法封装在不同的小的接口中,确保每一个接口使用起来都较为方便,并都承担某一单一角色,每个接口中只包含一个客户端(如模块和类)所需的方法即可。

优化后:

四.里氏替换原则 

*里氏替换原则:**所有引用基类(父类)的地方必须能透明地使用其子类的对象。也就是说子类可以扩展父类的功能,但不能改变父类原有功能。

透明使用的关键就是,子类不能改变父类原有功能。

里氏替换原则由 Barbara Liskov 提出,这个原则很明显,Java 的多态或者 C++ 的虚函数本身就允许把指向基类的指针或引用,在调用其方法或函数的时候,调用实际类型的方法或函数。

里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程式中代替其基类(超类)对象。

继承

优点

  1. 提高代码的重用性,子类拥有父类的方法和属性。
  2. 提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性。

缺点(侵入性、不够灵活、高耦合)

  1. 继承是侵入性的,只要继承就必须拥有父类的所有方法和属性,在一定程度上约束了子类,降低了代码的灵活性。
  2. 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能会造成非常糟糕的结果,要重构大量的代码。

为什么需要里氏替换原则

任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,即基类随便怎么改动子类都不受此影响,那么基类才能真正被复用。

因为继承带来的侵入性,增加了耦合性,也降低了代码灵活性,父类修改代码,子类也会受到影响,此时就需要里氏替换原则。

 我们来看一个简单的例子:Circle 和 Square 继承了基类 Shape,然后在应用的方法中,根据输入 Shape 对象类型进行判断,根据对象类型选择不同的绘图函数将图形画出来。

void drawShape(Shape shape) {

    if (shape.type == Shape.Circle ) {
        drawCircle((Circle) shape);
    } else if (shape.type == Shape.Square) {
        drawSquare((Square) shape);
    } else {
        ……
    }
}

这种写法的代码既常见又糟糕,它同时违反了开闭原则和里氏替换原则。

  • 首先看到这样的 if/else 代码,就可以判断违反了(我们刚刚在上个部分讲过的)开闭原则:当增加新的 Shape 类型的时候,必须修改这个方法,增加 else if 代码。
  • 其次也因为同样的原因违反了里氏替换原则:当增加新的 Shape 类型的时候,如果没有修改这个方法,没有增加 else if 代码,那么这个新类型就无法替换基类 Shape。

要解决这个问题其实也很简单,只需要在基类 Shape 中定义 draw 方法,所有 Shape 的子类,Circle、Square 都实现这个方法就可以了:

public abstract Shape{
  public abstract void draw();
}

上面那段 drawShape() 代码也就可以变得更简单:

void drawShape(Shape shape) {
  shape.draw();
}

注意事项

  • 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
  • 子类中可以增加自己特有的方法。
  • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

小结

核心思想:在使用基类的的地方可以任意使用其子类,能保证子类完美替换基类。

通俗来讲:只要父类能出现的地方子类就能出现。反之,父类则未必能胜任。

好处:增强程序的健壮性,即使增加了子类,原有的子类还可以继续运行。

需注意:如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系 采用依赖、聚合、组合等关系代替继承。

五.依赖倒置原则

依赖倒置原则的英文是 Dependency Inversion Principle,缩写为 DIP。依赖倒置原则说的是:高层模块不依赖低层模块,它们共同依赖同一个抽象,这个抽象接口通常是由高层模块定义,低层模块实现同时抽象不要依赖具体实现细节,具体实现细节依赖抽象。高层模块就是调用端,低层模块就是具体实现类,抽象就是指接口或抽象类,细节就是实现类

依赖倒置原则:

  1. 高层模块不应该依赖底层模块,二者都应该依赖其抽象。(高层理解为吃动作,底层理解为面包等具体食物)
  2. 抽象不应该依赖细节,细节应该依赖抽象。
  3. 依赖倒置的中心思想就是面向接口编程

在继承时要遵循里氏替换原则。

错误示例:

class DependencyInversionTest {
    public static void main(String[] args) {
        Person person = new Person();
        person.receive(new Email());
    }
}

//电子邮箱类
class Email{
    public String getInfo(){
        return "获得电子邮件信息";
    }
}

//完成Person接受消息的功能
//缺点:如果要接受消息的对象是 微信类、短信类,就得增加相应的方法。
class Person{
    public void receive(Email email){
        System.out.println(email.getInfo());
    }
}
————————————————
版权声明:本文为优快云博主「做不秃头的程序员」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weixin_41804870/article/details/109132932

解决示例:

引入一个抽象的接口IReceiver,表示接收者,这样Person类就只与接口发送依赖

class DependencyInversionTest {
    public static void main(String[] args) {
        Person person = new Person();
        person.receive(new Email());
        person.receive(new Wechat());
    }
}

interface IReceiver{
    String getInfo();
}

//电子邮箱类
class Email implements IReceiver{
    public String getInfo(){
        return "获得电子邮件信息";
    }
}

//微信类
class Wechat implements IReceiver{
    @Override
    public String getInfo() {
        return "获得微信信息";
    }
}

//完成Person接受消息的功能
class Person{
    //这样就不需要每添加一个消息对象就写相应的方法
    public void receive(IReceiver receiver){
        System.out.println(receiver.getInfo());
    }
}
————————————————
版权声明:本文为优快云博主「做不秃头的程序员」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weixin_41804870/article/details/109132932

上面的示例是基于接口实现的,下面的示例是基于构造函数来实现的:

class DependencyInversionTest {
    public static void main(String[] args) {
        Person person = new Person(new Email());
        person.receive();
        person = new Person(new Wechat());
        person.receive();
    }
}

interface IReceiver{
    String getInfo();
}

//电子邮箱类
class Email implements IReceiver{
    public String getInfo(){
        return "获得电子邮件信息";
    }
}

//微信类
class Wechat implements IReceiver{
    @Override
    public String getInfo() {
        return "获得微信信息";
    }
}

//完成Person接受消息的功能
class Person{
    private IReceiver iReceiver;

    public Person(IReceiver iReceiver) {
        this.iReceiver = iReceiver;
    }

    public void receive(){
        System.out.println(iReceiver.getInfo());
    }
}
————————————————
版权声明:本文为优快云博主「做不秃头的程序员」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weixin_41804870/article/details/109132932

下面的示例是基于setter方法来实现:

class DependencyInversionTest {
    public static void main(String[] args) {
        Person person = new Person();
        person.setiReceiver(new Email());
        person.receive();
        person.setiReceiver(new Wechat());
        person.receive();
    }
}

interface IReceiver{
    String getInfo();
}

//电子邮箱类
class Email implements IReceiver{
    public String getInfo(){
        return "获得电子邮件信息";
    }
}

//微信类
class Wechat implements IReceiver{
    @Override
    public String getInfo() {
        return "获得微信信息";
    }
}

//完成Person接受消息的功能
class Person{
    private IReceiver iReceiver;

    public void setiReceiver(IReceiver iReceiver) {
        this.iReceiver = iReceiver;
    }

    public void receive(){
        System.out.println(iReceiver.getInfo());
    }
}
————————————————
版权声明:本文为优快云博主「做不秃头的程序员」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weixin_41804870/article/details/109132932

小结

核心思想:高层模块不应该依赖底层模块,二者都该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象;

通俗来讲:依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,互不影响,实现模块间的松耦合。

好处:依赖倒置的好处在小型项目中很难体现出来。但在大中型项目中可以减少需求变化引起的工作量。使并行开发更友好。

六.迪米特法则

什么是迪米特法则

迪米特法则(Law of Demeter )又叫做最少知识原则,也就是说,一个对象应当对其他对象尽可能少的了解。不和陌生人说话。英文简写为: LoD。

迪米特法则最初是用来作为面向对象的系统设计风格的一种法则,于1987年秋天由 lan holland 在美国东北大学为一个叫做迪米特的项目设计提出的。

迪米特法则的模式与意义

迪米特法则可以简单说成:talk only to your immediate friends。

对于OOD来说,又被解释为下面几种方式:

一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。

迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系——这在一定程度上增
加了系统的复杂度。


狭义的迪米特法则

如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一类的某一个方法的话,可以通过第三者转发这个调用。

这么看不太形象,我们来举个例子,和陌生人说话,甲和朋友认识,朋友和陌生人认识,而甲和陌生人不认识,这时甲可以直接和朋友说话,朋友可以直接和陌生人说话,而如果甲想和陌生人说话,就必须通过朋友

 这种方式,甲和陌生人之间就没有了任何直接联系,这样就避免了甲和陌生人的耦合度过高。当然还有一种更好的方式,与依赖倒转原则结合,为陌生人创建一个接口。

 这样的方式,和甲直接通信的是陌生人的抽象父类,和具体陌生人没有直接关系,所以符合迪米特法则。

小结

缺点

  1. 在系统里造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的商务逻辑无关。
  2. 遵循类之间的迪米特法则会是一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。

应用实例

  1. 外观模式
  2. 中介者模式

七.合成复用原则

合成复用原则的定义

合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。

实现

合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。

下面以汽车分类管理程序为例来介绍合成复用原则的应用。

分析:汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。下图所示是用继承关系实现的汽车分类的类图。

原则是尽量使用合成/聚合的方式。而不是使用继承。

小结

通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

参考文章

参考文链接:https://blog.youkuaiyun.com/weixin_41804870/article/details/109132932

Java-面向对象设计七大基本原则 - 知乎


总结

例如:以上就是今天要讲的内容,本文仅仅简单介绍了面向对象设计原则一共有七个:开闭原则、里氏替换原则、依赖倒转原则、单一职责原则、接口隔离原则、组合/聚合复用原则、迪米特法则。举出了大量的实例


评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值