探索设计模式的魅力:揭秘模版方法模式-让你的代码既灵活又可维护

news2024/12/25 12:31:35

在这里插入图片描述


设计模式专栏:http://t.csdnimg.cn/U54zu


目录

  • 一、开篇
  • 二、应用场景
    • 一坨坨代码实现
    • 存在的问题
  • 三、解决方案
    • 模式方法结构示意图及说明
    • 用模板方法模式重构示例
    • 解决的问题
  • 四、工作原理
    • 使用模板方法模式重写示例结构图
    • 核心结构:抽象类和具体实现
  • 五、总结
    • 优点
    • 缺点
    • 最佳实践
    • 与其他设计模式相结合

一、开篇

在软件开发中,设计一个灵活且易于维护的系统至关重要。模板方法模式作为面向对象设计模式之一,通过分离稳定与变化的部分,提高了代码复用性,确保了软件的可扩展性。它是构建高效软件蓝图的关键,为设计可扩展架构提供了高效方法。

    在开发复杂的软件时,我们经常会遇到两个主要的问题:

1. 如何减少代码冗余,提高复用性?
2. 如何确保软件容易维护和扩展?

    为了解决这些问题,我们需要采用一种设计模式,它能够将不变的部分与变化的部分清晰地分离开来,从而允许程序员在不改变稳定算法结构的前提下,自由地扩展和修改那些容易变化的部分。

模板方法模式是一种基于继承的设计模式,它定义了一个操作中的算法骨架,并将一些步骤的实现延迟到子类。通过这种方式,模板方法允许子类在不改变算法结构的情况下,重新定义算法的某些特定步骤。

二、应用场景

你正在编写一个应用程序,该程序可以处理多种类型的文档转换。尽管转换过程中的一些步骤对所有文档类型都是通用的,比如打开文档、读取数据、和储存转换后的文档,但是具体的转换逻辑对每种文档类型来说都是不同的。

一坨坨代码实现

    不使用设计模式的情况下,你可以简单地对每种转换类型创建一个方法,并在这些方法中复制粘贴相同的步骤代码。以下是一个未使用模板方法模式的示例代码实现:

public class DocumentConverter {
    
    public void convertPDFToWord(String inputFile, String outputFile) {
        // 打开 PDF 文档
        System.out.println("Opening PDF document: " + inputFile);

        // 读取数据
        System.out.println("Reading data from PDF document.");

        // PDF to Word 转换的特定逻辑
        System.out.println("Converting PDF to Word.");

        // 储存转换后的 Word 文档
        System.out.println("Saving Word document: " + outputFile);
    }

    public void convertXMLToCSV(String inputFile, String outputFile) {
        // 打开 XML 文档
        System.out.println("Opening XML document: " + inputFile);

        // 读取数据
        System.out.println("Reading data from XML document.");

        // XML to CSV 转换的特定逻辑
        System.out.println("Converting XML to CSV.");

        // 储存转换后的 CSV 文档
        System.out.println("Saving CSV document: " + outputFile);
    }

    // 可能还有更多针对不同文档类型的转换方法...
}

public class DocumentConversionExample {
    public static void main(String[] args) {
        DocumentConverter converter = new DocumentConverter();

        // 转换PDF到Word
        converter.convertPDFToWord("example.pdf", "example.docx");

        // 转换XML到CSV
        converter.convertXMLToCSV("example.xml", "example.csv");

        // ...执行更多转换
    }
}

    在这个例子中,convertPDFToWord 和 convertXMLToCSV 方法各自包含打开文档、读取数据、执行转换及存储文档的步骤。这意味着代码中存在大量重复,如果转换流程的某些通用步骤发生变更,你需要在每一个转换方法中单独进行修改,这显然违背了 DRY(Don’t Repeat Yourself)原则。

