首页 > 其他分享 >设计模式六大原则

设计模式六大原则

时间:2023-11-06 09:24:40浏览次数:35  
标签:西红柿 六大 原则 ... void 炒鸡蛋 逻辑 设计模式 class

前言

什么是设计模式?

设计模式是软件设计人员、软件开发人员在程序代码编写中总结出来的一套编码规范,设计模式起一个指导作用,用来指导我们写出高内聚低耦合,具有良好的可扩展性和可维护性的代码。

为什么要学设计模式?

当然,设计模式不是非学不可,不了解设计模式一样可以在工作中写出符合产品要求的功能。但是随着功能的不断迭代,需求不断增加和变更,项目中的代码会不断在在原有功能代码的基础之上堆叠,最终会形成难以维护的一坨屎山。另外,作为程序员,写出好的代码是我们基本的追求,也可以从专业的角度提升自己。

设计模式怎么学?

设计模式有非常多种,作为一个程序员,在日常写代码的过程中肯定有意无意的用到过某些模式。现在我们知道的23种设计模式,都是前辈们在各种实际开发场景中总结提取出来的,是一个通用的解决方案。虽说有23种之多,但这些模式都遵循了6大原则,了解了这6大原则再去看具体的设计模式就很容易理解了。

设计模式六大原则

单一职责原则

一个类只能有一个可以引起它变化的原因

说白了就是一个类只做一件事。那我们为什么需要单一职责?如果某个类A承担了多个职责A1,A2,A3,因为某些原因需要对这三个任何一个职责进行修改或变更都可能会影响到其他职责,可能导致发生故障。所以最好的做法是将A拆分成三个类,每个类只负责一个职责。
合理的遵循单一职责原则,可以提高类的可读性、可维护性,降低类复杂度,从而提升了系统的可维护性。但是在我们日常工作中,在各种或者复杂或者简单的业务需求背景下,如何确定一个类的职责范围就需要我们好好思考了。

开闭原则

软件(类、模块、函数等)应该要开放扩展,但是不能支持修改,即对修改关闭,对新增开放

我们在做任何系统的时候,都不能指望系统一开始时需求确定,就再也不会变化,这是不现实也不科学的想法,而既然需求是一定会变化的,那么如何在面对需求的变化时,设计的软件可以相对容易修改,不至于说新需求一来,就要把整个程序推倒重来。怎样的设计才能面对需求的改变却可以保持相对稳定,并且预留好一些可扩展的点,从而使得系统可以在基于第一个版本以后不断扩展处新功能?我们用一个例子来说明怎样对扩展开放,对修改关闭。
假设我们有一个厨师类,该类有一些成员变量和一个方法:炒菜,这个方法接收一个菜名,并且方法内部根据菜名进行不同的制作,具体类如下。

class 厨师 {
    int 年龄;
    string 姓名;
    string 身份证;
    
    public void 炒菜(string 菜名){
        ...
        洗锅、洗菜、准备调味料等
        ...
        if(菜名=="西红柿炒鸡蛋"){
          ...
          炒西红柿逻辑;
          ...
          炒鸡蛋逻辑;
          ...    
          其他逻辑继续追加
          ...
        } else if (菜名=="酸辣土豆丝"){
          ...
        }
    }
}

如果有一天厨师突发灵感,想要对西红柿炒鸡蛋的制作工艺进行改良,那么应该怎么做?按照当前的做法是直接在炒菜这个方法内,对if块的逻辑进行逻辑修改,在足够细心的情况下修改此逻辑或许没什么问题,但在一个团队内,开发人员的风格和习惯各不相同,很难保证每个人在修改完西红柿炒鸡蛋的逻辑后可以不影响其他逻辑,此时就需要一个代码层面上的规范来强制约束大家,必须按照某个规范修改逻辑,并且这个规范天然不会影响其他逻辑,这个规范就是设计模式,在当前场景就是单一职责原则开闭原则
单一职责原则和开闭原则在这种场景下要求我们,需要将各种菜的逻辑单独拆分出来,并且将厨师制作的逻辑进行抽象。拆分后的逻辑类如下。

abstract class 菜谱{
    string 菜名;
    public abstract void 准备配料();
    public abstract void 制作();
}

class 西红柿炒鸡蛋 extend 菜谱{
    string 菜名;
    
    public void 准备配料(){
        ...
        准备盐醋酱油
        ...
    }
    
    public void 制作(){
        ...
        炒西红柿逻辑;
        ...
        炒鸡蛋逻辑;
        ...    
    }
}

class 酸辣土豆丝 extend 菜谱{
  string 菜名;
  
  public void 准备配料(){
        ...
    }
    
    public void 制作(){
        ...
    }
}

class 厨师{
    int 年龄;
    string 姓名;
    string 身份证;
   
