《软件体系结构与设计实用教程》笔记(1) 软件工程方法学用处面向对象方法面向对象方法是分析对象本身在UML中有常用的表达方式如用例图和类图。类图在教科书上通常会介绍类图是由类名属性·方法和性质层组成的太晦涩难懂了我们直接从Java代码的角度来讲解这个概念。public class User { private String nickname; // 昵称 private int level; // 等级 protected int points; // 积分 public String getNickname() { return nickname; } public void publish(Content c) { // 发布作品 } private void recalcLevel() { // 内部重算等级外部不可见 } }比如有一个User类类名也就是User用户有昵称等级和积分这么一些属性方法名就是这个类能够干什么那所谓的性质层就是对应访问修饰符比如public是公有的在类图中用➕表示其他的见下面这个表格自行查阅对照。符号读作对应 Java 关键字公有public-私有private#保护protected~包内默认/包级私有代码对应的类图如下图所示public class User { // ┌──────────────────┐ // │ User │ ← 类名 private String nickname; // │──────────────────│ private int level; // │ - nickname:String│ ← 属性名 protected int points; // │ - level:int │ 性质层 前面的符号 // │ # points:int │ public String getNickname() { ... } // │──────────────────│ public void publish(Content c) { ... } // │ getNickname():String│ ← 方法名 private void recalcLevel() { ... } // │ publish(c:Content) │ } // │ - recalcLevel() │ // └──────────────────┘类图中的关系一般化关系abstract class Content { // 爹一般的 protected int likeCount; public void like() { likeCount; } } class PixelPainting extends Content { // 儿子特殊的 // 自动拥有 likeCount 和 like() }像泛化/继承就是一种一般化的关系比如上述示例代码“儿子”不需要重写“爸爸”的代码就能用它的代码。当然一般化关系也不止类与类之间的继承关系接口与接口之间的继承关系也是可以的如下面这个示例代码interface Exportable { void exportPng(); } interface Printable extends Exportable { // ← 接口继承接口也是泛化 void print(); // print() 之外还自动拥有了 exportPng() 这个承诺 }一个接口可以继承多个接口如下面示例代码interface Multi extends Exportable, Shareable { } // 接口可以继承多个接口当然不止继承接口实现也算是一种一般化关系用书上的原话就是Java语言中一般化关系就是可以直接翻译成关键字extends和implements。关联关系class A { private B b; // ← 有这一行就是关联。就这么简单 }关联是两个类之间的连接可以是双向的也可以是单向的。单项关联class User { private String nickname; } class Content { private User author; // ← 关联Content 长期持有 User }对应的图如下所示User ←──────── Content 实线箭头可省略箭头只画实线 1 *一个作者可发多个作品双向关联class User { private ListContent published; // ← 我发布的作品 } class Content { private User author; // ← 这作品的作者 }自关联class User { private ListUser following; // 我关注的人还是 User 类型 private ListUser followers; // 关注我的人 }关联是用直线箭头表示的这一点要记住依赖关系// 关联存成成员变量 —— 长期认识 class User { private Wallet wallet; // 钱包天天在随时用 public void pay() { wallet.deduct(); } } // 依赖只写在方法参数里 —— 临时借用 class User { public void publish(TagValidator v) { // 用完就还不保存 v.check(this); } }关联是可以随时用的而依赖是用完就还的那种上面的截图是我自己的项目的部分代码截图可以发现这种就是一种关联关系用实线箭头表示而这则是一种依赖关系用虚线箭头表示。聚合关系class Car { private String plate; // 车自己独立存在 public Car(String plate) { this.plate plate; } } class Fleet { // 车队整体 private ListCar cars; // ★ 关键Car 是外部 new 好传进来的 public Fleet(Listlt;Cargt; cars) { this.cars cars; } public void addCar(Car c) { cars.add(c); } } // 使用方 Car myCar new Car(京A88888); // 车在车队外就被创建了 Fleet fleet new Fleet(List.of(myCar)); // 就算 fleet null车队解散myCar 还好好的还能加别的车队聚合关系讲的是一个类中有哪些可以用的成分组成这个还得结合语义看看虽然从Java语法角度来讲和关联关系无区别但是可以从用法的角度来区分二者。类图如下所示┌─────────┐ ┌─────────┐ │ Fleet │◇───────────│ Car │ └─────────┘ 1 * └─────────┘ ↑ 空心菱形 ◇画在【整体】这一端菱形在整体这一侧组合关系组合关系相当于人和心脏之间的关系class Heart { private int bpm; public void beat() { /* ... */ } } class Person { // 人整体 // ★ 关键Heart 是在自己内部 new 出来的 private Heart heart new Heart(); public void exercise() { heart.beat(); } } // 外界拿不到这个 heart // new Person().heart → 编译不过除非提供 getter那也只是读 // person 没了 → heart 也没人引用了 → 被垃圾回收一起消失┌─────────┐ ┌─────────┐ │ Person │◆───────────│ Heart │ └─────────┘ 1 1 └─────────┘ ↑ 实心菱形 ◆同样贴着【整体】这一端总结一下一般化关系就是我们平时用的多的泛化与实现关联关系最常见的例子就是平时自动注入一个类依赖关系就是在传参数的时候用上这个类聚合关系和组合关系得结合语境看看。软件体系结构组成是什么书上例子你项目例子组件有独立功能、可重用的模块Spot 类存坐标登录模块、聊天模块连接件组件之间怎么交互Trans.move() 方法调用HTTP、WebSocket、JDBC实例把上面俩串起来演示Test 类调 move() 改坐标点赞→发消息→通知作者简单来说组件是可以复用的软件结构单元小到变量函数大到类文件程序数据库都是组件。如果还是觉得很抽象咱们就来看看Java示例代码吧。// 组件 1Spot存坐标的零件 class Spot { int x, y; int getX() { return x; } int getY() { return y; } } // 组件 2Trans负责移动的零件 class Trans { // 连接件通过方法调用使用 Spot void move(Spot s, int dx, int dy) { s.setX(s.getX() dx); s.setY(s.getY() dy); } } // 实例演示Test 把两个组件连起来跑一遍 class Test { public static void main(String[] args) { Spot s new Spot(); // 造组件 1 Trans t new Trans(); // 造组件 2 t.move(s, 3, 5); // 连接件生效坐标变了 System.out.println(s.getX()); // 输出 3 } }上述代码中move是连接件然后Test类中的main方法其实就是一种实例。软件体系结构研究的内容软件体系结构的研究内容 围绕结构图的 5 个问题 怎么把图画出来——体系结构描述ADL 人话图纸得有规范画法。ADL 就是专门画这张图的专业制图工具 Acme、Wright、Rapide 用哪种搭法——体系结构风格 人话前人总结的经典套路管道-过滤器、C/S、分层…… 搭得好不好——体系结构评估 人话图纸画完先检查性能行不行好改不好改别等楼盖塌了才发现 能不能照抄——体系结构复用与演化 人话好的搭法下个项目接着用系统运行后要扩建图纸怎么改 怎么照着盖——基于体系结构的开发 人话图纸 → 施工的全流程分析→定结构→设计→编码体系结构 贯穿软件生存周期用大白话来讲就是要研究怎么样才能让别人看得懂我的项目所以写成一种规范的写法构件Vue 前端、SpringBoot 服务、MySQL、Redis连接件HTTP前后端、JDBC后端→MySQL约束前端不许直连数据库每个请求必须带 token比如上面这种写法然后就是编写代码的时候采用什么风格有B/S分层风格有管道-过滤器风格。结构风格研究完之后就研究评价方法时刻你做的事研究内容开工前把“三块怎么连什么规矩”写成条文① 描述定套路挑了 B/S 分层 局部事件驱动② 风格动手前自问“加直播要改几处”③ 评估下个项目骨架直接搬去二手书平台④ 复用写代码每行代码照图填空⑤ 开发软件体系结构风格和建筑风格一样软件体系也有它的结构风格MVC// Controller只管接请求C RestController class ArticleController { GetMapping(/api/articles) Result list(int page) { return articleService.list(page); } } // Model管数据和业务M Service class ArticleService { public Result list(int page) { return Result.ok(articleMapper.selectPage(page)); // 读写数据 } } // View管显示V—— 前端 Vue 渲染后端只返回数据分层风格Controller → Service → Mapper一层只调相邻下一层 class ArticleController { private ArticleService service; // 只认识 Service } class ArticleService { private ArticleMapper mapper; // 只认识 Mapper } class ArticleMapper { /* 只管 SQL */ }C/S B/S架构面向对象设计问题域部分的设计比如分析阶段可能是分析出这种模型A B C 想同时继承 A 和 B \ / 比如在职研究生既是研究生又是教师 \ / 但 Java 只许 extends 一个 C但是一个类不可能同时继承多个类【接口可以继承多个接口】这种情况怎么解决class A { } class B extends A { } // B 先继承 A class C extends B { } // C 再继承 B —— 串成 A→B→C 一条链Java 不支持多重继承设计中需将分析模型中的多重继承化为单重继承方法包括调整继承结构串成单链、抽取一般类消除重复属性、将其中一个继承关系改为组合对象成员等。控制驱动部分设计这个说大白话就是设计“发动机”——什么东西负责把流程跑起来想清楚什么程序先跑什么程序后跑。举个例子// 被动类不调它它就静静躺着 Service class ArticleService { public ListArticle list() { ... } // 没人调用 永远不执行 } // 主动类系统一启动它自己就转起来了 Component class LikeCountSyncTask { // ← 主动类 Scheduled(cron 0 */10 * * * ?) // 每 10 分钟自己醒一次 public void syncRedisToDb() { ... } }component注解能够在系统启动的时候加载这个类而service注解下的这个类需要手动调用才能够运行起来。这两个类区别在于一个是主动类一个是被动类被动类主动类谁让它动别人调用它才动自己会主动跑起来人话像工具你不按它它不动像工人到点自己干活你的项目ArticleService被 Controller 调定时任务、WebSocket 监听线程、消息消费者经典软件体系结构风格接下来我将详细介绍主程序-子程序面向对象风格。主程序-子程序风格// 主程序唯一的入口控制整个流程 public class Main { public static void main(String[] args) { // ← 一切从这里开始 int a 10, b 5; System.out.println(minus(a, b)); // 按顺序喊子程序 System.out.println(print_msg(hello)); System.out.println(printx(999)); } } // 子程序被喊到才动是被动类 static int minus(int a, int b) { return a - b; } static void print_msg(String s) { ... } static int printx(int x) { return x; }可以看到上面这个代码连接件是调用-返回机制。这种风格的核心思路就是分而治之把一个大而复杂的程序系统拆成主程序加一堆子程序主程序负责调度子程序负责干活。好处很直接——复杂任务被分解成许多容易处理的小任务谁写哪个子程序分得清清楚楚开发分工容易出了问题也容易定位维护起来相对省心。但它的缺点同样明显可以归纳成下面几条程序规模一大开发就变慢子程序越拆越多主程序里的调用顺序越来越长改一处要顺着调用链一路看下去。测试困难子程序之间靠参数和全局数据耦合单独测一个子程序往往要先把它的上下游都准备好。重用性差子程序通常是为某个具体流程写的换个场景就很难直接搬走。数据安全差大量使用全局变量和共享数据谁都能改一处改错全盘受影响。难以开发大型软件和图形界面应用界面需要事件驱动、随时响应而这种风格是顺序调用、一条道走到黑交互性天然吃亏。数据结构一变就难维护数据结构往往被多个子程序共享改一个字段可能要连带改一大片子程序的参数和逻辑。一句话理解主程序-子程序风格适合流程固定、以计算和数据处理为主的中小型程序一旦系统变大、需要频繁交互或数据结构经常变动它的短板就会集中暴露这也是后来面向对象风格、事件驱动风格等被提出的原因。总结优点缺点主程序子程序优点是化繁为简任务分解分工明确便于开发但是缺点也有如果数据结构一变就很难维护程序太长运行起来也消耗更长的时间资源并且子程序重用性差难以开发大型的软件和图形化界面。面向的对象风格/* 1. 作者自己的积分和粉丝自己管 */ public class Author { private final String name; private int points; private ListString fans new ArrayList(); public Author(String name) { this.name name; } // 行为住在对象里加积分是作者自己的事 public void addPoints(int delta) { this.points delta; } public void notifyFans(String message) { for (String fan : fans) { System.out.println(→ 通知粉丝 fan message); } } public String getName() { return name; } public int getPoints() { return points; } public void addFan(String fan) { fans.add(fan); } }面向对象风格具有抽象封装继承和多态等特点。// 这是一个抽象接口定义了“发送消息”这个能力但不管具体怎么发 public interface MessageSender { boolean sendMessage(String receiver, String content); } // 客户端代码只依赖抽象接口 public class NotificationService { // 依赖抽象而不是具体的实现类 public void notifyUser(MessageSender sender, String user, String msg) { sender.sendMessage(user, msg); // 我不关心底层是发邮件还是发短信 } }public class RoomGroup { private Long groupId; // 私有属性外界不能直接修改 private String groupName; // 私有属性 private int memberCount; // 私有属性 // 对外提供安全的访问接口 public String getGroupName() { return groupName; } public void setGroupName(String groupName) { // 封装了业务逻辑名字不能为空且长度有限制 if (groupName null || groupName.trim().isEmpty() || groupName.length() 50) { throw new IllegalArgumentException(群名称不合法); } this.groupName groupName; } // 成员数量只能增加或减少不能随意修改数值 public void addMember() { this.memberCount; } }抽象// 三种作品文章、像素画、游戏 Demo // 共同点都有 id、作者、赞数都要发布 // 不同点怎么校验、怎么存、怎么通知——各不一样 abstract class Content { protected Long id; protected Author author; protected int likeCount; public abstract void validate(); // 只声明要有校验不说怎么校验 public abstract void store(); // 只声明要能存不说存哪 }这是体现出代码的抽象写法封装abstract class Content { private int likeCount; // 私有外部不能直接改 public void like() { // 只开放这个入口 likeCount; // 怎么加规则由我定 } public int getLikeCount() { // 只读不能写 return likeCount; } } // 外部代码 c.likeCount 999; // ❌ 编译不过private c.like(); // ✅ 唯一合法方式写Java写的多的朋友肯定对这种写法不陌生吧继承class Article extends Content { private String body; public void validate() { if (body.isEmpty()) throw new BizException(空文章); } public void store() { articleMapper.insert(this); } // ↑ 只写了这两个likeCount、like()、author 全是白拿的 } class PixelPainting extends Content { private int[][] grid; public void validate() { if (grid.length 64) throw new BizException(画布太大); } public void store() { ossService.upload(grid, png); } }多态// 调用方只认识 Content完全不知道具体类型 public void publishAll(ListContent works) { for (Content c : works) { c.validate(); // ← 多态在这里发生 c.store(); // ← 多态在这里发生 } } // 用法 ListContent works List.of( new Article(...), new PixelPainting(...), new GameDemo(...) ); publishAll(works); // 一行代码三种行为各走各的用大白话概括抽象是定义有哪些概念封装则是把数据隐藏起来只暴露方法继承是子类复用父类的东西只写差异然后多态稍微抽象一点你可以简单理解成面向对象的优缺点如下所示数据流风格数据流风格分为批处理风格和管道过滤器风格。批处理风格public class TranscriptMain { public static void main(String[] args) throws IOException { File infile new File(transcript.txt); // 原始数据一批学生记录 File outfile new File(transcript_result.txt); // 结果文件 Listlt;Stringgt; lines readLines(infile); // ① 整批读入 sortLines(lines); // ② 整批排序 writeLines(outfile, lines); // ③ 整批写出 } }这个是批处理风格关注的是数据如何流动而不是关注谁调用谁。注意ListString lines—— 它是整个文件全读进内存再处理。这就是批处理的“批”字数据必须先攒齐才能开工。优点缺点管道-过滤器风格书上说这种风格有很多优点比如组件具有高内聚低耦合有点可以复用可以扩展并且支持并行执行但是不适合处理交互应用如果还不懂可以看看下面这个图管道相当于传送带过滤器相当于一个一个工人数据就是传送带上的物品这个IMServer代码就能够很好的体现出我上面说的这种风格可以看到管道pipeline,各种handler处理器数据就是从处理器中通过。管道过滤器优点有以下几点首先是代码体现出高内聚低耦合的特点然后管道过滤器分工明确再者就是可以将整个系统行为看成多个过滤器的合成比较简单可复用性管道过滤器可以复用让开发工作方便起来了可扩展性就像IM服务器可以扩展新增自定义的过滤器可验证性在管道-过滤器/批处理架构中因为每个“过滤器Handler”或“处理步骤”都是高度独立、接口明确的所以它的可验证性非常高。支持并行执行但是这种风格也有缺点隐式调用风格显式调用是“点名做事”隐式调用是“拉响警报”——拉警报的人只负责拉救火的各自冲上来。代价是你再也看不出来谁会响应灵活性换来了可读性。// 发布者只管广播不知道谁会收到 public void like(int contentId) { contentMapper.incrLike(contentId); // 干自己的活 eventBus.post(new LikedEvent(contentId)); // 广播点赞了——完事 } // 订阅者 1收到事件自己决定做什么通知作者 Subscribe public void onLiked(LikedEvent e) { pushService.notifyAuthor(e.getContentId()); } // 订阅者 2也收到了自己决定做什么加积分 Subscribe public void onLiked2(LikedEvent e) { authorService.addPoints(e.getContentId()); }层次风格表示层Vue 页面 ↓ 只能往下调 业务层Service ↓ 数据层Mapper/DB仓库风格[组件A] [组件B] \ / [中央数据仓库] ← 所有人围着它转 / \ [组件C] [组件D]// 组件 A只负责写数据进仓库不知道谁会读 Service public class LikeService { Autowired private LikeMapper likeMapper; // 仓库MySQL public void like(Long contentId, Long userId) { likeMapper.insert(contentId, userId); // 写进中央仓库完毕 // 它不知道有个统计模块会去读这张表 } } // 组件 B只负责从仓库读不知道数据谁写的 Service public class RankService { Autowired private LikeMapper likeMapper; public Listlt;Contentgt; hotList() { return likeMapper.selectByLikeCountDesc(); // 从同一个仓库读 } }这段代码也算是仓库风格解释器风格// 规则是数据不是代码 —— 改规则不用改代码 private static final ListString WHITE_LIST List.of( /api/auth/**, ); public boolean preHandle(HttpServletRequest request, ...) { String uri request.getRequestURI(); for (String pattern : WHITE_LIST) { if (MATCHER.match(pattern, uri)) { // ← 解释器现场解释这条规则 return true; } } return false; }读规则现场解释这种代码风格叫解释器风格。反馈控制环风格// 简化版自动限流测当前 QPS超阈值就限流降下来再放开 Component public class AdaptiveRateLimiter { private static final int THRESHOLD 1000; // 目标QPS 不超过 1000 private volatile boolean limited false; Scheduled(fixedRate 1000) // ← 每秒采一次样传感器 public void controlLoop() { int qps Metrics.getCurrentQps(); // ① 测量 if (qps gt; THRESHOLD) { limited true; // ② 超了 → 收紧 } else if (qps lt; THRESHOLD * 0.7) { limited false; // ③ 降了 → 放开 } // 循环往复这就是反馈 } public boolean allowRequest() { return !limited; } }分布式软件体系结构风格分布式系统是指基于网络环境功能和数据分布在网络相连的多个计算机中通过他们之间通信协作完成的各项功能的系统。那为什么要有分布式软件体系结构呢其实以前是集中式也就是一台机器干所有事这种风格风险很高容易出现主机挂了的现象。分布式体系结构风格文件共享体系结构P2P体系结构风格2PPeer-to-Peer体系结构中没有专用的服务器每个节点既是客户端又是服务器对等体系统由对等节点组成节点之间直接通信通常为整个网络提供服务。三层C/S体系结构风格B/S体系结构风格B/S C/S混合软件结构中间件总结集中式主机终端 ↓ 加了文件服务器 文件共享NAS无并发控制 ↓ 客户端变胖 两层 C/S胖客户端快但不安全、难维护 ↓ 中间加一层业务层 三层 C/S瘦客户端 应用服务器 数据库 ↓ 客户端换成浏览器 B/S免安装、零维护 ← 你的 Vue SpringBoot MySQL 就是它 旁支P2P没有中心人人皆服务器 贯穿始终中间件RPC / JDBC / ODBC / 消息队列 —— 屏蔽差异的胶水MVC风格与Struts框架什么是MVCMVC发展史MVC的三种组件与连接件MVC在Java技术中的应用Structs框架