架构整洁之道——价值维度与编程范式

news2024/11/18 5:35:36

1 设计与架构究竟是什么

  结论:二者没有任何区别,一丁点区别都没有。

  架构图里实际上包含了所有底层设计细节,这些细节信息共同支撑了顶层的架构设计,底层设计信息和顶层架构设计共同组成了整个架构文档。底层设计细节和高层架构信息是不可分割的,它们组合在一起,共同定义了整个软件系统,缺一不可。所谓的底层和高层本身就是一系列决策组成的连续体,并没有清晰的分界线。

  软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。

  一个软件架构的优劣,可以用它满足用户需求所需要的成本来衡量。

  软件开发的核心特点:要想跑得快,先要跑得稳。

  工程师们常会用一句话来欺骗自己:“我们可以未来再重构代码,产品上线最重要!”,而通常情况下,产品上线后,重构工作是没有人会提起的,这是工程师们过度自信的表现。某些软件研发工程师会认为挽救一个系统的唯一办法是抛弃现有系统,设计一个全新的系统来替代,但这种思路并没有逃离过度自信:如果是工程师的过度自信导致了系统一团乱麻,那么,我们有什么理由认为让他们从头开始,结果就会更好呢?

2 两个价值维度

  对于每个软件系统,我们都可以通过行为和架构两个维度来体现它的实际价值。软件研发人员应该确保自己的系统在这两个维度上的实际价值都能长时间维持在很高的状态。

2.1 行为价值

  软件系统的行为是其最直观的价值维度。程序员的工作就是让机器按照某种指定方式运转,给系统的使用者创造或者提高利润。程序员们为达到这个目的,往往需要帮助系统使用者编写一个对系统功能的定义,也就是需求文档。然后程序员们再把需求文档转化为实际的代码。

  当机器出现异常行为时,程序员要负责调试,解决这些问题。

  大部分程序员认为这就是他们的全部工作。他们的工作是且仅是:按照需求文档编写代码,并且修复任何Bug。这真是大错特错。

2.2 架构价值

  软件系统的第二个价值维度,就体现在软件这个英文单词上:software。“ware”的意思是“产品”,而“soft”的意思,是指软件的灵活性。

  软件系统必须保持灵活。软件发明的目的,就是让我们可以以一种灵活的方式来改变机器的工作行为。对于机器上那些很难改变的工作行为,我们通常称之为硬件(hardware)。

  为了达到软件的本来目的,软件系统必须够“软”——也就是说,软件应该容易被修改。当需求方改变需求的时候,随之所需的软件变更必须可以简单而方便地实现。变更实施的难度应该和变更的范畴(Scope)成等比关系,而与变更的具体开着(scope)无关。

  需求变更的范畴与形状,是决定对应软件变更实施成本高低的关键。这就是为什么有的代码变更的成本与其实现的功能改变不成比例。从系统相关方(Stakeholder)的角度来看,他们所提出的一系列的变更需求的范畴都是类似的,因此成本也应该是固定的。但是从研发者的角度来看,系统用户持续不断地变更需求就像是要求他们不停地用一堆不同形状的拼图块,拼成一个新的形状。整个拼图的过程越来越困难,因为现有系统的形状永远和需求的形状不一致。

  问题的实际根源当然就是系统的架构设计。如果系统的架构设计偏向某种特定的“形状”,那么新的变更就会越来越难以实施。所以好的系统架构设计应该尽可能做到与“形状”无关。

3 编程范式

  直到今天,我们也一共只有三个编程范式,而且未来几乎不可能再出现新的,它们分别是结构化编程(structured programming)、面向对象编程(object-oriented programming)以及函数式编程(functional programming)。

  结构化编程是第一个被普遍采用的编程范式,由Edsger Wybe Dijkstra于1968年提出,并最先主张用我们现在熟知的if/then/else语句和do/while/until语句来替代跳转语句的,结构化编程范式可以归结为一句话:结构化编程对程序控制权的直接转移进行了限制和规范。

  面向对象编程是第二个被广泛采用的编程范式,由Ole Johan Dahl和Krise Nygaard于1966年提出,一句话总结即是:面向对象编程对程序控制权的间接转移进行了限制和规范。

  函数式编程是由Alonzo Church在1936年发明的 λ \lambda λ演算(lambda演算)的直接衍生物, λ \lambda λ演算法的一个核心思想是不可变性——某个符号所对应的值是永远不变的,所以从理论上来讲,函数式编程语言中应该是没有赋值语句的。大部分函数式编程语言只允许在非常严格的限制条件下,才可以更改某个变量的值。函数式编程可以总结为:函数式编程对程序中的赋值进行了限制和规范。