存在的问题

 1. 代码重复:convertPDFToWord 和 convertXMLToCSV 方法都包含了很多相同的代码,例如打开文档和读取数据。每种转换方法都重复了这些通用的步骤,这不仅增加了代码量也增加了维护成本。

 2. 难以维护:因为相同的代码分散在不同的方法中,所以如果通用步骤需要修改,你必须找到所有的副本并逐一进行修改,这非常容易导致错误。

 3. 扩展性差:如果需要新增一种文档类型的转换,你要再次复制粘贴同样的代码,并对新类型的文档实现转换逻辑。这不仅仅是低效的,更是容易出错的。

 4. 高耦合性:每种转换方法不仅包含了特定的转换逻辑,还混入了通用的处理步骤。这导致了转换逻辑与通用处理逻辑高度耦合,降低了代码的可读性和可测试性。

 5. 难以测试:因为每个转换方法都包含多步操作,这使得为特定步骤编写单元测试变得更加困难。你可能需要获取或模拟中间数据以便测试方法中的某个步骤。

 6. 违反了软件设计的原则:如上一点所述,这样的代码结构违反了DRY原则(不要重复自己),同时也违背了单一职责原则,因为每个转换方法除了处理特定的转换逻辑之外,还负责了文件的打开和保存等操作

    使用模板方法模式可以解决上述问题,它允许将通用逻辑抽取到一个基类中,并通过定义抽象方法来让子类实现特定实现细节。这样,通用代码只需要写一次,在基类中,减少了重复,同时提供了更好的维护性和可扩展性。


三、解决方案

 定义

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

模式方法结构示意图及说明

在这里插入图片描述

  • AbstractClass:抽象类。用来定义算法骨架和原语操作,具体的子类通过重定义这些原语操作来实现一个算法的各个步骤。在这个类里面,还可以提供算法中通用的实现。
  • ConcreteClass:具体实现类。用来实现算法骨架中的某些步骤,完成与特定子类相关的功能。

用模板方法模式重构示例

重构步骤

 1. 创建一个抽象基类,定义转换文档的通用步骤和模板方法。
 2. 在基类中实现通用步骤(打开文件、读取数据、保存文件)。
 3. 定义一个或多个抽象方法,子类必须实现这些方法以执行特定的转换逻辑。
 4. 为每种类型的文档创建具体子类,实现特定的转换逻辑。

实现代码

public abstract class AbstractDocumentConverter {
    // 模板方法,定义算法骨架
    public final void convertDocument(String inputFile, String outputFile) {
        openDocument(inputFile);
        readData();
        convert();
        saveDocument(outputFile);
    }

    protected void openDocument(String inputFile) {
        // 实现通用的文件打开逻辑
        System.out.println("Opening document: " + inputFile);
    }

    protected void readData() {
        // 实现通用的数据读取逻辑
        System.out.println("Reading data from document.");
    }

    protected abstract void convert(); // 抽象方法,子类将实现具体的转换逻辑

    protected void saveDocument(String outputFile) {
        // 实现通用的文件保存逻辑
        System.out.println("Saving document: " + outputFile);
    }
}

public class PDFToWordConverter extends AbstractDocumentConverter {
    @Override
    protected void convert() {
        // 实现 PDF 到 Word 的特定转换逻辑
        System.out.println("Converting PDF to Word.");
    }
}

public class XMLToCSVConverter extends AbstractDocumentConverter {
    @Override
    protected void convert() {
        // 实现 XML 到 CSV 的特定转换逻辑
        System.out.println("Converting XML to CSV.");
    }
}

// ...可以为其他类型的文档添加更多的子类

public class DocumentConversionExample {
    public static void main(String[] args) {
        AbstractDocumentConverter pdfToWordConverter = new PDFToWordConverter();
        pdfToWordConverter.convertDocument("example.pdf", "example.docx");

        AbstractDocumentConverter xmlToCsvConverter = new XMLToCSVConverter();
        xmlToCsvConverter.convertDocument("example.xml", "example.csv");

        // ...使用其他转换器执行更多转换
    }
}

  采用模板方法模式后,我们有以下改进:

  • 基类 AbstractDocumentConverter 定义了通用步骤的实现,并提供了一个模板方法 convertDocument,它定义了转换文档的步骤序列。
  • 各个步骤中,唯一必须由各个子类实现的步骤是 convert,它包含了特定于不同文档类型转换的逻辑。
  • PDFToWordConverterXMLToCSVConverter 是具体的转换器类,继承自 AbstractDocumentConverter,并实现了 convert 抽象方法,用于实现特定的转换逻辑。

