Python开发进阶:SOLID原则与设计模式实战指南
做Python开发这几年我面试过不少候选人也review过很多项目代码发现一个很有趣的现象很多人能把Python写得行云流水闭包、装饰器、列表推导式信手拈来但一谈到SOLID原则和设计模式就挠头。要么觉得这是Java那套东西跟Python没关系要么背了23个模式的UML图遇到实际问题还是不会用。这个现象背后藏着一个真实需求——Python进阶到一定阶段你一定绕不开代码结构这件事。脚本写多了业务逻辑越堆越厚if-else长得像城墙改一个功能要动三五个地方这时候你才知道设计模式从来不是考试题目而是前人用血泪总结出来的代码该怎么组织的经验手册。而这篇文章要聊的SOLID原则正是这23种设计模式的地基地基打不牢模式全白学。这篇文章我准备这样安排先讲清楚为什么Python开发者也需要设计模式然后把SOLID五个原则逐个用Python代码拆开讲透接着按创建型、结构型、行为型三大类挑最实用的模式做实战演示最后放一份我踩过的坑和排查经验。无论你是刚学完Python语法想进阶的初学者还是写了两三年业务代码想优化项目结构的开发者应该都能从里面拿到点能直接用的东西。1. 内容整体设计与思路拆解SOLID原则与设计模式到底在解决什么问题1.1 先破题设计模式不是让你背的是让你踩坑后认出来的很多人一看到23种设计模式就头大觉得这是学院派的东西。但换个角度想你写代码的时候有没有遇到过这种场景新来一个需求要给现有的类加功能结果你发现动一个方法旁边两个功能跟着出错或者代码里全是if-else每来一种新的数据类型就要在函数中间再补一个分支再或者你用着用着一个工具类发现它的构造函数参数变来变去根本没法复用。这些问题的本质都是代码结构出了问题。而23种设计模式其实是对怎么解决这些结构问题的答案做了归档分类。比如if-else太长你可以用策略模式对象创建逻辑太复杂你可以用工厂方法一个对象的状态变化牵动一堆依赖你可以用观察者。设计模式不是让你提前把所有代码都套上模式而是让你在遇到具体问题的时候能认出这个问题属于哪一类然后调用对应的解法。在学习思路上我强烈建议不要按23个模式逐个背的方式来学那样学完就忘。更好的路径是先掌握SOLID五个原则把好代码应该长什么样这个标准立住然后带着原则去做具体项目的重构在重构中发现哦这里其实可以用观察者那里其实就是个策略模式。模式是解决问题的产物不是为了用而用的装饰品。1.2 从能跑到好改SOLID原则点出的核心痛点SOLID是五个设计原则的首字母缩写分别是单一职责原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则。这五个原则解决的核心问题只有一个让代码在面对需求变化时修改成本最低。你自己想一下一个项目写完之后80%的精力其实是花在改代码上而不是写新代码上。改代码最怕什么最怕你改一个地方牵连了另外三个地方最怕你加一个新功能要把原来稳定的老代码动一遍。SOLID原则就是一条条规则帮你避免这几种牵一发动全身的情况。先解释几个可能会让新手困惑的概念。开闭原则说的是对扩展开放对修改关闭这句话听起来很高大上翻译成人话就是加新功能的时候尽可能不动老代码而是通过新增类、新增函数来实现。里氏替换原则说的是子类必须能替换父类且不破坏程序的正确性其实就是告诫你别写出那种父类是个鸟子类却是条鱼的继承结构。依赖倒置原则说的是高层模块不应该依赖低层模块两者都应该依赖抽象翻译一下就是你的业务代码不要直接new一个具体的数据库对象而是面向一个数据存取接口编程这样明天换数据库业务代码一行都不用改。这五个原则每个单独看都不难理解难的是组合使用。因为它们是互相配合的单一职责让类变小开闭原则让你扩展而不是修改里氏替换让你把继承关系设计得合理接口隔离让接口足够聚焦依赖倒置让你把依赖的方向搞正确。五条全部落地代码结构基本上就乱不到哪去了。1.3 别忘了Python的特性鸭子类型让很多模式隐形了学设计模式的时候你看到的例子大多数是Java或者C写的。到了Python里有两个语言特性会改变你对模式的感知。第一个是鸭子类型。Java里写策略模式你得先定义一个接口再写好几个实现类然后在运行时把具体的实现塞进去。Python里不需要强制定义接口只要对象有对应的方法它就能被调用所谓的接口很多时候是约定俗成的协议甚至用Protocol来定义就行。这让很多模式在Python里显得轻了很多。第二个是一等函数。Java里的策略模式、命令模式在Python里往往一个lambda函数就搞定了。你不需要为每一个策略建一个类直接传一个函数进来就完事。这会造成一种错觉觉得Python不需要设计模式。实际上恰恰相反正因为函数是一等公民你更需要理解模式背后的思想否则很容易把代码搅成一锅粥——所有逻辑都藏在回调函数里谁也看不清整体结构。所以我在讲后面这些模式的时候不会原样照搬Java的UML图而是用Python的思维方式去重写它们。让你看到什么是Pythonic的设计模式既保留模式的核心思想又利用语言特性去掉那些不必要的模板代码。2. SOLID原则拆解五个原则在Python中的落地实战2.1 单一职责原则一个类只干一件事改起来才不用提心吊胆单一职责原则的原文是A class should have one, and only one, reason to change一个类应该只有一个引起它变化的原因。判断标准非常简单你能不能用一句话说清楚这个类是干什么的如果一句话说不清楚说明它的职责已经超标了。给个最经典的坏味道例子。假设你有一个Report类既要负责从数据源取数、组装报表内容又要负责把报表保存成文件还要负责把报表内容格式化然后发送邮件。看起来功能齐全实际上每一次需求变动都会牵连它。比方说老板说报表文件格式改成CSV你要动Report产品说邮件标题改一下你还要动Report。这个类承担了太多职责测试也不好写因为你测生成内容还要顺便处理保存文件的副作用。改造思路就是拆。把取数和组装内容放一个类里叫ReportGenerator把保存文件抽出去叫FileSaver把发邮件抽出去叫EmailSender。然后ReportGenerator干完活把最终内容交给FileSaver去存或者交给EmailSender去发。这三个类各管一段每一个都只有一条变化理由改动范围被限制住了测试也好写。这里有个实操心得拆职责不是拆得越细越好。你拆出来的每个类仍然要能一句话说清楚自己是干嘛的。如果拆到后来出现了一个就是放各种方法的工具箱那说明你拆过头了该收敛一下。一般来说我拆类的目标不是类越小越好而是改动一个需求最多只动一个类。能做到这点单一职责就落实到位了。# 反面示例一个类干三件事 class Report: def __init__(self, data_source): self.data_source data_source def generate(self): data self.data_source.fetch() content self._format(data) self._save_to_file(content) self._send_email(content) return content # 正面示例拆成三个各司其职的类 class ReportGenerator: def __init__(self, data_source, formatter): self.data_source data_source self.formatter formatter def generate(self): data self.data_source.fetch() return self.formatter.format(data) class FileSaver: def save(self, content, file_path): with open(file_path, w, encodingutf-8) as f: f.write(content) class EmailSender: def send(self, content, recipient): # 只负责把内容通过邮件发出去 pass2.2 开闭原则加新功能时不改老代码才是对稳定最大的尊重开闭原则可能是SOLID里最出名、也最容易误读的一条。它说的是Software entities should be open for extension, but closed for modification.对扩展开放对修改关闭。很多新手一听就懵不让修改代码那新功能怎么做总不能凭空变出来吧。关键要理解这里的修改指的是修改已经稳定、已经测试过的代码。开闭原则的真实诉求是新需求来的时候你尽量通过新增模块的方式来实现而不是去改那些已经跑得好好的老模块。举一个我实际重构过的例子。之前有个订单系统订单金额要根据用户等级计算折扣等级有普通、银卡、金卡三种。第一版代码很直接在函数里写if-elsedef calc_discount(order, user_level): if user_level normal: return order.amount * 0.9 elif user_level silver: return order.amount * 0.8 elif user_level gold: return order.amount * 0.7 else: raise ValueError(funknown level: {user_level})这段代码跑得很欢直到产品说我们新增一个钻石会员打六折。你只能打开这个函数再加一个elif分支。这还不是最糟的最糟的是这个函数在项目里有七八处调用每处都要跟着改或者有的地方已经写死了三种等级的映射表你要把所有地方都找出来改一遍。用开闭原则改造思路是把等级判断抽象成一个策略接口每种等级就是策略的一个实现。加新等级的时候你只需要新写一个类然后把它注册进去原来的代码一行不动from typing import Protocol class DiscountStrategy(Protocol): def calc(self, amount: float) - float: ... class NormalDiscount: def calc(self, amount: float) - float: return amount * 0.9 class SilverDiscount: def calc(self, amount: float) - float: return amount * 0.8 class GoldDiscount: def calc(self, amount: float) - float: return amount * 0.7 class DiamondDiscount: # 新增等级老代码零修改 def calc(self, amount: float) - float: return amount * 0.6 # 运行时通过注册表选择策略而不是在函数里堆if-else DISCOUNT_STRATEGIES { normal: NormalDiscount(), silver: SilverDiscount(), gold: GoldDiscount(), diamond: DiamondDiscount(), } def calc_discount(order, user_level): strategy DISCOUNT_STRATEGIES.get(user_level) if strategy is None: raise ValueError(funknown level: {user_level}) return strategy.calc(order.amount)这里有个细节要注意开闭原则不是说永远不能改老代码。实际开发中如果你发现老代码的结构已经僵化到扩展不如重构的程度那就应该重构。开闭原则追求的是延长老代码的寿命但它也默认代码最终会有重构的一天。关键在于重构是有计划的行为而每次需求都去改动核心代码会让代码腐化得特别快。2.3 里氏替换原则子类不是父类的马甲替换后要能正常工作里氏替换原则是SOLID里最容易在面试中聊起来、在实际代码里又最容易踩坑的一条。它的核心思想是如果S是T的子类那么T类型的对象应该可以无条件地被S类型的对象替换而且程序行为不会出现异常。说人话就是父类能用的地方你换成子类程序依然得正常跑。最经典的坏例子是正方形继承矩形。数学上正方形是矩形的特例所以很多人一开始都写Square继承Rectangle。Rectangle有set_width和set_height两个方法正方形要求宽高相等于是Square重写了这两个方法强制让宽高一致。看着没问题结果业务代码里有一段逻辑是这样写的def resize(rect): rect.set_width(10) rect.set_height(5) assert rect.get_area() 50这段逻辑传Rectangle进去断言能过传Square进去宽高被强制同步面积不是50而是25直接爆异常。Square作为Rectangle的子类却破坏了矩形宽高独立设置的行为约定。这就是典型的违反里氏替换原则。在Python里解决这类问题有一个非常实用的思路与其用继承不如考虑组合。让Square和Rectangle都不继承谁而是各自持有边长这个数据然后用一个共同的协议把计算面积这个行为定义出来。Python的Protocol正好干这个事from typing import Protocol class Shape(Protocol): def area(self) - float: ... class Rectangle: def __init__(self, width: float, height: float): self.width width self.height height def area(self) - float: return self.width * self.height class Square: def __init__(self, side: float): self.side side def area(self) - float: return self.side ** 2 # 只要实现area方法的对象都能被安全使用不存在替换陷阱 def print_area(shape: Shape): print(shape.area())里氏替换原则在实操中给我最大的启发是继承关系要想清楚is-a到底成不成立。如果你说Square is a Rectangle但在行为语义上并不满足矩形的约束那这个is-a就是假的。更稳的做法是优先用接口协议或者组合去表达能力用鸭子类型去放开调用方的限制只有当子类确实能无缝替换父类时才放心使用继承。2.4 接口隔离原则与依赖倒置原则把依赖具体改成依赖边界的两板斧接口隔离原则和依赖倒置原则往往一起出现因为它们都在处理依赖这件事。接口隔离原则说的是客户端不应该被迫依赖它用不到的接口。翻译成通俗话就是接口要小要聚焦别搞一个大而全的对象让每个调用方都背着一堆没用的方法。依赖倒置原则说的是高层模块不应该依赖低层模块两边都应该依赖抽象。还是那句话别让业务代码去new具体实现要面向协议编程。这两个原则合在一起处理的典型场景就是你项目里的数据库访问、消息队列、第三方API这些东西。比方说你的业务里面直接写了一个MySQLClient到处都是MySQLClient()和execute_query()哪天你说要换成PostgreSQL好全项目搜索替换中间还可能漏掉几个隐藏调用点整个重构变成噩梦。正确的做法是业务代码依赖一个Repository协议协议上只暴露业务真正需要的接口比如get_user_by_id、save_order。然后MySQLRepository实现这个协议。业务层完全不知道底层是MySQL还是PostgreSQL也不知道是数据库还是缓存它只认识Repository这个边界。这就是依赖倒置。接口隔离在这里的体现是Repository里只放业务要用的方法不要放一个execute_arbitrary_sql这种暴力万能接口因为执行任意SQL是底层能力不是业务接口。from typing import Protocol class UserRepository(Protocol): def get_user_by_id(self, user_id: int): ... def save_user(self, user): ... class MySQLUserRepository: def get_user_by_id(self, user_id: int): # 真实的MySQL查询逻辑 pass def save_user(self, user): # 真实的保存逻辑 pass class PostgresUserRepository: def get_user_by_id(self, user_id: int): pass def save_user(self, user): pass # 业务层不再依赖具体数据库实现 def handle_login(repo: UserRepository, user_id: int): user repo.get_user_by_id(user_id) # 业务逻辑 return user我踩过的坑是新建项目时激情满满引入依赖倒置给所有东西都写一个接口。结果项目里有几百个文件接口文件占了快两百个全是只有一个实现类的僵尸接口。接口隔离和依赖倒置的价值在于隔离变化点如果一个东西根本不会变接口纯属负担。判断标准很简单这个依赖有没有第二个实现的可能性有才值得抽象没有直接依赖具体类不要为了模式而模式。3. 23种设计模式实战从创建型到行为型Python可复刻的核心写法3.1 创建型模式工厂方法、抽象工厂、单例的精简实现创建型模式的核心话题是对象怎么创建。直接new当然是创建但如果创建过程很复杂或者需要根据条件动态决定创建哪个类new就会散落在业务代码各处制造耦合。工厂方法模式适合场景你有一个基类不同子类对应不同情况创建时根据参数决定返回哪个子类。用我项目里的DataLoader举例数据可能来自JSON文件也可能来自CSV文件。最朴素的写法是业务层自己判断文件后缀再决定用什么类但这样根据后缀创建对象的逻辑就散落在调用方了。工厂方法就是把这段逻辑收拢到一个工厂类里class DataLoader: def load(self, path: str): raise NotImplementedError class JSONLoader(DataLoader): def load(self, path: str): import json with open(path, r, encodingutf-8) as f: return json.load(f) class CSVLoader(DataLoader): def load(self, path: str): import csv with open(path, r, encodingutf-8) as f: return list(csv.DictReader(f)) class DataLoaderFactory: staticmethod def create(path: str) - DataLoader: if path.endswith(.json): return JSONLoader() elif path.endswith(.csv): return CSVLoader() else: raise ValueError(funsupported file type: {path}) # 业务层只依赖DataLoaderFactory loader DataLoaderFactory.create(data.csv) data loader.load(data.csv)抽象工厂模式再往上走一层解决的是一系列相关对象的创建问题。比如你的系统要支持本地存储和云端存储两套环境每一套环境都有一组对象文件存储、队列、缓存。抽象工厂就是为每一套环境提供一个工厂类让业务层面向环境工厂编程切换环境时替换工厂即可。Python里抽象工厂的实现不算复杂但坦白讲在中小型项目里抽象工厂的使用频率不算高它的重量级决定了它更适合插件体系和组件化框架。普通业务你大概率用工厂方法就够了。单例模式在Python里有几种实现方式。最简单的反而是模块级变量——Python模块天然就是单例的import到哪都是同一个对象。其次是用元类或者装饰器控制__init__只执行一次。但我要特别提醒的是全局单例要慎用。在测试里单例的状态会跨用例保留容易产生测试污染在多线程环境里懒汉式单例不做线程控制会出问题。我的习惯是能用模块级变量解决的用模块级变量真的需要一个进程内有状态的对象用依赖注入把单例传进去而不是让业务代码到处get_instance()。class DatabaseConnection: 模块级单例整个进程内只有一个实例 _instance None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance super().__new__(cls) return cls._instance def __init__(self, host: str, port: int): # 注意__init__会在每次构造时执行需要自行加锁保护 self.host host self.port port3.2 结构型模式装饰器、适配器、代理帮你把类之间的关系理顺结构型模式关注的是类的组合方式。这里我挑三个在Python里最实用的装饰器、适配器、代理。装饰器模式在Python里有一个特殊的地位因为语言原生支持decorator语法但很多人会忽略这背后的本质。装饰器模式的核心是在不修改原对象的基础上动态地给对象增加功能。Python里的函数装饰器和类装饰器干的就是这件事。之前我在项目里做一个重试功能不可能去改每一个需要重试的函数的内部代码写一个retry(times3)装饰器包上就行。这个装饰器就是装饰器模式在语言层面的体现。import functools import time def retry(times: int 3, delay: float 1.0): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(times): try: return func(*args, **kwargs) except Exception: if attempt times - 1: raise time.sleep(delay) return wrapper return decorator retry(times3, delay0.5) def call_external_api(): # 模拟不稳定接口 pass适配器模式解决的是接口不匹配的问题。你封装了一个第三方SDK它提供的API签名和你业务层预期的接口对不上你又不方便改SDK源码那就写一个Adapter把SDK的接口翻译成你业务层熟悉的接口。这个模式在对接支付渠道、短信平台时特别常见。每家支付的接口长得都不一样你的业务层不该为每一家适配一次而是统一为一个PaymentGateway协议每个渠道写一个适配器实现这个协议。class PaymentGateway(Protocol): def pay(self, order_id: str, amount: float): ... class AlipayAdapter: 把支付宝SDK的接口适配成PaymentGateway协议 def __init__(self, sdk): self._sdk sdk def pay(self, order_id: str, amount: float): self._sdk.alipay_do_pay(order_id, amount) class WechatPayAdapter: 把微信支付SDK的接口适配成PaymentGateway协议 def __init__(self, sdk): self._sdk sdk def pay(self, order_id: str, amount: float): self._sdk.wechat_submit(order_noorder_id, totalamount)代理模式最常见的一种用途是懒加载。当对象创建的成本很高而你可能根本用不到它时先创建一个代理壳真正调用的瞬间才去加载真实对象。在Python里可以用__getattr__来实现一个很优雅的懒加载代理class LazyProxy: def __init__(self, factory): self._factory factory self._object None def __getattr__(self, name): if self._object is None: self._object self._factory() return getattr(self._object, name) # 使用传入一个创建成本高的对象的工厂函数 proxy LazyProxy(lambda: HeavyInitializer()) proxy.do_something() # 直到真正调用时才执行初始化装饰器、适配器、代理这三个模式有个共同点都追求不改原有对象的前提下做扩展。区别在于目的不同装饰器加功能适配器改接口代理控访问。实际开发中它们经常混着用比如一个缓存代理配上重试装饰器外面再套一层日志适配器这在大型系统里很常见。3.3 行为型模式用策略、观察者、模板方法告别乱糟糟的if-else行为型模式是所有类型里数量最多、也最贴近业务逻辑的一类。它们管的是对象之间的行为和协作方式。我挑几个在业务代码里最高频的讲讲。策略模式前面在开闭原则里已经用折扣计算展示过核心思想了这里补充一个观点在Python里策略模式最简单的方式不一定是写类而是直接传函数。比如你需要给数据排序排序规则可能有多种直接用keylambda就是策略模式的范式——排序算法本身是一套骨架key函数决定具体策略。真正需要写策略类的时候往往是策略本身还有状态、还有组合方法光靠一个函数表达不了才需要升级成类。观察者模式是很多Python后端开发者容易忽略但其实一直在用的模式。它的核心是事件发布-订阅。业务场景比如用户下单成功后要发短信、发邮件、更新库存、推送通知。这些动作如果全部串行写在下单逻辑里每加一个动作就得改下单代码。用观察者模式下单模块只负责发布下单成功事件其他模块各自订阅事件、各自执行class EventBus: def __init__(self): self._subscribers {} def subscribe(self, event: str, callback): self._subscribers.setdefault(event, []).append(callback) def publish(self, event: str, *args, **kwargs): for callback in self._subscribers.get(event, []): callback(*args, **kwargs) # 业务侧下单成功后发布事件不用关心谁在听 def create_order(order): # 保存订单逻辑... event_bus.publish(order.created, order) # 其他模块订阅后自己决定要不要响应 event_bus.subscribe(order.created, send_sms) event_bus.subscribe(order.created, send_email) event_bus.subscribe(order.created, update_stock)模板方法模式则解决流程骨架固定部分步骤灵活可变的问题。比如做数据清洗流程永远是读数据→清洗→校验→入库。但不同数据源的清洗规则不一样。模板方法模式就是在基类里把流程串好把可变步骤声明为钩子由子类去实现class DataPipeline: def run(self, source): data self.extract(source) cleaned self.clean(data) self.validate(cleaned) self.load(cleaned) def extract(self, source): raise NotImplementedError def clean(self, data): return data # 默认不做清洗子类可覆盖 def validate(self, data): if not data: raise ValueError(data is empty) def load(self, data): raise NotImplementedError class UserDataPipeline(DataPipeline): def extract(self, source): # 从用户表抽数据 pass def clean(self, data): # 去掉空字段、去重 pass def load(self, data): # 写入数据仓库 pass模板方法模式对做数据处理、ETL、测试框架的人特别实用。它把公共骨架和可变逻辑分开保证每个流程都按同样顺序跑不会这边漏了校验那边忘了入库。我见过不少项目里的ETL流程是每个表写一套独立脚本看起来复用性为零本质就是没找到1个模板方法多个子类实现这个最佳解。3.4 23种设计模式速查表先建立全局认知再按需深入前面挑了部分模式做了实战演示这里把GoF《设计模式》里的23种模式整理成一张速查表。你可以先浏览一遍做项目时遇到问题再回来看这张表快速定位到底该用哪个。分类模式名称一句话场景创建型工厂方法创建哪种对象由参数决定消除业务代码里的new创建型抽象工厂创建一整套相关对象适合多套环境切换创建型建造者对象构造步骤多且复杂把构造过程从使用中抽离创建型原型通过克隆已有对象创建新对象避免重复初始化成本创建型单例一个进程内只保留一个实例结构型适配器两个类接口不匹配写一层翻译层结构型桥接抽象与实现分离让两者各自独立变化结构型组合把部分和整体的关系统一成树形结构结构型装饰器不修改原对象动态追加新功能结构型外观给复杂的子系统提供一个简单的统一入口结构型享元共享细粒度对象减少内存开销结构型代理控制对象的访问如缓存、延迟加载、权限控制行为型责任链多个处理者依次尝试处理请求适合审批流、日志分级行为型命令把请求封装成对象支持撤销、排队、日志回放行为型解释器定义语言语法并解释执行适合规则引擎行为型迭代器提供统一方式遍历集合Python的for循环天然是它行为型中介者对象间不直接交互通过中介转发降低耦合行为型备忘录保存对象状态以便后续恢复适合撤销功能行为型观察者一个对象状态变化通知多个订阅者行为型状态对象根据内部状态切换行为适合订单状态机行为型策略算法可切换把if-else替换成策略对象行为型模板方法固定流程骨架步骤由子类实现行为型访问者在不改对象结构的前提下给对象增加新操作4. 常见问题与避坑实录模式用错的代价比不用还大4.1 过度设计的边界什么时候不该用设计模式设计模式最大的坑不是用错了而是滥用、早用。我见过一个同学写一个只跑一次的小脚本硬套了抽象工厂策略观察者三个模式代码文件从50行膨胀到300行读起来无比痛苦。对他来说每个模式都是标准答案但他忽略了没有问题的时候答案就没意义。我个人的判断标准有两个。第一这段代码未来大概率会不会变化如果只是教学练习、内部工具、一次性脚本直接写简单逻辑别引入模式。第二这个变化是类型新增还是逻辑修改如果是新增一种类型、新增一个策略设计模式价值很大如果是修改已有规则那引入模式也救不了你还不如先把逻辑写清楚。记住一个铁律先写能跑的简单代码等到你确实看到了重复、看到了if-else阶梯、看到了改一处要动三处的痛点再引入模式解决。设计模式是重构的产物不是预支的设计。4.2 单例的线程安全陷阱Python里要命的懒汉单例单例模式看起来简单但Python里有几个隐蔽的坑。第一个坑是__init__和__new__的区别。很多人只重写__new__保证实例唯一却忘了__init__在每次调用ClassName()时都会再次执行导致单例状态被反复重置。上面代码块里我写了需要注意加锁就是因为这个。第二个坑是懒汉式单例在多线程环境下会创建多个实例。两个线程同时判断_instance is None都成立然后各自创建实例单例失效。解法有几种模块级变量在import时初始化天然规避这个问题或者用threading.Lock包住创建逻辑。我的建议是除非你确认自己需要第一次使用才初始化的懒加载语义否则直接用模块级变量就行简单可靠。第三个坑是单例和测试的冲突。单例一旦持有状态测试用例之间就会互相污染。你在A用例里往单例里写了一条数据B用例一跑发现数据还在。我的规避手法是生产代码里用单例没问题但测试代码里要提供重置单例状态的手段或者干脆让业务代码通过参数注入依赖测试时注入一个干净的临时对象。4.3 Python让设计模式隐形了这些模式你早就在用学设计模式的时候很多Python老手会有一种这些东西我好像每天都在用的感觉。这种感觉是对的。迭代器模式Python的for循环天然就靠__iter__和__next__实现你写for x in collection就是在用迭代器模式。外观模式你的工具函数库就是一堆子系统前面的外观。命令模式函数作为一等公民传递本质上比命令模式还轻量。模板方法模式标准库的unittest框架里setUp、tearDown就是钩子方法的经典体现。这恰恰是我推荐用先掌握原则再认领模式的方式来学习的原因。Python的鸭子类型和一等函数已经把模式实现简化到了极致有时候你甚至不需要创建类就能套用模式思想。当你看到有人用一段不长的Python代码实现了某个观察者某个策略不要惊讶很多模式在Python里就是一个字典映射加一个回调函数的事。4.4 模式落地时的命名与Type Hints别让好架构烂在注释里很多模式实现得不错但可读性很差原因在于命名和类型标注没跟上。你写了个策略模式类名叫Handler1、Handler2别人看到根本不知道这两个策略到底有什么区别。你写了个工厂方法叫create参数是*args调用方完全不知道传什么。我建议在实现模式的同时做好两件事。第一用有业务语义的类名比如OrderDiscountHandler、UserDataPipeline让类名自己解释属于哪一类模式在解决什么问题。第二善用Protocol和类型标注把这个函数接受有load方法的对象这种隐式约定显式成DataLoader协议IDE自动补全和静态检查都能跟得上。另外不要把一个模式类里的核心方法叫execute、handle这种万金油名字。策略接口里叫calc命令接口里叫execute观察者回调叫on_event这些名字是有讲究的——命名本身就传达了模式意图代码自己会说话。4.5 从背模式到用模式的转变我自己的实操建议最后这段是我很早就想总结的实践路线。如果你是个初学者不要去背23种模式的UML图而是每个模式找一个自己熟悉的业务场景手写一遍精简实现。昨天你刚写过订单折扣的if-else今天就可以把它重构成策略模式上个月你还在处理各种数据源的导入脚本这个月就可以试试模板方法。模式必须跟真实场景绑定才能内化为你的思维方式。等你写了两三年代码再回头看设计模式自然会明白一件事设计模式不是让你用的剑谱而是你写完代码以后认得出来的解剖图谱。真正的高手不会天天念叨自己用了什么模式而是已经把这些原则融入了本能。看到一堆if-else会直觉地觉得这块会膨胀看到一个胖类会下意识地思考能不能拆。这种直觉比记住23个模式的名字重要得多。我自己的体会是SOLID原则的价值被大多数人严重低估了。它不像某个具体模式那样立竿见影但你所有的代码质量提升追根溯源都能回到这五个原则上。你不需要在代码里加一行注释这里使用了开闭原则但你要保证下次新需求到来时你的代码能做到新增文件而不是修改旧文件。这才是SOLID和23种设计模式留给你的最大遗产。