3.1 结构化编程

  结构化编程范式是一种程序设计方法论,它强调通过使用有限的控制结构来组织程序逻辑,以提高代码的清晰度、可读性、可维护性和可靠性。结构化编程的核心原则是避免无条件跳转(如goto语句),而是采用三种基本结构来构造任何复杂的程序:

  (1) 顺序结构:按照代码书写的顺序依次执行指令;

  (2) 选择结构(条件分支):基于某个或某些条件决定执行路径,例如if-else或switch-case语句;

  (3) 循环结构:重复执行一段代码直到满足特定条件为止,如while循环、for循环等;

  通过这些结构,程序员可以将程序划分为模块化的子程序或函数,每个子程序都有一个明确定义的任务,并且可以独立地进行理解和验证。这样做的好处是可以降低复杂度,减少错误和潜在的“面条式”代码问题(即难以理解的程序流程),并促进软件工程中的分层抽象和功能分解。

  结构化编程在20世纪60年代至70年代由Edsger Dijkstra等人倡导并推广,成为了现代编程实践的基础,许多当代主流编程语言的设计都遵循这一范式。

3.2 面向对象编程

  面向对象编程(Object-Oriented Programming, OOP)是一种重要的编程范式,它以“对象”为核心概念组织和设计软件系统。在面向对象编程中,数据(属性或变量)以及对这些数据进行操作的方法(函数或过程)被捆绑在一起作为一个整体,即“对象”。

  很多时候,我们会把“类与对象”、“封装”、“继承”、“多态”、“抽象”这样的概念与面向对象编程强绑定在一起,但实际情况并不是这样。

  OOP强调通过定义类(Class)来创建对象的蓝图或模板,类描述了对象共有的状态(属性)和行为(方法),根据类创建的具体实例称为对象。

  封装是指数据和处理该数据的代码结合在一起,并对外部隐藏内部细节,对象通过接口(public方法)暴露其功能,而内部实现则受到保护,增强了安全性与模块化。但实际上,如Java和C#这样号称面向对象的语言因彻底抛弃了头文件与实现文件分离的编程方式,反而削弱了封装性,相反C语言却进行了完美封装。因此,封装不能算是面向对象编程的必要条件。

  继承是指子类可以继承父类(基类或超类)的特征和行为,并在此基础上扩展或修改,这有助于代码重用和层次结构的设计,减少冗余。继续这一特性在面向对象理论被提出来之前,对继承性的支持就已经存在很久了,面向对象编程在继承性方面并没有开创出新,但是的确在数据结构的伪装上提供了相当程度的便利。

  多态是指同一个消息(方法调用)可以被不同的对象响应并产生不同的结果,多态有两种形式:静态多态(编译时多态,例如函数重载)和动态多态(运行时多态,例如虚函数机制)。多态其实是函数指针的一种应用,自从20世纪40年代末期冯诺依曼架构诞生那天起,程序员们就一直在使用函数指针模拟多态了,也就是说,面向对象编程在多态方面没有提出任何新概念,但确实让多态变得更安全、更便于使用了。

  用函数指针显式实现多态的问题就在于函数指针的危险性,毕竟函数指针的调用依赖于一系列需要人为遵守的约定,程序员必须严格按照固定约定来初始化函数指针,并共同严格地按照约定来调用这些指针,只要有一个程序员没有遵守这些约定,整个程序就会产生极其难以跟踪和消除的Bug。

  面向对象编程语言为我们消除了人工遵守这些约定的必要,也就等于消除了这方面的危险性,采用面向对象编程语言让多态实现变得非常简单,让一个传统C程序员可以去做以前不敢想的事情。综上所述,面向对象编程其实是对程序间接控制权的转移进行了约束。

  抽象是指通过抽象类和接口可以定义类之间的共同协议,而不涉及具体实现,抽象帮助程序员关注问题的本质而不是具体的实现细节。

  面向对象编程范式鼓励开发者从现实世界的实体及其相互关系出发构建模型,从而更直观地理解和解决问题,这种编程风格不仅提高了程序的可读性、可维护性和可扩展性,还促进了代码复用和模块化的软件开发实践。