解决的问题

 1. 减少了代码重复:通用逻辑(如打开文件、读取数据、保存文件)现在在基类中实现一次即可,避免在每个转换器中重复相同的代码。

 2. 更易于维护:所有通用步骤都集中在基类中,如果需要更改这些步骤的实现,只需在一个地方修改即可,而不是在每个具体实现中逐一更改。

 3. 提高了扩展性:新的文档转换器可以通过创建新的子类很容易地添加,只需实现特定的转换逻辑而无需关心通用流程。

 4. 降低耦合性:模板方法模式将通用过程和具体步骤分开,将变化部分封装在子类中,基类和子类之间的耦合度降低,使代码更加模块化。

 5. 易于测试:可以独立测试文档转换的通用步骤和特定步骤。例如,可以单独对子类的convert方法进行单元测试,而不必担心文件打开、数据读取等通用逻辑。

 6. 遵守设计原则:通过使用模板方法模式,代码遵守了DRY原则(Don’t Repeat Yourself,不重复自己),因为它消除了代码重复。同时也遵循了单一职责原则,因为基类只关心定义算法骨架和执行通用步骤,而具体的转换逻辑则由各个子类负责。

 7. 更清晰的职责划分:模板方法定义了算法的骨架,使得算法的结构更加明晰,同时职责分配也更加清楚:基类负责算法的整体流程和通用步骤,子类负责具体的细节和个性化实现。

  通过使用模板方法模式,重构的代码变得更加健壮、易于扩展与维护,同时更贴近面向对象设计的最佳实践。

四、工作原理

使用模板方法模式重写示例结构图

在这里插入图片描述

    模板方法模式属于行为型设计模式的一种,它定义了一个操作中的算法的骨架,将某些步骤延迟到子类中实现。这使得子类可以在不改变算法结构的情况下重新定义算法中某些特定步骤的实现。

核心结构:抽象类和具体实现

 抽象类:这是模板方法模式的关键所在。抽象类包含了算法的模板,即一系列定义好的操作(方法)序列,我们称之为“模板方法”。这个方法将各个需要在子类实现的抽象操作定义为算法的一个部分。此外,它还可以包含一些实现,为子类提供辅助函数和通用功能。

 具体实现:在具体实现中,子类将重写抽象类中的抽象方法,提供特定步骤的实现。子类可以有不同的行为,但它们通过父类提供的模板方法来调用这些方法,从而保持一致的算法结构。

五、总结

模板方法模式是一个非常有用的设计模式,特别是在算法步骤分明、稳定,并且可以由子类在没有影响整体执行顺序的情况下提供或修改部分实现的情况下。然而,开发人员应该评估这种模式是否能够满足实际项目需求,抑或是它可能会带来的设计复杂性和灵活性限制。正确的做法是,在确信模板方法模式可以为项目带来显著优势的时候,再将其作为解决特定问题的工具。

优点

  • 代码复用:
    模板方法通过在抽象类中实现通用代码,减少了子类中的重复代码,有助于避免错误并提高代码维护性。
  • 扩展性:
    新增特定行为的子类很容易,不需要修改已有的抽象类代码,只需扩展并实现必要的抽象操作即可。
  • 封装不变部分:
    模板方法模式封装了算法框架和不变部分,使得变化可局部化,这有助于代码的稳定性和可预测性。
  • 控制反转:
    子类重写的方法由父类的模板方法在适当的时间调用,这在软件工程中称为“控制反转”,有助于解耦算法的实现和客户端的使用。
  • 符合开闭原则:
    算法的核心流程一旦定义完成就不再改变,新的步骤实现和变化都在子类中完成,符合面向对象设计的开闭原则(对扩展开放,对修改封闭)。

缺点

  • 限制灵活性:
    由于算法框架是固定的,如果需要改动算法的某个特定步骤可能会很困难,尤其是当步骤的执行顺序在算法中非常重要时。
  • 高层次耦合:
    子类的实现必须依赖于抽象类中定义的模板结构,这可能导致较强的耦合性,同时也可能会违反里氏替换原则(子类能够替换掉它们的基类)。
  • 难以理解:
    抽象和子类之间的关系对于新的开发人员来说可能不那么直观,需要花费额外时间理解模板方法的整个流程和执行顺序。
  • 过多使用导致继承膨胀:
    如果过度依赖模板方法模式,可能会造成一个庞大的继承体系,增加了理解和维护这些类的难度。
  • 设计限制:
    模板方法模式通常需要在设计之初就有预见性地将变与不变分离,一旦系统需要变动不在预期内的部分,模板方法可能就显得不够灵活。

