面向对象程序分析与设计六大设计原则


一、面向对象相关概念

1、面向对象程序设计(oop)

面向对象程序设计(Object Oriented Programming,OOP)是一种计算机编程架构。OOP的一条基本原则是计算机程序由单个能够起到子程序作用的单元或对象组合而成。OOP达到了软件工程的三个主要目标:重用性、灵活性和扩展性。OOP=对象+类+继承+多态+消息,其中核心概念是类和对象。

2、面向对象分析与设计(ooad)

OOAD(Object Oriented Analysis Design,,面向对象分析与设计)是现代软件企业广为采用的一项有效技术。OOAD方法要求在设计中要映射现实世界中指定问题域中的对象和实体,例如:顾客、汽车和销售人员等。这就需要设计要尽可能地接近现实世界,即以最自然的方式表述实体。所以面向对象技术的优点即为能够构建与现实世界相对应的问题模型,并保持他们的结构、关系和行为为模式。OOAD包含面向对象分析(OOA)和面向对象设计(OOD),也读作OOA/D。

3、六大设计原则(SOLID)

  • Single Responsibility Principle:单一职责原则
  • Open Closed Principle:开闭原则
  • Liskov Substitution Principle:里氏替换原则
  • Law of Demeter:迪米特法则
  • Interface Segregation Principle:接口隔离原则
  • Dependence Inversion Principle:依赖倒置原则