3.3 函数式编程

  函数式编程借鉴了数学中的函数概念,将程序视为一系列无副作用的纯函数的组合。在函数式编程中,计算被看作是数据的变换过程,而非对状态的操作序列,以下是函数式编程的核心特点:

  (1) 纯函数:纯函数是指给定相同的输入总是产生相同的输出,并且没有可观察到的副作用,这意味着函数不依赖于任何外部状态,也不会修改其作用域之外的数据;

  (2) 一等公民(First-class functions):函数可以像其他数据类型一样被赋值给变量、作为参数传递给其他函数或从函数返回,这允许高阶函数(Higher-order functions)的存在,即接受函数作为参数或返回函数的函数;

  (3) 不可变数据:函数式编程通常提倡使用不可变数据结构,一旦创建后就不会改变,这种策略减少了由于共享状态导致的问题,增强了并行和并发处理的安全性;

  (4) 递归:函数式编程倾向于用递归来解决问题,而不是循环或其他迭代结构,因为递归更符合函数式思想,可以通过函数调用自身来实现复杂计算;

  (5) 列表推导与函数组合:函数式编程提供了一系列工具和语法来简洁地表达列表操作和其他集合转换,如map、filter、reduce等,以及通过组合简单的函数构建复杂的逻辑;

  (6) 声明式编程风格:相比于命令式编程描述“如何做”,函数式编程更多地关注“做什么”,因此代码往往更侧重于表达关系和转换规则,而非具体的执行步骤;

  (7) 副作用管理:虽然函数式编程强调无副作用,但实际应用中不可能完全避免副作用,函数式编程提供了诸如Monad等抽象概念来管理和控制副作用,确保它们在可控范围内发生;

  函数式编程因其具备的特性而具有易于推理、利于测试、并行友好等特点,在现代编程语言如Haskell、Scala、Clojure和部分支持函数式编程特性的语言如JavaScript、Python等中得到了广泛应用。

  函数式编程语言中的变量(Variable)是不可变(Vary)的,不可变性是软件架构设计需要考虑的重点,为什么软件架构师要操心变量的可变性呢?那是因为,所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。如果变量永远不会被更改,那就不可能产生竞争或者并发更新问题。如果锁状态是不可变的,那就永远不会产生死锁问题。

  换句话说,一切并发应用遇到的问题,一切由于使用多线程、多处理器而引起的问题,如果没有可变变量的话都不可能发生。

  作为一个软件架构师,当然应该要对并发问题保持高度关注。我们需要确保自己设计的系统在多线程、多处理器环境中能稳定工作。

  那么如何隔离可变性呢?一种常见方式是将应用程序,或者是应用程序的内部服务进行切分,划分为可变的和不可变的两种组件。不可变组件用纯函数的方式来执行任务,期间不更改任何状态。这些不可变的组件将通过与一个或多个非函数式组件通信的方式来修改变量状态:

image.png

  由于状态的修改会导致一系列并发问题的产生,所以我们通常会采用某种事务型内存来保护可变变量,避免同步更新和竞争状态的发生。

  事务型内存基本上与数据库保护磁盘数据的方式类似,通常采用的是事务或者重试机制。

  隔离可变性的要点是:一个架构设计良好的应用程序应该将状态修改的部分和不需要修改的部分隔离成单独的组件,然后用合适的机制来保护可变量。

  软件架构师应该着力于将大部分处理逻辑都归于不可变组件中,可变状态组件的逻辑应该越少越好。

3.4 总结

  总之:

  (1) 结构化编程是对程序控制权的直接转移的限制;

  (2) 面向对象编程是对程序控制权的间接转移的限制;

  (3) 函数式编程是对程序中赋值操作的限制;

  这三个编程范式都对程序员提出了新的限制,每个范式都约束了某种编写代码的方式,没有一个编程范式是在增加新能力。

  总而言之,软件,或者说计算机程序无一例外是由顺序结构、分支结构、循环结构和间接转移这几种行为组合而成的,无可增加,也缺一不可。

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

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

