-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdesign_patterns.py
More file actions
2613 lines (1941 loc) · 85.2 KB
/
Copy pathdesign_patterns.py
File metadata and controls
2613 lines (1941 loc) · 85.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
=============================================================================
设计模式学习全攻略:从入门到实战
=============================================================================
本文件涵盖 Python 中最常用的 12 种设计模式,分为三大类:
🏗️ 创建型模式(5 种):控制对象的创建过程
🧱 结构型模式(4 种):处理类与对象的组合
🧠 行为型模式(3 种):管理对象之间的交互与职责分配
每个模式包含四个部分:
📖 概念讲解 — 是什么、解决什么问题、何时使用
👀 完整示例 — 可运行的代码演示核心思想
✏️ 填空练习 — 关键代码留空,动手补全
🚀 RAG 实战 — 结合本项目的真实应用场景
学习路线(建议 6 天):
Day 1:单例模式 + 简单工厂 + 工厂方法
Day 2:抽象工厂 + 建造者模式
Day 3:装饰器模式 + 适配器模式
Day 4:代理模式 + 外观模式
Day 5:策略模式 + 观察者模式
Day 6:模板方法模式 + 综合复习
使用方法:
python design_patterns.py → 运行所有练习测试
=============================================================================
"""
import time
import json
from abc import ABC, abstractmethod
from functools import wraps
from typing import Dict, List, Any, Callable, Optional, Type
# =============================================================================
# 序言:什么是设计模式?
# =============================================================================
"""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🟢 设计模式是什么?
设计模式是软件开发中反复出现的问题的经典解决方案。
它们是前人在大量项目中总结出的"最佳实践",就像编程领域的"兵法"。
打个比方:
- 算法 = 菜谱(具体步骤)
- 设计模式 = 厨房布局(整体架构思路)
🟢 为什么要学设计模式?
1. 提高代码可读性 — "哦,这里用了策略模式"比读 200 行业务逻辑快得多
2. 提高可维护性 — 模式自带解耦,改一处不影响全局
3. 团队协作 — 统一的"设计语言",减少沟通成本
4. 面试必备 — 中高级岗位几乎必考
🟢 设计模式三大分类:
┌─────────────┬──────────────────────────┬──────────────────────┐
│ 创建型 │ 控制对象的创建过程 │ 单例、工厂、建造者 │
├─────────────┼──────────────────────────┼──────────────────────┤
│ 结构型 │ 处理类与对象的组合方式 │ 装饰器、适配器、代理 │
├─────────────┼──────────────────────────┼──────────────────────┤
│ 行为型 │ 管理对象间的交互与职责 │ 策略、观察者、模板 │
└─────────────┴──────────────────────────┴──────────────────────┘
🟢 Python 中的设计模式
Python 的"动态类型 + 一等函数 + 装饰器语法"让很多经典模式实现起来
比其他语言(如 Java)简洁得多。本教程会展示"Pythonic"的写法,
让你用最少的代码获得最大的收益。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
# =============================================================================
# 🏗️ 第一部分:创建型模式
# =============================================================================
# =============================================================================
# 1. 单例模式(Singleton Pattern)
# =============================================================================
print("=" * 60)
print("🏗️ 第一部分:创建型模式")
print("=" * 60)
print("\n" + "─" * 60)
print("模式 1:单例模式(Singleton Pattern)")
print("─" * 60)
"""
📖 概念讲解
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
问题:如何确保一个类在整个程序中只有一个实例?
场景:
• 数据库连接池 — 不需要创建多个连接池
• 配置管理器 — 全局配置应该只有一份
• 日志记录器 — 所有模块往同一个日志写
• 模型缓存 — Embedding 模型加载一次就够了
核心思想:无论调用多少次「创建」,始终返回同一个对象。
四种 Python 实现方式(由简到繁):
① 模块单例(最简单,推荐日常使用)
② __new__ 方法(经典 OOP 方式)
③ 装饰器(Pythonic 方式)
④ 元类(最强大,适合框架级代码)
"""
# ── 实现 ①:模块单例 ──
print("\n 📌 实现 ①:模块单例(最简单)")
print(" " + "-" * 40)
# Python 模块天然是单例 —— import 只会执行一次模块代码
# 下面模拟一个「配置管理器」模块
_module_config = {
"model_name": "qwen3:0.5b",
"temperature": 0.7,
"max_tokens": 2048,
"top_k": 5,
}
def get_config(key, default=None):
"""获取配置项(线程安全,因为有 GIL)"""
return _module_config.get(key, default)
def set_config(key, value):
"""设置配置项"""
_module_config[key] = value
def get_all_config():
"""获取所有配置的副本(防止外部修改内部数据)"""
return dict(_module_config)
# 测试:无论在多少个文件里 import,_module_config 始终是同一个字典
print(f' 配置项 model_name = {get_config("model_name")}')
set_config("temperature", 0.3)
print(f' 修改后 temperature = {get_config("temperature")}')
print(" ✅ 模块单例:简单、可靠、Pythonic!")
# ── 实现 ②:__new__ 方法 ──
print("\n 📌 实现 ②:__new__ 方法(经典 OOP)")
print(" " + "-" * 40)
class SingletonViaNew:
"""
通过重写 __new__ 实现单例
关键知识点:
- __new__ 负责「创建」对象(分配内存)
- __init__ 负责「初始化」对象(设置属性)
- __new__ 在 __init__ 之前调用
"""
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
# super().__new__(cls) 调用 object 的 __new__ 分配内存
cls._instance = super().__new__(cls) #不能调用自身的__new__方法,否则该函数会自身递归
return cls._instance
def __init__(self, value=None):
# 注意:每次「创建」都会调用 __init__
# 所以需要用 hasattr 防止重复初始化
if not hasattr(self, "_initialized"): #检查该类是否含有_initialized属性,若有则不进行初始化
self.value = value
self._initialized = True
a = SingletonViaNew("hello")
b = SingletonViaNew("world") # b 和 a 是同一个对象,value 仍是 "hello"
print(f" a is b: {a is b}") # True
print(f" a.value: {a.value}") # "hello"(没有因为 b 的初始化改变)
# ── 实现 ③:装饰器 ──
print("\n 📌 实现 ③:装饰器(Pythonic)")
print(" " + "-" * 40)
def singleton(cls):
"""
将任意类变为单例的装饰器
原理:
- instances 字典缓存「类 → 实例」的映射
- 每次调用被装饰的类时,先查缓存
- 闭包保存 instances,外部无法访问
"""
instances = {}
@wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs) #这里保证了单例模式,若创建的类已经存在,不能更新参数 使用构造函数初始化实例
return instances[cls]
return get_instance
@singleton #这个类使用了singleton包裹 相当于将参数传给get_instance
class DatabaseConnection:
def __init__(self, host="localhost", port=5432):
self.host = host
self.port = port
self.connected = False
def connect(self):
self.connected = True
return f"已连接 {self.host}:{self.port}"
db1 = DatabaseConnection(host="db-prod", port=5432)
db2 = DatabaseConnection(host="db-test", port=9999) # 这个参数被忽略!
print(f" db1 is db2: {db1 is db2}")
print(f" db1.host: {db1.host}") # 仍是 db-prod
# ── 实现 ④:元类 ──
print("\n 📌 实现 ④:元类(最强大)")
print(" " + "-" * 40)
class SingletonMeta(type):
"""
元类版单例
原理:
- type 是所有类的「类」,即元类,负责创建类,默认的元类是type,我们在这里重写了元类
- 当我们写 MyClass() 时,Python 实际调用的是 type.__call__
- 重写 __call__ 就能拦截实例化过程
"""
_instances = {} #用于存储所有单例实例,键是类,值是唯一实例
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs) #__call__方法创建类实例,这里重写了__call__方法,cls是传入的需要创建的ModelConfig类,创建时会寻找ModelConfig的元类,由元类进行创建,这里重写了元类和__call__方法,创建时仍然使用type类
return cls._instances[cls]
class ModelCache(metaclass=SingletonMeta):
"""模型缓存 —— 用元类确保只加载一次"""
def __init__(self):
self.models = {}
def load(self, model_name):
if model_name not in self.models:
# 模拟加载模型(实际项目中这里是加载大模型)
self.models[model_name] = f"<Model:{model_name}>"
print(f" [耗时操作] 正在加载模型 {model_name}...")
return self.models[model_name]
cache1 = ModelCache()
cache2 = ModelCache()
print(f" cache1 is cache2: {cache1 is cache2}")
cache1.load("qwen3:0.5b")
cache2.load("qwen3:0.5b") # 第二次不会重复加载,检测到该类已经有一个实例
print(" ✅ 元类单例:适合框架级别,一次编写到处使用")
print(f" 缓存内容: {cache1.models}")
# ── ✏️ 练习 1:补全单例装饰器 ──
print("\n ✏️ 练习 1:补全单例装饰器(在下面填空)")
print(" " + "-" * 40)
def singleton_exercise(cls):
"""
TODO: 补全这个装饰器,使得被装饰的类变成单例
提示:
1. 创建一个字典 instances = {}
2. 定义内部函数 get_instance
3. 如果 cls 不在 instances 中,创建实例并存入
4. 返回实例
5. get_instance 返回 get_instance
"""
# ===== 你的代码写在这里 =====
instance_dict={} # TODO: 创建 instances 字典
# ===== 你的代码写在这里 =====
@wraps(cls)
def get_instance(*args, **kwargs):
# ===== 你的代码写在这里 =====
if cls not in instance_dict:
instance_dict[cls] = cls(*args,**kwargs) # TODO: 检查 cls 是否在 instances 中
# ===== 你的代码写在这里 =====
return instance_dict[cls] # TODO: 返回实例
return get_instance
# ── 🚀 RAG 实战:Embedding 模型单例缓存 ──
print("\n 🚀 RAG 实战:Embedding 模型单例缓存")
print(" " + "-" * 40)
class EmbeddingModelCache:
"""
实战场景:
在你的 RAG 项目中,Embeddings.py 里加载模型(如 text2vec-large-chinese)
是一个非常耗时的操作。如果每次查询都重新加载模型,系统会非常慢。
解决方案:用一个单例缓存,确保模型只加载一次。
"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._models = {}
return cls._instance
def get_embedding_model(self, model_name: str):
"""
获取 Embedding 模型。
如果是第一次请求这个模型,则「模拟加载」;之后再请求直接返回缓存。
"""
if model_name not in self._models:
# 模拟耗时的模型加载过程
print(f" ⏳ 正在加载 Embedding 模型: {model_name}...")
time.sleep(0.3) # 模拟加载耗时
self._models[model_name] = {
"name": model_name,
"dim": 1024 if "large" in model_name else 768,
"loaded": True,
}
print(f" ✅ 模型 {model_name} 加载完成!")
else:
print(f" ⚡ 模型 {model_name} 已在缓存中,直接使用!")
return self._models[model_name]
def compute_embedding(self, model_name: str, text: str) -> List[float]:
"""模拟计算 embedding(实际项目中调用模型推理)"""
model = self.get_embedding_model(model_name)
# 模拟:用文本长度生成假的向量
import hashlib
h = hashlib.md5(text.encode()).hexdigest()
# 取前 dim 个字节转为 0~1 之间的 float
dim = model["dim"]
result = []
for i in range(min(dim, len(h))):
result.append(int(h[i], 16) / 16.0)
# 补齐到 dim 长度
while len(result) < dim:
result.append(0.0)
return result
embed_cache = EmbeddingModelCache()
v1 = embed_cache.compute_embedding("text2vec-large-chinese", "保研加分政策")
v2 = embed_cache.compute_embedding("text2vec-large-chinese", "推免细则")
# 第二次调用时,模型已经在缓存中,不会重新加载
print(" ✅ RAG Embedding 模型缓存:大幅提升查询效率!")
# =============================================================================
# 2. 简单工厂模式(Simple Factory)
# =============================================================================
print("\n" + "─" * 60)
print("模式 2:简单工厂模式(Simple Factory)")
print("─" * 60)
"""
📖 概念讲解
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
问题:客户端代码需要根据参数创建不同类型的对象,但不想关心
具体的创建逻辑(也不想写一堆 if-else)。
场景:
• 根据配置选择不同的 LLM(OpenAI / Ollama / 本地模型)
• 根据文件扩展名选择不同的解析器(PDF / DOCX / TXT)
• 根据用户等级创建不同的权限对象
核心思想:把「创建对象的逻辑」集中到一个工厂类中。
三种工厂的区别(面试常考):
┌──────────────┬─────────────────────────────────┬──────────────────┐
│ 简单工厂 │ 一个工厂类 + 一个创建方法 │ 最基础、最常用 │
│ 工厂方法 │ 父类定义接口 + 子类决定创建什么 │ 对扩展开放 │
│ 抽象工厂 │ 创建一系列相关的产品族 │ 最复杂、最强大 │
└──────────────┴─────────────────────────────────┴──────────────────┘
"""
# ── 完整示例 ──
print("\n 📌 完整示例:文档解析器工厂")
print(" " + "-" * 40)
class DocumentParser(ABC):
"""文档解析器抽象基类"""
@abstractmethod
def parse(self, file_path: str) -> str:
pass
class PDFParser(DocumentParser):
def parse(self, file_path: str) -> str:
return f"[PDF解析] 从 {file_path} 提取了文本内容"
class DocxParser(DocumentParser):
def parse(self, file_path: str) -> str:
return f"[DOCX解析] 从 {file_path} 提取了文本内容"
class TxtParser(DocumentParser):
def parse(self, file_path: str) -> str:
return f"[TXT解析] 从 {file_path} 读取了文本内容"
class ParserFactory:
"""简单工厂:根据文件扩展名创建对应的解析器"""
@staticmethod
def create_parser(file_path: str) -> DocumentParser:
ext = file_path.lower().split(".")[-1]
if ext == "pdf":
return PDFParser()
elif ext == "docx":
return DocxParser()
elif ext == "txt":
return TxtParser()
else:
raise ValueError(f"不支持的文件类型: .{ext}")
# 客户端代码:不需要知道具体的解析器类名
for f in ["论文.pdf", "报告.docx", "笔记.txt"]:
parser = ParserFactory.create_parser(f)
print(f" 解析 {f}: {parser.parse(f)}")
# ── ✏️ 练习 2:补全 LLM 客户端工厂 ──
print("\n ✏️ 练习 2:补全 LLM 客户端工厂")
print(" " + "-" * 40)
class LLMClient(ABC):
@abstractmethod
def chat(self, prompt: str) -> str:
pass
class OpenAIClient(LLMClient):
def __init__(self, api_key: str, model: str = "gpt-4"):
self.api_key = api_key
self.model = model
def chat(self, prompt: str) -> str:
return f"[OpenAI:{self.model}] 回复: 根据{prompt[:20]}...生成的回答"
class OllamaClient(LLMClient):
def __init__(self, model: str = "qwen3:0.5b"):
self.model = model
def chat(self, prompt: str) -> str:
return f"[Ollama:{self.model}] 回复: 根据{prompt[:20]}...生成的回答"
class LLMFactory:
"""LLM 客户端工厂"""
@staticmethod
def create(config: dict) -> LLMClient:
"""
根据配置字典创建对应的 LLM 客户端
配置格式:
{"provider": "openai", "api_key": "sk-xxx", "model": "gpt-4"}
{"provider": "ollama", "model": "qwen3:0.5b"}
TODO: 补全下面的创建逻辑
"""
provider = config.get("provider", "").lower()
model=config.get('model','').lower()
api_key=config.get('api_key','').lower()
# ===== 你的代码写在这里 =====
if provider=='openai':
return OpenAIClient()
elif provider=='ollama':
return OllamaClient()
else:
raise ValueError(f'Unexpected value:{provider}')
# TODO: 根据 provider 创建对应的 LLM 客户端
# ===== 你的代码写在这里 =====
# TODO: 处理其他 provider
# ── 🚀 RAG 实战:可配置的多 LLM 切换 ──
print("\n 🚀 RAG 实战:根据 .env 配置切换 LLM")
print(" " + "-" * 40)
"""
实战场景:
你的 LLM.py 中目前可能只接入了一个模型。使用工厂模式后,
可以通过修改 .env 文件中的配置,轻松在 OpenAI / Ollama / 其他模型之间切换,
而不需要修改业务代码。
.env 示例:
LLM_PROVIDER=ollama
LLM_MODEL=qwen3:0.5b
LLM_API_KEY=sk-xxx
"""
class RAGLLMService:
"""
RAG 项目的 LLM 服务类
使用工厂模式解耦「模型选择」和「业务逻辑」
"""
def __init__(self, provider: str = "ollama", model: str = "qwen3:0.5b",
api_key: str = ""):
config = {
"provider": provider,
"model": model,
"api_key": api_key,
}
self.client = LLMFactory.create(config)
def generate_answer(self, question: str, context: str) -> str:
prompt = f"""用户问题:{question}
参考材料:{context}
请基于以上材料回答问题。"""
return self.client.chat(prompt)
# 模拟使用
service = RAGLLMService(provider="ollama", model="qwen3:0.5b")
answer = service.generate_answer("加分政策是什么", "保研加分包括竞赛加分...")
print(f" RAG 回答: {answer}")
# =============================================================================
# 3. 工厂方法模式(Factory Method Pattern)
# =============================================================================
print("\n" + "─" * 60)
print("模式 3:工厂方法模式(Factory Method Pattern)")
print("─" * 60)
"""
📖 概念讲解
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
与简单工厂的区别:
简单工厂:一个工厂类根据参数创建不同产品(违反开闭原则,加新产品要改工厂)
工厂方法:定义抽象的工厂接口,让子类决定实例化哪个具体产品(对扩展开放)
开闭原则(OCP):对扩展开放,对修改关闭。
→ 加新产品时,只需新增子类,不需要修改已有代码。
场景:
• 不同检索策略:向量检索 / BM25 关键词检索 / 混合检索
• 不同存储后端:本地文件 / MySQL / Elasticsearch
• 不同评测指标:MRR / NDCG / Recall@K
"""
# ── 完整示例 ──
print("\n 📌 完整示例:物流运输")
print(" " + "-" * 40)
class Transport(ABC):
@abstractmethod
def deliver(self) -> str:
pass
class Truck(Transport):
def deliver(self):
return "🚛 卡车陆运配送"
class Ship(Transport):
def deliver(self):
return "🚢 货轮海运配送"
class Airplane(Transport):
def deliver(self):
return "✈️ 飞机空运配送"
class Logistics(ABC):
"""
物流基类 —— 定义了「工厂方法」接口
子类需要实现 create_transport()
"""
@abstractmethod
def create_transport(self) -> Transport:
"""工厂方法:由子类决定创建哪种运输工具"""
pass
def plan_delivery(self) -> str:
"""模板方法:固定的配送流程"""
transport = self.create_transport()
return f"物流计划:使用 {transport.deliver()}"
class RoadLogistics(Logistics):
def create_transport(self) -> Transport:
return Truck()
class SeaLogistics(Logistics):
def create_transport(self) -> Transport:
return Ship()
class AirLogistics(Logistics):
def create_transport(self) -> Transport:
return Airplane()
# 测试
for logistics in [RoadLogistics(), SeaLogistics(), AirLogistics()]:
print(f" {logistics.plan_delivery()}")
# ── ✏️ 练习 3:补全检索策略工厂 ──
print("\n ✏️ 练习 3:补全检索策略(工厂方法)")
print(" " + "-" * 40)
class SearchStrategy(ABC):
@abstractmethod
def search(self, query: str, top_k: int) -> List[dict]:
"""返回检索结果列表"""
pass
class VectorSearch(SearchStrategy):
def search(self, query: str, top_k: int) -> List[dict]:
return [
{"id": i, "method": "向量检索", "score": 0.9 - i * 0.1,
"content": f"向量结果{i}"}
for i in range(top_k)
]
class KeywordSearch(SearchStrategy):
def search(self, query: str, top_k: int) -> List[dict]:
return [
{"id": i, "method": "关键词检索", "score": 0.8 - i * 0.15,
"content": f"关键词结果{i}"}
for i in range(top_k)
]
class HybridSearch(SearchStrategy):
"""混合检索:结合向量检索和关键词检索的结果"""
def __init__(self):
self.vector = VectorSearch()
self.keyword = KeywordSearch()
def search(self, query: str, top_k: int) -> List[dict]:
vec_results = self.vector.search(query, top_k)
kw_results = self.keyword.search(query, top_k)
# 简单合并(实际项目中会用 RRF 等算法融合)
combined = vec_results + kw_results
combined.sort(key=lambda x: x["score"], reverse=True)
return combined[:top_k]
class SearchFactory(ABC):
"""检索工厂抽象类"""
@abstractmethod
def create_search(self) -> SearchStrategy:
"""
TODO: 子类需要实现这个方法,返回具体的检索策略
"""
pass
def execute_search(self, query: str, top_k: int = 3) -> List[dict]:
"""执行检索(不需要子类重写)"""
strategy = self.create_search()
return strategy.search(query, top_k)
# ===== 你的代码写在这里:实现具体的检索工厂 =====
class VectorSearchFactory(SearchFactory):
"""向量检索工厂"""
# TODO: 实现 create_search 方法
def create_search(self) -> SearchStrategy:
# ===== 你的代码 =====
return VectorSearch()
# TODO: 返回 VectorSearch 实例
class KeywordSearchFactory(SearchFactory):
"""关键词检索工厂"""
# TODO: 实现 create_search 方法
def create_search(self) -> SearchStrategy:
# ===== 你的代码 =====
return KeyboardInterrupt()
# TODO: 返回 KeywordSearch 实例
class HybridSearchFactory(SearchFactory):
"""混合检索工厂"""
# TODO: 实现 create_search 方法
def create_search(self) -> SearchStrategy:
# ===== 你的代码 =====
return HybridSearch() # TODO: 返回 HybridSearch 实例
# ── 🚀 RAG 实战:可切换的检索策略 ──
print("\n 🚀 RAG 实战:根据场景选择检索策略")
print(" " + "-" * 40)
"""
实战场景:
你的 HybridSearch.py 目前混合了多种检索方式。使用工厂方法模式后:
- 精确查询(如查规定条文) → 关键词检索
- 语义查询(如查相似内容) → 向量检索
- 综合查询 → 混合检索
业务代码只需选择对应的工厂,不关心底层实现。
"""
class RAGRetrievalService:
"""RAG 检索服务 —— 使用工厂方法切换策略"""
def __init__(self, factory: SearchFactory):
self.factory = factory
def retrieve(self, query: str, top_k: int = 3) -> List[dict]:
return self.factory.execute_search(query, top_k)
# 根据查询类型选择策略
query_type = "hybrid" # 可以是 "vector" / "keyword" / "hybrid"
if query_type == "vector":
factory = VectorSearchFactory()
elif query_type == "keyword":
factory = KeywordSearchFactory()
else:
factory = HybridSearchFactory()
service = RAGRetrievalService(factory)
results = service.retrieve("保研加分政策", top_k=3)
print(f" 查询 '保研加分政策'({query_type}模式):")
for r in results:
print(f" [{r['method']}] score={r['score']:.2f} → {r['content']}")
# =============================================================================
# 4. 抽象工厂模式(Abstract Factory Pattern)
# =============================================================================
print("\n" + "─" * 60)
print("模式 4:抽象工厂模式(Abstract Factory Pattern)")
print("─" * 60)
"""
📖 概念讲解
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
与工厂方法的区别:
工厂方法:创建一个产品
抽象工厂:创建「一族」相关的产品
场景:
• 跨平台 UI:一个工厂创建整套 UI 组件(按钮+文本框+菜单)
• RAG Pipeline:一个工厂创建一整套组件(Embedding 模型 + LLM + Reranker)
• 数据库层:一个工厂创建整套 DAO(UserDAO + OrderDAO + ProductDAO)
核心思想:确保同一工厂创建的产品之间是「兼容」的。
比如 Mac 工厂不会创建出 Windows 风格的按钮。
"""
# ── 完整示例:跨平台 UI ──
print("\n 📌 完整示例:跨平台 UI 组件")
print(" " + "-" * 40)
class Button(ABC):
@abstractmethod
def render(self) -> str:
pass
class TextBox(ABC):
@abstractmethod
def render(self) -> str:
pass
class MacButton(Button):
def render(self):
return "🍎 Mac 风格按钮 [圆角、灰色]"
class MacTextBox(TextBox):
def render(self):
return "🍎 Mac 风格文本框 [半透明、阴影]"
class WinButton(Button):
def render(self):
return "🪟 Windows 风格按钮 [方角、蓝色]"
class WinTextBox(TextBox):
def render(self):
return "🪟 Windows 风格文本框 [白色、边框]"
class GUIFactory(ABC):
"""抽象工厂:定义创建产品族的方法"""
@abstractmethod
def create_button(self) -> Button:
pass
@abstractmethod
def create_textbox(self) -> TextBox:
pass
class MacFactory(GUIFactory):
def create_button(self) -> Button:
return MacButton()
def create_textbox(self) -> TextBox:
return MacTextBox()
class WinFactory(GUIFactory):
def create_button(self) -> Button:
return WinButton()
def create_textbox(self) -> TextBox:
return WinTextBox()
def build_ui(factory: GUIFactory):
"""客户端代码:不关心具体平台,只使用抽象接口"""
button = factory.create_button()
textbox = factory.create_textbox()
return f"UI 构建完成:\n {button.render()}\n {textbox.render()}"
print(f" {build_ui(MacFactory())}")
print(f" {build_ui(WinFactory())}")
# ── 🚀 RAG 实战:RAG Pipeline 组件族 ──
print("\n 🚀 RAG 实战:RAG Pipeline 整套组件抽象工厂")
print(" " + "-" * 40)
"""
实战场景:
你的 RAG 项目可能需要多种配置:
方案A(轻量级):本地 Embedding + Ollama LLM + 无 Reranker
方案B(高精度): OpenAI Embedding + GPT-4 + Cohere Reranker
使用抽象工厂,切换方案只需换一个工厂实例。
"""
class EmbeddingService(ABC):
@abstractmethod
def encode(self, text: str) -> List[float]:
pass
class RerankerService(ABC):
@abstractmethod
def rerank(self, query: str, documents: List[str]) -> List[float]:
pass
class LocalEmbedding(EmbeddingService):
def encode(self, text: str) -> List[float]:
return [0.1 * (ord(c) % 10) for c in text[:10]]
class OpenAIEmbedding(EmbeddingService):
def encode(self, text: str) -> List[float]:
return [0.2 * (ord(c) % 10) for c in text[:10]]
class NoReranker(RerankerService):
def rerank(self, query: str, documents: List[str]) -> List[float]:
return [1.0 / (i + 1) for i in range(len(documents))]
class CohereReranker(RerankerService):
def rerank(self, query: str, documents: List[str]) -> List[float]:
# 模拟高精度重排序
return [0.95 - i * 0.15 for i in range(len(documents))]
class RAGPipelineFactory(ABC):
"""RAG Pipeline 抽象工厂"""
@abstractmethod
def create_embedding(self) -> EmbeddingService:
pass
@abstractmethod
def create_llm(self) -> LLMClient:
pass
@abstractmethod
def create_reranker(self) -> RerankerService:
pass
class LitePipelineFactory(RAGPipelineFactory):
"""轻量级 Pipeline"""
def create_embedding(self) -> EmbeddingService:
return LocalEmbedding()
def create_llm(self) -> LLMClient:
return OllamaClient(model="qwen3:0.5b")
def create_reranker(self) -> RerankerService:
return NoReranker()
class ProPipelineFactory(RAGPipelineFactory):
"""高精度 Pipeline"""
def create_embedding(self) -> EmbeddingService:
return OpenAIEmbedding()
def create_llm(self) -> LLMClient:
return OpenAIClient(api_key="sk-xxx", model="gpt-4")
def create_reranker(self) -> RerankerService:
return CohereReranker()
def run_rag_pipeline(factory: RAGPipelineFactory, query: str,
docs: List[str]) -> dict:
"""运行完整的 RAG Pipeline"""
embedding = factory.create_embedding()
llm = factory.create_llm()
reranker = factory.create_reranker()
query_vec = embedding.encode(query)
scores = reranker.rerank(query, docs)
context = "\n".join(docs)
answer = llm.chat(f"问题:{query}\n上下文:{context}")
return {
"embedding_dim": len(query_vec),
"rerank_scores": scores,
"answer": answer,
}
lite_result = run_rag_pipeline(
LitePipelineFactory(),
"保研加分", ["文档1内容", "文档2内容", "文档3内容"]
)
print(f" 轻量级 Pipeline 结果: embedding_dim={lite_result['embedding_dim']}, "
f"scores={[f'{s:.2f}' for s in lite_result['rerank_scores']]}")
# =============================================================================
# 5. 建造者模式(Builder Pattern)
# =============================================================================
print("\n" + "─" * 60)
print("模式 5:建造者模式(Builder Pattern)")
print("─" * 60)