面向对象五大基本原则详解

2022-09-14 00:00:00 面向对象 五大 基本原则

面向对象五大基本原则

  • 单一职责原则
    • 计算器实例
  • 开放封闭原则(OCP)
    • 开放封闭原则示例(书店售书)
  • 里氏替换原则(LSP)
  • 接口隔离原则
  • 依赖倒置原则

单一职责原则

一个类最好只有一个能引起变化的原因,只做一件事,单一职责原则可以看做是低耦合高内聚思想的延伸,提高高内聚来减少引起变化的原因。

计算器实例

通常在写计算器类是会有各种写法,简单的写法,例如:

package Recursion;

/**
 * Created by lirui on 2018/12/6.
 */
public class JiSuanQi {
    int a;
    int b;

    public JiSuanQi(int a, int b) {
        this.a = a;
        this.b = b;
    }

    //加法
    public int add(int a, int b) {
        return a + b;
    }

    //减法
    public int sub(int a, int b) {
        return a - b;
    }
}

但这种写法却没有很好的按照单一职责的原则去构建,计算器不仅有加法,减法,还有乘法,除法,倘若后续要加上乘法,除法,那就要在原来的类中进行改动,添加这两个方法,但如果按照单一职责原则,一个类专注做一件事,对上一个类进行拆分,例如:

  1. 加法类AddJiSuanQi
public class AddJiSuanQi extends  JiSuanQi {
    public AddJiSuanQi(int a, int b) {
        super(a, b);
    }
    //加法
    public int add(int a, int b) {
        return a + b;
    }
}
  1. 减法类SubJiSuanQi
public class SubJiSuanQi extends JiSuanQi {
    public SubJiSuanQi(int a, int b) {
        super(a, b);
    }
    //减法
    public int add(int a, int b) {
        return a - b;
    }
}

通过将加法减法分别抽出成单一的类,这样,如果后续还要加乘法,除法功能,只需再新添这两个类,而不需要在原有类中进行改动。

开放封闭原则(OCP)

软件实体(类,模块,函数等等)应该是可扩展的,但是不可修改。因为修改程序有可能会对原来的程序造成错误。不能修改,但是可以添加功能,尽可能的在外边添加新的类。

开放封闭原则示例(书店售书)

  1. 类图
    《面向对象五大基本原则详解》

  2. 代码实现

/**
 * 书籍接口
 * Created by lirui on 2018/12/6.
 */
public interface IBook {
    //书籍名称
    public String getName();
    //书籍售价
    public int getPrice();
    //书籍作者
    public String getAuthor();
}
package Book;

/**
 * Created by lirui on 2018/12/6.
 */
public class NovelBook implements IBook {
    //书籍名称
    private String name;
    //书籍的价格
    private int price;
    //通过构造函数传递书籍数据
    private String author;

    public NovelBook(String name, int price, String author) {
        this.name = name;
        this.price = price;
        this.author = author;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public int getPrice() {
        return this.price;
    }

    @Override
    public String getAuthor() {
        return this.author;
    }
}

package Book;

import java.text.NumberFormat;
import java.util.ArrayList;

/**
 * Created by lirui on 2018/12/6.
 */
public class BookStore {
    private final static ArrayList<IBook> bookList=new ArrayList<>();
    static {
        bookList.add(new NovelBook("三国演义",100,"罗贯中"));
        bookList.add(new NovelBook("西游记",200,"吴承恩"));
        bookList.add(new NovelBook("红楼梦",300,"曹雪芹"));
            }
    //模拟书店买书
    public static void main(String[] args) {
        NumberFormat format=NumberFormat.getCurrencyInstance();
        format.setMaximumFractionDigits(2);
        System.out.println("-=-=-=-=-=-=-=-=书店卖出书籍记录-=-=-=--=-=-=-=-");
        for (IBook book:bookList){
            System.out.println("书籍名称:"+book.getName()+"\t书籍作者:"+book.getAuthor()+"\t书籍价格:"+format.format(book.getPrice()/100.0)+"元");
        }
    }
}
运行结果
-=-=-=-=-=-=-=-=书店卖出书籍记录-=-=-=--=-=-=-=-
书籍名称:三国演义	书籍作者:罗贯中	书籍价格:¥1.00元
书籍名称:西游记	书籍作者:吴承恩	书籍价格:¥2.00元
书籍名称:红楼梦	书籍作者:曹雪芹	书籍价格:¥3.00元

项目投产,书店盈利,但为扩大市场,书店决定,40元以上打8折,40元以下打9 折。如何解决这个问题呢?