   public void 炒(菜谱 菜谱){
       菜谱.准备配料();
       菜谱.制作();
   }
}

在上面的例子中,我们定义了一个菜谱基类,并且将所有菜的制作逻辑单独创建类并且继承菜谱类,实现制作一道菜的准备配料制作逻辑。厨师则不局限于具体某道菜,而是根据菜谱炒菜。这样,即使要对西红柿炒鸡蛋的制作逻辑进行改良,也不会影响到其他菜的逻辑。并且以后如果引进新菜谱,厨师也可以直接按照新菜谱进行制作,这样就遵循了对修改关闭,对新增开放的原则了。

依赖倒置原则

高层模块不应该依赖底层模块,而应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。

我们首先看例子,然后再解释这句话。

class 西红柿炒鸡蛋{
    string 菜名;
    
    public void 准备配料(){
        ...
        准备盐醋酱油
        ...
    }
    
    public void 制作(){
        ...
        炒西红柿逻辑;
        ...
        炒鸡蛋逻辑;
        ...    
    }
}

class 厨师{
    int 年龄;
    string 姓名;
    string 身份证;
   
   public void 西红柿炒鸡蛋(){
       西红柿炒鸡蛋 菜 = new 西红柿炒鸡蛋();
       菜.准备配料();
       菜.制作();
   }
}

在上面的例子中,厨师类就是高层模块,而西红柿炒鸡蛋酸辣土豆丝属于底层模块,此时的厨师类依赖了底层的实现。假如西红柿炒鸡蛋这道菜增加了一个逻辑剥西红柿皮,那么厨师类也要增加调用方法,这样就减小了系统的可维护性。我们来看看修改后的逻辑实现。

abstract class 菜谱{
    string 菜名;
    public abstract void 制作();
}

class 西红柿炒鸡蛋 extend 菜谱{
    string 菜名;
    
    private void 剥西红柿皮(){
      ...
    }
    
    private void 准备配料(){
        ...
        准备盐醋酱油
        ...
    }
    
    private void 炒西红柿(){
    }
    
    private void 炒鸡蛋(){
    }
    
    public void 制作(){
        剥西红柿皮();
        准备配料();
        炒鸡蛋();
        炒西红柿();
    }
}

class 厨师{
    int 年龄;
    string 姓名;
    string 身份证;
   
   public void 炒(菜谱 菜谱){
       菜谱.制作();
   }
}

修改后的逻辑,厨师类只依赖于抽象类菜谱的抽象方法制作,此时高层模块厨师没有直接依赖具体实现,而是依赖了菜谱这个抽象类。具体的菜西红柿炒鸡蛋要怎么炒、哪块需要增加制作步骤,全部在西红柿炒鸡蛋的菜谱中进行修改。
如何理解“抽象不应该依赖细节,细节应该依赖抽象”这句话?我们有一个菜谱抽象类和西红柿炒鸡蛋实现类,此时如果西红柿炒鸡蛋要增加步骤剥西红柿皮,如果在抽象类中增加方法剥皮,并在西红柿炒鸡蛋类中将剥西红柿皮的实现逻辑写在剥皮方法中,就犯了抽象依赖细节的错误了。抽象类应该是从具有某一同一行为的一类活动中抽象出来的通用类,而在本例中,同一行为就是菜的制作,而对于西红柿炒鸡蛋的所有制作过程,都属于制作。所以在抽象类中提供了制作方法后,实现类西红柿炒鸡蛋的所有制作逻辑都应该在制作方法中实现,而非在抽象类中增加方法并在子类实现,这个就是细节应该依赖抽象

里氏替换原则

任何基类可以出现的地方,子类一定可以出现。

里氏替换原则要求我们在所有依赖父类的地方,子类可以完全替代父类并且对逻辑无影响。在子类重写了父类已实现逻辑的情况下很容易违反此原则,我们还是看具体栗子。

abstract class 厨师{
    
    abstract void 洗菜();
    
    abstract void 调味();
    
    void 炒(){
       洗菜();
       ...
       下锅逻辑
       ...
       调味();
       ...
       出锅逻辑
       ...
    }
}

class 张三 extend 厨师{
    
    string 洗菜(){
        ...
        洗菜逻辑
        ...
    }
    
    string 调味(){
        ...
        调味逻辑
        ...
    }
    //这里覆盖了父类的已实现方法
    void 炒(){
        ...
        下锅逻辑
        ...
        调味();
        ...
        出锅逻辑
        ...
        洗菜();
    }
}

class 饭店{
    void 炒菜(){
        厨师 张三 = new 张三();
        张三.炒();
    }
}