相关新闻

Claude API调用链路中的内存行为深度解析

Claude API调用链路中的内存行为深度解析

1. 项目概述:一个被误读的命名陷阱,以及它背后的真实技术逻辑“claude-mem”——这个词最近在多个技术社区和开发者群组里高频出现,但几乎没人能说清它到底指什么。有人把它当成Claude官方新推出的内存优化插件,有人猜测是某种本地…

2026/10/10 18:34:21 阅读更多 →
Ender args-parser 命令行解析器完全指南:从选项表定义到 toContextString 逆向还原命令

Ender args-parser 命令行解析器完全指南:从选项表定义到 toContextString 逆向还原命令

开发工具 【免费下载链接】Ender the no-library library: open module JavaScript framework 项目地址: https://gitcode.com/gh_mirrors/en/Ender 点击查看 免费下载 Ender 是浏览器端 JavaScript 包管理工具,其核心命令行解析器 args-parser.js 负责…

2026/10/10 18:34:21 阅读更多 →
RCWA计算一维光栅衍射效率:从物理模型到Python实现与避坑指南

RCWA计算一维光栅衍射效率:从物理模型到Python实现与避坑指南

简介:严格耦合波分析(RCWA)是分析周期性结构光学性质的常用方法,这份资源围绕一维光栅衍射效率计算展开,包含一个m格式脚本文件和一个caj格式研究文献。脚本可设置光栅周期、填充因子、材料折射率、入射波长、偏振与入…