  1. 第一个办法:

修改接口。在IBook上新增加一个方法getOffPrice(),专门进行打折,所有实现类实现这个方法。
但是这样修改的后果就是实现类NovelBook要修改,BookStore中的main方法也修改,同时Ibook作为接口应该是稳定且可靠的,不应该经常发生变化,否则接口做为契约的作用就失去了效能,其他不想打折的书籍也会因为实现了书籍的接口必须打折,因此该方案被否定。

  1. 第二个办法:

修改实现类。修改NovelBook 类中的方法,直接在getPrice()中实现打折处理,这个应该是大家在项目中经常使用的就是这样办法,通过class文件替换的方式可以完成部分业务(或是缺陷修复)变化,该方法在项目有明确的章程(团队内约束)或优良的架构设计时,是一个非常优秀的方法。
但是该方法还是有缺陷的,例如采购书籍人员也是要看价格的,由于该方法已经实现了打折处理价格,因此采购人员看到的也是打折后的价格,这就产生了信息的蒙蔽效果,导致信息不对称而出现决策失误的情况。该方案也不是一个最优的方案。

  1. 第三个办法:

最优方案,通过扩展实现变化。增加一个子类 OffNovelBook,覆写getPrice方法,高层次的模块(也就是static静态模块区)通过OffNovelBook类产生新的对象,完成对业务变化开发任务。好办法,风险也小

代码实现

/**
 * 书籍打折类
 * Created by lirui on 2018/12/6.
 */
public class OffNovelBook extends NovelBook {
    public OffNovelBook(String name, int price, String author) {
        super(name, price, author);
    }
    //覆写销售价格
    @Override
    public int getPrice(){
        //原价
        int prePrice=super.getPrice();
        int offPrice=0;
        if (prePrice>=200){
            offPrice=prePrice*80/100;
        }else{
            offPrice=prePrice*90/100;
        }return offPrice;
    }
}

书店类

/**
 * Created by lirui on 2018/12/6.
 */
public class BookStore {
    private final static ArrayList<IBook> bookList=new ArrayList<>();
    static {
        bookList.add(new OffNovelBook("三国演义",100,"罗贯中"));
        bookList.add(new OffNovelBook("西游记",200,"吴承恩"));
        bookList.add(new OffNovelBook("红楼梦",300,"曹雪芹"));
            }
    //模拟书店买书
    public static void main(String[] args) {
        NumberFormat format=NumberFormat.getCurrencyInstance();
        format.setMaximumFractionDigits(2);
        System.out.println("-=-=-=-=-=-=-=-=书店卖出书籍记录-=-=-=--=-=-=-=-");
        for (IBook book:bookList){
            System.out.println("书籍名称:"+book.getName()+"\t书籍作者:"+book.getAuthor()+"\t书籍价格:"+format.format(book.getPrice()/100.0)+"元");
        }
    }
}
运行结果
-=-=-=-=-=-=-=-=书店卖出书籍记录-=-=-=--=-=-=-=-
书籍名称:三国演义	书籍作者:罗贯中	书籍价格:¥0.90元
书籍名称:西游记	书籍作者:吴承恩	书籍价格:¥1.60元
书籍名称:红楼梦	书籍作者:曹雪芹	书籍价格:¥2.40元

归纳变化:

逻辑变化。只变化一个逻辑,而不涉及到其他模块,比如原有的一个算法是ab+c,现在要求ab*c,可能通过修改原有类中的方法方式来完成,前提条件是所有依赖或关联类都按此相同逻辑处理。
子模块变化。一个模块变化,会对其他模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的,刚刚的书籍打折处理就是类似的处理模块,该部分的变化甚至引起界面的变化。