抽象类厨师类作为父类,定义了两个抽象方法和一个已实现方法。子类张三继承了厨师类,并实现了两个抽象方法:洗菜调味,并且又重写了父类已实现的方法,此时父类的方法和子类的逻辑就不一致。在父类方法中,逻辑流程是“洗菜-下锅-调味-出锅”,意味着子类所有的逻辑都必须按照这个流程执行。但子类张三重写的逻辑时下锅-调味-出锅-洗菜,逻辑不同,就不能在父类出现的地方替换成子类,否则可能会造成系统或者流程异常。

迪米特法则

一个对象应该对其他对象保持最少的了解,又叫最少知道原则。

在类的结构设计上,每个类都应当尽量降低成员的访问权限,不需要让别的类知道的字段或行为就不公开,否则会破坏类的预期行为和安全性,我们直接看例子。

class 西红柿炒鸡蛋{
    
    private int 鸡蛋;
    private int 西红柿;
    private int 盐;
    private int 醋;

    public 西红柿炒鸡蛋(int 鸡蛋,int 西红柿,int 盐,int 醋){
        this.鸡蛋=鸡蛋;
        this.西红柿=西红柿;
        this.盐=盐;
        this.醋=醋;
    }
    
    public void 制作(){
        ...
        搅拌鸡蛋(this.鸡蛋);
        ...
        切西红柿(this.西红柿);
        ...
        加入盐(this.盐);
        ...
        加入醋(this.醋);
        ...
    }
}

class 厨师{
    
    public void 炒(){
        西红柿炒鸡蛋 菜=new 西红柿炒鸡蛋(2,1,500克,1升);
        菜.炒();
    }
}

抛开前面讲的几个原则先不管,第一眼看上面的例子好像没什么问题,厨师类有方法,西红柿鸡蛋类也没其他无关逻辑,但我们看实例化西红柿炒鸡蛋的代码,实例化时传入的鸡蛋数2、西红柿1、盐500克、醋1升,看出问题了吧。谁家炒两个鸡蛋要放500克盐1升醋,这样做出来的菜还能吃吗?所以很明显这个实例化时的入参是有问题的,盐和醋作为西红柿炒鸡蛋这道菜中的关键参数,需要用多少应该是根据鸡蛋和西红柿的数量来确定的,而不是初始化时任意传入的。所以这个类的定义就违反了最少知道原则,将关键参数通过构造函数暴漏出来了。

class 西红柿炒鸡蛋{
    
    private int 鸡蛋;
    private int 西红柿;

    public 西红柿炒鸡蛋(int 鸡蛋,int 西红柿){
        this.鸡蛋=鸡蛋;
        this.西红柿=西红柿;
    }
    
    public void 制作(){
        ...
        搅拌鸡蛋(this.鸡蛋);
        ...
        切西红柿(this.西红柿);
        int 盐=0;
        int 醋=0;
        
        if(鸡蛋==2 && 西红柿==1){
            盐=10;
            醋=10;
        }else if(/*其他判断逻辑*/){
            
        }
    }
}

class 厨师{
    
    public void 炒(){
        西红柿炒鸡蛋 菜=new 西红柿炒鸡蛋(2,1);
        菜.制作();
    }
}

上面我们修改过后的类定义,西红柿炒鸡蛋构造函数只接受鸡蛋西红柿数量,而关键参数则是在正式制作的时候,根据鸡蛋和西红柿的数量来最终确定,这样,无论要炒多少个鸡蛋和西红柿都会有对应的被放入,确保炒出来的菜是真正可以吃的,即我们定义的类的行为是符合预期的。

接口隔离原则

使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

我们直接看示例

abstract class 人{
    abstract void 吃饭();
    
    abstract void 睡觉();
    
    abstract void 跑步();
    
    abstract void 工作();
    
   abstract void 爬();
}

class 婴儿 extends 人{
    void 吃饭(){
    }
    void 睡觉(){
    }
    
    void 跑步(){
        //没法跑
    }
    void 工作(){
        //没法工作
    }
    void 爬(){
    }
}

class 成人 extends 人{
    void 吃饭(){
    }
    void 睡觉(){
    }
    void 跑步(){
    }
    void 工作(){
    }
    void 爬(){
        //没必要
    }
}

在上面的代码中,我们定义了一个抽象类,并且定义了5个抽象方法。有两个子类婴儿成人,分别实现了抽象类定义的5个方法,但我们注意到,在婴儿子类中是没法实现跑步工作逻辑的,因为婴儿不具备这样的能力。而在成人子类中,也没必要实现的方法,因为成人没必要爬。此时虽然在基类中定义的所有行为都是属于人的,但并非所有继承自的子类都需要全部实现这些方法,此时就违背了接口隔离原则。那么我们看看如何修改基类定义。

abstract class 人{
    abstract void 吃饭();
    
    abstract void 睡觉();
}

abstract class 婴儿 extends 人{
    abstract void 爬();
}

abstract class 成人 extends 人{
    abstract void 跑步();
    
    abstract void 工作();
}