2026/10/10 18:33:20 阅读更多 →

最新新闻

ADT下ABAP Unit高效测试:Quick Actions与高亮工作流

ADT下ABAP Unit高效测试:Quick Actions与高亮工作流

做了这么多年 ABAP 开发,我前前后后在 SE80、SE24、Eclipse ADT 之间横跳了不少次。真正让我下决心彻底切到 ADT 的,倒不是编辑器多好看,而是 ABAP Unit 测试这件事——以前在 SE24 里写测试、跑测试、看覆盖率,每一步都像是把车停…

2026/10/10 23:13:49 阅读更多 →
Elasticsearch调优实战:JVM堆内存、分片规划与Windows避坑指南

Elasticsearch调优实战:JVM堆内存、分片规划与Windows避坑指南

说实话,Elasticsearch 这个调优话题,网上教程已经多到泛滥了。但你随便翻十篇帖子,八篇都在让你改indices.query.bool.max_clause_count、调refresh_interval,改完就扔给你一个“性能提升十倍”的结论。我见过太多人按这种教程改完…

2026/10/10 23:12:48 阅读更多 →
从“一个人整理分支“到“给 Agent 立法“:Worktrunk 爆火暴露了工作流工具的重心迁移

从“一个人整理分支“到“给 Agent 立法“:Worktrunk 爆火暴露了工作流工具的重心迁移