  1. 扩展接口再扩展实现:
    书店又增加了计算机类书籍,该类书还有一个独特特性:面向的是什么领域

代码实现

/**
 * 计算机书籍接口
 * Created by lirui on 2018/12/6.
 */
public interface IComputerBook extends IBook{
    //计算机书籍范围
    public String getScope();
}
**
 * 计算机书籍类
 * Created by lirui on 2018/12/6.
 */
public class ComputerBook implements IComputerBook {
   private String name;
   private int price;
   private String author;
   private String scope;
   public ComputerBook(String name,int price,String author,String scope){
       this.name=name;
       this.price=price;
       this.author=author;
       this.scope=scope;
   }
    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public int getPrice() {
        return this.price;
    }

    @Override
    public String getAuthor() {
        return this.author;
    }

    @Override
    public String getScope() {
        return this.scope;
    }
}
/**
 * 书店
 * Created by lirui on 2018/12/6.
 */
public class BookStore {
    private final static ArrayList<IBook> bookList=new ArrayList<>();
    static {
        bookList.add(new NovelBook("三国演义",100,"罗贯中"));
        bookList.add(new NovelBook("西游记",200,"吴承恩"));
        bookList.add(new NovelBook("红楼梦",300,"曹雪芹"));
          bookList.add(new ComputerBook("Think in Java",400,"Bruce Eckel","编程语言"));
            }
    //模拟书店买书
    public static void main(String[] args) {
        NumberFormat format=NumberFormat.getCurrencyInstance();
        format.setMaximumFractionDigits(2);
        System.out.println("-=-=-=-=-=-=-=-=书店卖出书籍记录-=-=-=--=-=-=-=-");
        for (IBook book:bookList){
            System.out.println("书籍名称:"+book.getName()+"\t书籍作者:"+book.getAuthor()+"\t书籍价格:"+format.format(book.getPrice()/100.0)+"元");
        }
    }
运行结果
-=-=-=-=-=-=-=-=书店卖出书籍记录-=-=-=--=-=-=-=-
书籍名称:三国演义	书籍作者:罗贯中	书籍价格:¥1.00元
书籍名称:西游记	书籍作者:吴承恩	书籍价格:¥2.00元
书籍名称:红楼梦	书籍作者:曹雪芹	书籍价格:¥3.00元
书籍名称:Think in Java	书籍作者:Bruce Eckel	书籍价格:¥4.00元

里氏替换原则(LSP)

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

  1. 问题描述
    一个功能T有类A来完成,后来由于需求的变化,该功能T被分为了T1和T2两部分,这两部分的功能分别有类A的子类:类B和类C来完成。如果功能T1发生了变化,修改类B的同事,有可能引起T2的功能产生故障。

  2. 产生原因
    在继承关系中,基类的存在是为整个继承的结构设定一系列的规定和约束,让整个结构都按照这个规定和约束来。例如说用一个基类来描述鸟类,根据我们对鸟类的一贯认知,会在基类中通过约定有羽毛属性,有飞翔行为的是鸟类。这样在实现布谷鸟或者杜鹃鸟的时候,它都有基类中规定的属性和行为约束,但是突然有一天boss过来说把企鹅也要加进来,因为企鹅也属于鸟类。此时我们在继承了鸟类这个基类的时候,把羽毛属性和飞翔的行为都改了。此时布谷鸟或者杜鹃鸟就都如企鹅一般没了羽毛,并且不会飞翔了。

  3. 解决办法
    当使用继承的时候,使用里氏替换原则。当使用继承的时候,尽量不覆盖或重写父类的方法。当扩展父类方法的时候,保证不影响父类功能的前提下扩展。

  4. 实例

用一个类描述猫的叫声

/**
 * 猫
 * Created by lirui on 2018/12/6.
 */
public class Cat {
    public void say(){
        System.out.println("喵喵喵");
    }
}

又来个高冷猫,它不叫

/**
 1. 高冷猫
 2. Created by lirui on 2018/12/6.
 */
public class SpcCat extends Cat {
    @Override
    public void say(){
        System.out.println("不叫了");
    }
}

根据里氏替换原则:任何出现基类的地方,都可以用子类替换。那么此刻就尴尬了,如果用里氏替换原则将它替换,那么所有的猫都变成高冷的猫了。这显然是不合理的,但是这种问题在实际应用中确实很常见的。

Liskov于1987年提出了一个关于继承的原则“Inheritance should ensure that any property proved about supertype objects also holds for subtype objects.”——“继承必须确保超类所拥有的性质在子类中仍然成立。”也就是说,当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。