把这六个原则的首字母联合起来(两个 L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。下面我们来分别看一下这六大设计原则。
这三个概念大家应该都懂,在这里插入这点废话只是想让大家了解OOP、OOA、OOD、OOAD、SOLID等概念,既然都开始研究设计原则了,这些基本名词概念还是要知道的

二、设计原则

1、单一职责原则(SRP)

概念

一个类应该只有一个发生变化的原因。

单一职责原则适用于类、接口、方法。这个原则有什么用呢,它让类的职责更单一。这样的话,每个类只需要负责自己的那部分,类的复杂度就会降低。如果职责划分得很清楚,那么代码维护起来也更加容易。试想如果所有的功能都放在了一个类中,那么这个类就会变得非常臃肿,而且一旦出现bug,要在所有代码中去寻找;更改某一个地方,可能要改变整个代码的结构,想想都非常可怕。当然一般时候,没有人会去这么写的。当然,这个原则不仅仅适用于类,对于接口和方法也适用,即一个接口/方法,只负责一件事,这样的话,接口就会变得简单,方法中的代码也会更少,易读,便于维护。事实上,实际开发过程中类的职责是模块级或层级结构的,一个类负责一个模块或一个层级的功能,方法的职责才是功能级的。单一职责原则是最容易理解与实现的,但它也是最难以遵循,它强调原子性,但是我们封装方法的时候基本不会细致到把每一步都封装(A方法中包含B、C、D步骤,很少有人会把B、C、D也封装成方法在A中调用)。

优点
  • 代码的粒度降低了,类的复杂度降低了。
  • 可读性提高了,每个类的职责都很明确,可读性自然更好。
  • 可维护性提高了,可读性提高了,一旦出现 bug ,自然更容易找到他问题所在。
  • 改动代码所消耗的资源降低了,更改的风险也降低了。

2、接口隔离原则(ISP)

概念

客户端不应该依赖它不需要的接口。
类间的依赖关系应该建立在最小的接口上。

以上两个定义的含义是:要为各个类建立它们需要的专用接口/抽象类,而不要试图去建立一个很庞大的接口/抽象类供所有依赖它的类去调用。接口隔离原则容易理解也容易实现,前提是不要把思维局限在代码的具体实现上,接口隔离原则强调的程序整体框架的构建。

这句话怎么理解呢,我们举个例子说明一下

/**
 * 动物接口
 * @author ren
 * @description
 * @date 2021年12月14日 09:54:08
 */
public interface Animal {
    /** 行走方法 */
    public void walk();
    /** 游泳方法 */
    public void swim();
    /** 飞行方法 */
    public void flight();
    /** 吃方法 */
    public void eat();
    /** 睡方法 */
    public void sleep();
}

在这个动物接口中,我们定义了五个方法,如果我们想要构建一个Dog类去实现动物接口

/**
 * @author ren
 * @description
 * @date 2021年12月14日 10:06:43
 */
public class Dog implements Animal{
    @Override
    public void walk() { System.out.println("修勾在跑!"); }
    @Override
    public void swim() {}
    @Override
    public void flight() {}
    @Override
    public void eat() { System.out.println("修勾在吃!"); }
    @Override
    public void sleep() { System.out.println("修勾在睡!"); }
}

很显然我们必须重写接口中所有的方法(包括不需要的方法),于是根据接口隔离原则我们可以把动物接口进行拆分

/**
 * 动物接口
 * @author ren
 * @description
 * @date 2021年12月14日 09:54:08
 */
public interface Animal {
    /** 吃方法 */
    public void eat();

    /** 睡方法 */
    public void sleep();
}
/**
 * 陆生动物
 * @author ren
 * @description
 * @date 2021年12月14日 10:00:15
 */
public interface TerrestrialAnimal extends Animal{
    /** 行走方法 */
    public void walk();
}
/**
 * 水生动物
 * @author ren
 * @description
 * @date 2021年12月14日 09:58:56
 */
public interface Aquatilia extends Animal{
    /** 游泳方法 */
    public void swim();
}
/**
 * 飞行动物
 * @author ren
 * @description
 * @date 2021年12月14日 10:00:52
 */
public interface FlyingAnimal extends Animal{
    /** 飞行方法 */
    public void flight();
}

这个时候我们想要构建一个Dog类去实现陆生动物接口就不会重写不需要的方法了

/**
 * @author ren
 * @description
 * @date 2021年12月14日 10:06:43
 */
public class Dog implements TerrestrialAnimal{
    @Override
    public void walk() { System.out.println("修勾在跑!"); }
    @Override
    public void eat() { System.out.println("修勾在吃!"); }
    @Override
    public void sleep() { System.out.println("修勾在睡!"); }
}
优点
  • 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
  • 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
  • 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
  • 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
  • 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
接口隔离原则和单一职责的区别
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的: 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

3、依赖倒转原则(DIP)

概念

上层模块不应该依赖底层模块,它应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。

所谓依赖就是指出现过的类型,一般来讲就是导入的包。假如A类中有B类型的属性,或者方法返回值或参数为B类型,或者方法得到B类型的对象就称为A依赖于B。上面两个定义就是指在程序设计时类中所依赖的都应该是接口,当接口被实现后,这些实现类也就依赖于抽象了。

那么我们通过代码来演示:

/**
 * @author ren
 * @description
 * @date 2021年11月23日 11:10:49
 */
public class Test {
    public static void main(String[] args) {
        Painter painter = new Painter();
        painter.drawing(new Circle());
    }
}
class Painter{
    public void drawing(Circle circle){circle.produce();}
}
class Circle{
    public void produce(){System.out.println("画一个圆");}
}

上面代码中主程序创建一个画家(Painter )对象,画家想要画什么图形由主程序决定,这样的代码一看就很局限,画家要画其他图案时这个方法就不能用了,所以绘画方法的参数不能太过局限,所以我们要抽象出一个图案接口,由不同的具体形状图案实现这个图案接口。

/**
 * @author ren
 * @description
 * @date 2021年11月23日 11:10:49
 */
public class Test2 {
    public static void main(String[] args) {
        Painter painter = new Painter();
        painter.drawing(new Circle());
        painter.drawing(new Rectangle());
    }
}
class Painter{
    public void drawing(Diagram diagram){diagram.produce();}
}
interface Diagram{
    public void produce();
}
class Circle implements Diagram{
    public void produce(){System.out.println("画一个圆");}
}
class Rectangle implements Diagram{
    public void produce(){System.out.println("画一个矩形");}
}

这代码是不是很熟悉,这不就是多态么。我们再结合依赖倒转原则的定义去理解,上层模块(调用者Painter)不应该依赖底层模块(具体图案类,Circle 和Rectangle),它应该依赖于抽象(图案接口Diagram)。抽象(图案接口Diagram)不应该依赖于细节(具体图案类,Circle 和Rectangle),细节应该依赖于抽象。依赖倒转原则容易理解也容易实现,严格遵循依赖倒转原则设计出的程序易于扩展与维护。

4、迪米特法则(LOD)

概念

只与你的直接朋友交谈,不跟“陌生人”说话

其含义是:如果两个类无需建立依赖关系就劲量避免建立依赖关系,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。如此说完其实还是云里雾里对吧,我们通过代码演示就一目了然了

/**
 * @author ren
 * @description
 * @date 2021年11月23日 11:10:49
 */
public class Test {
    public static void main(String[] args) {
        DogManager dogManager = new DogManager();
        dogManager.printAllDog();
    }
}
class Huskie{
    private String name;
    public Huskie(String name) { this.name = name; }
    public String getName(){ return this.name; }
}
class Alaska{
    private String name;
    public Alaska(String name) { this.name = name; }
    public String getName(){ return this.name; }
}
class HuskieManager{
    List<Huskie> list = new ArrayList<Huskie>();
    {
    	//模拟数据
        for(int i=1;i<6;i++){
            list.add(new Huskie("二哈"+i+"号"));
        }
    }
    public List<Huskie> getHuskieList(){ return this.list;}
}
class AlaskaManager{
    List<Alaska> list = new ArrayList<Alaska>();
    {
    	//模拟数据
        for(int i=1;i<6;i++){
            list.add(new Alaska("阿拉斯加"+i+"号"));
        }
    }
    public List<Alaska> getAlaskaList(){ return this.list;}
}
class DogManager{
    public void printAllDog(){
        HuskieManager huskieManager = new HuskieManager();
        List<Huskie> huskieList = huskieManager.getHuskieList();
        for(Huskie huskie:huskieList){
            System.out.println(huskie.getName());
        }
        AlaskaManager alaskaManager = new AlaskaManager();
        List<Alaska> alaskaList = alaskaManager.getAlaskaList();
        for(Alaska alaska:alaskaList){
            System.out.println(alaska.getName());
        }
    }
}

以上代码是打印出所有修勾的名字,这里要注意的是在总管理类(DogManager)中依赖有HuskieManager 、Huskie 、AlaskaManager 、Alaska。这样的设计不符合迪米特法则,原因在于HuskieManager 与Huskie和 AlaskaManager与Alaska在DogManager中存在依赖冗余,这四个依赖其实依赖两个就可以了,那么修改后的代码为

/**
 * @author ren
 * @description
 * @date 2021年11月23日 11:10:49
 */
public class Test {
    public static void main(String[] args) {
        DogManager dogManager = new DogManager();
        dogManager.printAllDog();
    }
}
class Huskie{
    private String name;
    public Huskie(String name) { this.name = name; }
    public String getName(){ return this.name; }
}
class Alaska{
    private String name;
    public Alaska(String name) { this.name = name; }
    public String getName(){ return this.name; }
}
class HuskieManager{
    List<Huskie> list = new ArrayList<Huskie>();
    {
        for(int i=1;i<6;i++){
            list.add(new Huskie("二哈"+i+"号"));
        }
    }
    public void printDog(){
        for(Huskie huskie:list){
            System.out.println(huskie.getName());
        }
    }
}
class AlaskaManager{
    List<Alaska> list = new ArrayList<Alaska>();
    {
        for(int i=1;i<6;i++){
            list.add(new Alaska("阿拉斯加"+i+"号"));
        }
    }
    public void printDog(){
        for(Alaska alaska:list){
            System.out.println(alaska.getName());
        }
    }
}
class DogManager{
    public void printAllDog(){
        HuskieManager huskieManager = new HuskieManager();
        huskieManager.printDog();
        AlaskaManager alaskaManager = new AlaskaManager();
        alaskaManager.printDog();
    }
}

这次再看DogManager,它只依赖了HuskieManager 和AlaskaManager ,减少了依赖关系。迪米特法则最难理解、最难实现、最难以遵循。

优点
  • 降低了类之间的耦合度,提高了模块的相对独立性。
  • 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
掌握使用迪米特法则的平衡
过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
迪米特法则的实现方法
  • 从依赖者的角度来说,只依赖应该依赖的对象。
  • 从被依赖者的角度说,只暴露应该暴露的方法。

5、里氏替换原则(LSP)

概念

所有引用基类的地方必须能透明地使用其子类的对象

里氏替换原则弥补继承的缺陷,它的意思是:所有基类在的地方,都可以换成子类,程序还可以正常运行。这个原则是与面向对象语言的继承特性密切相关的。

在学习java类的继承时,我们知道继承有一些优点:

  • 子类拥有父类的所有方法和属性,从而可以减少创建类的工作量。
  • 提高了代码的重用性。
  • 提高了代码的扩展性,子类不但拥有了父类的所有功能,还可以添加自己的功能。

但有优点也同样存在缺点:

  • 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
  • 降低了代码的灵活性。因为继承时,父类会对子类有一种约束。
  • 增强了耦合性。当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。

如何扬长避短呢?方法是引入里氏替换原则,里氏替换原则对继承进行了规则上的约束,这种约束主要体现在四个方面:

  • 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。

    在我们做系统设计时,经常会设计接口或抽象类,然后由子类来实现抽象方法,里氏替换原则规定,子类不能覆写父类已实现的方法。父类中已实现的方法其实是一种已定好的规范和契约,如果我们随意的修改了它,那么可能会带来意想不到的错误,违反了里氏替换原则。我们可以给父类的非抽象(已实现)方法加final修饰,这样就在语法层面控制了父类非抽象方法被子类重写而违反里氏替换原则。不过有时我们也不需要完全符合里氏替换原则,如我们经常重写equals()、toString()等方法就属于正常的反里氏替换原则。
  • 子类中可以增加自己特有的方法。

    这个很容易理解,子类继承了父类,拥有了父类和方法,同时还可以定义自己有,而父类没有的方法。这是在继承父类方法的基础上进行功能的扩展,符合里氏替换原则。
  • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。(即只能重载不能重写)
    先看代码:
public class Father {
    public void fun(HashMap map){
        System.out.println("父类被执行...");
    }
}

public class Son extends Father {
    public void fun(Map map){
        System.out.println("子类被执行...");
    }
}

public class Client {

    public static void main(String[] args) {
        System.out.print("父类的运行结果:");
        Father father=new Father();
        HashMap map=new HashMap();
        father.fun(map);
        
        //父类存在的地方,可以用子类替代
        //子类B替代父类A
        System.out.print("子类替代父类后的运行结果:");
        Son sun=new Son();
        son.fun(map);
    }
}

运行结果:

父类的运行结果:父类被执行...
子类替代父类后的运行结果:父类被执行...
可以发现两次都是执行的父类的方法,即使new的是子类的对象。因为子类并非重写了父类的方法,而是重载了父类的方法。子类和父类的方法的输入参数是不同的。子类方法的参数Map比父类方法的参数HashMap的范围要大,所以当参数输入为HashMap类型时,只会执行父类的方法,不会执行子类的重载方法。这符合里氏替换原则。

但如果我将子类方法的参数范围缩小会怎样?看代码:

public class Father {
    public void fun(Map map){
        System.out.println("父类被执行...");
    }
}

public class Son extends Father {
    public void fun(HashMap map){
        System.out.println("子类被执行...");
    }
}


public class Client {

    public static void main(String[] args) {
        System.out.print("父类的运行结果:");
        Father father=new Father();
        HashMap map=new HashMap();
        father.fun(map);
        
        //父类存在的地方,可以用子类替代
        //子类B替代父类A
        System.out.print("子类替代父类后的运行结果:");
        Son son=new Son();
        son.fun(map);
    }
}

运行结果:

父类的运行结果:父类被执行...
子类替代父类后的运行结果:子类被执行...
在父类方法没有被重写的情况下,子方法被执行了,这样就引起了程序逻辑的混乱。所以子类中方法的前置条件必须与父类中被覆写的方法的前置条件相同或者更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
    先看代码:
public abstract class Father {
    public abstract Map fun();
}

public class Son extends Father {
    @Override
    public HashMap fun() {
        System.out.println("子类被执行...");
        return null;
    }
}

public class Client {

    public static void main(String[] args) {
        Father father=new Son();
        father.fun();
    }
}

运行结果:

子类被执行...
注意这里是实现了父类的抽象方法,而不是父类的非抽象(已实现)方法,不然就违法了第一条。若在继承时,子类的方法返回值类型范围比父类的方法返回值类型范围大,在子类重写该方法时编译器会报错。

6、开闭原则(OCP)

概念

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

在软件的生命周期内,因为变化,升级和维护等原因需要对软件原有代码进行修改,可能会给旧代码引入错误,也有可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。

开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统,开闭原则只定义了对修改关闭,对扩展开放。其实只要遵循SOLID中的另外5个原则,设计出来的软件就是符合开闭原则的。开闭原则很容易理解但很难实现与遵循。

三、参考:

六大设计原则(SOLID)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值