最佳实践

 1. 当你有一系列步骤形成算法,并且算法的部分步骤在不同上下文中会有不同实现时,使用模板方法模式。
 2. 尽量减少抽象类中的具体方法,避免抽象类变得臃肿。
 3. 仅在算法的步骤确实固定,且变化不大时选用模板方法模式。
 4. 尝试用钩子方法给子类更多的扩展点,以增加灵活性。
 5. 确保使用模板方法模式不会使得你的类层次变得过于复杂。

与其他设计模式相结合

    模板方法模式可以与其他设计模式相结合,以进一步提升软件设计的优雅性和效率。

  • 工厂方法模式与模板方法模式结合,可以在模板方法的某些步骤中使用工厂方法来创建对象,进一步解耦了对象的创建与算法的实现。
  • 策略模式可以用来增加算法步骤的灵活性,特别是当你想在运行时动态改变算法的某些行为而不仅仅是在编译时。
  • 状态模式很自然地与模板方法模式相结合,尤其是当对象的行为需要根据它的状态变化而变化时,可以将这些行为作为模板方法中的步骤。
  • 装饰者模式可以在不更改现有对象的代码的情况下,为对象添加新的行为,这在模板方法的步骤中非常有用。

    通过将模板方法模式与其他设计模式相结合,我们可以赋予软件架构更大的灵活性和可扩展性,这将有助于应对软件发展过程中的变化,同时保持代码的简洁和清晰。如同艺术家在画布上混合色彩以达到完美的色调和层次感,软件设计师也可以灵活运用不同的设计模式,去构筑更为强大和优雅的软件架构。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1452849.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IDEA 的28 个天花板技巧,yyds!

IDEA 作为Java开发工具的后起之秀,几乎以碾压之势把其他对手甩在了身后,主要原因还是归功于:好用;虽然有点重,但依旧瑕不掩瑜,内置了非常多的功能,大大提高了日常的开发效率,下面汇总了常用的28个使用小技巧,学会之后,让你的撸码效率直接起飞... 注意:不同idea版本菜…

计算机二级之sql语言的学习(数据模型—概念模型)

概念模型 含义: 概念模型用于信息世界(作用对象)的建模,是实现现实世界到信息世界(所以万丈高楼平地起,不断地学习相关的基础知识,保持不断地重复才能掌握最为基础的基础知识)的概念抽象&#…

SG5032VEN晶体振荡器SPXO

在高速数字通信和精密电子系统中,时钟信号的质量至关重要。SG5032VEN晶体振荡器(SPXO)凭借其低相位抖动的LVDS输出,为这些应用提供了理想选择。提供频率范围:200.1 MHz ~ 500mhz,满足了从高速网络到数据中心等不同应用…

Java编程在工资信息管理中的最佳实践

✍✍计算机编程指导师 ⭐⭐个人介绍:自己非常喜欢研究技术问题!专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目:有源码或者技术上的问题欢迎在评论区一起讨论交流! ⚡⚡ Java实战 |…

【c++】析构函数

1.特征 析构函数是特殊的成员函数,其特征如下: 1.析构函数名是在类名前加上字符~。 2.无参数无返回值类型。 3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。 4.对象生命周…

HTML-多媒体嵌入-MDN文档学习笔记

HTML-多媒体与嵌入 查看更多学习笔记&#xff1a;GitHub&#xff1a;LoveEmiliaForever MDN中文官网 HTML-中的图片 将图片放入网页 可以使用<img/>来将图片嵌入网页&#xff0c;它是一个空元素&#xff0c;最少只需src属性即可工作 <img src"图片链接"…

【Python】测量WAV文件播放时长

问题 windows播放WAV音频文件&#xff0c;一般使用API函数&#xff0c;如PlaySound。实际使用发现&#xff0c;从调用PlaySound到实际开始播放存在200ms以上的延时&#xff0c;在游戏编程中音效实时性是个需要解决的问题。 本文主要讨论&#xff0c;windows播放WAV文件的衍生…

JVM-JVM中对象的生命周期

申明&#xff1a;文章内容是本人学习极客时间课程所写&#xff0c;文字和图片基本来源于课程资料&#xff0c;在某些地方会插入一点自己的理解&#xff0c;未用于商业用途&#xff0c;侵删。 原资料地址&#xff1a;课程资料 对象的创建 常量池检查:检查new指令是否能在常量池…

ESP32-Cam学习(1)——拍摄第一张照片