   如果用里氏替换原则来判断一个类的框架是否合理的话,继承和多态是不是就没用了?答案显然是否定的。就上

面的猫的这个例子来看,喜欢叫的猫和高冷的猫显然不应该是继承关系,而是并行的关系。在处理这种情况的时候,我们只需要定义一个共同的基类,创建一个纯虚函数来实现。那么假如我们非要用到继承来实现一个框架的时候怎么办呢?此时就要遵守里氏替换原则的四层含义:

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

 总结起来就是:子类实现父类的抽象方法优先,但是不能覆盖父类的抽象方法。但是当子类必须要实现父类的方法的时候,那么就要遵守里氏替换原则中的第三条和第四条。

接口隔离原则

接口端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。

  1. 问题描述
    当一个提供接口的类中对于它的子类来说不是最小的接口,那么它的子类在实现该类的时候就必须要实现一些自己不需要的功能。如此一来,整个系统就会变得臃肿难以维护。

  2. 问题由来
    当类A通过接口I来依赖B,类C也通过接口I来依赖D,那么对于类A和类C来说,如果接口I不是最小接口,那么类B和类D就必须要实现他们不需要的方法。

  3. 解决问题
    遵守接口隔离原则,将“胖大”接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口类来建立依赖关系。这样他们依赖的类就不需要实现他们不需要的方法。

  4. 实例
    场景:某高三学生需要进行一次模拟测试,由于文理科教学内容之别,那么他们考试内容也有区别。假如现在考试的内容有语文,数学,地理,物理这些科目。 作为文科生,他们只考语文,数学,地理;而作为理科生,他们要考语文,数学,物理。用Java来实现这个功能

没有实现接口隔离的代码

考试科目接口类

public interface IExam {

    public void chinese();
    public void math();
    public void physics();
    public void geograp();
}

文科考试类

public class ArtsExam implements IExam {
    @Override
    public void chinese() {
        System.out.println("语文");
    }

    @Override
    public void math() {
        System.out.println("数学");
    }

    @Override
    public void physics() {

    }

    @Override
    public void geograp() {
        System.out.println("地理");
    }
}

理科考试类

public class PhyExam implements IExam {
    @Override
    public void chinese() {
        System.out.println("语文");
    }

    @Override
    public void math() {
        System.out.println("数学");
    }

    @Override
    public void physics() {
        System.out.println("物理");
    }

    @Override
    public void geograp() {

    }
}
如此实现的话显然是有问题的,为什么一个类里面会出现空方法这么尴尬的东西。如果现在文理科目不止这四科,增加了生物,历史等等,那是不是出现的空方法就更多了。这时候就需要使用接口隔离原则,让类之间的依赖建立在最小接口的原则上。
  1. 用接口隔离原则实现
    考试接口基类
public interface IExam {

    public void chinese();
    public void math();
}

文科接口类

/**
 * 文科接口类
 * Created by lirui on 2018/12/6.
 */
public interface IArtExam extends IExam {
    public void geograp();
}

理科接口类

/**
 * 理科接口类
 * Created by lirui on 2018/12/6.
 */
public interface IPhyExam extends IExam {
    public void physics();
}

文科考试类

public class ArtsExam implements IArtExam {
    @Override
    public void chinese() {
        System.out.println("语文");
    }

    @Override
    public void math() {
        System.out.println("数学");
    }

    @Override
    public void geograp() {
        System.out.println("地理");
    }
}

理科考试类

public class PhyExam implements IPhyExam {
    @Override
    public void chinese() {
        System.out.println("语文");
    }

    @Override
    public void math() {
        System.out.println("数学");
    }

    @Override
    public void physics() {
        System.out.println("物理");
    }
}

但是在使用接口隔离原则的时候,还是需要根据情况来控制接口的粒度,接口太小会引起系统中接口泛滥,不利于维护;太大则有违背了接口隔离规则,容易出现“胖大”接口。所以一般接口中只为被依赖的类提供定制的方法即可,不要让客户去实现他们不需要的方法。

依赖倒置原则

简单来说
A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
B.抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

