欢迎来到 W10N 的博客

本站主要是个人使用的读书笔记和技术文档。 收集整理了一些技术资料,希望能帮助到有需要的人。

原型模式, Prototype Pattern

原型模式(Prototype Pattern)是一种创建型模式,通过复制(克隆)一个已有对象来创建新对象,而不是通过 new 重新实例化。 核心思想 当创建一个对象的代价较大(如需要复杂初始化、数据库查询、网络请求等)时,可以先创建一个原型对象,后续通过克隆该原型来快速获得新对象。 角色 Prototype(抽象原型):声明克隆方法 clone()。 ConcretePrototype(具体原型):实现克隆方法,返回自身的副本。 Client(客户端):通过调用原型的克隆方法来创建新对象。 浅克隆与深克隆 浅克隆(Shallow Clone) 深克隆(Deep Clone) 基本类型字段 复制值 复制值 引用类型字段 复制引用(共享对象) 递归复制,独立对象 修改互不影响 ❌ 引用字段会互相影响 ✅ 完全独立 示例:细胞克隆 Java 通过实现 Cloneable 接口来支持浅克隆: public class Cell implements Cloneable { private String cellWall; // 细胞壁 private String cellMembrane; // 细胞膜 private String cellularTissue; // 细胞组织 public String getCellWall() { return cellWall; } public void setCellWall(String cellWall) { this.cellWall = cellWall; } // 其他 getter/setter 省略 @Override public Cell clone() { try { return (Cell) super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(e.getMessage()); } } } 客户端使用: ...

2026-04-16 · 1 min · 192 words · -

中介者模式, Mediator Pattern

中介者模式(Mediator Pattern)定义一个中介对象来封装一系列对象之间的交互,使各对象不需要显式地相互引用,从而降低耦合度,并可以独立地改变它们之间的交互。 核心思想 当系统中多个对象之间存在复杂的多对多交互时,对象之间的依赖关系会形成网状结构,难以维护。中介者模式将这种网状结构转化为星形结构——所有对象只与中介者通信,由中介者协调各对象之间的交互。 # 不使用中介者:网状结构 A ←→ B A ←→ C B ←→ C B ←→ D ... # 使用中介者:星形结构 A → Mediator ← B ↑ ↓ C D 角色 Mediator(抽象中介者):定义各同事对象通信的接口。 ConcreteMediator(具体中介者):实现协调逻辑,持有所有同事对象的引用。 Colleague(同事类):每个同事只知道中介者,通过中介者与其他同事交互。 示例:聊天室 聊天室是中介者模式的典型场景:用户之间不直接通信,而是通过聊天室(中介者)转发消息。 抽象中介者: public interface ChatRoom { void sendMessage(String message, User sender); void addUser(User user); } 具体中介者: import java.util.ArrayList; import java.util.List; public class ChatRoomImpl implements ChatRoom { private final List<User> users = new ArrayList<>(); @Override public void addUser(User user) { users.add(user); } @Override public void sendMessage(String message, User sender) { for (User user : users) { if (user != sender) { user.receive(message, sender.getName()); } } } } 同事类: ...

2026-04-16 · 2 min · 266 words · -

责任链模式, Chain of Responsibility Pattern

责任链模式(Chain of Responsibility)将请求沿着一条处理者链传递,每个处理者决定是否处理该请求,或将其传递给链上的下一个处理者。请求的发送者无需知道最终由哪个处理者来处理,从而实现了发送者与接收者的解耦。 核心思想 不使用责任链时,请求方往往需要硬编码逻辑来判断应该由哪个对象处理,导致紧耦合和大量条件判断。责任链将这些处理者串联起来,请求沿链传递,直到被某个处理者处理(或到达链尾)。 角色 Handler(抽象处理者):定义处理请求的接口,通常包含设置下一个处理者的方法。 ConcreteHandler(具体处理者):实现处理逻辑,决定自己是否处理请求,如果不处理则转发给下一个处理者。 Client(客户端):构建责任链并向链头发起请求。 示例:请假审批 不同级别的请假天数由不同层级的领导审批:组长审批 1 天以内,经理审批 3 天以内,总监审批 7 天以内,超过 7 天不予批准。 抽象处理者: public abstract class Approver { protected Approver next; public Approver setNext(Approver next) { this.next = next; return next; } public abstract void approve(int days); } 具体处理者: public class TeamLeader extends Approver { @Override public void approve(int days) { if (days <= 1) { System.out.println("组长批准了 " + days + " 天假。"); } else if (next != null) { next.approve(days); } } } public class Manager extends Approver { @Override public void approve(int days) { if (days <= 3) { System.out.println("经理批准了 " + days + " 天假。"); } else if (next != null) { next.approve(days); } } } public class Director extends Approver { @Override public void approve(int days) { if (days <= 7) { System.out.println("总监批准了 " + days + " 天假。"); } else { System.out.println("请假 " + days + " 天,超出审批权限,不予批准。"); } } } 客户端构建链并发起请求: ...

2026-04-16 · 2 min · 237 words · -

状态模式, State Pattern

状态模式(State Pattern)允许一个对象在其内部状态发生改变时改变其行为,使该对象看起来像是改变了它的类。 核心思想 在不使用状态模式的情况下,对象的行为往往通过大量 if-else 或 switch 判断当前状态来分支处理,导致代码臃肿、难以扩展。状态模式将每种状态封装成独立的类,对象将行为委托给当前状态对象,状态切换时只需替换状态对象即可。 角色 Context(上下文):持有当前状态的引用,对外暴露行为接口,将行为委托给当前状态对象处理。 State(抽象状态):定义所有具体状态共同的接口。 ConcreteState(具体状态):实现 State 接口,封装该状态下的行为,并在必要时触发状态转换。 示例:交通灯 交通灯有红、黄、绿三种状态,不同状态下的行为不同,且状态之间按规则流转。 抽象状态接口: public interface TrafficLightState { void handle(TrafficLight light); String getColor(); } 具体状态类: public class RedState implements TrafficLightState { @Override public void handle(TrafficLight light) { System.out.println("红灯停。"); light.setState(new GreenState()); } @Override public String getColor() { return "红"; } } public class GreenState implements TrafficLightState { @Override public void handle(TrafficLight light) { System.out.println("绿灯行。"); light.setState(new YellowState()); } @Override public String getColor() { return "绿"; } } public class YellowState implements TrafficLightState { @Override public void handle(TrafficLight light) { System.out.println("黄灯警示,请注意。"); light.setState(new RedState()); } @Override public String getColor() { return "黄"; } } Context(上下文)类: ...

2026-04-16 · 2 min · 224 words · -

策略模式, Strategy Pattern

策略模式(Strategy Pattern)定义了一系列算法,把这些算法一个个封装成单独的类,使它们可以相互替换,且算法的改变不会影响使用算法的客户。 策略模式重点是封装不同的算法和行为,不同的场景下可以相互替换。策略模式是开闭原则的体现——对扩展开放,对修改关闭:新增策略时不影响其他类,且场景类只依赖抽象而不依赖具体实现。 示例:字符串替换策略 这里以字符串替换为例:读取一个文件后,需要替换其中的变量再输出。替换方式可能有多种,我们用策略模式来支持运行时切换。 首先,建立抽象策略类 RepTempRule,定义公用变量和方法: public abstract class RepTempRule { protected String oldString = ""; public void setOldString(String oldString) { this.oldString = oldString; } protected String newString = ""; public String getNewString() { return newString; } public abstract void replace() throws Exception; } 现在有两个具体策略:将文本中的 aaa 替换成 bbbb,或替换成 ccc: public class RepTempRuleOne extends RepTempRule { @Override public void replace() throws Exception { newString = oldString.replaceFirst("aaa", "bbbb"); System.out.println("this is replace one"); } } public class RepTempRuleTwo extends RepTempRule { @Override public void replace() throws Exception { newString = oldString.replaceFirst("aaa", "ccc"); System.out.println("this is replace two"); } } 算法解决类,提供运行时自由选择和切换算法的能力: ...

2026-04-16 · 1 min · 210 words · -

设计模式 – 模板方法, Template Method

模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。 模式结构 模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术。 模板方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。代表这些具体逻辑步骤的方法称做基本方法(primitive method);而将这些基本方法汇总起来的方法叫做模板方法(template method),这个设计模式的名字就是从此而来。 模板方法所代表的行为称为顶级行为,其逻辑称为顶级逻辑。 这里涉及到两个角色: 抽象模板(Abstract Template) 角色的责任: 定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。 定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。 具体模板(Concrete Template) 角色的责任: 实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。 每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。 基础示例 抽象模板角色类,abstractMethod()、hookMethod() 等基本方法是顶级逻辑的组成步骤,这个顶级逻辑由 templateMethod() 方法代表。 public abstract class AbstractTemplate { /** * 模板方法 */ public void templateMethod() { // 调用基本方法 abstractMethod(); hookMethod(); concreteMethod(); } /** * 基本方法的声明(由子类实现) */ protected abstract void abstractMethod(); /** * 基本方法(空方法) */ protected void hookMethod() {} /** * 基本方法(已经实现) */ private final void concreteMethod() { // 业务相关的代码 } } 具体模板角色类实现了父类所声明的基本方法,abstractMethod() 方法所代表的就是强制子类实现的剩余逻辑,而 hookMethod() 方法是可选择实现的逻辑。 ...

2026-04-16 · 3 min · 481 words · -

适配器模式, Adapter Pattern

概念 通常,客户类通过类的接口访问它提供的服务。有时,现有的类可以提供客户类需要的功能,但它所提供的接口不一定是客户类所期望的——可能接口过于详细、缺少细节,或者名称不匹配。 在这种情况下,需要将现有接口转化为客户类期望的接口,从而保证对现有类的重用。适配器模式(Adapter Pattern)通过定义一个包装类来完成这种转化: Target:客户类期望的接口 Adaptee:现有的、接口不兼容的类 Adapter:包装 Adaptee,实现 Target 接口 当客户类调用 Adapter 的方法时,Adapter 内部转而调用 Adaptee 的对应方法,这个过程对客户类透明。 类适配器 vs 对象适配器 类适配器 对象适配器 实现方式 继承 Adaptee 组合 Adaptee 灵活性 静态绑定,无法适配 Adaptee 子类 可适配 Adaptee 及其所有子类 方法重载 可重载 Adaptee 的方法 不能直接重载,但可在包装方法中修改行为 可见性 客户类可见 Adaptee 的 public 方法 客户类与 Adaptee 完全解耦 Java 限制 仅适用于 Target 是接口(Java 单继承) Target 可以是接口或抽象类 类适配器示例 Adapter 通过继承 Adaptee 来复用其接口: // Target:客户类期望的接口 public interface Target { void sampleOperation1(); } // Adaptee:现有的第三方类,接口不兼容 public class Adaptee { public void sampleOperation2() { System.out.println("Adaptee.sampleOperation2()"); } } // Adapter:继承 Adaptee,实现 Target public class Adapter extends Adaptee implements Target { @Override public void sampleOperation1() { this.sampleOperation2(); } } // Client public class Client { public static void main(String[] args) { Target target = new Adapter(); target.sampleOperation1(); } } 对象适配器示例 Adapter 通过组合 Adaptee 来复用其接口: ...

2026-04-16 · 1 min · 205 words · -

代理模式, proxy pattern

代理模式, proxy pattern 为其他对象提供一种代理以控制对这个对象的访问。 代理模式,即Proxy,它和 Adapter 模式很类似。我们先回顾Adapter模式,它用于把A接口转换为B接口: Adapter 模式 public BAdapter implements B { private A a; public BAdapter(A a) { this.a = a; } public void b() { a.a(); } } Proxy模式 而Proxy模式不是把A接口转换成B接口,它还是转换成A接口: public AProxy implements A { private A a; public AProxy(A a) { this.a = a; } public void a() { this.a.a(); } } 看起来 Proxy 只是把 A 接口又包了一层,这有什么意义呢? ...

2026-04-16 · 1 min · 117 words · -

外观模式, Facade Pattern

概念 看到"门面"这个词,大家一定都觉得很熟悉。日常生活中的"门面"就是我们买东西的地方,它跟各种商品的生产商打交道,收集商品后再卖给我们。如果没有"门面",我们将不得不直接跟各种各样的生产商买商品。 Facade 模式正是这样一个"门面":我们本来需要与后台的多个类或接口打交道,而 Facade 模式在客户端和后台之间插入一个中间层——门面,这个门面跟后台的多个类或接口打交道,客户端只需要跟门面打交道即可。 Facade 类是一个简化的用户接口,它和后台中的多个类产生依赖关系,而客户类则只跟 Facade 类产生依赖关系。后台的开发者熟悉自己开发的各个类,容易解决与多个类的依赖关系;而使用者不太熟悉后台的各个类,通过 Facade 可以大大降低使用难度。 情况一:功能分布在多个无关类中 客户类要使用的功能分布在多个类中,客户必须先初始化各个类才能使用。这时适合将这些功能集中在一个 Facade 类里,同时替用户做初始化工作。 场景: 商店里出售三种商品——衣服、电脑和手机,分别由各自的生产厂商提供。 各厂商类: public class CoatFactory { public Coat saleCoat() { // ... return coat; } } public class ComputerFactory { public Computer saleComputer() { // ... return computer; } } public class MobileFactory { public Mobile saleMobile() { // ... return mobile; } } 没有商店时,客户需要分别跟各厂商打交道: ...

2026-04-16 · 2 min · 373 words · -

组合模式, Composite Pattern

概念 组合模式(Composite Pattern)将对象组合成树状层次结构,使客户端对单个对象(叶子节点)和组合对象(容器节点)具有一致的访问方式。 核心思想:部分与整体的统一接口。 角色: Component:抽象组件,定义叶子和容器的公共接口 Leaf:叶子节点,没有子节点,实现具体操作 Composite:容器节点,包含子组件,将操作委托给子节点 示例:文件系统 文件系统是 Composite 模式的经典场景——文件夹可以包含文件或其他文件夹,但对外都提供统一的 getSize() 接口。 // Component public interface FileSystemNode { String getName(); long getSize(); void print(String indent); } // Leaf public class File implements FileSystemNode { private final String name; private final long size; public File(String name, long size) { this.name = name; this.size = size; } @Override public String getName() { return name; } @Override public long getSize() { return size; } @Override public void print(String indent) { System.out.println(indent + "📄 " + name + " (" + size + " bytes)"); } } // Composite public class Directory implements FileSystemNode { private final String name; private final List<FileSystemNode> children = new ArrayList<>(); public Directory(String name) { this.name = name; } public void add(FileSystemNode node) { children.add(node); } public void remove(FileSystemNode node) { children.remove(node); } @Override public String getName() { return name; } @Override public long getSize() { return children.stream() .mapToLong(FileSystemNode::getSize) .sum(); } @Override public void print(String indent) { System.out.println(indent + "📁 " + name + "/"); for (FileSystemNode child : children) { child.print(indent + " "); } } } // Client public class Client { public static void main(String[] args) { Directory root = new Directory("root"); Directory src = new Directory("src"); src.add(new File("Main.java", 1200)); src.add(new File("Utils.java", 800)); Directory resources = new Directory("resources"); resources.add(new File("config.yml", 300)); resources.add(new File("banner.txt", 50)); root.add(src); root.add(resources); root.add(new File("README.md", 500)); root.print(""); System.out.println("Total size: " + root.getSize() + " bytes"); } } 输出: ...

2026-04-16 · 2 min · 300 words · -

创建者模式, 建造者模式, Builder

创建者模式, 建造者模式, Builder 定义: 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示 使用场景 多个部件或零件,都可以装配到一个对象中,但产生的结果又不相同时。 当初始化一个对象特别复杂的时候,比如参数多,而且很多参数都有默认值。 它分为抽象建造者 (Builder) 角色、具体建造者 (ConcreteBuilder) 角色、导演者 (Director) 角色、产品 (Product) 角色四个角色。 抽象建造者 (Builder) 角色: 给 出一个抽象接口,以规范产品对象的各个组成成分的建造。 具体建造者 (ConcreteBuilder) 角色: 要完成的任务包括: 1.实现抽象建造者Builder所声明的接口,给出一步一步地完成创建产品实例的操作。2.在建造过程完成后,提供产品的实例。 导演者 (Director) 角色: 担任这个角色的类调用具体建造者角色以创建产品对象。 产品 (Product) 角色: 产品便是建造中的复杂对象。 对于Builder模式很简单,但是一直想不明白为什么要这么设计,为什么要向builder要Product而不是向知道建造过程的Director要。刚才google到一篇文章,总算清楚了。在这里转贴一下这位richardluo的比喻。 简单地说,就好象我要装修一套房子,可是我不懂施工——水电怎么走、木作怎么做,每个工种都有自己专注的领域;也不懂整体设计——用什么风格、先做哪道工序、各部分如何协调,这是设计师的职责。于是我需要找一支装修队,各工种各司其职;还得找个设计师,他来规划整体方案、协调各工种按图施工,而设计师本身不动手,只负责下命令。最后,我可以向工人要装修好的房间了。在这个过程中,设计师手里什么都没有,只有图纸和指令,所以要房间也是跟工人要,记住了! 以下是richardluo的代码,我根据他的思路加上了相应的注释。 定义工人接口,即一支装修队所需具备的通用技能。 // 工人接口。水电工负责布线,木工负责木作,油漆工负责涂装。 // 装修完成后,由装修队把房间交还给房主。 public interface Builder { void doElectrical(); // 水电工 void doCarpentry(); // 木工 void doPainting(); // 油漆工 Room getRoom(); } 定义设计师(Director),他持有图纸,协调装修队按方案施工。 // 设计师(Director)。持有图纸,规定施工顺序,但不亲自动手。 public class Designer { public void coordinate(Builder builder) { builder.doElectrical(); builder.doCarpentry(); builder.doPainting(); } } 装修队(ConcreteBuilder),负责具体的施工实施。 // 各专职工人 class ElectricalWorker { public String work() { return "electrical done"; } } class Carpenter { public String work() { return "carpentry done"; } } class Painter { public String work() { return "painting done"; } } // 装修队(ConcreteBuilder)。由水电工、木工、油漆工组成,各司其职。 // 装修完成后,由装修队把房间交还给房主。 public class WorkerTeam implements Builder { private ElectricalWorker electrician = new ElectricalWorker(); private Carpenter carpenter = new Carpenter(); private Painter painter = new Painter(); private String electrical = ""; private String carpentry = ""; private String painting = ""; public void doElectrical() { electrical = electrician.work(); } public void doCarpentry() { carpentry = carpenter.work(); } public void doPainting() { painting = painter.work(); } // 把装修好的房间交还给房主 public Room getRoom() { if (!electrical.equals("") && !carpentry.equals("") && !painting.equals("")) { System.out.println("room ready!"); return new Room(); } else { return null; } } } 房主(Client),雇人、收房。 // 房主。聘请一支装修队和一位设计师,让设计师协调装修队按图施工,最后从装修队手上收房。 public class Client { public static void main(String[] args) { Builder team = new WorkerTeam(); Designer designer = new Designer(); designer.coordinate(team); team.getRoom(); } } 好了,我觉得这样大概能说明白了。不知各位觉得如何呢?或者有更好的应用场景解释,敬请赐教。 ...

2026-04-16 · 4 min · 730 words · -

SOLID 面向对象设计原则

SOLID 是面向对象设计的五大基本原则的首字母缩写,由 Robert C. Martin(Uncle Bob)整理归纳。这五个原则是编写可维护、可扩展代码的基础。 S — 单一职责原则 (Single Responsibility Principle, SRP) 一个类应该只有一个引起它变化的原因。 一个类只做一件事。如果一个类承担了多个职责,那么每个职责的变化都可能影响这个类,导致它越来越难以维护。 反例: 简单工厂模式中的工厂类 Driver 同时负责判断车型和创建所有车对象,违反了单一职责。 O — 开闭原则 (Open-Closed Principle, OCP) 软件实体应该对扩展开放,对修改封闭。 添加新功能时,应该通过新增代码实现,而不是修改已有代码。详见开闭原则。 反例: 简单工厂的 if/else 判断链,每新增一种产品都要修改工厂方法。 L — 里氏替换原则 (Liskov Substitution Principle, LSP) 子类必须能够替换其父类,且程序行为不变。 任何使用父类的地方,换成子类后程序应该仍然正确运行。子类不应该破坏父类的契约。 示例: Benze 和 Bmw 都实现了 Car 接口,客户端代码使用 Car 类型,无论实际是哪种车,drive() 都能正确调用。 I — 接口隔离原则 (Interface Segregation Principle, ISP) 客户端不应该被迫依赖它不使用的方法。 接口应该尽量细化,不要把不相关的方法放在同一个接口里。大接口应该拆分成多个小接口,让实现类只需要关心自己用到的方法。 反例: 一个包含 fly()、swim()、run() 的 Animal 接口,鱼类被迫实现 fly() 和 run()。 D — 依赖倒置原则 (Dependency Inversion Principle, DIP) 高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。 ...

2026-04-16 · 1 min · 87 words · -

开闭原则

开闭原则 开闭原则(Open-Close Principle/OCP)是面向对象设计中"可复用设计"的基石,是面向对象设计中最重要的原则之一,其它很多的设计原则都是实现开闭原则的一种手段。 1988年,Bertrand Meyer在他的著作《Object Oriented Software Construction》中提出了开闭原则,它的原文是这样: “Software entities should be open for extension,but closed for modification”。翻译过来就是: “软件实体应当对扩展开放,对修改关闭”。这句话说得略微有点专业,我们把它讲得更通俗一点,也就是: 软件系统中包含的各种组件,例如模块 (Modules) 、类 (Classes) 以及功能 (Functions) 等等,应该在不修改现有代码的基础上,引入新功能。开闭原则中"开",是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中"闭",是指对于原有代码的修改是封闭的,即不应该修改原有的代码。实现开闭原则的关键就在于"抽象"。把系统的所有可能的行为抽象成一个抽象底层,这个抽象底层规定出所有的具体实现必须提供的方法的特征。作为系统设计的抽象层,要预见所有可能的扩展,从而使得在任何扩展情况下,系统的抽象底层不需修改;同时,由于可以从抽象底层导出一个或多个新的具体实现,可以改变系统的行为,因此系统设计对扩展是开放的。 我们在软件开发的过程中,一直都是提倡需求导向的。这就要求我们在设计的时候,要非常清楚地了解用户需求,判断需求中包含的可能的变化,从而明确在什么情况下使用开闭原则。 关于系统可变的部分,还有一个更具体的对可变性封装原则 (Principle of Encapsulation of Variation, EVP) ,它从软件工程实现的角度对开闭原则进行了进一步的解释。EVP要求在做系统设计的时候,对系统所有可能发生变化的部分进行评估和分类,每一个可变的因素都单独进行封装。 我们在实际开发过程的设计开始阶段,就要罗列出来系统所有可能的行为,并把这些行为加入到抽象底层,根本就是不可能的,这么去做也是不经济的,费时费力。另外,在设计开始阶段,对所有的可变因素进行预计和封装也不太现实,也是很难做得到。所以,开闭原则描绘的愿景只是一种理想情况或是极端状态,现实世界中是很难被完全实现的。我们只能在某些组件,在某种程度上符合开闭原则的要求。 通过以上的分析,对于开闭原则,我们可以得出这样的结论: 虽然我们不可能做到百分之百的封闭,但是在系统设计的时候,我们还是要尽量做到这一点。 对于软件系统的功能扩展,我们可以通过继承、重载或者委托等手段实现。以接口为例,它对修改就是是封闭的,而对具体的实现是开放的,我们可以根据实际的需要提供不同的实现,所以接口是符合开闭原则的。如果一个软件系统符合开闭原则的,那么从软件工程的角度来看,它至少具有这样的好处: 可复用性好。 我们可以在软件完成以后,仍然可以对软件进行扩展,加入新的功能,非常灵活。因此,这个软件系统就可以通过不断地增加新的组件,来满足不断变化的需求。 可维护性好。 由于对于已有的软件系统的组件,特别是它的抽象底层不去修改,因此,我们不用担心软件系统中原有组件的稳定性,这就使变化中的软件系统有一定的稳定性和延续性。开闭原则具有理想主义的色彩,它是面向对象设计的终极目标。因此,针对开闭原则的实现方法,一直都有面向对象设计的大师费尽心机,研究开闭原则的实现方式。后面要提到的里氏代换原则 (LSP) 、依赖倒转原则 (DIP) 、接口隔离原则 (ISP) 以及抽象类 (Abstract Class) 、接口(Interface)等等,都可以看作是开闭原则的实现方法。 http://baike.baidu.com/view/866233.htm

2026-04-16 · 1 min · 53 words · -

设计模式 — 工厂模式, Factory

设计模式 — 工厂模式, Factory 一、引子 话说十年前,有一个老板,他家有三辆汽车 —— Benz奔驰、Bmw宝马、Audi奥迪,还雇了司机为他开车。不过,老板坐车时总是怪怪的: 上Benz车后跟司机说"开奔驰车!",坐上Bmw后他说"开宝马车!",坐上Audi说"开奥迪车!"。你一定说: 这人有病!直接说开车不就行了?! 而当把这个老板的行为放到我们程序设计中来时,会发现这是一个普遍存在的现象。幸运的是,这种有病的现象在 OO (面向对象) 语言中可以避免了。下面就以 Java 语言为基础来引入我们本文的主题: 工厂模式。 二、分类 工厂模式主要是为创建对象提供过渡接口,以便将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。 工厂模式在《Java与模式》中分为三类: 简单工厂模式 (Simple Factory) 工厂方法模式 (Factory Method) 抽象工厂模式 (Abstract Factory) 这三种模式从上到下逐步抽象,并且更具一般性。 GOF在《设计模式》一书中将工厂模式分为两类: 工厂方法模式 (Factory Method) 与抽象工厂模式 (Abstract Factory) 。将简单工厂模式 (Simple Factory) 看为工厂方法模式的一种特例,两者归为一类。 两者皆可,在本文使用《Java与模式》的分类方法。下面来看看这些工厂模式是怎么来"治病"的。 简单工厂模式, 简单工厂 (Simple Factory) 简单工厂模式又称静态工厂方法模式。从命名上就可以看出这个模式一定很简单。它存在的目的很简单: 定义一个用于创建对象的接口。 先来看看它的组成: 工厂类角色: 这是本模式的核心,含有一定的业务逻辑和判断逻辑。在 java 中它往往由一个具体类实现。 抽象产品角色: 它一般是具体产品继承的父类或者实现的接口。在 java 中由接口或者抽象类来实现。 具体产品角色: 工厂类所创建的对象就是此角色的实例。在 java 中由一个具体类实现。 来用类图来清晰的表示下的它们之间的关系 (如果对类图不太了解,请参考我关于类图的文章) : 那么简单工厂模式怎么来使用呢?我们就以简单工厂模式来改造老板坐车的方式——现在老板只需要坐在车里对司机说句: “开车"就可以了。 // 抽象产品 interface Car { void drive(); } // 具体产品 class Benze implements Car { public void drive() { System.out.println("Driving Benz"); } } // 具体产品 class Bmw implements Car { public void drive() { System.out.println("Driving Bmw"); } } // 工厂类 class Driver { // 工厂方法,注意返回类型为抽象产品 public static Car createCar(String s) throws Exception { // 判断逻辑,返回具体的产品角色给 Client if (s.equalsIgnoreCase("Benz")) { return new Benze(); } else if (s.equalsIgnoreCase("Bmw")) { return new Bmw(); } else { throw new Exception(); } } } // 客户端 public class Magnate { public static void main(String[] args) throws Exception { // 告诉司机我今天坐奔驰 Car car = Driver.createCar("benz"); // 下命令: 开车 car.drive(); car = Driver.createCar("bmw"); car.drive(); } } 将本程序空缺的其他信息填充完整后即可运行。如果你将所有的类放在一个文件中,请不要忘记只能有一个类被声明为public。本程序在jdk1.4 下运行通过。 ...

2026-04-16 · 3 min · 440 words · -

缓存

缓存 https://xie.infoq.cn/article/0134f29b0c0895df548dd929b?utm_source=rss&utm_medium=article 缓存的存在,是为了调和差异。 差异有多种,比如处理器和存储之间的速度差异、用户对产品的使用体验和服务处理效率的差异等等。 CPU 缓存[2]。为了调和 CPU 和内存之间巨大的速度差异,设置了 L1/L2/L3 三级缓存,离 CPU 越近,速度越快。 Ehcache[3]。是最流行了 Java 缓存框架之一。因为其开源属性,在 spring/Hibernate 等框架上被广泛使用。支持磁盘持久化和堆外内存。缓存功能齐全。 Guava cache。灵感来源于 ConcurrentHashMap,但具有更丰富的元素失效策略,功能没有 ehcache 齐全,如只支持 jvm 内存,但比较轻量简洁。 memcached。[5] memcached 是一个高效的分布式内存 cache,搭建与操作使用都比较简单,整个缓存都是基于内存的,因此响应时间很快,但是没有持久化的能力。 Redis 以优秀的性能和丰富的数据结构,以及稳定性和数据一致性的支持,被业内越来越普遍的使用。 缓存预热 缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候, 先查询数据库, 然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! 解决思路: 直接写个缓存刷新页面,上线时手工操作下; 数据量不大,可以在项目启动的时候自动进行加载; 定时刷新缓存; 缓存更新 除了缓存服务器自带的缓存失效策略之外 (Redis默认的有6中策略可供选择) ,我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种: 定时去清理过期的缓存; 当有用户请求过来时, 再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。 两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。 缓存穿透 Cache Penetration 穿透形象一点就是: 请求过来了 转了一圈 一无所获 就像穿过透明地带一样。 在高并发系统中缓存穿透,如果一个 req 需要请求的 key 在缓存中没有,这时业务线程就会访问磁盘数据库系统,然而磁盘数据库也没有这个 key,无奈业务线程只能返回 null,白白处理一圈。 查询的是数据库中不存在的数据,没有命中缓存而数据库查询为空,也不会更新缓存。导致每次都查库,如果不加处理,遇到恶意攻击,会导致数据库承受巨大压力,直至崩溃。 解决方案 接口层防护(第一道防线) API 限流、防 DDoS 网关黑名单、用户鉴权 参数合法性校验(如 ID 必须 > 0) 缓存空对象 如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间会很短,不超过 5 分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓存中获取就有值了,而不会继续访问数据库。当修改或者新增该 key 的数据信息的时候,需要删除或者更新 null 缓存值。 ...

2026-04-14 · 2 min · 379 words · -

distributed lock 分布式锁

distributed lock 分布式锁 分布式是一种分布式协调技术,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。 分布式锁是由于单机锁无法满足分布式系统锁,在多进程/分布式环境下,需要分布式锁来控制共享内容,保证线程的安全。 为何需要分布式锁 Martin Kleppmann 是英国剑桥大学的分布式系统的研究员,之前和 Redis 之父 Antirez 进行过关于 RedLock (红锁,后续有讲到) 是否安全的激烈讨论。 Martin 认为一般我们使用分布式锁有两个场景: 效率: 使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。 正确性: 加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作, 比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。 分布式锁的一些特点 当我们确定了在不同节点上需要分布式锁,那么我们需要了解分布式锁到底应该有哪些特点? 分布式锁的特点如下: 互斥性: 和本地锁一样互斥性是锁最基本的特性, 任意时刻,只有一个客户端能持有锁。 可重入性: 同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。 超时释放: 锁失效机制, 防止死锁。正常情况下,请求获取锁之后,处理任务,处理完成之后释放锁。 但是如果在处理任务发生服务异常,或者网络异常时,导致锁无法释放。其他请求都无法获取锁,变成死锁。 为了防止锁变成死锁,需要设置锁的超时时间。过了超时时间后,锁自动释放,其他请求能正常获取锁。 自动续期: 锁设置了超时机制后,如果持有锁的节点处理任务的时候过长超过了超时时间,就会发生线程未处理完任务锁就被释放了, 其他线程就能获取到该锁,导致多个节点同时访问共享资源。对此,就需要延长超时时间。 开启一个监听线程,定时监听任务,监听任务线程还存活就延长超时时间。当任务完成、或者任务发生异常就不继续延长超时时间。 高可用, 高性能: 加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。 安全性: 锁只能被持有锁的客户端释放, 不能被其它客户端释放. 支持阻塞和非阻塞: 和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)。即没有获取到锁将直接返回获取锁失败 支持公平锁和非公平锁(可选): 公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。 分布式锁一般有三种实现方式: 数据库乐观锁 基于 Redis 的分布式锁 基于 ZooKeeper 的分布式锁 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 互斥性。在任意时刻,只有一个客户端能持有锁。 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。 具有容错性。只要大部分的 Redis 节点正常运行,客户端就可以加锁和解锁。 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。 Redis 实现分布式锁 Redis 实现分布式锁,性能会比关系式数据库高一些. ...

2026-04-14 · 10 min · 2091 words · -

PostgreSQL

PostgreSQL commands \l 或 \list meta-command 列出所有数据库 sudo -u postgres psql -c "\l" 用 \c + 数据库名 来进入数据库: \dt 列出所有数据库表: # 查看表结构, 索引 \d table0 # 比上面多几个字段 Storage | Stats target | Description \d+ table0 # Turn off printing of column names and result row count footers, etc. This is equivalent to \t or \pset tuples_only. \t tuples only on/off, tuples only on 的时候 select 语句的输出不带 header \h \? \du 列出所有的用户 # 创建用户 CREATE USER user_0 WITH PASSWORD 'password_0'; # create database, 所有者 user_0 create database database_0 OWNER user_0; psql -h 192.168.1.100 -p 5432 -U user_0 -d database_0 PGPASSWORD=password_0 psql -h 192.168.1.100 -p 5432 -U user_0 -d database_0 --command 'select version();' #当表没有其他关系时 TRUNCATE TABLE tablename; #当表中有外键时,要用级联方式删所有关联的数据 TRUNCATE TABLE tablename CASCADE; # 查看 表大小 select pg_size_pretty(pg_relation_size('table0')); # 查看配置文件路径, 切换到 postgres 用户执行 psql -c "show config_file" # 查看版本 select version(); pacman -S postgresql psql -h 127.0.0.1 -p 5432 -d database0 -U user0 # create table create table test(id int, c1 int); create table table0(field0 json); # delete table DROP TABLE table0; # 查看字段类型 select column_name, data_type from information_schema.columns where table_name='table0'; select * length( "abc"::TEXT) insert into test select generate_series(1,10000), random()*10; # 复制表结构到另外一个数据库 pg_dump -U postgres --schema-only source_db | psql -U postgres target_db 导入/导出 export, 导出, 备份 # https://www.postgresql.org/download/linux/ubuntu/ # install pg_dump sudo apt install -y postgresql-common # This script will enable the PostgreSQL APT repository on apt.postgresql.org sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh # ubuntu install pg_dump sudo apt-get install postgresql-client-17 # -h, host 127.0.0.1 # -p, port 5432 # -t, table: table0, 不加 -t 参数时会导出所有表结构 # -s, 不导出数据 # database: database0 # -F : 指定输出文件的格式,它可以是以下格式之一: c: 自定义格式 d: 目录格式存档 t: tar 文件包 p: SQL 脚本文件 # -W 命令执行时提示输入用户密码(不会直接在命令中写密码)。 pg_dump -h 127.0.0.1 -U username -W -F t db_name > foo.tar # 导入 # -c --clean 创建数据库对象前先清理(删除)它们。 pg_restore -h 127.0.0.1 -U username -W -d db_name -c foo.tar pg_dump -h 127.0.0.1 -p 5432 -t table_0 -U postgres database0 > foo.sql pg_dump -h 127.0.0.1 -p 5432 -s -t table_0 -U postgres database0 > foo.sql # 导出并压缩 pg_dump -d db_name | gzip > db.gz pg_dump -a -t table_0 "host=127.0.0.1 hostaddr=127.0.0.1 port=5432 user=user_0 password=password_0 dbname=db_0" # export insert sql pg_dump -a -t table_0 "host=127.0.0.1 hostaddr=127.0.0.1 port=5432 user=user_0 password=password_0 dbname=db_0" --inserts 导入 # sql psql -h 127.0.0.1 -p 5432 -t table0 -U postgres -d database0 -f foo.sql # csv, https://stackoverflow.com/questions/26701735/extra-data-after-last-expected-column-while-trying-to-import-a-csv-file-into-p \COPY agency (agency_name, agency_url, agency_timezone) FROM 'myFile.txt' CSV HEADER DELIMITER ','; 导出指定的行 https://stackoverflow.com/questions/12815496/export-specific-rows-from-a-postgresql-table-as-insert-sql-script ...

2026-04-14 · 22 min · 4514 words · -

LevelDB

使用场景 持久化缓冲队列(削峰) LevelDB 可以作为持久化写缓冲使用,实现「一边写入、一边读出」的管道模式: 写入端(Producer):将数据以递增的序列号或时间戳作为 Key 写入 LevelDB,得益于 LSM-Tree 架构,写入速度极快(顺序写磁盘),可以吸收突发流量峰值 读取端(Consumer):按 Key 顺序扫描读取,处理完毕后删除对应记录,以自身能处理的速率消费数据 数据持久化到磁盘:与纯内存队列不同,LevelDB 的数据落盘,进程崩溃后数据不丢失,重启后可继续消费 这种模式可以起到削峰填谷的作用:写入端的流量高峰被 LevelDB 缓冲到磁盘,读取端按匀速消费,避免下游系统因瞬间流量过大而崩溃。 适合此场景的条件 数据量超出内存容量,需要落盘缓冲 对消息顺序有要求(LevelDB Key 有序) 单机嵌入式场景,不想引入 Kafka/RabbitMQ 等重量级中间件 可以接受 LevelDB 不提供原生队列语义(需要自行管理消费位点和删除逻辑) 注意事项 LevelDB 不是消息队列,没有 ACK、重试、多消费者等内置机制,需要自行实现 频繁删除已消费数据会触发 Compaction,建议批量删除 如果对高吞吐、多消费者、分布式有需求,优先考虑 Kafka 等专用消息队列 其他典型场景 本地缓存层:缓存热点数据到本地磁盘,减少远端数据库访问 索引存储:存储倒排索引、元数据索引等有序 KV 数据 嵌入式数据库:Chrome 浏览器使用 LevelDB 存储 IndexedDB 数据 区块链节点:比特币、以太坊节点使用 LevelDB 存储区块链状态数据 LevelDB 数据写入流程 数据写入的整个流程为: 数据首先会被写入 memtable 和 WAL 当 memtable 达到上限后,会转换为 immutable memtable,之后持久化到 L0 (称为 flush),L0 中每个文件都是一个持久化的 immutable memtable,多个文件间可以有相互重叠的 Key 当 L0 中的文件达到一定数量时,便会和 L1 中的文件进行合并 (称为 compaction) 自 L1 开始所有文件都不会再有相互重叠的 Key,并且每个文件都会按照 Key 的顺序存储。每一层通常是上一层大小的 10 倍,当一层的大小超过限制时,就会挑选一部分文件合并到下一层 ...

2026-04-09 · 3 min · 583 words · -

Harness Engineering 与状态锚点

什么是 Harness Engineering Harness Engineering 是一种以"脚手架优先"为核心思想的软件开发方法,尤其在 AI 辅助开发场景下越来越受到关注。 “Harness”(脚手架/支撑框架)这个词借用自工程领域,在软件中指围绕核心系统搭建的一套结构化支撑层,包括: 明确的文档约定(说明系统当前状态、设计决策) 测试框架和实验入口(可快速验证变更) 状态记录文件(捕捉项目在某个时间点的快照) 接口契约和边界定义 它的目标是让代码库对人和 AI 助手都更"可理解、可修改、可安全演进"。 状态锚点是什么 状态锚点(State Anchor) 是 Harness Engineering 的核心实践之一。 它是一个显式的文档文件(通常命名为 harness-state.md、project-state.md 等),用来在某个时间点精确记录系统的当前状态,就像 Git commit 对代码的作用,它对"上下文"做了一次快照。 典型内容 一个状态锚点文件通常包含: ## 当前状态(2026-04-09) ### 已完成 - [x] 核心数据模型定义完毕 - [x] API 接口初版上线 ### 进行中 - [ ] 用户认证模块(50%) ### 已知问题 - /api/user 接口在并发场景下偶发 500 ### 下一步 - 完成认证模块 → 接入集成测试 → 上线 v0.2 状态锚点的作用 1. 消除"现在在哪"的认知负担 每次回到一个项目,最耗时的事情不是写代码,而是重建上下文:“上次做到哪了?““这个模块稳定了吗?““还有什么坑没踩完?” ...

2026-04-09 · 1 min · 140 words · -

zsh, oh-my-zsh, oh my zsh

zsh, oh-my-zsh, oh my zsh # 查看已经安装的 shell 列表 cat /etc/shells ## 查看当前 shell echo $SHELL # archlinux install zsh sudo pacman -S git zsh # ubuntu install zsh sudo apt install zsh install oh my zsh # install oh-my-zsh, will set default shell to zsh sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" # after oh my zsh installed, ubuntu user should logout and login. update oh my zsh # need vpn to access github omz update 错过更新提示后手动触发更新,直接运行: ...

2026-04-09 · 3 min · 514 words · -