class 张三 extends 成人{
    void 吃饭(){
    }
    void 睡觉(){
    }
    void 跑(){
    }
    void 工作(){
    }
}

class 宝宝 extends 婴儿{
    void 吃饭(){
    }
    void 睡觉(){
    }
    void 爬(){
    }
}

在上面修改后的代码中,抽象类只定义了两个抽象方法吃饭睡觉,继承自的两个子类抽象类婴儿成人分别定义各自的抽象方法跑步工作。那么在具体的实现类中,我们就可以继承不同的类:张三作为一个成人拥有基本的吃饭、睡觉、跑、工作行为,而宝宝作为婴儿则有吃饭、睡觉、爬的行为。这样各个类根据各自需求,继承满足要求的单一接口,而不用继承一个大而全但其中的许多行为都没法实现的接口,也避免了在依赖方调用对应对象方法时,某些行为未实现导致的功能异常。

总结

设计模式可以指导我们代码的结构搭建,而这六大原则则指明了设计模式的基本遵循的准则,在我们日常编写代码的时候,如果能比较好的遵循这些原则,那么即便我们没有按照某个具体的模式套在对应的场景上,写出来的代码也会具有较好的可维护性。

标签:西红柿,六大,原则,...,void,炒鸡蛋,逻辑,设计模式,class
From: https://www.cnblogs.com/EricZhao-/p/17804452.html

相关文章

  • 代码规范和编码原则
    在《构建之法》第四章中,提出了一些代码规范和编码原则,这些规范和原则有助于提高代码质量和可维护性。以下是其中的一些要点:1.规范命名选择的理由:使用有意义的命名方式,命名应具有清晰的描述性,遵循命名规范,使用驼峰命名或下划线命名等。2.合理代码结构选择的理由:尽可能使用模......
  • 【设计模式】策略模式在项目中的实战运用
    目录前言思考实现落地小结前言思考随着业务需求不断迭代更新,系统逻辑越来越复杂。ifelse堆砌让人眼花缭乱。那么此时就可以考虑使用设计模式,重构代码逻辑采用什么设计模式,或者哪几种设计模式组合,与实际业务场景、逻辑有关系。以下面这个场景为例:现在要将一批货物从A地点运往B地......
  • java IO设计模式:观察者模式
    NIO中的文件目录监听服务使用到了观察者模式。NIO中的文件目录监听服务基于WatchService接口和Watchable接口。WatchService属于观察者,Watchable属于被观察者。Watchable接口定义了一个用于将对象注册到WatchService(监控服务)并绑定监听事件的方法register。WatchServ......
  • 设计原则与思想-面向对象
    极客时间-设计模式之美学习笔记1理论一:当谈论面向对象的时候,我们到底在谈论什么?1.1什么是面向对象编程?面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石。1.2什么是面向对象编程......
  • 设计模式-单例械
    //Seehttps://aka.ms/new-console-templateformoreinformation//设计模式-单例模式//目的:唯一性,内存资源,GCtffu//保证整个系统中一个类只有一个对象的实例usingSystem.Threading.Channels;Singleton.GetInstance().GetGuid();Singleton.GetInstance().GetGuid()......
  • 设计模式-策略模式
    策略模式:定义一系列的算法,将每个算法分别封装起来,让它们可以互相替换。策略模式用于算法的自由切换和扩展,它是使用较为广泛的设计模式之一。策略模式对应于解决某一问题的一个算法族,允许用户从该算法中任选一个算法解决某一问题,同时可以方便地更换算法或者增加新算法。策略模式......
  • 设计模式—结构型模式之适配器模式
    设计模式—结构型模式之适配器模式将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,适配器模式分为类结构型模式(继承)和对象结构型模式(组合)两种,前者(继承)类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少......
  • 设计模式
    一、工厂模式1、简单工厂模式通过一个具体的工厂类,根据传入不同的参数,生成实际对象2、工厂方法模式在工厂方法模式中,不再由单一的工厂类生产产品,而是由工厂类的子类实现具体的产品创建。当增加一个产品时,只需增加一个相应的工厂类的子类,实现生产这种产品,从而解决简单工厂生产......
  • 设计模式-单例模式概述
    我们常把23种经典的设计模式分为三类:创建型、结构型、行为型,其中创建型设计模式主要解决“对象的创建”问题,将创建和使用代码解耦,结构型设计模式主要解决“类或对象的组合或组装”问题,将不同功能代码解耦,行为型设计模式主要解决“类或对象之间的交互”问题,将不同的行为代码解耦。......
  • 每日总结Java设计模式之原型模式
    今天完成了设计模式的原型模式实验用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式其实就是从一个对象再创建另外一个可定制的对象,而且不需知道任何创建的细节简单说就是先创建一个原型类实例,然后通过克隆的方法来复制一个一样的新对象,这个对象和原来......