  1. 为什么要遵循依赖倒置原则
     
      很多时候我们更改一个需求,发现更改一处地方需要更改多个文件,看见很多的报错我们自己都觉得烦,我们很清醒的意识到这是因为严重的耦合导致的,所以自然要想办法解决这个问题
  2. 依赖倒置有什么好处

简单来说,解决耦合。一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。

  1. 例子
    公司是奇瑞和江淮公司的金牌合作伙伴,现要求开发一套自动驾驶系统,只要汽车上安装该系统就可以实现无人驾驶,该系统可以在奇瑞和江淮车上使用,只要这两个品牌的汽车使用该系统就能实现自动驾驶。

  2. 常规写法

既然是两种不同的汽车,那我们分别定义出来,一个QQ一个JAC,代码如下:

/**
 * 奇瑞汽车
 * Created by lirui on 2018/12/6.
 */
public class QQ {
    public  void run(){
        System.out.println("奇瑞汽车启动");
    }
    public void stop(){
        System.out.println("奇瑞汽车停止");
    }
}

/**
 * 江淮汽车
 * Created by lirui on 2018/12/6.
 */
public class JAC {
    public void run(){
        System.out.println("江淮汽车启动");
    }
    public void stop(){
        System.out.println("江淮汽车停止");
    }
}
/**
 * 自动驾驶系统
 * Created by lirui on 2018/12/6.
 */
public class AutoSystem {
    private String mType;
    private QQ qq;
    private JAC jac;
    public AutoSystem(String mtype){
        this.mType=mtype;
        qq=new QQ();
        jac=new JAC();
    }
    public void AutoRun(){
        if ("qq".equals(mType)){
            qq.run();
        }else{
            jac.run();
        }
    }
    public void AutoStop(){
        if ("qq".equals(mType)){
            qq.stop();
        }else{
            jac.stop();
        }
    }
}

代码很简单,相信有java基础的人一看就懂了,但是缺点也很明显,扩展性特别差,现在只有两种车,if-else语句还算简单,假设以后有100种车呢?是不是要改很多文件,写很多的if-else语句,效率很低。
那么,接下来我们开始改造。

  1. 首先观察江淮和奇瑞两个类

完全一模一样的代码,我们开始抽离,写个抽象类或者接口都可以,这里我们写一个接口,有人纳闷为啥不写抽象类呢?根据我的个人见解,抽象类的抽象方法可以自己实现,但是接口的方法都是由子类实现的,车的功能我们具体细节我们不知道,没必要写出它的实现,所以,我们由子类实现,代码如下

public interface ICar {
    public void run();
    public void stop();
}
  1. 其次来我们看定义

A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
B.抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

在上述代码中,我们的AutoSystem类严重依赖于具体的子类,比如江淮,奇瑞,依赖了低层模块,而定义里已经说明,我们应该依赖于抽象,于是我们改造AutoSystem代码
代码如下:

/**
 * 自动驾驶系统
 * Created by lirui on 2018/12/6.
 */
public class AutoSystem {
   private ICar iCar;
    public AutoSystem(ICar iCar){
        this.iCar=iCar;
    }
    public void AutoRun(){
        iCar.run();
    }
    public void AutoStop(){
       iCar.stop();
    }
}

/**
 * 奇瑞汽车
 * Created by lirui on 2018/12/6.
 */
public class QQ implements ICar{
    public  void run(){
        System.out.println("奇瑞汽车启动");
    }
    public void stop(){
        System.out.println("奇瑞汽车停止");
    }
}

/**
 * 江淮汽车
 * Created by lirui on 2018/12/6.
 */
public class JAC implements ICar{
    public void run(){
        System.out.println("江淮汽车启动");
    }
    public void stop(){
        System.out.println("江淮汽车停止");
    }
}

这样,我们就抽出了它的共同点。正好也呼应了B点,具体应该依赖于抽象,而不是抽象依赖于具体

    原文作者:程序员蛋蛋
    原文地址: https://blog.csdn.net/qq_34375473/article/details/84852650
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。

相关文章