相关文章

滑木块H5小游戏

欢迎来到程序小院 滑木块 玩法&#xff1a;点击木块横着的只能左右移动&#xff0c;竖着的只能上下移动&#xff0c; 移动到箭头的位置即过关&#xff0c;不同关卡不同的木块摆放&#xff0c;快去滑木块吧^^。开始游戏https://www.ormcc.com/play/gameStart/260 html <can…

JavaEE 网络编程

JavaEE 网络编程 文章目录 JavaEE 网络编程引子1. 网络编程-相关概念1.1 基本概念1.2 发送端和接收端1.3 请求和响应1.4 客户端和服务端 2. Socket 套接字2.1 数据包套接字通信模型2.2 流套接字通信模型2.3 Socket编程注意事项 3. UDP数据报套接字编程3.1 DatagramSocket3.2 Da…

pip 安装出现报错 SSLError(SSLError(“bad handshake

即使设置了清华源&#xff1a; pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simplepip 安装包不能配置清华源&#xff0c;出现报错: Retrying (Retry(total2, connectNone, readNone, redirectNone, statusNone)) after connection broken by ‘SSLE…

适用于 Windows 的 10 款免费 MP4 转 MP3 转换神器

每当我们观看歌曲或视频剪辑时&#xff0c;我们经常会想到将其转换为 MP3 格式&#xff0c;以便我们可以将其保存在设备上&#xff0c;因为它占用的空间更少。在将 MP4 转换为 MP3 的过程中&#xff0c;第一步也是最重要的一步是选择正确的工具来转换它&#xff0c;如果您想添加…

API网关-Apisix RPM包方式自动化安装配置教程

文章目录 前言一、简介1. etcd简介2. APISIX简介3. apisix-dashboard简介 二、Apisix安装教程1. 复制脚本2. 增加执行权限3. 执行脚本4. 浏览器访问5. 卸载Apisix 三、命令1. Apisix命令1.1 启动apisix服务1.2 停止apisix服务1.3 优雅地停止apisix服务1.4 重启apisix服务1.5 重…

SG-8506CA 可编程晶体振荡器 (SPXO)

输出: LV-PECL频率范围: 50MHz ~ 800MHz电源电压: 2.5V to 3.3V外部尺寸规格: 7.0 5.0 1.5mm (8引脚)特性:用户指定一个起始频率, 7-bit I2C 地址:用户可编程: I2C 接口:基频的高频晶体:低抖动PLL技术应用:OTN, BTS, 测试设备 规格&#xff08;特征&#xff09; *1 这包括初…

链表--543. 二叉树的直径/medium 理解度C

543. 二叉树的直径 1、题目2、题目分析3、复杂度最优解代码示例4、适用场景 1、题目 给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 …

Python Flask与APScheduler构建简易任务监控

1. Flask Web Flask诞生于2010年&#xff0c;是用Python语言&#xff0c;基于Werkzeug工具箱编写的轻量级、灵活的Web开发框架&#xff0c;非常适合初学者或小型到中型的 Web 项目。 Flask本身相当于一个内核&#xff0c;其他几乎所有的功能都要用到扩展&#xff08;邮件扩展…

案例分享 | 助力数字化转型:嘉为科技项目管理平台上线

嘉为科技项目管理平台&#xff08;一期&#xff09;基于易趋&#xff08;EasyTrack&#xff09;进行实施&#xff0c;通过近一年的开发及试运行&#xff0c;现已成功交付上线、推广使用&#xff0c;取得了良好的应用效果。 1.关于广州嘉为科技有限公司&#xff08;以下简称嘉为…

外卖跑腿系统开发:构建高效、安全的服务平台

在当今快节奏的生活中&#xff0c;外卖跑腿系统的开发已成为技术领域的一个重要课题。本文将介绍如何使用一些常见的编程语言和技术框架&#xff0c;构建一个高效、安全的外卖跑腿系统。 1. 技术选择 在开始开发之前&#xff0c;我们需要选择适合的技术栈。常用的技术包括&a…

idea使用注释时如何不从行首开始

1、File—>setting 2、找到Editor&#xff0c;点Code Style 1.对于java注释设置 点java&#xff0c;然后选择Code Generation,去掉Line comment at first column,选择Add a space at comment start 2.对于xml注释设置 点XML&#xff0c;然后选择Code Generation,去掉Line c…

java-数组(以及jvm的内存分布)

文章目录 数组的基本概念数组的作用数组的创建以及初始化数组的创建数组的初始化 数组的使用数组中元素的访问遍历打印数组 数组是引用类型初始jvm的内存分布基本类型变量和引用类型变量的区别引用变量 认识null 数组的基本概念 数组可以看作是一种类型的集合我们在内存空间上…

Go 命令行解析 flag 包之快速上手

本篇文章是 Go 标准库 flag 包的快速上手篇。 概述 开发一个命令行工具&#xff0c;视复杂程度&#xff0c;一般要选择一个合适的命令行解析库&#xff0c;简单的需求用 Go 标准库 flag 就够了&#xff0c;flag 的使用非常简单。 当然&#xff0c;除了标准库 flag 外&#x…

Mac网线上网绿联扩展坞连接网线直接上网-无脑操作

声明&#xff1a;博主使用的绿联扩展坞 以下为绿联扩展坞Mac网线使用方法 1.首先需要下载电脑对应版本的驱动 直接点击即可下载 2. 下载好以后 解压 点进去 对应版本 博主直接使用最新的12-14 3. 安装包好了以后 会提示重启电脑 此时拔掉扩展坞 再重启动 拔掉扩展坞 再重启…

【Tomcat与网络1】史前时代—没有Spring该如何写Web服务

在前面我们介绍了网络与Java相关的问题&#xff0c; 最近在调研的时候发现这块内容其实非常复杂&#xff0c;涉及的内容多而且零碎&#xff0c;想短时间梳理出整个体系是不太可能的&#xff0c;所以我们还是继续看Tomcat的问题&#xff0c;后面有网络的内容继续补充吧。 目录 …

【python爬虫】爬虫编程技术的解密与实战

​&#x1f308;个人主页&#xff1a;Sarapines Programmer&#x1f525; 系列专栏&#xff1a; 爬虫】网络爬虫探秘⏰诗赋清音&#xff1a;云生高巅梦远游&#xff0c; 星光点缀碧海愁。 山川深邃情难晤&#xff0c; 剑气凌云志自修。 目录 &#x1f33c;实验目的 &#x1f…

Redis客户端之Jedis(一)介绍

目录 一、Jedis介绍&#xff1a; 1、背景&#xff1a; 2、Jedis连接池介绍&#xff1a; 二、Jedis API&#xff1a; 1、连接池API 2、其他常用API&#xff1a; 三、SpringBoot集成Jedis&#xff1a; 1、Redis集群模式&#xff1a; &#xff08;1&#xff09;配置文件…

如何用甘特图跟踪项目进度

甘特图是一个简单但是极其强大的项目管理工具,能够清晰可视化复杂项目的进度,在项目跟踪和控制上发挥重要作用。任何一个严肃的项目组织者都会使用甘特图来规划和管理项目中的任务。 甘特图的纵坐标表示项目的各项活动或任务,横坐标表示项目的时间进度。每个任务用一条横条表示…

杰理方案——WIFI连接物联网配置阿里云操作步骤

demo——DevKitBoard 注意:最好用这个Demo,其它Demo可能会有莫名其妙的错误问题。 wifi配置 需要在app_config.h文件中定义USE_DEMO_WIFI_TEST,工程会在wifi_demo_task.c文件中自动启动wifi相关的任务, 我们将工程配置为连接外部网络STA模式 默认工程会使用如下账号密码 这…

微信小程序 仿微信聊天界面

1. 需求效果图 2. 方案 为实现这样的效果&#xff0c;首先要解决两个问题&#xff1a; 2.1.点击输入框弹出软键盘后&#xff0c;将已有的少许聊天内容弹出&#xff0c;导致看不到的问题 点击输入框弹出软键盘后&#xff0c;将已有的少许聊天内容弹出&#xff0c;导致看不到的问…