从"一个人整理分支"到"给 Agent 立法":Worktrunk 爆火暴露了工作流工具的重心迁移 【免费下载链接】worktrunk Worktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows 项目地址: https://gitcode.com/G…

2026/10/10 23:12:48 阅读更多 →
蓝桥杯省赛真题怎么刷?第十六届真题解析与备赛策略

蓝桥杯省赛真题怎么刷?第十六届真题解析与备赛策略

每一届蓝桥杯省赛结束之后,都有大量同学来找我聊同一个问题:“真题到底该怎么刷?我现在开始备赛,是先做题库还是先做真题?”我的答案一直没变过:真题永远是效率最高的切入点。第十六届省赛题目我也在持续整…

2026/10/10 23:12:48 阅读更多 →
没有显卡也能跑:GLiNER2.5-Decide CPU 部署全流程,含批量请求与长文档处理

没有显卡也能跑:GLiNER2.5-Decide CPU 部署全流程,含批量请求与长文档处理

没有显卡也能跑:GLiNER2.5-Decide CPU 部署全流程,含批量请求与长文档处理 【免费下载链接】GLiNER2.5-Decide 项目地址: https://ai.gitcode.com/hf_mirrors/fastino/GLiNER2.5-Decide 当一个 340M 参数、基于 DeBERTa-v3-large 的英文分类模型…

2026/10/10 23:12:48 阅读更多 →
验证码识别全链路实战:从数据清洗到CRNN部署

验证码识别全链路实战:从数据清洗到CRNN部署

简介:本资源是一套面向深度学习初学者与图像识别实践者的字符型数字验证码识别完整实现方案,聚焦网络安全中验证码攻防场景下的模型训练与部署实战。资源包含1210个文件,主体为978张PNG与202张JPG格式的验证码样本图像,辅以17个核…

2026/10/10 23:11:47 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 11:14:25 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 11:14:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 10:38:42 阅读更多 →