C++设计模式之策略模式、迭代器模式、适配器模式、工厂模式、超级工厂模式、享元模式、代理模式
文章目录
- 一、介绍
- 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(vector<int> v) {int res = 0;for (int i = 0; i < v.size(); i++) {res = res + v[i];}return res;
}int product(vector<int> v) {int res = 1;for (int i = 0; i < v.size(); i++) {res = res * v[i];}return res;
}
注意到这里面有很多代码重复!
我们观察一下 sum 和 product 之间有哪些相似的部分,把两者产生不同的部分用 ??? 代替。
int reduce(vector<int> v) {int res = ???; // sum 时这里是 0,product 时这里是 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(vector<int> 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_limits<int>::max();}int add(int a, int b) override {return min(a, b);}
};struct MaxReducer : Reducer {int init() override {return numeric_limits<int>::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(vector<int> 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 optional<int> 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 {optional<int> fetch() override {int tmp;cin >> tmp;if (tmp == -1)return nullopt;return tmp;}
};struct VectorInputer : Inputer {vector<int> v;int pos = 0;VectorInputer(vector<int> v) : v(v) {}optional<int> 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 {optional<int> fetch() override {int tmp;cin >> tmp;if (tmp == -1)return nullopt;return tmp;}
};struct VectorInputer : Inputer {vector<int> v;int pos = 0;VectorInputer(vector<int> v) : v(v) {}optional<int> 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){}optional<int> 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){}optional<int> 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){}optional<int> 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) 组合起来,实现过滤出 0~100 范围内的整数。
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;optional<int> next;PoostInputerAdapter(PoostInputer *poostIn): poostIn(poostIn){}optional<int> 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_of<Bullet, 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 GunWithBullet<AK47Bullet>());
player(new GunWithBullet<MagicBullet>());
};
这样就不必每次添加新子弹类型时,都得新建一个相应的枪类型了,进一步避免了代码重复。可见模板元编程完全可与传统面向对象强强联手。
2.超级工厂模式
Gun *getGun(string name) {if (name == "AK47") {return new GunWithBullet<AK47Bullet>();} else if (name == "Magic") {return new GunWithBullet<MagicBullet>();} else {throw runtime_error("没有这种枪");}
}player(getGun("AK47"));
player(getGun("Magic"));
3.RAII 自动管理内存
template <class B>
struct GunWithBullet : Gun {static_assert(is_base_of<Bullet, 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 GunWithBullet<AK47Bullet>());
player(new GunWithBullet<MagicBullet>());
现在的工厂一般都会返回智能指针就没有这个问题。
具体来说就是用 unique_ptr 代替 T *,用 make_unique(xxx) 代替 new T(xxx)。
template <class B>
struct GunWithBullet : Gun {static_assert(is_base_of<Bullet, B>::value, "B 必须是 Bullet 的子类");unique_ptr<Bullet> shoot() override {return make_unique<B>();}
};void player(Gun *gun) {for (int i = 0; i < 100; i++) {auto bullet = gun->shoot();bullet->explode();// unique_ptr 在退出当前 {} 时会自动释放,不用你惦记着了}
}player(make_unique<GunWithBullet<AK47Bullet>>().get());
player(make_unique<GunWithBullet<MagicBullet>>().get());
这里 C++ 标准保证了 unique_ptr 的生命周期是这一整行(; 结束前),整个 player 执行期间都活着,不会提前释放 正如 func(string().c_str()) 不会有任何问题,string 要到 func 返回后才释放呢!
4.工厂模式实战
回到数组求和问题。
int sum(vector<int> v) {int res = 0;for (int i = 0; i < v.size(); i++) {res = res + v[i];}return res;
}int product(vector<int> v) {int res = 1;for (int i = 0; i < v.size(); i++) {res = res * v[i];}return res;
}int average(vector<int> 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(vector<int> v) {int res = ???; // sum 时这里是 0,product 时这里是 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_ptr<ReducerState> 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_ptr<ReducerState> init() override {return make_unique<SumReducerState>();}
};struct ProductReducer : Reducer {unique_ptr<ReducerState> init() override {return make_unique<ProductReducerState>();}
};struct AverageReducer : Reducer {unique_ptr<ReducerState> init() override {return make_unique<AverageReducerState>();}
};
这里 Reducer 就成了 ReducerState 的工厂。
int reduce(Inputer *inputer, Reducer *reducer) {unique_ptr<ReducerState> state = reducer->init();while (auto val = inputer->fetch()) {state->add(val);}return state->result();
}int main() {vector<int> v;reduce(make_unique<VectorInputer>(v).get(), make_unique<SumReducer>().get());reduce(make_unique<VectorInputer>(v).get(), make_unique<ProductReducer>().get());reduce(make_unique<VectorInputer>(v).get(), make_unique<AverageReducer>().get());
}
并行版需要创建很多个任务,每个任务需要有一个自己的中间结果变量,最后的结果计算又需要一个中间变量。 还好你早已提前采用工厂模式,允许函数体内多次创建 ReducerState 对象。
int reduce(Inputer *inputer, Reducer *reducer) {tbb::task_group g;list<unique_ptr<ReducerState>> local_states;vector<int> 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;vector<char> texture;void draw() {glDrawPixels(position, texture);}
};
texture 里面存储着贴图的 RGB 数据,他直接就是 Bullet 的成员。 这样的话,如果我们的玩家打出了 100 颗子弹,就需要存储 100 个贴图数组。 如果我们的玩家同时打出了 1000 颗子弹,就需要存储 1000 个贴图数组。 这样的话,内存消耗将会非常大。然而所有同类型的 Bullet,其贴图数组其实是完全相同的,完全没必要各自存那么多份拷贝。
为解决这个问题,我们可以使用享元模式:共享多个对象之间相同的部分,节省内存开销。
这里每颗子弹的 position、velocity 显然都是各有不同的,不可能所有子弹都在同一个位置上。 但是很多子弹都会有着相同的贴图,只有不同类型的子弹贴图会不一样。 比如火焰弹和寒冰弹会有不同的贴图,但是当场上出现 100 颗火焰弹时,显然不需要拷贝 100 份完全相同的火焰弹贴图。
struct Sprite { // Sprite 才是真正持有(很大的)贴图数据的vector<char> texture;void draw(glm::vec3 position) {glDrawPixels(position, texture);}
};struct Bullet {glm::vec3 position;glm::vec3 velocity;shared_ptr<Sprite> 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 {vector<char> fireTexture;FireSprite() : fireTexture(loadTexture("fire.jpg")) {}void draw(glm::vec3 position) override {glDrawPixels(position, fireTexture);}
};struct IceSprite : Sprite { // 假如寒冰弹需要两张贴图,也没问题!因为虚接口类允许子类有不同的成员,不同的结构体大小vector<char> iceTexture1;vector<char> 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_ptr<Sprite> 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 <iostream>class 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 <algorithm>class Entity {
public:void addComponent(std::shared_ptr<Component> component) {components.push_back(component);}void update() {for (auto& component : components) {component->update();}}private:std::vector<std::shared_ptr<Component>> components;
};
(4)使用组件模式
我们可以创建实体,并向其中添加各种组件,最后更新实体来触发所有组件的更新。
int main() {Entity entity;std::shared_ptr<TransformComponent> transform = std::make_shared<TransformComponent>(10, 20);std::shared_ptr<RenderComponent> render = std::make_shared<RenderComponent>();entity.addComponent(transform);entity.addComponent(render);entity.update();return 0;
}
(5)扩展
组件模式的实现可以根据需要进行扩展和改进,例如:
-
类型安全的组件获取:可以使用模板方法或类型信息(如typeid)来安全地获取特定类型的组件。
事件系统: -
引入事件系统,让组件之间能够相互通信和响应事件。系统类:
-
创建独立的系统类来处理特定类型的组件,例如物理系统、渲染系统等。
eg:类型安全的组件获取
class Entity {
public:template<typename T>void addComponent(std::shared_ptr<T> component) {components[typeid(T).hash_code()] = component;}template<typename T>std::shared_ptr<T> getComponent() {auto it = components.find(typeid(T).hash_code());if (it != components.end()) {return std::dynamic_pointer_cast<T>(it->second);}return nullptr;}void update() {for (auto& pair : components) {pair.second->update();}}private:std::unordered_map<size_t, std::shared_ptr<Component>> components;
};
通过这种方式,可以确保以类型安全的方式添加和获取组件。
int main() {Entity entity;auto transform = std::make_shared<TransformComponent>(10, 20);auto render = std::make_shared<RenderComponent>();entity.addComponent(transform);entity.addComponent(render);auto retrievedTransform = entity.getComponent<TransformComponent>();if (retrievedTransform) {std::cout << "Retrieved Transform: (" << retrievedTransform->x << ", " << retrievedTransform->y << ")\n";}entity.update();return 0;
}
参考:
- 让虚函数再次伟大!
- 【C++】速通面向对象设计模式(1):策略、工厂、迭代器、适配器、享元、代理
相关文章:

C++设计模式之策略模式、迭代器模式、适配器模式、工厂模式、超级工厂模式、享元模式、代理模式
文章目录 一、介绍1.毫无价值的使用虚函数例子 二、策略模式1.策略模式2.多重策略与迭代器模式3.不要什么东西都塞一块 三、适配器模式1.跨接口的适配器2.跨接口的适配器 四、工厂模式1.工厂模式2.超级工厂模式3.RAII 自动管理内存4.工厂模式实战 五、享元模式1.享元模式2.代理…...

18 js时间对象
时间对象是一种复杂数据类型,用来存储时间 创建时间对象 内置构造函数创建 语法:var 时间名new Date() var datenew Date()console.log(date) //Wed May 29 2024 16:03:47 GMT0800 (中国标准时间) 创建指定日期 当参数为数字——>在格林威治的时间基…...

安卓赤拳配音v1.0.2Ai配音神器+百位主播音色
Ai配音神器 本人自用版本!超级稳定!百位主播音色 登陆即可用 链接:https://pan.baidu.com/s/1WVsrYZqLaPAriHMMLMdPBg?pwdz9ru 提取码:z9ru...

前端面试题日常练-day40 【面试题】
题目 希望这些选择题能够帮助您进行前端面试的准备,答案在文末 1. Bootstrap 的栅格系统是基于( )进行布局的。A. 像素 B. 百分比 C. 媒体查询 2. 在 Bootstrap 中,要创建一个按钮,可以使用( ÿ…...

UG NX二次开发(C#)-UFun函数-利用UFPart.Export导出模型中的对象并创建一个新的part
文章目录 1、前言2、UF_PART_export函数定义3、UF_PART_export_with_options函数定义4、代码1、前言 在UG NX 10.0二次开发中,需要用到将装配体中通过几何建模创建的对象独立创建一个part文件,所以查找了下UFun函数,即是UF_PART_export 和UF_PART_export_with_options两个函…...

SFOS2:组件介绍
一、前言 在sailfish os application的开发过程中,几乎是困难重重,因为我暂未找到具有完整性、指导性、易懂性的开发文档,特别是组件的使用,现决定将自己的探究结果记录下来。因此,这篇文章只会具有参考价值࿰…...

交换机的三层交换技术
现有pc1与pc2不在同一个网段之下,通过交换机相连接。 进人交换机1,创建两个vlan 10和vlan 20 ,进入串口2设置串口模式为access,并且设置默认vlan为10.进入串口3设置串口模式为access,并且设置默认vlan为20. 进入串口1…...

探秘URL的奥义:JavaScript中轻松获取页面参数值的N种姿势【含代码示例】
探秘URL的奥义:JavaScript中轻松获取页面参数值的N种姿势【含代码示例】 URL基础知识补给站基础案例:直接解析URL案例一:使用URLSearchParams案例二:传统字符串分割法 高级策略:动态与安全案例三:封装与模块…...

VSCode小技巧,忽略不想格式化的代码行
零.格式化工具文档 1 . Black Ignoring sections功能 2 . autopep8 disabling-line-by-line功能;;–line-range选项 3 . Prettier prettier-ignore功能(例:适用于JS的// prettier-ignore,适用于CSS的/* prettier-igno…...

揭秘网络编程:同步与异步IO模型的实战演练
摘要 在网络编程领域,同步(Synchronous)、异步(Asynchronous)、阻塞(Blocking)与非阻塞(Non-blocking)IO模型是核心概念。尽管这些概念在多篇文章中被广泛讨论,它们的抽象性使得彻底理解并非易事。本文旨在通过具体的实验案例,将这些抽象…...

在Visual Studio Code和Visual Studio 2022下配置Clang-Format,格式化成Google C++ Style
项目开发要求好的编写代码格式规范,常用的是根据Google C Style Guide 网上查了很多博文,都不太一样有的也跑不起来,通过尝试之后,自己可算折腾好了,整理一下过程 背景: 编译器主要有三部分:前…...

民国漫画杂志《时代漫画》第32期.PDF
时代漫画32.PDF: https://url03.ctfile.com/f/1779803-1248635561-0ae98a?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了,截止1937年6月战争来临被迫停刊共发行了39期。 ps: 资源来源网络!...

RTKLIB学习--前向滤波
#前言 如果要详细了解RTKLIB或进行二次开发,了解obs指针所存储每个历元的卫星观测数据是必不可少的环节,此文对RTKLIB的(由于后处理和实时运行都要用到前向滤波)前向滤波(从文件头读取观测数据到obs结构体中࿰…...

利用C++与Python调用千帆免费大模型,构建个性化AI对话系统
千帆大模型已于2024年4月25日正式免费,调用这个免费的模型以实现自己的AI对话功能,遵循以下步骤: 了解千帆大模型: 千帆大模型是百度智能云推出的一个平台,提供了一系列AI能力和工具,用于快速开发和应用A…...

VTK9.2.0+QT5.14.0绘制三维显示背景
背景 上一篇绘制点云的博文中,使用的vtkCameraOrientationWidget来绘制的坐标轴,最近又学习到两种新的坐标轴绘制形式。 vtkOrientationMarkerWidget vtkAxesActor 单独使用vtkAxesActor能够绘制出坐标轴,但是会随着鼠标操作旋转和平移时…...

Vue.js2+Cesium1.103.0 十六、多模型轨迹运动
Vue.js2Cesium1.103.0 十六、多模型轨迹运动 Demo <template><div id"cesium-container" style"width: 100%; height: 100%;"><ul class"ul"><li v-for"(item, index) of deviceInfo" :key"index" cl…...

Matlab|基于PMU相量测量单元进行电力系统电压幅值和相角状态估计
主要内容 程序采用三种方法对14节点和30节点电力系统状态进行评估: ①PMU同步相量测量单元结合加权最小二乘法(WLS)分析电力系统的电压幅值和相角状态; ②并采用牛顿-拉夫逊方法进行系统潮流计算,结果作为理论分…...

【C++】---二叉搜索树
【C】---二叉搜索树 一、二叉搜索树概念二、二叉搜索树操作(非递归)1.二叉搜索树的查找 (非递归)(1)查找(2)中序遍历 2.二叉搜索树的插入(非递归)3.二叉搜索树…...

FastAPI - 依赖注入3
在FastAPI中,依赖注入是一种强大的功能,它允许你轻松地将依赖项注入到你的路由处理程序函数中,以处理不同的任务,例如数据库访问、认证和配置管理。 FastAPI支持依赖注入通过以下方式: 使用参数注解: 你可…...

【网络运维的重要性】
🌈个人主页: 程序员不想敲代码啊 🏆CSDN优质创作者,CSDN实力新星,CSDN博客专家 👍点赞⭐评论⭐收藏 🤝希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共…...

YOLOv5改进 | 注意力机制 | 添加双重注意力机制 DoubleAttention【附代码/涨点能手】
💡💡💡本专栏所有程序均经过测试,可成功执行💡💡💡 在图像识别中,学习捕捉长距离关系是基础。现有的CNN模型通常通过增加深度来建立这种关系,但这种形式效率极低。因此&…...

自用网站合集
总览 线上工具-图片压缩 TinyPNG线上工具-url参数解析 线上工具-MOV转GIF UI-Vant微信小程序版本其他-敏捷开发工具 Leangoo领歌 工具 线上工具-图片压缩 TinyPNG 不能超过5m,别的没啥缺点 线上工具-url参数解析 我基本上只用url参数解析一些常用的操作在线…...

【Golang】gin框架如何在中间件中捕获响应并修改后返回
【Golang】gin框架如何在中间件中捕获响应并修改后返回 本文讲述如何捕获中间件响应以及重写响应如果想在中间件中记录响应日志等操作,我们该如何获取响应数据呢?假如需要统一对响应数据做加密,如何修改这个返回数据再响应给客户端呢…...

电脑同时配置两个版本mysql数据库常见问题
1.配置时,要把bin中的mysql.exe和mysqld.exe 改个名字,不然两个版本会重复,当然,在初始化数据库的时候,如果时57版本的,就用mysql57(已经改名的)和mysqld57 代替 mysql 和 mysqld 例如 mysql -u root -p …...

Java | Leetcode Java题解之第112题路径总和
题目: 题解: class Solution {public boolean hasPathSum(TreeNode root, int sum) {if (root null) {return false;}if (root.left null && root.right null) {return sum root.val;}return hasPathSum(root.left, sum - root.val) || has…...

HaloDB 的 Oracle 兼容模式
↑ 关注“少安事务所”公众号,欢迎⭐收藏,不错过精彩内容~ 前倾回顾 前面介绍了“光环”数据库的基本情况和安装办法。 哈喽,国产数据库!Halo DB! 三步走,Halo DB 安装指引 ★ HaloDB是基于原生PG打造的新一代高性能安…...

【Python】解决Python报错:TypeError: ‘xxx‘ object does not support item assignment
🧑 博主简介:阿里巴巴嵌入式技术专家,深耕嵌入式人工智能领域,具备多年的嵌入式硬件产品研发管理经验。 📒 博客介绍:分享嵌入式开发领域的相关知识、经验、思考和感悟,欢迎关注。提供嵌入式方向…...

Spring-注解
Spring 注解分类 Spring 注解驱动模型 Spring 元注解 Documented Retention() Target() // 可以继承相关的属性 Inherited Repeatable()Spirng 模式注解 ComponentScan 原理 ClassPathScanningCandidateComponentProvider#findCandidateComponents public Set<BeanDefin…...

旧手机翻身成为办公利器——PalmDock的介绍也使用
旧手机有吧!!! 破电脑有吧!!! 那恭喜你,这篇文章可能对你有点用了。 介绍 这是一个旧手机废物利用变成工作利器的软件。可以在 Android 手机上快捷打开 windows 上的文件夹、文件、程序、命…...

期货交易的雷区
一、做自己看不懂的行情做交易计划一样要做有把握的,倘若你在盘中找机会交易,做自己看不懂的行情,即便你做进去了,建仓时也不会那么肯定,自然而然持仓也不自信,有点盈利就想平仓,亏损又想扛单。…...