1.开发板介绍 使用的ESP32-Cam实物图为&#xff1a; 在某宝可以轻易买到。它分为主板&#xff0c;和底板。底板的主要功能是供电、程序下载等等。主板才是ESP32芯片的核心。 2.固件烧录 使用摄像头之前&#xff0c;需要给ESP32刷入支持摄像头的固件库&#xff0c;其下载地址为…

(07)Hive——窗口函数详解

一、 窗口函数知识点 1.1 窗户函数的定义 窗口函数可以拆分为【窗口函数】。窗口函数官网指路&#xff1a; LanguageManual WindowingAndAnalytics - Apache Hive - Apache Software Foundationhttps://cwiki.apache.org/confluence/display/Hive/LanguageManual%20Windowing…

线索化二叉树(先序,中序,后序)+线索化二叉树的遍历【java详解】

目录 线索化二叉树的基本介绍&#xff1a; 举个栗子&#xff1a; 二叉树的中序线索化&#xff1a; 创建HeroNode类&#xff0c;表示节点信息&#xff1a; 编写中序线索化方法代码&#xff1a; 中序线索化遍历代码&#xff1a; 测试代码&#xff1a; 测试结果&#xff1a…

OpenAI发布Sora技术报告深度解读!真的太强了!

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主、前后端开发、人工智能研究生。公粽号&#xff1a;洲与AI。 &#x1f388; 本文专栏&#xff1a;本文收录…

NodeJS背后的人:Express

NodeJS背后的人&#xff1a;Express 本篇文章&#xff0c;学习记录于&#xff1a;尚硅谷&#x1f3a2; 文章简单学习总结&#xff1a;如有错误 大佬 &#x1f449;点. 前置知识&#xff1a;需要掌握了解&#xff1a; JavaScript基础语法 、Node.JS环境API 、前端工程\模块化 …

代码随想录算法训练营第53天 | 121.买卖股票的最佳时机 + 122.买卖股票的最佳时机II

今日任务 121. 买卖股票的最佳时机 122.买卖股票的最佳时机II 121.买卖股票的最佳时机 - Easy 题目链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第…

Rust 语言学习杂谈 (end) (各种工作中遇到的疑难杂症)

1.在运行 “cargo build --release” 的时候&#xff0c;到底发生了什么&#xff1f; 源 (GPT4.0) : 当我们运行 cargo build --release 命令时&#xff0c;实际上在进行一系列复杂的步骤来编译和构建 Rust 项目的发布版本。这个过程大致可以分解为以下几个步骤&#xff1a;…

Java - SPI机制

本文参考&#xff1a;SPI机制 SPI&#xff08;Service Provider Interface&#xff09;&#xff0c;是JDK内置的一种服务提供发现机制&#xff0c;可以用来启动框架扩展和替换组件&#xff0c;主要是被框架的开发人员使用&#xff0c;比如 java.sql.Driver接口&#xff0c;其他…

【C语言】实现队列

目录 &#xff08;一&#xff09;队列 &#xff08;二&#xff09;头文件 &#xff08;三&#xff09; 功能实现 &#xff08;1&#xff09;初始化 &#xff08;2&#xff09; 销毁队列 &#xff08;3&#xff09; 入队 &#xff08;4&#xff09;出队 &#xff08;5&a…

【论文精读】GPT1

摘要 如何从大量未标注文本中获取词级别的信息有两个主要挑战&#xff0c;使用何种优化目标能有效地学习文本表示&#xff0c;如何有效地将学习到的表示迁移到目标任务。针对这些问题&#xff0c;本文提出一种无监督预训练和有监督微调的组合的半监督方法&#xff0c;具体为&am…

Vue3+Ant-Design-Vue:报错Cannot read properties of null (reading ‘isCE‘)

问题描述 在使用Ant-Design-Vue内置的Table表格组件&#xff0c;实现expand展开行功能时&#xff0c;报错&#xff1a;Uncaught TypeError: Cannot read properties of null (reading ‘isCE‘) 。 报错信息图示&#xff1a; 在GitHub上找到如下描述&#xff0c; 解决方案 网上…

【maya 入门笔记】基本视图和拓扑

1. 界面布局 先看基本窗口布局&#xff0c;基本窗口情况如下&#xff1a; 就基本窗口布局的情况来看&#xff0c;某种意义上跟blender更像一点&#xff08;与3ds max相比&#xff09;。 那么有朋友就说了&#xff0c;玛格基&#xff0c;那blender最下面的时间轴哪里去了&…