免费做logo的网站,营销策划方案模板,坪山网站建设设计,wordpress主题缩略图文章目录 一、介绍1.毫无价值的使用虚函数例子 二、策略模式1.策略模式2.多重策略与迭代器模式3.不要什么东西都塞一块 三、适配器模式1.跨接口的适配器2.跨接口的适配器 四、工厂模式1.工厂模式2.超级工厂模式3.RAII 自动管理内存4.工厂模式实战 五、享元模式1.享元模式2.代理… 文章目录 一、介绍1.毫无价值的使用虚函数例子 二、策略模式1.策略模式2.多重策略与迭代器模式3.不要什么东西都塞一块 三、适配器模式1.跨接口的适配器2.跨接口的适配器 四、工厂模式1.工厂模式2.超级工厂模式3.RAII 自动管理内存4.工厂模式实战 五、享元模式1.享元模式2.代理模式 六、组件模式参考 一、介绍
1.毫无价值的使用虚函数例子
struct Pet {virtual void speak() 0;
};struct CatPet Pet {void speak() override {puts(喵);}
};struct DogPet Pet {void speak() override {puts(汪);}
};int main() {Pet *cat new CatPet();Pet *dog new DogPet();cat-speak();dog-speak();
}然而在这个案例中虚函数可有可无并没有发挥任何价值因为普通成员函数也可以实现同样效果。
虚函数真正的价值在于作为一个参数传入其他函数时可以复用那个函数里的代码。
void feed(Pet *pet) {puts(喂食);pet-speak();puts(喂食完毕);
}int main() {Pet *cat new CatPet();Pet *dog new DogPet();feed(cat);feed(dog);
}优点在于feed 函数只用实现一遍了。如果没有虚函数
void feed(DogPet *pet) {puts(喂食); // 重复的代码puts(汪);puts(喂食完毕); // 重复的代码
}void feed(CatPet *pet) {puts(喂食); // 重复的代码puts(喵);puts(喂食完毕); // 重复的代码
}喂食 和 喂食完毕 重复两遍如果我们又要引入一种新动物 PigPet 呢你又要手忙脚乱复制粘贴一份新的 feed 函数
void feed(PigPet *pet) {puts(喂食); // 重复的代码puts(拱);puts(喂食完毕); // 重复的代码
}现在老板突然改了需求他说动物现在要叫两次。 采用了虚函数的你只需要在 feed 函数内增加一次 speak 即可轻松
void feed(Pet *pet) {puts(喂食);pet-speak();pet-speak(); // 加这里puts(喂食完毕);
}而如果一开始没用虚函数就得连改 3 个地方
void feed(DogPet *pet) {puts(喂食);puts(汪); // 改这里puts(汪); // 改这里puts(喂食完毕);
}void feed(CatPet *pet) {puts(喂食);puts(喵); // 改这里puts(喵); // 改这里puts(喂食完毕);
}void feed(PigPet *pet) {puts(喂食);puts(拱); // 改这里puts(拱); // 改这里puts(喂食完毕);
}而且万一复制粘贴的时候有个地方写错了非常隐蔽很容易发现不了
void feed(PigPet *pet) {puts(喂食);puts(拱);puts(喵); // 把猫的代码复制过来的时候漏改了 puts(喂食完毕);
}二、策略模式
1.策略模式
虚函数实战案例eg
这里有一个求和函数可以计算一个数组中所有数字的和。 还有一个求积函数可以计算一个数组中所有数字的积。
int sum(vectorint v) {int res 0;for (int i 0; i v.size(); i) {res res v[i];}return res;
}int product(vectorint v) {int res 1;for (int i 0; i v.size(); i) {res res * v[i];}return res;
}注意到这里面有很多代码重复
我们观察一下 sum 和 product 之间有哪些相似的部分把两者产生不同的部分用 ??? 代替。
int reduce(vectorint v) {int res ???; // sum 时这里是 0product 时这里是 1for (int i 0; i v.size(); i) {res res ??? v[i]; // sum 时这里是 product 时这里是 *}return res;
}把 ??? 部分用一个虚函数顶替
struct Reducer {virtual int init() 0;virtual int add(int a, int b) 0;
};int reduce(vectorint v, Reducer *reducer) {int res reducer.init();for (int i 0; i v.size(); i) {res reducer.add(res, v[i]);}return res;
}这样不论我们想要求和还是求积只需要实现其中不同的部分就可以了公共部分已经在 reduce 里实现好就实现了代码复用。
struct SumReducer : Reducer {int init() override {return 0;}int add(int a, int b) override {return a b;}
};struct ProductReducer : Reducer {int init() override {return 1;}int add(int a, int b) override {return a * b;}
};reduce(v, new SumReducer()); // 等价于之前的 sum(v)
reduce(v, new ProductReducer()); // 等价于之前的 product(v)这就是所谓的策略模式。
很容易添加新的策略进来
struct MinReducer : Reducer {int init() override {return numeric_limitsint::max();}int add(int a, int b) override {return min(a, b);}
};struct MaxReducer : Reducer {int init() override {return numeric_limitsint::min();}int add(int a, int b) override {return max(a, b);}
};2.多重策略与迭代器模式
现在老板需求改变他想要 sum 和 product 函数从输入数据直接计算而不用先读取到一个 vector
还好你早已提前抽出公共部分现在只需要修改 reduce 函数本身就可以了。
SumReducer 和 ProductReducer 无需任何修改体现了开闭原则。
int reduce(Reducer *reducer) {int res reducer.init();while (true) {int tmp;cin tmp;if (tmp -1) break;res reducer.add(res, tmp);}return res;
}现在老板需求又改回来他突然又想要从 vector 里读取数据了。
在破口大骂老板出尔反尔的同时你开始思考这两个函数似乎还是有一些重复可以抽取出来
int cin_reduce(Reducer *reducer) {int res reducer.init();while (true) {int tmp;cin tmp;if (tmp -1) break;res reducer.add(res, tmp);}return res;
}int vector_reduce(vectorint v, Reducer *reducer) {int res reducer.init();for (int i 0; i v.size(); i) {res reducer.add(res, v[i]);}return res;
}现在我们只有表示如何计算的类 Reducer 做参数。
你决定再定义一个表示如何读取的虚类 Inputer。
struct Inputer {virtual optionalint fetch() 0;
};int reduce(Inputer *inputer, Reducer *reducer) {int res reducer.init();while (int tmp inputer.fetch()) {res reducer.add(res, tmp);}return res;
}这样我们满足了单一职责原则每个类只负责一件事。
这里的 Inputer 实际上运用了迭代器模式提供一个抽象接口来顺序访问一个集合中各个元素而又无须暴露该集合的内部表示。 底层是 cin 还是 vector我不在乎我只知道他可以依次顺序取出数据。 struct CinInputer : Inputer {optionalint fetch() override {int tmp;cin tmp;if (tmp -1)return nullopt;return tmp;}
};struct VectorInputer : Inputer {vectorint v;int pos 0;VectorInputer(vectorint v) : v(v) {}optionalint fetch() override {if (pos v.size())return nullopt;return v[pos];}
};reduce(new CinInputer(), new SumReducer());
reduce(new VectorInputer(v), new SumReducer());
reduce(new CinInputer(), new ProductReducer());
reduce(new VectorInputer(v), new ProductReducer());Inputer 负责告诉 reduce 函数如何读取数据Reducer 负责告诉 reduce 函数如何计算数据。
这就是依赖倒置原则高层模块reduce 函数不要直接依赖于低层模块二者都依赖于抽象Inputer 和 Reducer 类来沟通。
3.不要什么东西都塞一块
有些糟糕的实现会把分明不属于同一层次的东西强行放在一起比如没能分清 Inputer 和 Reducer 类错误地把他们设计成了一个类
int reduce(Reducer *reducer) {int res reducer.init();while (int tmp reducer.fetch()) { // fetch 凭什么和 init、add 放在一起res reducer.add(res, tmp);}return res;
}fetch 明明属于 IO 操作但他被错误地放在了本应只负责计算的 Reducer 里
这导致你必须实现四个类罗列所有的排列组合
struct CinSumReducer : Reducer { ... };
struct VectorSumReducer : Reducer { ... };
struct CinProductReducer : Reducer { ... };
struct VectorProductReducer : Reducer { ... };这显然是不符合单一责任原则的。
满足单一责任原则、开闭原则、依赖倒置原则的代码更加灵活、易于扩展、易于维护。请务必记住并落实起来 否则即你装模作样地用了虚函数也一样会导致代码重复、难以维护
三、适配器模式
1.跨接口的适配器
刚才的例子中我们用到了 Inputer 虚接口类。
struct CinInputer : Inputer {optionalint fetch() override {int tmp;cin tmp;if (tmp -1)return nullopt;return tmp;}
};struct VectorInputer : Inputer {vectorint v;int pos 0;VectorInputer(vectorint v) : v(v) {}optionalint fetch() override {if (pos v.size())return nullopt;return v[pos];}
};如果我们想要实现读取到 0 截止而不是 -1 呢难道还得给 CinInputer 加个参数 但是 vector 有时候也可能有读到 -1 就提前截断的需求呀
这明显违背了单一责任原则。
更好的设计是让 CinInputer 无限读取永远成功。 然后另外弄一个 StopInputerAdapter其接受一个 CinInputer 作为构造参数。 当 StopInputerAdapter 被读取时他会检查是否为 -1如果已经得到 -1那么就返回 nullopt不会进一步调用 CinInputer 了。
StopInputerAdapter 负责处理截断问题CinInputer 只是负责读取 cin 输入。满足了单一责任原则。
struct StopInputerAdapter : Inputer {Inputer *inputer;int stopMark;StopInputerAdapter(Inputer *inputer, int stopMark): inputer(inputer), stopMark(stopMark){}optionalint fetch() override {auto tmp inputer.fetch();if (tmp stopMark)return nullopt;return tmp;}
};这里的 StopInputerAdapter 就是一个适配器他把 CinInputer 的接口无限读取叠加上了一个额外功能读到指定的 stopMark 值就停止产生了一个新的 Inputer。
reduce(new StopInputerAdapter(new CinInputer(), -1), new SumReducer()); // 从 cin 读到 -1 为止
reduce(new StopInputerAdapter(new VectorInputer(v), -1), new SumReducer()); // 从 vector 读到 -1 为止
reduce(new VectorInputer(), new SumReducer()); // 从 vector 读但无需截断这就是适配器模式将一个类的接口添油加醋转换成客户希望的另一个接口。
StopInputerAdapter 这个适配器本身也是一个 Inputer可以直接作为 reduce 的参数适应了现有的策略模式。StopInputerAdapter 并不依赖于参数 Inputer 的底层实现可以是 CinInputer、也可以是 VectorInputer满足了依赖倒置原则。未来即使新增了不同类型的 Inputer甚至是其他 InputerAdapter一样可以配合 StopInputerAdapter 一起使用而无需任何修改满足了开闭原则。
如果我们还想实现过滤出所有正数和零负数直接丢弃呢
struct FilterInputerAdapter {Inputer *inputer;FilterInputerAdapter(Inputer *inputer): inputer(inputer){}optionalint fetch() override {while (true) {auto tmp inputer.fetch();if (!tmp.has_value()) {return nullopt;}if (tmp 0) {return tmp;}}}
};改进Filter 的条件不应为写死的 tmp 0而应该是传入一个 FilterStrategy允许用户扩展。
struct FilterStrategy {virtual bool shouldDrop(int value) 0; // 返回 true 表示该值应该被丢弃
};struct FilterStrategyAbove : FilterStrategy { // 大于一定值threshold才能通过int threshold;FilterStrategyAbove(int threshold) : threshold(threshold) {}bool shouldPass(int value) override {return value threshold;}
};struct FilterStrategyBelow : FilterStrategy { // 小于一定值threshold才能通过int threshold;FilterStrategyBelow(int threshold) : threshold(threshold) {}bool shouldPass(int value) override {return value threshold;}
};struct FilterInputerAdapter : Inputer {Inputer *inputer;FilterStrategy *strategy;FilterInputerAdapter(Inputer *inputer, FilterStrategy *strategy): inputer(inputer), strategy(strategy){}optionalint fetch() override {while (true) {auto tmp inputer.fetch();if (!tmp.has_value()) {return nullopt;}if (strategy-shouldPass(tmp)) {return tmp;}}}
};FilterStrategy 又可以进一步运用适配器模式例如我们可以把 FilterStrategyAbove(0) 和 FilterStrategyBelow(100) 组合起来实现过滤出 0100 范围内的整数。
struct FilterStrategyAnd : FilterStrategy { // 要求 a 和 b 两个过滤策略都为 true才能通过FilterStrategy *a;FilterStrategy *b;FilterStrategyAnd(FilterStrategy *a, FilterStrategy *b): a(a), b(b){}bool shouldPass(int value) override {return a-shouldPass(value) b-shouldPass(value);}
};reduce(new FilterInputerAdapter(new StopInputerAdapter(new CinInputer(),-1),new FilterStrategyAnd(new FilterStrategyAbove(0),new FilterStrategyBelow(100))),new SumReducer());是不是逻辑非常清晰而且容易扩展呢 实际上函数式和模板元编程更擅长做这种工作但今天先介绍完原汁原味的 Java 风格面向对象他们复用代码的思路是共通的。 2.跨接口的适配器
适配器模式还可以使原本由于接口不兼容而不能一起工作的那些类可以一起工作例如一个第三方库提供了类似于我们 Inputer 的输入流接口也是基于虚函数的。但是他的接口显然不能直接传入我们的 reduce 函数我们的 reduce 函数只接受我们自己的 Inputer 接口。这时就可以用适配器把接口翻译成我们的 reducer 能够理解的。
以下是一个自称 “Poost” 的第三方库提供的接口
struct PoostInputer {virtual bool hasNext() 0;virtual int getNext() 0;
};他们要求的用法是先判断 hasNext()然后才能调用 getNext 读取出真正的值。
设计了一个 Poost 适配器把 PoostInputer 翻译成我们的 Inputer
struct PoostInputerAdapter : Inputer {PoostInputer *poostIn;optionalint next;PoostInputerAdapter(PoostInputer *poostIn): poostIn(poostIn){}optionalint fetch() override {if (next.has_value()) {auto res next;next nullopt;return res;}if (poostIn.hasNext()) {return poostIn.getNext();} else {return nullopt;}}
};当我们得到一个 PoostInputer 时如果想要调用我们自己的 reducer就可以用这个 PoostInputerAdapter 套一层
auto poostStdIn poost::getStandardInput();
reduce(new PoostInputerAdapter(poostStdIn), new SumReducer());这样就可以无缝地把 PoostInputer 作为 reduce 的参数了。
四、工厂模式
1.工厂模式
现在你是一个游戏开发者你的玩家可以装备武器不同的武器可以发出不同的子弹
你使用小彭老师教的策略模式把不同的子弹类型作为不同的策略传入 player 函数造成不同类型的伤害。
struct Bullet {virtual void explode() 0;
};struct AK47Bullet : Bullet {void explode() override {puts(物理伤害);}
};struct MagicBullet : Bullet {void explode() override {puts(魔法伤害);}
};void player(Bullet *bullet) {bullet-explode();
}player(new AK47Bullet());
player(new MagicBullet());但是这样就相当于每个玩家只有一发子弹听个响就没了…
如何允许玩家源源不断地创造新子弹出来我们可以把“创建子弹”这一过程抽象出来放在一个“枪”类里。
struct Gun {virtual Bullet *shoot() 0;
};struct AK47Gun : Gun {Bullet *shoot() override {return new AK47Bullet();}
};struct MagicGun : Gun {Bullet *shoot() override {return new MagicBullet();}
};void player(Gun *gun) {for (int i 0; i 100; i) {Bullet *bullet gun-shoot();bullet-explode();}
}player(new AK47Gun());
player(new MagicGun());这就是所谓的工厂模式“枪”就是“子弹”对象的工厂。 传给玩家的是子弹的工厂——枪而不是子弹本身。 只要调用工厂的 shoot 函数玩家可以源源不断地创建新子弹出来。 正所谓授人以鱼不如授人以渔你的玩家不再是被动接受子弹而是可以自己创造子弹了
工厂还可以具有一定的参数例如我们需要模拟 AK47 可能“受潮”导致产生的子弹威力降低。 就可以给枪加一个 isWet 参数给子弹加一个 damage 参数让 AK47 生成子弹的时候根据 isWet 为子弹构造函数设置不同的 damage。
struct AK47Bullet {int damage;AK47Bullet(int damage) : damage(damage) {}void explode() {printf(造成 %d 点物理伤害\n, damage);}
};struct AK47Gun : Gun {bool isWet;AK47Gun(bool isWet) : isWet(isWet) {}Bullet *shoot() override {if (isWet)return new AK47Bullet(5); // 受潮了伤害降低为 5elsereturn new AK47Bullet(10); // 正常情况下伤害为 10}
};我们还可以利用模板自动为不同的子弹类型批量定义工厂
template class B
struct GunWithBullet : Gun {static_assert(is_base_ofBullet, B::value, B 必须是 Bullet 的子类);Bullet *shoot() override {return new B();}
};void player(Gun *gun) {for (int i 0; i 100; i) {Bullet *bullet gun-shoot();bullet-explode();}
}player(new GunWithBulletAK47Bullet());
player(new GunWithBulletMagicBullet());
};这样就不必每次添加新子弹类型时都得新建一个相应的枪类型了进一步避免了代码重复。可见模板元编程完全可与传统面向对象强强联手。
2.超级工厂模式
Gun *getGun(string name) {if (name AK47) {return new GunWithBulletAK47Bullet();} else if (name Magic) {return new GunWithBulletMagicBullet();} else {throw runtime_error(没有这种枪);}
}player(getGun(AK47));
player(getGun(Magic));3.RAII 自动管理内存
template class B
struct GunWithBullet : Gun {static_assert(is_base_ofBullet, B::value, B 必须是 Bullet 的子类);Bullet *shoot() override {return new B();}
};void player(Gun *gun) {for (int i 0; i 100; i) {Bullet *bullet gun-shoot();bullet-explode();delete bullet; // 刚才没有 delete会产生内存泄漏}
}player(new GunWithBulletAK47Bullet());
player(new GunWithBulletMagicBullet());现在的工厂一般都会返回智能指针就没有这个问题。
具体来说就是用 unique_ptr 代替 T *用 make_unique(xxx) 代替 new T(xxx)。
template class B
struct GunWithBullet : Gun {static_assert(is_base_ofBullet, B::value, B 必须是 Bullet 的子类);unique_ptrBullet shoot() override {return make_uniqueB();}
};void player(Gun *gun) {for (int i 0; i 100; i) {auto bullet gun-shoot();bullet-explode();// unique_ptr 在退出当前 {} 时会自动释放不用你惦记着了}
}player(make_uniqueGunWithBulletAK47Bullet().get());
player(make_uniqueGunWithBulletMagicBullet().get());这里 C 标准保证了 unique_ptr 的生命周期是这一整行; 结束前整个 player 执行期间都活着不会提前释放 正如 func(string().c_str()) 不会有任何问题string 要到 func 返回后才释放呢 4.工厂模式实战
回到数组求和问题。
int sum(vectorint v) {int res 0;for (int i 0; i v.size(); i) {res res v[i];}return res;
}int product(vectorint v) {int res 1;for (int i 0; i v.size(); i) {res res * v[i];}return res;
}int average(vectorint v) {int res 0;int count 0;for (int i 0; i v.size(); i) {res res v[i];count count 1;}return res / count;
}我们想要加一个求平均值的函数 average这该如何与 sum 合起来
注意因为我们要支持从 CinInputer 读入数据并不一定像一样 VectorInputer 能够提前得到数组大小不然也不需要 count 了。
int reduce(vectorint v) {int res ???; // sum 时这里是 0product 时这里是 1int count? ???; // sum 和 product 用不到该变量只有 average 需要for (int i 0; i v.size(); i) {res res ??? v[i]; // sum 时这里是 product 时这里是 *count? count? ???; // average 时这里还需要额外修改 count 变量}return res;
}看来我们需要允许 Reducer 的 init() 返回 “任意数量的状态变量” 以前的设计让 init() 只能返回单个 int 是个错误的决定。 这时候就可以把 “任意数量的状态变量” 封装成一个新的类。 然后改为由这个类负责提供虚函数 add()。 且只需要提供一个右侧参数了左侧的 res 变量已经存在 ReducerState 体内了。
struct ReducerState {virtual void add(int val) 0;virtual int result() 0;
};struct Reducer {virtual unique_ptrReducerState init() 0;
};struct SumReducerState : ReducerState {int res;SumReducerState() : res(0) {}void add(int val) override {res res val;}int result() override {return res;}
};struct ProductReducerState : ReducerState {int res;ProductReducerState() : res(1) {}void add(int val) override {res res * val;}int result() override {return res;}
};struct AverageReducerState : ReducerState {int res;int count;AverageReducerState() : res(0), count(0) {}void add(int val) override {res res val;count count 1;}int result() override {return res / count;}
};struct SumReducer : Reducer {unique_ptrReducerState init() override {return make_uniqueSumReducerState();}
};struct ProductReducer : Reducer {unique_ptrReducerState init() override {return make_uniqueProductReducerState();}
};struct AverageReducer : Reducer {unique_ptrReducerState init() override {return make_uniqueAverageReducerState();}
};这里 Reducer 就成了 ReducerState 的工厂。
int reduce(Inputer *inputer, Reducer *reducer) {unique_ptrReducerState state reducer-init();while (auto val inputer-fetch()) {state-add(val);}return state-result();
}int main() {vectorint v;reduce(make_uniqueVectorInputer(v).get(), make_uniqueSumReducer().get());reduce(make_uniqueVectorInputer(v).get(), make_uniqueProductReducer().get());reduce(make_uniqueVectorInputer(v).get(), make_uniqueAverageReducer().get());
}并行版需要创建很多个任务每个任务需要有一个自己的中间结果变量最后的结果计算又需要一个中间变量。 还好你早已提前采用工厂模式允许函数体内多次创建 ReducerState 对象。
int reduce(Inputer *inputer, Reducer *reducer) {tbb::task_group g;listunique_ptrReducerState local_states;vectorint chunk;auto enqueue_chunk []() {local_chunks.emplace_back();g.run([chunk move(chunk), back local_chunks.back()]() {auto local_state reducer-init();for (auto c: chunk) {local_state-add(c);}back move(local_state); // list 保证已经插入元素的引用不会失效所以可以暂存 back 引用});chunk.clear();};while (auto tmp inputer-fetch()) {if (chunk.size() 64) { // 还没填满 64 个chunk.push_back(tmp);} else { // 填满了 64 个可以提交成一个单独任务了enqueue_chunk();}}if (chunk.size() 0) {enqueue_chunk(); // 提交不足 64 个的残余项}g.wait();auto final_state reducer-init();for (auto local_state: local_states) {res final_state-add(local_state-result());}return final_state-result();
}只需要把 reducer 参数替换为 MinReducer、AverageReducer……就自动适用于不同的计算任务而不用为他们每个单独编写并行版本的代码。
五、享元模式
1.享元模式
在二维游戏开发中常常会提到一种称为 Sprite精灵贴图的黑话实际上就是每个对象自己有一张贴图贴图跟着物体的位置走。
struct Bullet {glm::vec3 position;glm::vec3 velocity;vectorchar texture;void draw() {glDrawPixels(position, texture);}
};texture 里面存储着贴图的 RGB 数据他直接就是 Bullet 的成员。 这样的话如果我们的玩家打出了 100 颗子弹就需要存储 100 个贴图数组。 如果我们的玩家同时打出了 1000 颗子弹就需要存储 1000 个贴图数组。 这样的话内存消耗将会非常大。然而所有同类型的 Bullet其贴图数组其实是完全相同的完全没必要各自存那么多份拷贝。
为解决这个问题我们可以使用享元模式共享多个对象之间相同的部分节省内存开销。
这里每颗子弹的 position、velocity 显然都是各有不同的不可能所有子弹都在同一个位置上。 但是很多子弹都会有着相同的贴图只有不同类型的子弹贴图会不一样。 比如火焰弹和寒冰弹会有不同的贴图但是当场上出现 100 颗火焰弹时显然不需要拷贝 100 份完全相同的火焰弹贴图。
struct Sprite { // Sprite 才是真正持有很大的贴图数据的vectorchar texture;void draw(glm::vec3 position) {glDrawPixels(position, texture);}
};struct Bullet {glm::vec3 position;glm::vec3 velocity;shared_ptrSprite sprite; // 允许多个子弹对象共享同一个精灵贴图的所有权void draw() {sprite-draw(position); // 转发给 Sprite 让他帮忙在我的位置绘制贴图}
};需要绘制子弹时Bullet 的 draw 只是简单地转发给 Sprite 类的 draw。 只要告诉 Sprite 子弹的位置就行贴图数据已经存在 Sprite 内部让他来负责真正绘制。 Bullet 类只需要专注于位置、速度的更新即可不必去操心着贴图绘制的细节实现了解耦。
这种函数调用的转发也被称为代理模式。
2.代理模式
这样还有一个好处那就是Sprite 可以设计成一个虚函数接口类
struct Sprite {virtual void draw(glm::vec3 position) 0;
};struct FireSprite : Sprite {vectorchar fireTexture;FireSprite() : fireTexture(loadTexture(fire.jpg)) {}void draw(glm::vec3 position) override {glDrawPixels(position, fireTexture);}
};struct IceSprite : Sprite { // 假如寒冰弹需要两张贴图也没问题因为虚接口类允许子类有不同的成员不同的结构体大小vectorchar iceTexture1;vectorchar iceTexture2;IceSprite(): iceTexture1(loadTexture(ice1.jpg)), iceTexture2(loadTexture(ice2.jpg)){}void draw(glm::vec3 position) override {glDrawPixels(position, iceTexture1);glDrawPixels(position, iceTexture2);}
};struct Bullet {glm::vec3 position;glm::vec3 velocity;shared_ptrSprite sprite; // Sprite 负责含有虚函数void draw() { // Bullet 的 draw 就不用是虚函数了sprite-draw(position);}
};六、组件模式
组件模式的基本概念
组件Component一个组件表示对象的一部分行为或属性。每个组件通常只负责一项特定的功能。实体Entity实体是由多个组件组合而成的对象。实体本身通常只是一个容器用来聚合各种组件。组件系统Component System组件系统是管理和操作组件的逻辑。它通常负责更新组件、处理组件间的交互等。
1组件基类 首先我们定义一个组件的基类。所有的组件都将继承自这个基类。
class Component {
public:virtual ~Component() default;virtual void update() 0;
};
2具体组件 接下来我们定义一些具体的组件。例如一个TransformComponent用于表示实体的位置和方向一个RenderComponent用于处理渲染。
#include iostreamclass TransformComponent : public Component {
public:float x, y;TransformComponent(float x 0, float y 0) : x(x), y(y) {}void update() override {// 更新位置的逻辑std::cout Updating Transform: ( x , y )\n;}
};class RenderComponent : public Component {
public:void update() override {// 渲染逻辑std::cout Updating Render\n;}
};
3实体类 实体类是组件的容器。它包含一个组件列表并提供添加和更新组件的功能。
#include vector
#include memory
#include algorithmclass Entity {
public:void addComponent(std::shared_ptrComponent component) {components.push_back(component);}void update() {for (auto component : components) {component-update();}}private:std::vectorstd::shared_ptrComponent components;
};
4使用组件模式 我们可以创建实体并向其中添加各种组件最后更新实体来触发所有组件的更新。
int main() {Entity entity;std::shared_ptrTransformComponent transform std::make_sharedTransformComponent(10, 20);std::shared_ptrRenderComponent render std::make_sharedRenderComponent();entity.addComponent(transform);entity.addComponent(render);entity.update();return 0;
}
5扩展 组件模式的实现可以根据需要进行扩展和改进例如 类型安全的组件获取可以使用模板方法或类型信息如typeid来安全地获取特定类型的组件。 事件系统 引入事件系统让组件之间能够相互通信和响应事件。系统类 创建独立的系统类来处理特定类型的组件例如物理系统、渲染系统等。
eg类型安全的组件获取
class Entity {
public:templatetypename Tvoid addComponent(std::shared_ptrT component) {components[typeid(T).hash_code()] component;}templatetypename Tstd::shared_ptrT getComponent() {auto it components.find(typeid(T).hash_code());if (it ! components.end()) {return std::dynamic_pointer_castT(it-second);}return nullptr;}void update() {for (auto pair : components) {pair.second-update();}}private:std::unordered_mapsize_t, std::shared_ptrComponent components;
};
通过这种方式可以确保以类型安全的方式添加和获取组件。
int main() {Entity entity;auto transform std::make_sharedTransformComponent(10, 20);auto render std::make_sharedRenderComponent();entity.addComponent(transform);entity.addComponent(render);auto retrievedTransform entity.getComponentTransformComponent();if (retrievedTransform) {std::cout Retrieved Transform: ( retrievedTransform-x , retrievedTransform-y )\n;}entity.update();return 0;
}
参考
让虚函数再次伟大【C】速通面向对象设计模式1策略、工厂、迭代器、适配器、享元、代理