C++多态性实战:从虚函数到设计模式,提升代码可维护性 1. 项目概述为什么多态性是C项目的“灵魂”干了这么多年C从桌面应用到游戏引擎再到后台服务我越来越觉得多态性Polymorphism这东西远不止是教科书里“一个接口多种实现”那几句干巴巴的定义。它更像是项目架构里的“润滑剂”和“连接器”用好了代码的扩展性、可维护性会提升一个量级用不好或者干脆不用后期维护起来那真是“牵一发而动全身”改一个小功能可能得把半个系统翻个底朝天。很多新手甚至一些工作了几年的朋友对多态的理解还停留在“虚函数、继承、指针”这个语法层面面试时能背出“静态多态重载、模板和动态多态虚函数”的区别但一回到实际项目面对具体的业务逻辑就不知道该怎么把这套理论用起来了。其实多态性的核心价值在于管理变化和解耦依赖。软件需求是不断变化的今天要求渲染圆形明天可能就要加个三角形后天说不定要支持自定义形状。如果你的代码里到处都是if (shapeType “circle”) { drawCircle(); } else if (shapeType “triangle”) { drawTriangle(); }那每加一个新形状你就得在所有用到形状判断的地方加上新的else if这违反了开闭原则对扩展开放对修改关闭。多态性通过将“做什么”接口和“怎么做”具体实现分离让你只需要关心“我需要一个能绘制的形状”至于它具体是圆是方由运行时传入的对象决定。这样新增一种形状时你只需要增加一个新的派生类而无需修改任何已有的、处理形状的客户端代码。这种能力在实际项目中就是应对需求变更、提升代码复用性的利器。2. 核心需求解析从“能用”到“好维护”的跨越我们写代码尤其是工程代码首要目标往往不是追求极致的性能当然C项目很多时候也需要而是可控的复杂度和可持续的维护性。一个项目初期可能只有几个人开发功能简单所有逻辑堆在几个文件里也能跑。但随着功能迭代、团队扩大这种“面条式”代码很快就会变成灾难。多态性正是为了解决这类工程问题而生的设计思想在语言层面的体现。它的核心需求可以归结为以下几点降低模块间的耦合度模块A不应该直接依赖于模块B的具体实现而应该依赖于一个稳定的抽象接口。这样模块B的内部实现再怎么翻天覆地地改只要接口不变模块A就完全感知不到也不需要做任何改动。比如一个日志模块今天输出到文件明天要改成输出到网络或数据库如果调用方直接FileLogger::Write()那就得全改。如果调用方依赖的是ILogger::Write()这个接口那么只需要更换接口背后的具体实现对象即可。实现“开闭原则”这是面向对象设计的一个核心原则。软件实体应该对扩展开放对修改关闭。多态性让我们可以通过增加新的派生类来扩展新的行为而无需修改那些依赖于抽象接口的既有代码。这极大地减少了引入新功能时带来的风险和工作量。统一处理机制当我们需要处理一组具有共同特征但又各有不同的对象时多态性允许我们使用一个基类指针或引用的容器来统一管理它们并用一套逻辑来遍历和处理代码简洁而富有表现力。想象一下一个游戏场景里有敌人、道具、障碍物它们都需要每帧更新Update和渲染Render。如果没有多态你可能需要维护三个不同的列表写三个不同的循环。有了多态你可以让它们都继承自一个GameObject基类然后一个std::vectorGameObject*列表加一个循环就搞定了。实现策略模式和插件架构很多行为模式如策略模式、状态模式、责任链模式其实现基石就是多态。它允许在运行时动态替换算法或策略。比如一个数据压缩模块可以根据用户选择或文件类型动态地在ZipCompressionStrategy、GzipCompressionStrategy等具体策略间切换而使用压缩功能的代码完全不用关心当前用的是哪一种。3. 典型应用场景深度剖析光讲理论有点虚我们直接看几个我亲身经历过的、最能体现多态价值的场景。3.1 场景一图形渲染引擎中的绘制系统这可能是教科书里最经典的例子但也是最直观、最有效的。假设我们在开发一个简单的2D图形编辑器。没有多态的实现灾难的开始void RenderScene(const std::vectorShape shapes) { for (const auto shape : shapes) { if (shape.type “Circle”) { DrawCircle(shape.center, shape.radius); } else if (shape.type “Rectangle”) { DrawRectangle(shape.topLeft, shape.bottomRight); } else if (shape.type “Triangle”) { DrawTriangle(shape.point1, shape.point2, shape.point3); } // 每增加一种新形状这里就要加一个else if } }这种代码的坏处显而易见RenderScene函数知道所有具体形状的绘制细节它与所有具体形状类紧密耦合。增加一个“五角星”你需要1. 修改Shape枚举2. 在RenderScene里添加新的else if分支3. 可能还要修改其他地方类似的switch-case或if-else链。这违反了开闭原则且极易出错。基于多态的重构优雅的解决方案// 抽象基类定义稳定的接口 class Shape { public: virtual ~Shape() default; // 基类析构函数必须是虚的 virtual void Draw() const 0; // 纯虚函数接口契约 virtual double Area() const 0; // ... 其他公共接口如移动、旋转等 }; // 具体实现类 class Circle : public Shape { public: Circle(Point center, double radius) : center_(center), radius_(radius) {} void Draw() const override { // 调用底层图形API绘制圆的具体实现 std::cout “Drawing Circle at (” center_.x “, ” center_.y “) with radius ” radius_ std::endl; } double Area() const override { return 3.14159 * radius_ * radius_; } private: Point center_; double radius_; }; class Rectangle : public Shape { // 类似实现省略... }; // 客户端代码变得极其简洁和稳定 void RenderScene(const std::vectorstd::unique_ptrShape shapes) { for (const auto shape : shapes) { shape-Draw(); // 多态调用这里完全不知道shape具体是圆还是方 } } // 使用智能指针管理生命周期避免内存泄漏 int main() { std::vectorstd::unique_ptrShape scene; scene.push_back(std::make_uniqueCircle(Point{0, 0}, 5)); scene.push_back(std::make_uniqueRectangle(Point{1, 1}, Point{4, 5})); // 未来新增一个Star类只需要 // class Star : public Shape { ... }; // scene.push_back(std::make_uniqueStar(...)); // 下面的RenderScene函数一行都不用改 RenderScene(scene); return 0; }注意这里使用了std::unique_ptr来管理堆上分配的对象。这是现代C的推荐做法可以自动管理内存避免手动new/delete带来的内存泄漏风险。基类Shape的析构函数声明为virtual是关键否则通过基类指针删除派生类对象会导致未定义行为通常只调用了基类的析构函数派生类部分资源泄漏。实操心得在这个场景里多态性将“绘制”这个动作抽象成了一个接口。渲染循环RenderScene从此只依赖于“可绘制”这个抽象概念与具体绘制细节彻底解耦。新增图形类型变成了一个纯粹的“增量”工作编写新类实现接口然后像搭积木一样放入系统即可。这极大地提升了代码的模块化和可测试性。你可以单独测试Circle::Draw()也可以用一个MockShape来测试RenderScene的逻辑是否正确调用了Draw。3.2 场景二游戏开发中的状态机与行为模式游戏里充满了状态和行为变化。比如一个敌人AI可能有“巡逻”、“追击”、“攻击”、“逃跑”等状态。用if-else或switch硬编码这些状态转换会非常混乱。多态实现状态模式// 状态接口 class EnemyState { public: virtual ~EnemyState() default; virtual void Enter(Enemy* enemy) 0; virtual void Execute(Enemy* enemy, float deltaTime) 0; virtual void Exit(Enemy* enemy) 0; }; // 具体状态 class PatrolState : public EnemyState { void Enter(Enemy* enemy) override { std::cout enemy-GetId() “: 开始巡逻...” std::endl; // 初始化巡逻路径点等 } void Execute(Enemy* enemy, float deltaTime) override { // 巡逻逻辑 if (enemy-DetectPlayer()) { enemy-ChangeState(std::make_uniqueChaseState()); // 切换到追击状态 } } void Exit(Enemy* enemy) override { // 清理巡逻相关资源 } }; class ChaseState : public EnemyState { // 类似实现... }; class AttackState : public EnemyState { // 类似实现... }; // 敌人实体类持有一个状态指针 class Enemy { public: Enemy(std::unique_ptrEnemyState initialState) : currentState_(std::move(initialState)) { if (currentState_) currentState_-Enter(this); } void Update(float deltaTime) { if (currentState_) currentState_-Execute(this, deltaTime); } void ChangeState(std::unique_ptrEnemyState newState) { if (currentState_) currentState_-Exit(this); currentState_ std::move(newState); if (currentState_) currentState_-Enter(this); } private: std::unique_ptrEnemyState currentState_; // ... 其他属性和方法 };在这个设计中Enemy类完全不知道具体状态是如何工作的。它只是简单地调用当前状态的Execute方法。状态之间的转换逻辑被封装在各个具体状态类的Execute方法中比如PatrolState发现玩家后触发转换。这样增加一个新的状态比如“装死”PlayDeadState只需要新增一个类并在适当的状态如AttackState受到重创时触发转换即可Enemy类的核心逻辑完全不变。踩过的坑早期我试过用枚举大的switch来实现状态机当状态超过10个状态转换条件复杂交错时那个Update函数会膨胀到几百行阅读和维护简直是噩梦。改用多态的状态模式后每个状态的行为被隔离在自己的类中清晰多了。但要注意状态对象的创建和销毁开销对于更新非常频繁的对象比如每帧更新的成千上万个粒子可能需要配合对象池来优化避免频繁的堆内存分配。3.3 场景三数据序列化与持久化框架项目经常需要把对象保存到文件或通过网络传输。不同的格式JSON, XML, 二进制需要不同的序列化方式。基于多态的序列化器class Serializable { public: virtual ~Serializable() default; virtual void Serialize(Serializer serializer) const 0; virtual void Deserialize(Serializer serializer) 0; }; // 抽象的序列化器接口 class Serializer { public: virtual ~Serializer() default; virtual void Write(const std::string key, int value) 0; virtual void Write(const std::string key, const std::string value) 0; virtual void Read(const std::string key, int value) 0; virtual void Read(const std::string key, std::string value) 0; // ... 其他基本类型 }; // 具体格式实现 class JsonSerializer : public Serializer { public: JsonSerializer(std::ostream output) : jsonOutput_(output) { /* 初始化JSON writer */ } void Write(const std::string key, int value) override { // 将键值对写入JSON结构 } // ... 实现其他Write/Read方法 private: // 内部使用某个JSON库如nlohmann/json, RapidJSON }; class BinarySerializer : public Serializer { // 实现二进制格式的读写... }; // 业务数据类 class UserProfile : public Serializable { public: void Serialize(Serializer serializer) const override { serializer.Write(“user_id”, userId_); serializer.Write(“username”, username_); // 序列化vector等复杂成员可能需要特殊处理 } void Deserialize(Serializer serializer) override { serializer.Read(“user_id”, userId_); serializer.Read(“username”, username_); } private: int userId_; std::string username_; }; // 使用 UserProfile profile{123, “Alice”}; // 保存为JSON { JsonSerializer jsonSer(std::ofstream(“profile.json”)); profile.Serialize(jsonSer); // 多态调用JsonSerializer的Write方法 } // 保存为二进制 { BinarySerializer binSer(std::ofstream(“profile.bin”)); profile.Serialize(binSer); // 多态调用BinarySerializer的Write方法 } // UserProfile类完全不知道序列化的格式细节这个架构的美妙之处在于UserProfile这样的业务类只需要关心“序列化哪些字段”而完全不用关心这些字段是被写成了JSON字符串还是二进制字节流。如果要支持一种新的格式比如YAML只需要新增一个YamlSerializer类实现Serializer接口所有已有的Serializable类立刻就能支持新格式无需做任何修改。3.4 场景四插件化系统与动态加载大型软件如Photoshop、Visual Studio Code、游戏引擎经常支持插件来扩展功能。多态结合动态库DLL/SO是实现插件化的核心技术。核心思路主程序定义一套标准的插件接口抽象基类。插件动态库实现这些接口并导出一个或多个创建插件实例的函数。主程序在运行时加载动态库获取函数指针创建插件对象并通过基类接口使用它。// 主程序定义plugin_interface.h class IPlugin { public: virtual ~IPlugin() default; // 必须虚析构 virtual std::string GetName() const 0; virtual void Execute() 0; }; // 约定插件必须导出的函数签名 extern “C” IPlugin* CreatePlugin(); extern “C” void DestroyPlugin(IPlugin* plugin); // 插件实现my_awesome_plugin.cpp class AwesomePlugin : public IPlugin { std::string GetName() const override { return “Awesome Plugin”; } void Execute() override { std::cout “Doing something awesome!\n”; } }; extern “C” IPlugin* CreatePlugin() { return new AwesomePlugin(); // 主程序负责删除 } extern “C” void DestroyPlugin(IPlugin* plugin) { delete plugin; } // 主程序加载和使用插件 #ifdef _WIN32 #include windows.h using ModuleHandle HMODULE; #define LoadLib(path) LoadLibraryA(path) #define GetFunc(handle, name) GetProcAddress(handle, name) #define CloseLib(handle) FreeLibrary(handle) #else #include dlfcn.h using ModuleHandle void*; #define LoadLib(path) dlopen(path, RTLD_LAZY) #define GetFunc(handle, name) dlsym(handle, name) #define CloseLib(handle) dlclose(handle) #endif int main() { ModuleHandle pluginHandle LoadLib(“./awesome_plugin.dll”); // 或 .so if (!pluginHandle) { /* 处理错误 */ } using CreatePluginFunc IPlugin*(*)(); auto createFunc (CreatePluginFunc)GetFunc(pluginHandle, “CreatePlugin”); if (!createFunc) { /* 处理错误 */ } std::unique_ptrIPlugin plugin(createFunc()); // 通过多态接口使用插件 std::cout “Loaded plugin: ” plugin-GetName() std::endl; plugin-Execute(); // plugin 智能指针析构时需要调用插件的DestroyPlugin这里简化了。 // 实际需要获取并调用DestroyPlugin函数。 CloseLib(pluginHandle); return 0; }重要提示跨动态库边界使用多态时必须确保1. 接口类的析构函数是虚函数2. 主程序和插件使用相同版本、相同编译器、相同编译设置的C运行时库否则在new/delete内存时可能导致崩溃。一个更安全的做法是接口类提供统一的Release()虚函数并在插件内部实现delete this主程序只调用Release()。4. 实现细节与性能考量多态不是免费的午餐它主要带来的是运行时开销Runtime Overhead理解这些开销对于编写高性能C代码至关重要。4.1 虚函数表vtable与动态绑定机制这是动态多态的核心实现机制。编译器会为包含虚函数的类生成一个虚函数表vtable这是一个函数指针数组每个条目指向该类的一个虚函数实现。每个该类的对象实例中会隐含一个指向其所属类的vtable的指针vptr。当通过基类指针或引用调用虚函数时代码实际上是通过对象的vptr找到对应的vtable。在vtable中找到该虚函数对应的条目索引在编译时确定。通过函数指针间接调用具体的函数。这个过程称为“动态绑定”或“晚期绑定”。与之相对的是“静态绑定”即普通函数调用在编译时就直接确定了调用地址。开销分析空间开销每个对象增加一个vptr通常是一个指针大小8字节。每个类有一个vtable多个对象共享。时间开销每次虚函数调用比普通函数调用多一次指针解引用和一次跳转。现代CPU有很好的分支预测对于频繁调用、模式固定的虚函数这个开销可以接受。但对于在紧凑循环中调用数百万次的虚函数这个开销就可能成为瓶颈。无法内联由于调用地址在运行时才确定编译器通常无法对虚函数进行内联优化。4.2 静态多态模板作为替代方案对于性能极其敏感且类型在编译期就能确定的场景可以考虑使用静态多态主要是模板和CRTP奇异递归模板模式。模板示例策略模式变体template typename CompressionStrategy class DataCompressor { public: std::vectorchar Compress(const std::vectorchar data) { return CompressionStrategy::compress(data); // 编译期确定调用哪个compress } }; // 具体策略作为静态类 struct ZipStrategy { static std::vectorchar compress(const std::vectorchar data) { /* ... */ } }; struct GzipStrategy { static std::vectorchar compress(const std::vectorchar data) { /* ... */ } }; // 使用 DataCompressorZipStrategy zipCompressor; auto result zipCompressor.Compress(myData); // 调用ZipStrategy::compress可能被内联这种方式在编译期就生成了DataCompressorZipStrategy和DataCompressorGzipStrategy两个不同的类型函数调用是静态绑定的可以被内联性能更高。缺点是1. 代码膨胀模板实例化会生成多份代码2. 策略无法在运行时动态切换3. 错误信息可能难以阅读。CRTP示例template typename Derived class ShapeBase { public: void Draw() const { // 静态转换编译期确定类型 static_castconst Derived*(this)-DrawImpl(); } double Area() const { return static_castconst Derived*(this)-AreaImpl(); } }; class Circle : public ShapeBaseCircle { public: void DrawImpl() const { /* 画圆 */ } double AreaImpl() const { /* 计算圆面积 */ } }; class Rectangle : public ShapeBaseRectangle { /* ... */ }; template typename Shape void RenderShape(const ShapeBaseShape shape) { shape.Draw(); // 这里调用的是ShapeBaseShape::Draw内部静态派发到具体类的Impl }CRTP通过模板将派生类类型“注入”基类基类通过static_cast调用派生类的方法实现了编译期多态。它没有虚函数开销但失去了运行时动态替换类型的能力。选择建议需要运行时灵活替换对象类型关系是“是一个”is-a- 用动态多态虚函数。性能极端敏感类型在编译期已知且关系是“行为像”behaves-like-a或策略选择- 考虑静态多态模板。大部分业务逻辑和架构设计场景动态多态的开销是完全可以接受的其带来的设计收益远大于那一点性能损失。4.3 对象切片Object Slicing问题与防范这是C多态使用中一个经典的坑。class Base { public: virtual void foo() { std::cout “Base\n”; } }; class Derived : public Base { public: void foo() override { std::cout “Derived\n”; } }; void badFunction(Base b) { // 按值传递 b.foo(); // 这里调用的是 Base::foo()而不是 Derived::foo() } int main() { Derived d; badFunction(d); // 发生对象切片只拷贝了d的Base部分Derived部分被“切”掉了。 return 0; }当派生类对象被按值传递给一个接受基类对象的函数时会发生对象切片。编译器只会拷贝对象的基类部分因为函数参数类型是Base派生类特有的成员和数据都会丢失。通过这个被“切过”的对象调用虚函数调用的也是基类的版本多态行为失效。防范措施永远使用指针或引用来传递多态对象。函数参数应声明为Base或Base*或const版本。如果需要在容器中存储多态对象应存储基类的智能指针如std::vectorstd::unique_ptrBase而不是std::vectorBase。5. 设计模式中的多态实践多态是众多设计模式的基石。这里再简要提两个经典模式看看多态是如何发挥核心作用的。5.1 责任链模式Chain of Responsibility让多个对象都有机会处理请求从而避免请求发送者与接收者耦合。将处理对象连成一条链并沿着这条链传递请求直到有一个对象处理它为止。class Handler { public: virtual ~Handler() default; void SetNext(std::shared_ptrHandler next) { next_ next; } virtual void HandleRequest(const std::string request) { if (next_) { next_-HandleRequest(request); // 传递给链上的下一个 } else { std::cout “Request \”” request “\” was not handled.\n”; } } protected: std::shared_ptrHandler next_; }; class ConcreteHandlerA : public Handler { public: void HandleRequest(const std::string request) override { if (request “A”) { std::cout “Handler A processing: ” request std::endl; } else { std::cout “Handler A passing through.\n”; Handler::HandleRequest(request); // 调用基类方法传递 } } }; class ConcreteHandlerB : public Handler { // 类似实现处理条件为“B” }; // 构建链并使用 auto handlerA std::make_sharedConcreteHandlerA(); auto handlerB std::make_sharedConcreteHandlerB(); handlerA-SetNext(handlerB); handlerA-HandleRequest(“B”); // 会被HandlerB处理 handlerA-HandleRequest(“C”); // 无人处理每个具体的Handler决定自己是否能处理请求如果不能就通过多态调用基类的HandleRequest传递给下一个。新增一个处理器只需要新建一个类并插入链中即可。5.2 工厂方法模式Factory Method定义一个创建对象的接口但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。class Document { public: virtual ~Document() default; virtual void Open() 0; virtual void Save() 0; }; class PdfDocument : public Document { /* ... */ }; class WordDocument : public Document { /* ... */ }; // 抽象创建者 class Application { public: virtual ~Application() default; // 工厂方法 virtual std::unique_ptrDocument CreateDocument() 0; void NewDocument() { auto doc CreateDocument(); // 多态调用创建具体的Document docs_.push_back(std::move(doc)); docs_.back()-Open(); } private: std::vectorstd::unique_ptrDocument docs_; }; // 具体创建者 class PdfApplication : public Application { public: std::unique_ptrDocument CreateDocument() override { return std::make_uniquePdfDocument(); } }; class WordApplication : public Application { std::unique_ptrDocument CreateDocument() override { return std::make_uniqueWordDocument(); } };Application的NewDocument方法依赖于抽象的CreateDocument()。PdfApplication和WordApplication分别提供了创建具体文档的实现。这样Application的核心流程就和具体的文档类型解耦了。要支持一种新文档只需要新增一个Document派生类和一个对应的Application派生类。6. 常见陷阱、调试技巧与最佳实践6.1 必须将基类析构函数声明为虚函数这是铁律。如果基类的析构函数不是虚函数那么通过基类指针删除派生类对象就是未定义行为。class Base { public: /* ~Base() 不是虚函数 */ }; class Derived : public Base { public: ~Derived() { std::cout “Derived dtor\n”; } }; int main() { Base* ptr new Derived(); delete ptr; // 错误只调用了~Base()~Derived()没被调用资源泄漏。 return 0; }最佳实践如果一个类设计出来是要被继承的即作为多态基类那么它的析构函数就应该是virtual的。即使它看起来是空的。6.2 小心多重继承与菱形继承C支持多重继承但这会引入复杂性尤其是“菱形继承”问题。class A { public: int data; }; class B : public A {}; class C : public A {}; class D : public B, public C {}; // D中有两份A的副本D对象内部有两份A的成员data访问时会产生二义性。需要使用虚继承来解决。class B : virtual public A {}; class C : virtual public A {}; class D : public B, public C {}; // 现在D中只有一份A的副本虚继承本身也有开销通常通过虚基类指针实现。在工程中除非有非常明确的需求否则建议优先使用单一继承组合的方式来设计类体系谨慎使用多重继承。6.3 使用override和final关键字C11及以上override明确告知编译器和读者这个函数是重写基类的虚函数。如果拼写错误或签名不匹配编译器会报错避免难以调试的bug。class Derived : public Base { public: void someFunction() override; // 如果Base没有虚函数someFunction编译错误 };final用于类表示该类不能被继承用于虚函数表示该函数在派生类中不能被重写。class CannotBeInherited final { /* ... */ }; class Base { public: virtual void cannotOverride() final {} };使用final可以防止意外的继承或重写有时也能给编译器更多的优化提示。6.4 调试多态代码当程序崩溃在虚函数调用时调试器可能只显示基类指针的类型。你需要检查vptr是否被破坏如果对象内存被越界写入可能会覆盖vptr导致程序在查找虚函数表时崩溃。这种错误很难定位需要仔细检查数组越界、野指针等问题。使用RTTI运行时类型识别在调试时可以使用typeid(*ptr).name()来查看指针实际指向的对象的类型名但返回的是编译器修饰过的名字可能需要demangle。注意RTTI也有开销且某些嵌入式环境会禁用。确保对象的生命周期典型错误是使用了一个已经被销毁的对象的指针悬垂指针。使用智能指针std::unique_ptr,std::shared_ptr可以极大缓解这类问题。6.5 性能分析与权衡如果怀疑虚函数调用成为性能热点Profile使用性能分析工具如perf、VTune等查看热点函数和调用关系。考虑是否能用编译期多态替代如果类型在编译期已知且调用非常频繁可以考虑模板或CRTP。减少虚函数调用频率例如在循环外获取函数指针在循环内通过函数指针调用但这牺牲了部分灵活性。虚函数的缓存友好性虚函数调用是间接跳转可能破坏CPU的指令缓存预取。对于需要紧密循环处理的大量对象可以考虑使用“数据导向设计”将同类型对象连续存储并对类型进行分支判断虽然失去了统一接口的优雅但可能获得显著的性能提升。这是一个高级优化技巧仅在性能瓶颈明确时使用。多态性是C赋予我们构建大型、灵活、可维护系统的强大工具。理解其原理熟知其应用场景避开其陷阱并在性能与设计之间做出明智的权衡是每一个C开发者从“会写代码”走向“会设计软件”的必经之路。在实际项目中不要为了用多态而用多态但当你在代码中嗅到大量重复的if-else或switch-case或者发现添加新功能需要修改许多看似不相关的模块时多态很可能就是你要找的那把钥匙。