├── .gitignore ├── LICENSE ├── README.md ├── docs ├── chapter-01-introduction.md ├── chapter-02-builder.md ├── chapter-03-factories.md ├── chapter-04-prototype.md ├── chapter-05-singleton.md ├── chapter-06-adapter.md ├── chapter-07-bridge.md ├── chapter-08-composite.md ├── chapter-09-decorator.md ├── chapter-10-facade.md ├── chapter-11-flyweight.md ├── chapter-12-proxy.md ├── chapter-13-chain_of_responsibility.md ├── chapter-14-command.md ├── chapter-15-interpreter.md ├── chapter-16-iterator.md ├── chapter-17-Mediator.md ├── chapter-18-Memento.md ├── chapter-19-null_object.md ├── chapter-20-observer.md ├── chapter-21-state.md ├── chapter-22-strategy.md ├── chapter-23-template_method.md ├── chapter-24-visitor.md └── pics │ └── ch02_composite_builder.png └── src └── singleton.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution 4.0 International Public License 58 | 59 | By exercising the Licensed Rights (defined below), You accept and agree 60 | to be bound by the terms and conditions of this Creative Commons 61 | Attribution 4.0 International Public License ("Public License"). To the 62 | extent this Public License may be interpreted as a contract, You are 63 | granted the Licensed Rights in consideration of Your acceptance of 64 | these terms and conditions, and the Licensor grants You such rights in 65 | consideration of benefits the Licensor receives from making the 66 | Licensed Material available under these terms and conditions. 67 | 68 | 69 | Section 1 -- Definitions. 70 | 71 | a. Adapted Material means material subject to Copyright and Similar 72 | Rights that is derived from or based upon the Licensed Material 73 | and in which the Licensed Material is translated, altered, 74 | arranged, transformed, or otherwise modified in a manner requiring 75 | permission under the Copyright and Similar Rights held by the 76 | Licensor. For purposes of this Public License, where the Licensed 77 | Material is a musical work, performance, or sound recording, 78 | Adapted Material is always produced where the Licensed Material is 79 | synched in timed relation with a moving image. 80 | 81 | b. Adapter's License means the license You apply to Your Copyright 82 | and Similar Rights in Your contributions to Adapted Material in 83 | accordance with the terms and conditions of this Public License. 84 | 85 | c. Copyright and Similar Rights means copyright and/or similar rights 86 | closely related to copyright including, without limitation, 87 | performance, broadcast, sound recording, and Sui Generis Database 88 | Rights, without regard to how the rights are labeled or 89 | categorized. For purposes of this Public License, the rights 90 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 91 | Rights. 92 | 93 | d. Effective Technological Measures means those measures that, in the 94 | absence of proper authority, may not be circumvented under laws 95 | fulfilling obligations under Article 11 of the WIPO Copyright 96 | Treaty adopted on December 20, 1996, and/or similar international 97 | agreements. 98 | 99 | e. Exceptions and Limitations means fair use, fair dealing, and/or 100 | any other exception or limitation to Copyright and Similar Rights 101 | that applies to Your use of the Licensed Material. 102 | 103 | f. Licensed Material means the artistic or literary work, database, 104 | or other material to which the Licensor applied this Public 105 | License. 106 | 107 | g. Licensed Rights means the rights granted to You subject to the 108 | terms and conditions of this Public License, which are limited to 109 | all Copyright and Similar Rights that apply to Your use of the 110 | Licensed Material and that the Licensor has authority to license. 111 | 112 | h. Licensor means the individual(s) or entity(ies) granting rights 113 | under this Public License. 114 | 115 | i. Share means to provide material to the public by any means or 116 | process that requires permission under the Licensed Rights, such 117 | as reproduction, public display, public performance, distribution, 118 | dissemination, communication, or importation, and to make material 119 | available to the public including in ways that members of the 120 | public may access the material from a place and at a time 121 | individually chosen by them. 122 | 123 | j. Sui Generis Database Rights means rights other than copyright 124 | resulting from Directive 96/9/EC of the European Parliament and of 125 | the Council of 11 March 1996 on the legal protection of databases, 126 | as amended and/or succeeded, as well as other essentially 127 | equivalent rights anywhere in the world. 128 | 129 | k. You means the individual or entity exercising the Licensed Rights 130 | under this Public License. Your has a corresponding meaning. 131 | 132 | 133 | Section 2 -- Scope. 134 | 135 | a. License grant. 136 | 137 | 1. Subject to the terms and conditions of this Public License, 138 | the Licensor hereby grants You a worldwide, royalty-free, 139 | non-sublicensable, non-exclusive, irrevocable license to 140 | exercise the Licensed Rights in the Licensed Material to: 141 | 142 | a. reproduce and Share the Licensed Material, in whole or 143 | in part; and 144 | 145 | b. produce, reproduce, and Share Adapted Material. 146 | 147 | 2. Exceptions and Limitations. For the avoidance of doubt, where 148 | Exceptions and Limitations apply to Your use, this Public 149 | License does not apply, and You do not need to comply with 150 | its terms and conditions. 151 | 152 | 3. Term. The term of this Public License is specified in Section 153 | 6(a). 154 | 155 | 4. Media and formats; technical modifications allowed. The 156 | Licensor authorizes You to exercise the Licensed Rights in 157 | all media and formats whether now known or hereafter created, 158 | and to make technical modifications necessary to do so. The 159 | Licensor waives and/or agrees not to assert any right or 160 | authority to forbid You from making technical modifications 161 | necessary to exercise the Licensed Rights, including 162 | technical modifications necessary to circumvent Effective 163 | Technological Measures. For purposes of this Public License, 164 | simply making modifications authorized by this Section 2(a) 165 | (4) never produces Adapted Material. 166 | 167 | 5. Downstream recipients. 168 | 169 | a. Offer from the Licensor -- Licensed Material. Every 170 | recipient of the Licensed Material automatically 171 | receives an offer from the Licensor to exercise the 172 | Licensed Rights under the terms and conditions of this 173 | Public License. 174 | 175 | b. No downstream restrictions. You may not offer or impose 176 | any additional or different terms or conditions on, or 177 | apply any Effective Technological Measures to, the 178 | Licensed Material if doing so restricts exercise of the 179 | Licensed Rights by any recipient of the Licensed 180 | Material. 181 | 182 | 6. No endorsement. Nothing in this Public License constitutes or 183 | may be construed as permission to assert or imply that You 184 | are, or that Your use of the Licensed Material is, connected 185 | with, or sponsored, endorsed, or granted official status by, 186 | the Licensor or others designated to receive attribution as 187 | provided in Section 3(a)(1)(A)(i). 188 | 189 | b. Other rights. 190 | 191 | 1. Moral rights, such as the right of integrity, are not 192 | licensed under this Public License, nor are publicity, 193 | privacy, and/or other similar personality rights; however, to 194 | the extent possible, the Licensor waives and/or agrees not to 195 | assert any such rights held by the Licensor to the limited 196 | extent necessary to allow You to exercise the Licensed 197 | Rights, but not otherwise. 198 | 199 | 2. Patent and trademark rights are not licensed under this 200 | Public License. 201 | 202 | 3. To the extent possible, the Licensor waives any right to 203 | collect royalties from You for the exercise of the Licensed 204 | Rights, whether directly or through a collecting society 205 | under any voluntary or waivable statutory or compulsory 206 | licensing scheme. In all other cases the Licensor expressly 207 | reserves any right to collect such royalties. 208 | 209 | 210 | Section 3 -- License Conditions. 211 | 212 | Your exercise of the Licensed Rights is expressly made subject to the 213 | following conditions. 214 | 215 | a. Attribution. 216 | 217 | 1. If You Share the Licensed Material (including in modified 218 | form), You must: 219 | 220 | a. retain the following if it is supplied by the Licensor 221 | with the Licensed Material: 222 | 223 | i. identification of the creator(s) of the Licensed 224 | Material and any others designated to receive 225 | attribution, in any reasonable manner requested by 226 | the Licensor (including by pseudonym if 227 | designated); 228 | 229 | ii. a copyright notice; 230 | 231 | iii. a notice that refers to this Public License; 232 | 233 | iv. a notice that refers to the disclaimer of 234 | warranties; 235 | 236 | v. a URI or hyperlink to the Licensed Material to the 237 | extent reasonably practicable; 238 | 239 | b. indicate if You modified the Licensed Material and 240 | retain an indication of any previous modifications; and 241 | 242 | c. indicate the Licensed Material is licensed under this 243 | Public License, and include the text of, or the URI or 244 | hyperlink to, this Public License. 245 | 246 | 2. You may satisfy the conditions in Section 3(a)(1) in any 247 | reasonable manner based on the medium, means, and context in 248 | which You Share the Licensed Material. For example, it may be 249 | reasonable to satisfy the conditions by providing a URI or 250 | hyperlink to a resource that includes the required 251 | information. 252 | 253 | 3. If requested by the Licensor, You must remove any of the 254 | information required by Section 3(a)(1)(A) to the extent 255 | reasonably practicable. 256 | 257 | 4. If You Share Adapted Material You produce, the Adapter's 258 | License You apply must not prevent recipients of the Adapted 259 | Material from complying with this Public License. 260 | 261 | 262 | Section 4 -- Sui Generis Database Rights. 263 | 264 | Where the Licensed Rights include Sui Generis Database Rights that 265 | apply to Your use of the Licensed Material: 266 | 267 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 268 | to extract, reuse, reproduce, and Share all or a substantial 269 | portion of the contents of the database; 270 | 271 | b. if You include all or a substantial portion of the database 272 | contents in a database in which You have Sui Generis Database 273 | Rights, then the database in which You have Sui Generis Database 274 | Rights (but not its individual contents) is Adapted Material; and 275 | 276 | c. You must comply with the conditions in Section 3(a) if You Share 277 | all or a substantial portion of the contents of the database. 278 | 279 | For the avoidance of doubt, this Section 4 supplements and does not 280 | replace Your obligations under this Public License where the Licensed 281 | Rights include other Copyright and Similar Rights. 282 | 283 | 284 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 285 | 286 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 287 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 288 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 289 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 290 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 291 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 292 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 293 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 294 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 295 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 296 | 297 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 298 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 299 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 300 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 301 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 302 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 303 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 304 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 305 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 306 | 307 | c. The disclaimer of warranties and limitation of liability provided 308 | above shall be interpreted in a manner that, to the extent 309 | possible, most closely approximates an absolute disclaimer and 310 | waiver of all liability. 311 | 312 | 313 | Section 6 -- Term and Termination. 314 | 315 | a. This Public License applies for the term of the Copyright and 316 | Similar Rights licensed here. However, if You fail to comply with 317 | this Public License, then Your rights under this Public License 318 | terminate automatically. 319 | 320 | b. Where Your right to use the Licensed Material has terminated under 321 | Section 6(a), it reinstates: 322 | 323 | 1. automatically as of the date the violation is cured, provided 324 | it is cured within 30 days of Your discovery of the 325 | violation; or 326 | 327 | 2. upon express reinstatement by the Licensor. 328 | 329 | For the avoidance of doubt, this Section 6(b) does not affect any 330 | right the Licensor may have to seek remedies for Your violations 331 | of this Public License. 332 | 333 | c. For the avoidance of doubt, the Licensor may also offer the 334 | Licensed Material under separate terms or conditions or stop 335 | distributing the Licensed Material at any time; however, doing so 336 | will not terminate this Public License. 337 | 338 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 339 | License. 340 | 341 | 342 | Section 7 -- Other Terms and Conditions. 343 | 344 | a. The Licensor shall not be bound by any additional or different 345 | terms or conditions communicated by You unless expressly agreed. 346 | 347 | b. Any arrangements, understandings, or agreements regarding the 348 | Licensed Material not stated herein are separate from and 349 | independent of the terms and conditions of this Public License. 350 | 351 | 352 | Section 8 -- Interpretation. 353 | 354 | a. For the avoidance of doubt, this Public License does not, and 355 | shall not be interpreted to, reduce, limit, restrict, or impose 356 | conditions on any use of the Licensed Material that could lawfully 357 | be made without permission under this Public License. 358 | 359 | b. To the extent possible, if any provision of this Public License is 360 | deemed unenforceable, it shall be automatically reformed to the 361 | minimum extent necessary to make it enforceable. If the provision 362 | cannot be reformed, it shall be severed from this Public License 363 | without affecting the enforceability of the remaining terms and 364 | conditions. 365 | 366 | c. No term or condition of this Public License will be waived and no 367 | failure to comply consented to unless expressly agreed to by the 368 | Licensor. 369 | 370 | d. Nothing in this Public License constitutes or may be interpreted 371 | as a limitation upon, or waiver of, any privileges and immunities 372 | that apply to the Licensor or You, including from the legal 373 | processes of any jurisdiction or authority. 374 | 375 | 376 | ======================================================================= 377 | 378 | Creative Commons is not a party to its public 379 | licenses. Notwithstanding, Creative Commons may elect to apply one of 380 | its public licenses to material it publishes and in those instances 381 | will be considered the “Licensor.” The text of the Creative Commons 382 | public licenses is dedicated to the public domain under the CC0 Public 383 | Domain Dedication. Except for the limited purpose of indicating that 384 | material is shared under a Creative Commons public license or as 385 | otherwise permitted by the Creative Commons policies published at 386 | creativecommons.org/policies, Creative Commons does not authorize the 387 | use of the trademark "Creative Commons" or any other trademark or logo 388 | of Creative Commons without its prior written consent including, 389 | without limitation, in connection with any unauthorized modifications 390 | to any of its public licenses or any other arrangements, 391 | understandings, or agreements concerning use of licensed material. For 392 | the avoidance of doubt, this paragraph does not form part of the 393 | public licenses. 394 | 395 | Creative Commons may be contacted at creativecommons.org. 396 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Design Patterns In Modern C++ 中文版翻译 2 | 3 | ### 动机 4 | 5 | 1. 本书的示例是用C++11、14、17和更高版本的现代C++编写的,有助于熟悉现代C++的语法。 6 | 2. 设计模式是编程经验的总结,广泛存在于工程实践中,牵扯出非常多的相关内容(比大家熟悉的单例模式为例,可以引出C++11后的多线程内存模型,除了用局部静态变量还可以用Acquire and Release栅栏, Sequentially Consistent 原子操作等无锁方式实现,以及folly中如何在工业实践中实现Singleton来管理多个Singletons),以此为线索梳理所学的知识。 7 | 3. 打算在原书的基础上补充大量的相关知识,如STL、Boost和folly中的设计模式,举例Leetcode题目中的设计模式,还有融入多线程并发情况下的一些例子。 8 | 9 | ### TODO 10 | 11 | - [x] Chapter01: Introduction. 我直接使用了[@soyoo的翻译结果](https://github.com/soyoo/design_patterns_in_modern_cpp_zh_sample) 12 | - [x] Chapter02: Builder。 13 | - [x] Chapter03: Factories. 涉及工厂方法、工厂、内部工厂、抽象工厂和函数工厂。 14 | - [x] Chapter04: Prototype. 原型模式,对深拷贝的实现做了一些讨论(拷贝构造函数和拷贝复制运算符;序列化和反序列化)。 15 | - [x] Chapter05: Singleton. 线程安全,单例的问题,控制反转和Monostate. 16 | - [x] Chapter06: Adapter. 额外补充了STL中queue的实现,提供了一个更安全和方法的Queue。需要了解boost库中的hash是怎么做的。 17 | - [x] Chapter07: Bridge. 增加了Pimpl编程技法的说明。 18 | - [x] Chapter08: Composite. 19 | - [x] Chapter09:Decorator. 涉及动态装饰器、静态装饰器 和 函数装饰器。 20 | - [x] Chapter10: Facade. 外观模式, 缓冲-视窗-控制台。 21 | - [x] Chapter11: Flyweight. 享元模式。Boost库中Flyweight的实现,以及Bimap 22 | - [x] Chapter12: Proxy. 智能指针、属性代理、虚代理和通信代理。 23 | - [x] Chapter13: Chain of Responsibility. 指针链;代理链涉及中介模式和观察者模式。 24 | - [x] Chapter14: Command. 25 | - [x] Chapter15: Interpreter.涉及编译原理里面的词法分析,语法分析,`Boost.spirit`的使用。后面会补充LeetCode上实现计算器的几道题目和正则表达式的题目,也许会增加`Lex/Yacc`工具的使用介绍,以及tinySQL解释器实现的简单解释。 26 | - [x] Chapter16: Iterator. STL库中的迭代器,涉及二叉树的迭代器,使用协程来简化迭代过程。 27 | - [x] Chapter17: Mediator. 28 | - [x] Chapter18: Memento. 29 | - [x] Chapter19: Nulll Object. 涉及到对代理模式和pimpl编程技法的运用,以及std::optional 30 | - [x] Chapter20: Observer. 属性观察者、模板观察者Observer\、可观察Observable\ 、依赖问题, 取消订阅与线程安全 和使用Boost.Signals2 来实现 Observer。 31 | - [x] Chapter21: State. 补充字符串匹配、例子 32 | - [x] Chapter22: Strategy. 动态策略和静态策略 33 | - [x] Chapter23: Template Method. 模版方法模式和策略模式的异同。 34 | - [x] Chapter24: Visitor. 入侵式、反射式、经典式的访问者的设计思路,std::visitor在variant类型上的访问。 35 | 36 | ## 补充 37 | 38 | 在原著的基础上补充了很多相关的东西, 39 | - 第5章-单例:补充了无锁的double-check实现。 40 | - 第6章-适配器:探讨了STL中queue的适配器设计,提供了一个更方便和更安全的`Queue`适配器实现。 41 | - 第7章-桥接:对C++中的Pimpl编程技法进行了补充,给出了在编译器方面的应用。 42 | - 第12章-代理:讨论C++中智能指针的实现,给出一个半线程安全的智能指针`shared_ptr`的实现。 43 | - 第20章-观察者:补充了由观察者模式衍生出来的发布-订阅模式,总结了消息队列使用注意事项,提供了2种用redis来实现消息队列的解决方案。 44 | 45 | ### 第5章-单例:无锁的double-check实现 46 | 47 | ```c++ 48 | class Singleton 49 | { 50 | protected: 51 | Singleton(); 52 | private: 53 | static std::mutex m_mutex; 54 | static std::atomic m_instance = nullptr; 55 | public: 56 | static Singleton* Singleton::getInstance() 57 | { 58 | Singleton* tmp = m_instance.load(std::memory_order_acquire); 59 | if (tmp == nullptr) 60 | { 61 | //std::scoped_lock(m_mutex); 62 | std::lock_guard lock(m_mutex); 63 | tmp = m_instance.load(std::memory_order_relaxed); 64 | if (tmp == nullptr) 65 | { 66 | tmp = new Singleton; 67 | m_instance.store(tmp, std::memory_order_release); 68 | } 69 | } 70 | return tmp; 71 | } 72 | Singleton(const Singleton&) = delete; 73 | Singleton& operator=(const Singleton&) = delete; 74 | Singleton(Singleton&&) = delete; 75 | Singleton& operator=(Singleton&&) = delete; 76 | 77 | }; 78 | ``` 79 | ### 第6章-适配器:设计更安全方便的Queue 80 | 81 | STL中`queue`是一个FIFO队列,提供的核心接口函数为 82 | 83 | - push( ) : 插入一个元素到队列中 84 | - front( ) : 返回队首元素 85 | - back( ) : 返回队尾元素 86 | - pop( ) : 移除队首元素 87 | 88 | 89 | 我们可以看到在STL头文件``中`queue`中定义: 90 | 91 | ```c++ 92 | namespace std 93 | { 94 | template > 95 | class queue; 96 | } 97 | ``` 98 | 99 | 注意到第二个可选参数,说明在`queue`中默认使用`deuqe`作为实际存储`T`类的容器, 当然也可以使用任何提供成员函数`front()`、`back()`、`push_back()`和`pop_front()`的序列容器类,如`list`。标准库中的`queue`的实现更注重效率而不是方便和安全。这并不是适合于所有场合。我们可以在一个提供`deque`容器做出修改,适配出一个不同于标准库但符合自己风格的`Queue`。 100 | 101 | 下面实现的Queue提供了抛出异常的处理,以及返回队首元素的新的`pop`方法。 102 | 103 | ```c++ 104 | template > 105 | class Queue 106 | { 107 | protected: 108 | Container c; 109 | 110 | //异常类:在一个空队列中调用 pop() 和 front() 111 | class EmptyQueue : public exception 112 | { 113 | public: 114 | virtual const char* what const throw( ) 115 | { 116 | return "empty queue!"; 117 | } 118 | 119 | } 120 | 121 | typename Container::size_type size( ) const 122 | { 123 | return c.size( ); 124 | } 125 | 126 | bool empty( ) bool 127 | { 128 | return c.empty(); 129 | } 130 | 131 | void push( const T& item ) 132 | { 133 | c.push( item ); 134 | } 135 | 136 | void push( T&& item ) 137 | { 138 | c.push( item ); 139 | } 140 | 141 | const T& front( ) const 142 | { 143 | if( c.empty( ) ) 144 | throw EmptyQueue(); 145 | return c.front(); 146 | } 147 | 148 | T& front( ) 149 | { 150 | if( c.empty( ) ) 151 | throw EmptyQueue( ); 152 | return c.front( ); 153 | } 154 | 155 | const T& back( ) const 156 | { 157 | if( c.empty( ) ) 158 | throw EmptyQueue(); 159 | return c.back(); 160 | } 161 | 162 | T& front( ) 163 | { 164 | if( c.empty( ) ) 165 | throw EmptyQueue( ); 166 | return c.back( ); 167 | } 168 | 169 | // 返回队首元素 170 | T pop( ) const 171 | { 172 | if( c.empty() ) 173 | throw EmptyQueue(); 174 | 175 | T elem( c.front( ) ); 176 | c.pop(); 177 | return elem; 178 | } 179 | 180 | }; 181 | 182 | ``` 183 | 184 | ### 第7章-桥接:Pimpl编程技法-减少编译依赖 185 | 186 | PImpl(Pointer to implementation)是一种C++编程技术,其通过将类的实现的详细信息放在另一个单独的类中,并通过不透明的指针来访问。这项技术能够将实现的细节从其对象中去除,还能减少编译依赖。有人将其称为“编译防火墙(Compilation Firewalls)”。 187 | 188 | #### Pimpl技法的定义和用处 189 | 190 | 在C ++中,如果头文件类定义中的任何内容发生更改,则必须重新编译该类的所有用户-即使唯一的更改是该类用户甚至无法访问的私有类成员。这是因为C ++的构建模型基于文本包含(textual inclusion),并且因为C ++假定调用者知道一个类的两个主要方面,而这两个可能会受到私有成员的影响: 191 | 192 | - 因为类的私有数据成员参与其对象表示,影响大小和布局, 193 | - 也因为类的私有成员函数参与重载决议(这发生于成员访问检查之前),故对实现细节的任何更改都要求该类的所有用户重编译。 194 | 195 | 为了减少这些编译依赖性,一种常见的技术是使用不透明的指针来隐藏一些实现细节。这是基本概念: 196 | 197 | ```c++ 198 | // Pimpl idiom - basic idea 199 | class widget { 200 | // ::: 201 | private: 202 | struct impl; // things to be hidden go here 203 | impl* pimpl_; // opaque pointer to forward-declared class 204 | }; 205 | ``` 206 | 207 | 类widget使用了handle/body编程技法的变体。handle/body主要用于对一个共享实现的引用计数,但是它也具有更一般的实现隐藏用法。为了方便起见,从现在开始,我将widget称为“可见类”,将impl称为“ Pimpl类”。 208 | 209 | 编程技法的一大优势是,它打破了编译时的依赖性。首先,系统构建运行得更快,因为使用Pimpl可以消除额外的#include。我从事过一些项目,在这些项目中,仅将几个广为可见的类转换为使用Pimpls即可使系统的构建时间减少一半。其次,它可以本地化代码更改的构建影响,因为可以自由更改驻留在Pimpl中的类的各个部分,也就是可以自由添加或删除成员,而无需重新编译客户端代码。由于它非常擅长消除仅由于现在隐藏的成员的更改而导致的编译级联,因此通常被称为“编译防火墙”。 210 | 211 | #### Pimpl技法的实践 212 | 213 | 避免使用原生指针和显式的`delete`。要仅使用C++标准设施表达`Pimpl`,最合适的选择是通过`unique_ptr`来保存`Pimpl`对象,因为Pimpl对象唯一被可见类拥有。使用`unique_ptr`的代码很简单: 214 | 215 | ```c++ 216 | // in header file 217 | class widget { 218 | public: 219 | widget(); 220 | ~widget(); 221 | private: 222 | class impl; 223 | unique_ptr pimpl; 224 | }; 225 | 226 | // in implementation file 227 | class widget::impl { 228 | // ::: 229 | }; 230 | 231 | widget::widget() : pimpl{ new impl{ /*...*/ } } { } 232 | widget::~widget() { } // or =default 233 | 234 | ``` 235 | 236 | //TODO: handle/body编程技法 237 | 238 | #### 239 | 240 | ### 第12章-代理:实现一个半线程安全的智能指针 241 | 242 | 1. 智能指针(shared_ptr)线程安全吗? 243 | 244 | - ("half thread-safe") 245 | 246 | - 是: 引用计数控制单元线程安全, 保证对象只被释放一次 247 | 248 | - 否: 对于数据的读写没有线程安全 249 | 250 | 2. 如何将智能指针变成线程安全? 251 | 252 | - 使用 mutex 控制智能指针的访问 253 | - 使用全局非成员原子操作函数访问, 诸如: std::atomic_load(), atomic_store(), … 254 | - 缺点: 容易出错, 忘记使用这些操作 255 | - C++20: atomic>, atomic> std::atomic_shared_ptrand astd::atomic_weak_ptr. 256 | - 内部原理可能使用了 mutex 257 | - 全局非成员原子操作函数标记为不推荐使用(deprecated) 258 | 259 | 3. 数据竞争 260 | 261 | - 一个shared_ptr对象实体可以被多个线程同时读取 262 | - 两个shared_ptr对象实体可以被两个线程同时写入,"析构"算写操作 263 | - 如果要从多个线程读写同一个shared_ptr,那么需要加锁。 264 | 265 | 4. 代码实现 266 | 267 | ```c++ 268 | #include 269 | 270 | // 非完全线程安全的。 271 | // 引用计数 272 | template 273 | class ReferenceCounter 274 | { 275 | 276 | ReferenceCounter( ):count(0) {}; 277 | ReferenceCounter(const ReferenceCounter&) = delete; 278 | ReferenceCounter& operator=(const ReferenceCounter&) = delete; 279 | ReferenceCounter(ReferenceCounter&&) = default 280 | ReferenceCounter& operator=(ReferenceCounter&&) = default; 281 | // 前置++, 这里不提供后置,因为后置返回一个ReferenceCounter的拷贝,而之前禁止ReferenceCounter拷贝。 282 | ReferenceCounter& operator++() 283 | { 284 | count.fetch_add(1); 285 | return *this; 286 | } 287 | 288 | ReferenceCounter& operator--() 289 | { 290 | count.fetch_sub(1); 291 | return *this; 292 | } 293 | 294 | size_t getCount() const 295 | { 296 | return count.load(); 297 | } 298 | 299 | private: 300 | // 原子类型,或上锁 301 | std::atomic count; 302 | 303 | }; 304 | 305 | template 306 | class SharedPtr{ 307 | 308 | explicit SharedPtr(T* ptr_ = nullptr) : ptr(ptr_) { 309 | count = new ReferenceCounter(); 310 | if(ptr) 311 | { 312 | ++(*count); 313 | } 314 | } 315 | ~SharedPtr() { 316 | --(*count); 317 | if(count->getCount() == 0) 318 | { 319 | delete ptr; 320 | delete count; 321 | } 322 | } 323 | 324 | SharedPtr(const SharedPtr& other) : 325 | ptr(other.ptr), 326 | count(other.count) 327 | { 328 | ++(*count); 329 | } 330 | 331 | SharedPtr& operator= (const SharedPtr& other) 332 | { 333 | if(this != &other) 334 | { 335 | --(*count); 336 | if(count.getCount() == 0) 337 | { 338 | delete count; 339 | delete ptr; 340 | } 341 | ptr = other.ptr; 342 | count = other.count; 343 | ++(*count); 344 | } 345 | return *this; 346 | } 347 | 348 | SharedPtr(SharedPtr&& other) = default; 349 | SharedPtr& operator=(SharedPtr&& other) = default; 350 | 351 | T& operator*() const { return *ptr; } 352 | T* operator->() const{ 353 | return ptr; 354 | } 355 | T* get() const { return ptr; } 356 | 357 | int use_count() const { return count->getCount(); } 358 | 359 | bool unique() const { return use_count() == 1; } 360 | 361 | private: 362 | T* ptr; 363 | ReferenceCounter* count; 364 | } 365 | ``` 366 | 367 | 368 | 5. 重新回顾C++线程池中使用的虚调用方法 369 | 370 | 371 | ### 第20章:观察者模式 372 | 373 | 从观察者模式出发,了解了发布订阅模式,到消息队列,再到用redis实现消息队列以及实现消息队列的注意事项 374 | 375 | #### 观察者模式 376 | 377 | #### 发布订阅模式 378 | - 发布者和订阅者不直接关联,借助消息队列实现了松耦合,降低了复杂度,同时提高了系统的可伸缩性和可扩展性。 379 | - 异步 380 | 381 | #### 消息队列 382 | 383 | ##### 使用redis实现消息队列 384 | 385 | - 如何保证有序:FIFO数据结构 386 | - 如何保证不重复:全局Id(LIST法1:生产者和消费者约定好;STREAMS法二:消息队列自动产生(时间戳)) 387 | - 如何保证可靠性:备份 388 | 389 | |功能和适用场景|基于List|基于Streams| 备注 390 | |:- |:- |:- |:- | 391 | | 适用场景 |Redis5.0版本的部署环境,消息总量小| Redis5.0及以后的版本的部署环境,消息总量大,需要消费组形式读取数据 | | 392 | | 消息保序 |LPUSH/RPOP| XADD/XREAD | | 393 | | 阻塞读取 |BRPOP|XREAD block | 阻塞式读取在客户端没有读到队列数据时,自动阻塞,节省因不断peek的CPU开销 | 394 | | 重复消息处理|生产者自行实现全局唯一ID| STREAMs自行生成全局唯一ID | | 395 | | 消息可靠性|适用BRPOPRPUSH| 使用PENDING List 自动存留消息,使用XPENGDING查看,使用XACK确认消息 | | 396 | 397 | ##### 消息队列的注意事项 398 | 399 | 400 | 在使用消息队列时,重点需要关注的是如何保证不丢消息? 401 | 402 | 那么下面就来分析一下,哪些情况下,会丢消息,以及如何解决? 403 | 404 | 1、生产者在发布消息时异常: 405 | 406 | a) 网络故障或其他问题导致发布失败(直接返回错误,消息根本没发出去) 407 | b) 网络抖动导致发布超时(可能发送数据包成功,但读取响应结果超时了,不知道结果如何) 408 | 409 | 情况a还好,消息根本没发出去,那么重新发一次就好了。但是情况b没办法知道到底有没有发布成功,所以也只能再发一次。所以这两种情况,生产者都需要重新发布消息,直到成功为止(一般设定一个最大重试次数,超过最大次数依旧失败的需要报警处理)。这就会导致消费者可能会收到重复消息的问题,所以消费者需要保证在收到重复消息时,依旧能保证业务的正确性(设计幂等逻辑),一般需要根据具体业务来做,例如使用消息的唯一ID,或者版本号配合业务逻辑来处理。 410 | 411 | 2、消费者在处理消息时异常: 412 | 413 | 也就是消费者把消息拿出来了,但是还没处理完,消费者就挂了。这种情况,需要消费者恢复时,依旧能处理之前没有消费成功的消息。使用List当作队列时,也就是利用老师文章所讲的备份队列来保证,代价是增加了维护这个备份队列的成本。而Streams则是采用ack的方式,消费成功后告知中间件,这种方式处理起来更优雅,成熟的队列中间件例如RabbitMQ、Kafka都是采用这种方式来保证消费者不丢消息的。 414 | 415 | 3、消息队列中间件丢失消息 416 | 417 | 上面2个层面都比较好处理,只要客户端和服务端配合好,就能保证生产者和消费者都不丢消息。但是,如果消息队列中间件本身就不可靠,也有可能会丢失消息,毕竟生产者和消费这都依赖它,如果它不可靠,那么生产者和消费者无论怎么做,都无法保证数据不丢失。 418 | 419 | a) 在用Redis当作队列或存储数据时,是有可能丢失数据的:一个场景是,如果打开AOF并且是每秒写盘,因为这个写盘过程是异步的,Redis宕机时会丢失1秒的数据。而如果AOF改为同步写盘,那么写入性能会下降。另一个场景是,如果采用主从集群,如果写入量比较大,从库同步存在延迟,此时进行主从切换,也存在丢失数据的可能(从库还未同步完成主库发来的数据就被提成主库)。总的来说,Redis不保证严格的数据完整性和主从切换时的一致性。我们在使用Redis时需要注意。 420 | 421 | b) 而采用RabbitMQ和Kafka这些专业的队列中间件时,就没有这个问题了。这些组件一般是部署一个集群,生产者在发布消息时,队列中间件一般会采用写多个节点+预写磁盘的方式保证消息的完整性,即便其中一个节点挂了,也能保证集群的数据不丢失。当然,为了做到这些,方案肯定比Redis设计的要复杂(毕竟是专们针对队列场景设计的)。 422 | 423 | 综上,Redis可以用作队列,而且性能很高,部署维护也很轻量,但缺点是无法严格保数据的完整性(个人认为这就是业界有争议要不要使用Redis当作队列的地方)。而使用专业的队列中间件,可以严格保证数据的完整性,但缺点是,部署维护成本高,用起来比较重。 424 | 425 | 所以我们需要根据具体情况进行选择,如果对于丢数据不敏感的业务,例如发短信、发通知的场景,可以采用Redis作队列。如果是金融相关的业务场景,例如交易、支付这类,建议还是使用专业的队列中间件。 426 | 427 | ##### 消息队列实现方式对比 428 | 429 | 关于Redis是否适合做消息队列,业界一直存在争议。很多人认为使用消息队列就应该采用KafKa,RabbitMQ这些专门没安心消息队列场景的软件,而Redis更适合做缓存。 430 | 431 | 432 | |功能和适用场景|基于Redis|基于Kafka/RabbitMQ| 备注 433 | |:- |:- |:- |:- | 434 | | 适用场景 |对丢数据不敏感的业务,如发短信和通知| 严格数据完整的应用,金融相关业务场景,支付和交易 | | 435 | | 属性 |轻量级,部署和维护简单,性能高| 重量级 | | 436 | | 数据完整性 |不保证严格的数据完整性和主从切换的一致性| 部署集群,多结点+预写磁盘保证数据完整性 | | 437 | | 重复消息处理|生产者自行实现全局唯一ID| STREAMs自行生成全局唯一ID | | 438 | | 消息可靠性|适用BRPOPRPUSH| 使用PENDING List 自动存留消息,使用XPENGDING查看,使用XACK确认消息 | | 439 | 440 | ### 如何学习设计模式(即如何学习C++的面向对象思想) 441 | [该内容来自@franktea](https://github.com/franktea) 442 | #### 面向对象的三大特征 443 | 所有的人都知道,面向对象的三大特征是封装继承多态(这里的多态指的是运行时多态,本文中所有多态均指运行时多态),那么哪个特征才是面向对象最重要的特征呢? 444 | 445 | 先说封装。其实C语言的struct也是一种封装,所以封装并不是面向对象所独有。再看继承,继承可以非常方便地重用代码,相对面向过程来说是一种非常强大的功能,在面向对象刚被发明出来不久的一段时间里,继承被很多人看成面向对象最强大的特征。 446 | 447 | 到后来,人们发现面向对象最强大的特征是多态,因为代码不仅仅是需要重用,扩展也很重要。「设计模式」中,几乎每种模式,都是用多态来实现的。 448 | 449 | 一个问题:只支持多态,不支持继承的编程语言,算是面向对象的编程语言吗? 450 | 451 | 我的答案:不是。虽然继承不如多态重要,但是它不是多余的。多态往往是配合继承才更强大。 452 | 453 | #### 类的设计以及多态 454 | 第一点,前面说了,面向对象最重要的是多态,多态就是使用虚函数,在自己设计的类中,将哪些成员函数定义为虚函数,这是一个重要的问题。对于新手,我的建议是:在搞清规则之前,可以将所有的成员函数都定义为虚函数。(其实在java这样的编程语言中,根本不需要程序员自己去指定哪个成员函数是virtual,从语法上来说,任何一个非static非private的都是virtual。) 455 | 456 | 在虚函数的定义上,先将所有能定义成虚函数的的成员函数全部声明为virtual,然后再在使用中慢慢做减法,根据自己的理解,将多余的virtual去掉。 457 | 458 | 第二点,在使用面向对象的时候,尽量使用父类指针,而不是子类指针。100分的设计是永远使用父类指针、永远不使用子类指针。父类指针向子类指针转换需要用dynamic_cast,不使用dynamic_cast的设计就是最好的设计。新手可以遵循这个原则。 459 | 460 | 当然,在一些非常复杂的系统中,无法做到100分,有时候还是需要向下转换成子类指针,这样的设计肯定是扣分的,但是对于复杂系统肯定有一个平衡。我自己做服务器,所有设计都可以做到永远使用父类指针,但是对于复杂的像客户端unnreal代码,向下转换几乎不可避免。 461 | 462 | #### 虚函数表和虚函数指针 463 | 464 | 关于面向对象的语法知识,我唯一觉得需要强调的就是对于vtable的实现,推荐大家用实验的方法,一定要自己写代码亲自操作一遍(按照随便一篇vtable原理的文章动手操作一遍即可),无论是通过单步调试去查看vtable,还是通过编译器的各种命令来查看,都要自己亲自动手操作一下,加深印象。 465 | 466 | #### 补充 467 | gdb调试要点: 64位, 设置断点,打印虚表。 468 | ``` 469 | g++ 多继承有虚函数重写.cpp -o 多继承有虚函数重写 -m64 -g 470 | break 30 471 | set print pretty on 472 | info vtbl d 473 | ``` 474 | 475 | 476 | 477 | ### 一篇引文:从vtable到设计模式——我的C++面向对象学习心得 478 | 479 | [该内容来自@franktea](https://github.com/franktea) 480 | #### 前言 481 | 482 | 按照很多教程的内容安排,学习C++语法以后很快就会进入到面向对象的学习,在初学者的心中,面向对象有非常重要的地位。但是如何才能快速学习面向对象、多久学会面向对象才算正常,这是新手常见的问题。 483 | 484 | 面向对象的语法书上都有说,vtable的原理也有非常多的文章进行讲述,这些东西再一再重复没有意义,我想写一些我自己在学习过程中的心得体验。 485 | 486 | 关于面向对象的语法知识,我唯一觉得需要强调的就是对于vtable的实现,推荐大家用实验的方法,一定要自己写代码亲自操作一遍(按照随便一篇vtable原理的文章动手操作一遍即可),无论是通过单步调试去查看vtable,还是通过编译器的各种命令来查看,都要自己亲自动手操作一下,加深印象。 487 | 488 | 面向对象的语法风格出现以后,无数的程序员基于这些特性写出了很多代码,各显神通,后来被总结提取出一些可复用的方法论,叫做「设计模式」。设计模式是学习和掌握面向对象思想的重要课程。那么问题来了,何时学习设计模式?如何学习?在理解设计模式之前应该做什么、能做什么? 489 | 490 | #### 封装、继承、(运行时)多态,哪个才是面向对象最重要的特征 491 | 492 | 所有的人都知道,面向对象的三大特征是封装继承多态(这里的多态指的是运行时多态,本文中所有多态均指运行时多态),那么哪个特征才是面向对象最重要的特征呢? 493 | 494 | 先说封装。其实C语言的struct也是一种封装,所以封装并不是面向对象所独有。再看继承,继承可以非常方便地重用代码,相对面向过程来说是一种非常强大的功能,在面向对象刚被发明出来不久的一段时间里,继承被很多人看成面向对象最强大的特征。 495 | 496 | 到后来,人们发现面向对象最强大的特征是多态,因为代码不仅仅是需要重用,扩展也很重要。「设计模式」中,几乎每种模式,都是用多态来实现的。 497 | 498 | 一个问题:只支持多态,不支持继承的编程语言,算是面向对象的编程语言吗? 499 | 500 | 我的答案:不是。虽然继承不如多态重要,但是它不是多余的。多态往往是配合继承才更强大。 501 | 502 | #### 设计模式的意义 503 | 504 | 设计模式对于如何用面向对象的思想解决软件中的设计和实现问题提供了一些可重用的思路,它还有一个重要的意义,就是为每种设计思路都取了名字,便于程序员之间的交流。 505 | 506 | 有些人在设计类的名字的时候就包含了使用的设计模式,比如一个使用了adapter模式的类名字叫xxxAdapter;xxxFactory一看就知道它使用了factory模式,给其它使用和维护这些代码的人节省了大量的时间。 507 | 508 | #### 何时开始学习设计模式 509 | 510 | 知乎上见过一个问题:『你想对刚毕业的人说些什么』,这个问题就是一个刚踏入社会的小鲜肉,向在社会上摸爬滚打多年的人取经,想获得一些生存闯关的金句宝典,从而让自己少踩坑。 511 | 512 | 这样的问题的答案有意义吗?有一些是有的,可以直接理解,但是很多是要结合自己过去的经验教训才能有体会的,知道得早也没有什么收获。 513 | 514 | 如果面向对象的初学者也提问:「你想对刚学习面向对象的人说什么?」,答案就在设计模式这本书中。 515 | 516 | 所以何时开始学习设计模式呢?我的答案是任何时候都可以。但是唯一要注意的就是,不要强迫自己去理解,设计模式的书可以摆在那里,想看就看一下,能理解多少就理解多少。但是越早看设计模式这本书,共鸣就越少,因为共鸣是要结合自己写面向对象代码的经验的。 517 | 518 | 学习面向对象几年以后再看设计模式是否可以行? 519 | 520 | 我觉得可行,结合自己几年之内在学习各种面向对象的库和自己写代码的经验,学习设计模式会很快。 521 | 522 | 永远不学设计模式行不行? 523 | 524 | 我觉得不行,我前面提到了,设计模式不仅仅是总结思想,思想可以通过模仿现有的库来学习,但是设计模式还有一个重要的作用是给模式命名,命名可以更好地与其他程序员沟通交流。 525 | 526 | #### 如何学习设计模式(即如何学习C++的面向对象思想) 527 | 528 | 除了学习C++语法,还需要学习一下UML类图,不会的自己去搜,UML有好几种图,其中类图、状态图、序列图最为常用,学这3种即可。 529 | 530 | 在C++中可以通过学Qt库来学习面向对象。Qt除了可以用来写跨平台的UI,还可以写一些简单的网络程序,在学校里可以用来做各种大作业,无论是学生成绩管理系统、图书管理系统、足球俱乐部,等等,用Qt都可以很好地完成。我学Qt用的是这本书:<> 531 | 532 | Qt里面本身就用了很设计模式,从它的类里面继承一个子类,覆盖一个或几个虚函数,就可以将自己的类融入到Qt的体系中。其实这就是学习面向对象的第一步,也是最好的开始,不吃猪肉、先看猪跑,从它的类继承多了,自己也会慢慢理解如何从自己写的类继承。 533 | 534 | 学习面向对象有什么减少弯路又能加速理解的套路呢?根据我自己的经验总结,对于新手我至少可以说两点。 535 | 536 | 第一点,前面说了,面向对象最重要的是多态,多态就是使用虚函数,在自己设计的类中,将哪些成员函数定义为虚函数,这是一个重要的问题。对于新手,我的建议是:在搞清规则之前,可以将所有的成员函数都定义为虚函数。(其实在java这样的编程语言中,根本不需要程序员自己去指定哪个成员函数是virtual,从语法上来说,任何一个非static非private的都是virtual。) 537 | 538 | 在虚函数的定义上,先将所有能定义成虚函数的的成员函数全部声明为virtual,然后再在使用中慢慢做减法,根据自己的理解,将多余的virtual去掉。 539 | 540 | 第二点,在使用面向对象的时候,尽量使用父类指针,而不是子类指针。100分的设计是永远使用父类指针、永远不使用子类指针。父类指针向子类指针转换需要用dynamic_cast,不使用dynamic_cast的设计就是最好的设计。新手可以遵循这个原则。 541 | 542 | 当然,在一些非常复杂的系统中,无法做到100分,有时候还是需要向下转换成子类指针,这样的设计肯定是扣分的,但是对于复杂系统肯定有一个平衡。我自己做服务器,所有设计都可以做到永远使用父类指针,但是对于复杂的像客户端unnreal代码,向下转换几乎不可避免。 543 | 544 | #### 多久学会设计模式才算优秀 545 | 546 | 初学者都很急于求成,希望一天就能学会。但是从另一个角度来说,一天都能学会的东西,肯定不是什么有价值的东西。 547 | 548 | 我大概用了4年左右的时间,理解了面向对象。从大一开始学习C++,到大四毕业工作以后一年内设计出来了一个总共有一千多个类的系统,可以按照需求无限扩展。我现在可以设计任意多个类的系统。 549 | 550 | 我相信很多人比我更优秀,但是我更相信我自己的方法,我的学习方法其实就是不给自己设置时间期限,盲人摸象,今天摸这里明天摸那里,时间长了总会知道大象的全貌。 551 | 552 | 我是打算用几十年的时间从事编程的工作,到底是一天理解还是几年理解,对我来说并没有区别。至于做题、考试、工作等等,不用理解一样可以完成,按照现有的系统模仿即可。 553 | 554 | 我很清楚地记得,我第一次体会到面向对象的意义,是模仿MFC的一个机制。MFC在90年代的时候就做到了可以用字符串来动态创建一个对象(C++没有反射机制这在语法层面是无法做到的),MFC用的方法非常简单,将所有的类的名字和其构造函数放在一个全局的链表中,通过字符串在链表中去查找对应的构造函数,从而调用该构造函数new出对应的对象。 555 | 556 | 需要添加新的功能的时候,只要新添加一个.h一个.cpp,在两个文件中实现一个子类的代码,并调用宏将该类的构造函数添加到全局链表中。 557 | 558 | 通过添加新文件(一个.h和一个.cpp)的方法,不用修改之前的任何代码,就扩展了程序的功能,这就是面向对象的意义之一。 559 | 560 | 后来我在鹅厂做服务器,这个方法我一直使用,只是将链表改成了map或unordered_map。以后如果我找到合适的例子,我想通过例子说明此思想的应用,作为面向对象思想理解的入门级素材,我觉得挺好的,当然,那就是另外一篇文章了。 561 | 562 | #### 总结 563 | 564 | 设计模式是一些方法论,自己通过学习优秀的C++框架(如Qt)慢慢去体会和应用这些方法,最终可以慢慢理解。 565 | 566 | 不要刻意急于求成,人生很长,每一步都有它的意义,走过的路哪怕是弯路,都有它的意义。 567 | 568 | 在理解之前,注重于模仿,即使不理解,靠模仿已经能解决很多问题。 569 | 570 | 如果硬要问捷径是什么,我的答案就是抓紧时间多写代码,写了几万行代码就慢慢理解了。如果你不能改变几万行这个数字,那就去改变积累几万行代码的时间。比如说从3年缩短到2年,这完全是可能的。 571 | 572 | 我非常讨厌写长文,这篇文章在没有任何代码凑字数的情况下还是超过了3000字,也是源于我对面向对象思想的热爱,它帮我解决了很多问题,我现在用面向对象的思想来写代码,已经成了一件很自然的事情。 573 | 574 | 其实面向对象的思想在C++中并不是主流,自从90年代STL被作为标准库纳入C++那一刻起,泛型编程在C++里面就占据了上风,并且后来一直在迅速发展。同样的设计模式在C++中不仅仅可以用面向对象的思想实现,也可以用泛型编程的思想实现,不少时候后者可能更神奇更优雅更高效。 575 | 576 | 面向对象注重的是代码的扩展和维护,而不是高性能,在一些需要高性能的场合,像我所在的游戏领域需要优化性能的地方,不能用面向对象,以后如果我找到合适的例子作为素材,我会再写一篇「面向对象的缺点」的文章。 577 | 578 | ### 声明 579 | 580 | 译者纯粹出于学习目的与个人兴趣翻译本书,不追求任何经济利益。 581 | 582 | 本译文只供学习研究参考之用,不得公开传播发行或用于商业用途。有能力阅读英文书籍者请购买正版支持。 583 | 584 | ### 许可 585 | 586 | [CC-BY 4.0](LICENSE) 587 | 588 | 589 | ### 参考 590 | 591 | 1. [《Design Patterns In Modern C++》](https://book.douban.com/subject/30200080/) 592 | 2. [《The C++ Standard Library - A Tutorial and Reference, 2nd Edition》](http://cppstdlib.com/) 593 | -------------------------------------------------------------------------------- /docs/chapter-02-builder.md: -------------------------------------------------------------------------------- 1 | ### 建造者模式 2 | 3 | 建造者模式(`Builder`)涉及到复杂对象的创建,即不能在单行构造函数调用中构建的对象。这些类型的对象本身可能由其他对象组成,可能涉及不太明显的逻辑,需要一个专门用于对象构造的单独组件。 4 | 5 | 我认为值得事先注意的是,虽然我说建造者适用于复杂的对象的创建,但我们将看一个相当简单的示例。这样做纯粹是为了空间优化,这样领域逻辑的复杂性就不会影响读者欣赏模式实现的能力。 6 | 7 | #### 场景 8 | 9 | 让我们想象一下,我们正在构建一个呈现`web`页面的组件。首先,我们将输出一个简单的无序列表,其中有两个`item`,其中包含单词`hello`和`world`。一个非常简单的实现如下所示: 10 | 11 | ```c++ 12 | string words[] = { "hello", "world" }; 13 | ostringstream oss; 14 | oss << "
    "; 15 | for (auto w : words) 16 | oss << "
  • " << w << "
  • "; 17 | oss << "
"; 18 | printf(oss.str().c_str()) 19 | ``` 20 | 21 | 这实际上给了我们想要的东西,但是这种方法不是很灵活。如何将项目符号列表改为编号列表?在创建了列表之后,我们如何添加另一个`item`?显然,在我们这个死板的计划中,这是不可能的。 22 | 23 | 24 | 因此,我们可以通过`OOP`的方法定义一个`HtmlElement`类来存储关于每个`tag`的信息: 25 | 26 | 27 | ```c++ 28 | struct HtmlElement { 29 | string name; 30 | string text; 31 | vector elements; 32 | HtmlElement() {} 33 | HtmlElement(const string& name, const string& text) 34 | : name(name), text(text) {} 35 | 36 | string str(int indent = 0) const { 37 | // pretty-print the contents 38 | } 39 | }; 40 | ``` 41 | 42 | 有了这种方法,我们现在可以以一种更合理的方式创建我们的列表: 43 | 44 | ```c++ 45 | string words[] = {"hello", "world"}; 46 | HtmlElement list{"ul", ""}; 47 | for (auto w : words) list.elements.emplace_back{HtmlElement{"li", w}}; 48 | printf(list.str().c_str()); 49 | ``` 50 | 51 | 这做得很好,并为我们提供了一个更可控的、`OOP`驱动的条目列表表示。但是构建每个`HtmlElement`的过程不是很方便,我们可以通过实现建造者模式来改进它。 52 | 53 | #### 简单建造者 54 | 55 | 建造者模式只是试图将对象的分段构造放到一个单独的类中。我们的第一次尝试可能会产生这样的结果: 56 | 57 | ```c++ 58 | struct HtmlBuilder { 59 | HtmlElement root; 60 | 61 | HtmlBuilder(string root_name) { root.name = root_name; } 62 | 63 | void add_child(string child_name, string child_text) { 64 | HtmlElement e{child_name, child_text}; 65 | root.elements.emplace_back(e); 66 | } 67 | 68 | string str() { return root.str(); } 69 | }; 70 | ``` 71 | 72 | 这是一个用于构建`HTML`元素的专用组件。`add_child()`方法是用来向当前元素添加额外的子元素的方法,每个子元素都是一个名称-文本对。它可以如下使用: 73 | 74 | ```c++ 75 | HtmlBuilder builder{ "ul" }; 76 | builder.add_child("li", "hello"); 77 | builder.add_child("li", "world"); 78 | cout << builder.str() << endl; 79 | ``` 80 | 81 | 你会注意到,此时`add_child()`函数是返回空值的。我们可以使用返回值做许多事情,但返回值最常见的用途之一是帮助我们构建流畅的接口。 82 | 83 | 84 | #### 流畅的建造者 85 | 86 | 让我们将`add_child()`的定义改为如下: 87 | 88 | ```c++ 89 | HtmlBuilder& add_child(string child_name, string child_text) { 90 | HtmlElement e{child_name, child_text}; 91 | root.elements.emplace_back(e); 92 | return *this; 93 | } 94 | ``` 95 | 96 | 通过返回对建造者本身的引用,现在可以在建造者进行链式调用。这就是所谓的流畅接口(`fluent interface`): 97 | 98 | ```c++ 99 | HtmlBuilder builder{ "ul" }; 100 | builder.add_child("li", "hello").add_child("li", "world"); 101 | cout << builder.str() << endl; 102 | ``` 103 | 104 | 引用或指针的选择完全取决于你。如果你想用`->`操作符,可以像这样定义`add_child()` 105 | 106 | ```c++ 107 | HtmlBuilder* add_child(string child_name, string child_text) { 108 | HtmlElement e{child_name, child_text}; 109 | root.elements.emplace_back(e); 110 | return this; 111 | } 112 | ``` 113 | 114 | 像这样使用: 115 | 116 | ```c++ 117 | HtmlBuilder builder = new HtmlBuilder{ "ul" }; 118 | builder->add_child("li", "hello")->add_child("li", "world"); 119 | cout << builder->str() << endl; 120 | ``` 121 | 122 | #### 交流意图 123 | 124 | 我们为`HTML`元素实现了一个专用的建造者,但是我们类的用户如何知道如何使用它呢?一种想法是,只要他们构造对象,就强制他们使用建造者。你需要这样做: 125 | 126 | ```c++ 127 | struct HtmlElement { 128 | string name; 129 | string text; 130 | vector elements; 131 | const size_t indent_size = 2; 132 | static unique_ptr build(const string& root_name) { 133 | return make_unique(root_name); 134 | } 135 | 136 | protected: // hide all constructors 137 | HtmlElement() {} 138 | HtmlElement(const string& name, const string& text) 139 | : name{name}, text{text} {} 140 | }; 141 | ``` 142 | 143 | 我们的做法是双管齐下。首先,我们隐藏了所有的构造函数,因此它们不再可用。但是,我们已经创建了一个工厂方法(这是我们将在后面讨论的设计模式),用于直接从`HtmlElement`创建一个建造者。它也是一个静态方法。下面是如何使用它: 144 | 145 | ```c++ 146 | auto builder = HtmlElement::build("ul"); 147 | (*builder).add_child("li", "hello").add_child("li", "world"); 148 | cout << builder.str() << endl; 149 | ``` 150 | 151 | 但是不要忘记,我们的最终目标是构建一个`HtmlElement`,而不仅仅是它的建造者!因此,锦上添花的可能是建造者上的 `operator HtmlElement`的实现,以产生最终值: 152 | 153 | ```c++ 154 | struct HtmlBuilder { 155 | operator HtmlElement() const { return root; } 156 | HtmlElement root; 157 | // other operations omitted 158 | }; 159 | ``` 160 | 161 | 前面的一个变体是返回std::move(root),但是否这样做实际上取决于你自己。不管怎样,运算符的添加允许我们写下以下内容: 162 | 163 | ```c++ 164 | HtmlElement e = *(HtmlElement::build("ul")) 165 | .add_child("li", "hello") 166 | .add_child("li", "world"); 167 | cout << e.str() << endl; 168 | ``` 169 | 170 | 遗憾的是,没有办法明确地告诉其他用户以这种方式使用API。对构造函数的限制加上静态`build()`函数的存在,希望用户能够使用构造函数,但是,除了操作符之外,还可以向`HtmlBuilder`本身添加一个相应的`build()`函数: 171 | 172 | ```c++ 173 | HtmlElement HtmlBuilder::build() const { 174 | return root; // again, std::move possible here 175 | } 176 | ``` 177 | #### Groovy风格的建造者 178 | 179 | 这个例子稍微偏离了专用建造者,因为实际上没有看到任何建造者。它只是一种对象构造的替代方法。 180 | 181 | 诸如`Groovy、Kotlin`等编程语言都试图通过支持使构建过程更好的语法结构来展示它们在构建`DSL`方面有多么出色。但是为什么c++应该有所不同呢?多亏了初始化列表,我们可以使用普通的类有效地构建一个兼容`HTML`的`DSL` 182 | 183 | 首先,我们将定义一个`HTML`标签: 184 | 185 | ```c++ 186 | struct Tag { 187 | std::string name; 188 | std::string text; 189 | std::vector children; 190 | std::vector> attributes; 191 | friend std::ostream& operator<<(std::ostream& os, const Tag& tag) { 192 | // implementation omitted 193 | } 194 | }; 195 | ``` 196 | 197 | 到目前为止,我们已经有了一个可以存储其名称、文本、子标签(内部标签),甚至`HTML`属性的标签。我们也有一些打印代码,但在这里显示太无聊了。 198 | 199 | 现在我们可以给它提供两个在保护字段的构造函数(因为我们不希望任何人直接实例化它)。我们之前的实验告诉我们,我们至少有两种情况: 200 | 201 | - 一个由名称和文本初始化的标签(例如,一个列表项) 202 | - 一个由名称和一组子元素初始化的标签 203 | 204 | 第二种情况更有趣;我们将使用一个`std::vector`类型的形参 205 | 206 | ```c++ 207 | struct Tag { 208 | ... protected : Tag(const std::string& name, const std::string& text) 209 | : name{name}, text{text} {} 210 | Tag(const std::string& name, const std::vector& children) 211 | : name{name}, children{children} {} 212 | }; 213 | ``` 214 | 215 | 现在,我们可以继承这个标签类,但仅限于有效的`HTML`标签(因此限制了我们的DSL)。让我们定义两个标签:一个用于段落,另一个用于图像 216 | 217 | ```c++ 218 | struct P : Tag { 219 | explicit P(const std::string& text) : Tag{"p", text} {} 220 | 221 | P(std::initializer_list children) : Tag("p", children) {} 222 | }; 223 | struct IMG : Tag { 224 | explicit IMG(const std::string& url) : Tag{"img", ""} { 225 | attributes.emplace_back({"src", url}); 226 | } 227 | }; 228 | ``` 229 | 230 | 前面的构造函数进一步约束了我们的API。根据前面的构造函数,段落只能包含文本或一组子元素。另一方面,图像不能包含其他标记,但必须具有一个名为img的属性,该属性具有提供的地址。 231 | 232 | 现在,由于统一初始化和派生的所有构造函数,我们可以写以下内容: 233 | 234 | ```c++ 235 | std::cout << P{IMG{"http://pokemon.com/pikachu.png"}} << std::endl; 236 | ``` 237 | 238 | 这不是很棒吗?我们已经为段落和图像构建了一个小型`DSL`,这个模型可以很容易地扩展以支持其他标签。并且没有看到`add_child()`调用。 239 | 240 | 241 | 242 | #### 组合建造者 243 | 244 | 我们将通过一个使用多个建造者构建单个对象的例子来结束对建造者的讨论。假设我们决定记录关于一个人的一些信息: 245 | 246 | ```c++ 247 | class Person { 248 | // address 249 | std::string street_address, post_code, city; 250 | 251 | // employment 252 | std::string company_name, position; 253 | int annual_income = 0; 254 | Person() {} 255 | }; 256 | ``` 257 | 258 | `Person`的成员变量中包含:地址信息和就业信息。如果我们想为这两类信息提供单独的建造者,我们如何提供最方便的API呢?为此,我们将构建一个复合建造者。这个构造不是简单的,所以请注意,即使我们想为就业和地址信息创建不同的建造者,我们也会生成不少于四个不同的类。 259 | 260 | 我在本书中选择完全避免使用 `UML`,但这是类图有意义的一种情况,所以这是我们实际要构建的内容: 261 | 262 | ![ch02_composite_builder](pics/ch02_composite_builder.png) 263 | 264 | 我们将调用第一个类 `PersonBuilderBase`: 265 | 266 | ```c++ 267 | class PersonBuilderBase { 268 | protected: 269 | Person& person; 270 | explicit PersonBuilderBase(Person& person) : person{person} {} 271 | 272 | public: 273 | operator Person() { return std::move(person); } 274 | // builder facets 275 | PersonAddressBuilder lives() const; 276 | PersonJobBuilder works() const; 277 | }; 278 | ``` 279 | 280 | 这比我们之前的简单 `Builder` 要复杂得多,所以让我们依次讨论每个成员。 281 | 282 | - 引用 `person` 是对正在构建的对象的引用。这可能看起来很奇怪,但它是为子建造者特意完成的。请注意,此类中不存在 `Person` 的物理存储。这很关键!根类只持有一个引用,而不是构造的对象。 283 | - 引用拷贝构造函数受到保护,因此只有继承者(`PersonAddressBuilder` 和 `PersonJobBuilder`)可以使用它。 284 | - 运算符 `Person` 是我们之前使用过的一个技巧。 假设 `Person` 具有正确定义的移动构造函数 - `ReSharper` 轻松生成一个。 285 | - `life()` 和 `works()` 是返回建造者方面的函数:那些分别初始化地址和就业信息的子建造者。 286 | 287 | 现在,上一个基类中唯一缺少的是正在构造的实际对象。它在哪里?嗯,它实际上存储在我们称之为`PersonBuilder`的继承者中。这是我们希望人们实际使用的类: 288 | 289 | ```c++ 290 | class PersonBuilder : public PersonBuilderBase { 291 | Person p; // object being built 292 | public: 293 | PersonBuilder() : PersonBuilderBase{p} {} 294 | }; 295 | ``` 296 | 297 | 所以这是构建对象实际构建的地方。这个类不是要继承的:它只是一个实用程序,可以让我们启动设置建造者的过程。[1] 298 | 299 | 注释1: GitHub 上的@CodedByATool 建议采用这种将层次结构分成两个单独的基类以避免重复 Person 实例的方法 - 感谢你的想法! 300 | 301 | 为了找出我们最终使用不同的公共和受保护构造函数的确切原因,让我们看看其中一个子创建者的实现: 302 | 303 | ```c++ 304 | class PersonAddressBuilder : public PersonBuilderBase { 305 | typedef PersonAddressBuilder self; 306 | 307 | public: 308 | explicit PersonAddressBuilder(Person& person) : PersonBuilderBase{person} {} 309 | self& at(std::string street_address) { 310 | person.street_address = street_address; 311 | return *this; 312 | } 313 | self& with_postcode(std::string post_code){...} self& in(std::string city) { 314 | ... 315 | } 316 | }; 317 | ``` 318 | 319 | 如你所见,`PersonAddressBuilder` 提供了流畅的界面 320 | 用于建立一个人的地址。请注意,它实际上是从 `PersonBuilderBase` 继承的(意味着它已经获得了 `life()` 和 `works()` 成员函数)并调用了基构造函数,传递了一个引用。 321 | 322 | 不过它并没有从 PersonBuilder 继承——如果是这样,我们会创建太多的 Person 实例,实际上,我们只需要一个。 323 | 324 | 你可以猜到,`PersonJobBuilder` 是用相同的方式实现的。这两个类以及 `PersonBuilder` 都在 `Person` 内部声明为友元类,以便能够访问其私有成员。 325 | 326 | 现在,你一直在等待的那一刻:这些建设者在行动的例子: 327 | 328 | ```c++ 329 | Person p = Person::create() 330 | .lives() 331 | .at("123 London Road") 332 | .with_postcode("SW1 1GB") 333 | .in("London") 334 | .works() 335 | .at("PragmaSoft") 336 | .as_a("Consultant") 337 | .earning(10e6); 338 | ``` 339 | 340 | 你能看到这里发生了什么吗?我们使用 `create()` 函数为我们自己创建一个创建者,并使用 `life()` 函数为我们获取一个 `PersonAddressBuilder`,但是一旦我们完成了地址信息的初始化,我们只需调用 `works()` 并切换到使用 `PersonJobBuilder`。 341 | 342 | 当我们完成构建过程时,我们使用相同的技巧像以前一样将对象构建为 `Person`。请注意,一旦完成,建造者将无法使用,因为我们使用 `std::move()` 移动了 `Person`。 343 | 344 | #### 总结 345 | 346 | 建造者模式的目标是定义一个完全用于分段构造复杂对象或一组对象的组件。我们已经观察到建造者的以下关键特征: 347 | 348 | - 建造者可以拥有流畅的接口,可用于使用单个调用链进行复杂的构造。为了支持这个,构造函数应该返回`this`或`*this`。 349 | - 为了强制`API`的用户使用一个构造器,我们可以使目标对象的构造器不可访问,然后定义一个静态的`create()`函数来返回这个构造器。 350 | - 通过定义适当的操作符,可以将构造器强制转换为对象本身。 351 | - 多亏了统一的初始化器语法,`groovy`风格的建造者在c++中是可能的。这种方法非常普遍,并且允许创建不同的领域特定语言(DSLs)。 352 | - 单个建造者接口可以公开多个子建造者。通过巧妙地使用继承和流畅接口,可以轻松地从一个建造者跳转到另一个。 353 | 354 | 重申一下我已经提到过的内容,当对象的构造是一个重要的过程时,使用建造者模式是有意义的。由有限数量的合理命名的构造函数参数明确构造的简单对象可能应该使用构造函数(或依赖注入),而不需要这样的建造者。 -------------------------------------------------------------------------------- /docs/chapter-03-factories.md: -------------------------------------------------------------------------------- 1 | ### 第三章 工厂 2 | 3 | > I had a problem and tried to use Java, now I have a 4 | ProblemFactory. –Old Java joke. 5 | 6 | 本章同时覆盖了GoF中的两个设计模式:工厂方法和抽象工厂。因为这两个设计模式是紧密相关的,所以接下来我们讲它们合在一起讨论。 7 | 8 | #### 场景 9 | 10 | 让我们从一个实际的例子开始吧!假设你想要存储笛卡尔空间(Cartesian space)下某个点的信息,你可以直接了当像下面这样实现: 11 | 12 | ```c++ 13 | struct Point 14 | { 15 | Point(const float x, const float y): 16 | x{x}, y{y} {} 17 | float x, y; // 严格的直角坐标表示 18 | }; 19 | ``` 20 | 21 | 目前为止,一切都表现良好。但是现在你也想通过极坐标(Polar coordinates )的方式来初始化这个点。你需要提供另外版本的构造函数: 22 | 23 | ```c++ 24 | Point(const float r, const float theta) 25 | { 26 | x = r * cos(theta); 27 | y = r * sin(theta); 28 | } 29 | ``` 30 | 31 | 但是不幸的是,上面的两个构造函数的函数签名是相同的[^1],在c++语言里面是不允许这样的函数重载出现的。那么你打算怎么做呢?一种方法是引入一个枚举类型: 32 | 33 | 注1:一些编程语言,尤其是Objective-C和Swift,确实允许重载仅因参数名不同的函数。不幸的是,这种想法导致了所有调用中参数名的病毒式传播。大多数时候,我仍然更喜欢位置参数。 34 | 35 | ```c++ 36 | enum class PointType 37 | { 38 | cartesian 39 | polar 40 | }; 41 | ``` 42 | 43 | 在原来的构造函数上增加一个额外的参数。 44 | 45 | ```c++ 46 | Point(float a, float b, PointType type = PointType::cartesian) 47 | { 48 | if(type == PointType::cartesian) 49 | { 50 | x = a; 51 | y = b; 52 | } 53 | else 54 | { 55 | x = a * cos(b); 56 | y = a * sin(b); 57 | } 58 | } 59 | ``` 60 | 61 | 注意构造函数的前两个参数的名称被为了a和b, 我们无法告诉用户a和b的值应该来自哪个坐标系。与使用x、y、rho和theta这种清晰的名称来传达构造意图相比,这显然缺乏表现力。 62 | 63 | 总之,我们的构造函数的设计在功能上是可用的,但是在形式上是丑陋的。接下来让我们看看能否在此基础上改进。 64 | 65 | 66 | #### 工厂方法 67 | 68 | 上述构造函数的问题在于它的名称总是与类型匹配。这与普通函数不同,意味着我们不能用它来传递任何额外的信息。同时,给定名字总是相同的,我们不能有两个重载一个取x, y;而另一个取r, theta。 69 | 70 | 那么我们该怎么做呢? 把构造函数放入保护字段,然后暴露一些静态方法用于创建新的坐标点怎么样? 71 | 72 | ```c++ 73 | class Point 74 | { 75 | protected: 76 | Point(const float x, const float y): 77 | x{x}, y{y} {} 78 | public: 79 | static Point NewCartesian(float x, float y) 80 | { 81 | return {x, y}; 82 | } 83 | static Point NewPloar(float r, float theta) 84 | { 85 | return {r * cos(theta), r * sin(theta)}; 86 | } 87 | // 其他的成员 88 | private: 89 | float x; 90 | float y; 91 | } 92 | ``` 93 | 94 | 上面每一个静态方法都被称为**工厂方法**。它所做的只是创建一个点并返回它,其优点是方法的名称和参数的名称清楚地传达了需要哪种坐标。现在创建一个点,我们可以简单的这样写: 95 | 96 | ```c++ 97 | auto = Point::NewPloar(5, M_PI_4) 98 | ``` 99 | 100 | 从上面的代码看出,我们清楚的意识到我们正在用极坐标下$r=5, theta =\pi/4$的方式创建一个新的点。 101 | 102 | #### 工厂 103 | 104 | 就和建造者模式(Builder Pattern)一样,我们可以把Point类中所有的构建方法都从Point类中提取出来放置到单独一个类中,我么称之为该类为**工厂**。首先我么重新定义Point类: 105 | 106 | ```c++ 107 | struct Point 108 | { 109 | float x, y; 110 | friend class PointFactory; 111 | private: 112 | Point(float x, float y): 113 | x(x), y(y) {}; 114 | }; 115 | ``` 116 | 117 | 以上代码中有两点需要注意: 118 | 119 | - Point类的构造函数被放置在私有段,因为我么不希望其他类能直接调用该方法。这并不是一个严格的要求,但是将其声明为公有的,会造成一些歧义,因为它向用户提供了另外一种构造对象的方法,在这里的设计中,我们希望用户只能调用PointFactory中的方法构造对象。 120 | - Point中声明了PointFactory为其友元类。这样是有意为之的,以便Point的私有构造函数对PointFactory可用,否则,工厂将无法实例化对象!这里暗示这以上两种类型都是同时被创建的,而不在Point创建之后很久才创建工厂。 121 | 现在我们只需在另外一个名为PointFactory的类中定义我么的NewXxx()函数: 122 | 123 | ```c++ 124 | struct PointFactory 125 | { 126 | static Point NewCartesian(float x, float y) 127 | { 128 | return Point{x, y}; 129 | } 130 | static Point NewPolar(float r, float theta) 131 | { 132 | return Point{ r*cos(theta), r*sin(theta); }; 133 | } 134 | }; 135 | ``` 136 | 这样而来, 我们现在有了一个专门为创建点的实例而设计的类,可以像下面这样使用它: 137 | 138 | ```c++ 139 | auto PointFactory::NewCatesian(3, 4); 140 | ``` 141 | 142 | #### 内部工厂 143 | 144 | 内部工厂也是一个工厂,只不过它是它创建的类型的内部类。公平地说,内部工厂是c#、Java和其他缺乏friend关键字的语言的典型组件,但没有理由不能在c++中使用它。 145 | 146 | 内部工厂存在的原因是,内部类可以访问外部类的私有成员,反过来,外部类也可以访问内部类的私有成员。这意味着我们的Point类也可以这样定义: 147 | 148 | ```c++ 149 | struct Point 150 | { 151 | private: 152 | Point(float x, float y): 153 | x(x), y(y) {} 154 | 155 | struct PointFactory 156 | { 157 | static Point NewCartesian(float x, float y) 158 | { 159 | return Point{x, y}; 160 | } 161 | static Point NewPolar(float r, float theta) 162 | { 163 | return Point{ r*cos(theta), r*sin(theta); }; 164 | } 165 | }; 166 | 167 | public: 168 | float x, y; 169 | static PointFactory Factory; 170 | }; 171 | ``` 172 | 好的,让我们来看发生了什么?我们把工厂类放到了工厂创建的类中。如果一个工厂只工作于一个单一类型,那么这很方便,而如果一个工厂依赖于多个类型,那么就不那么方便了(如果还需要其他类的的私有成员,那就几乎不可能了)。 173 | 174 | 您会注意到我在这里有点狡猾:整个工厂都在一个私有块中,而且它的构造函数也被标记为私有。本质上,即使我们可以将这个工厂暴露为Point::PointFactory,这也相当拗口。相反,我定义了一个名为Factory的静态成员。这允许我们使用工厂作为 175 | 176 | ```c++ 177 | auto pp = Point::Factory.NewCartesian(2, 3); 178 | ``` 179 | 180 | 当然,如果出于某种原因,您不喜欢混合使用::和.,您可以向下面这样更改代码,以便在任何地方使用::。 181 | 182 | - 将工厂公开,这允许你编写如下形式的代码: 183 | 184 | ```c++ 185 | Point::PointFactory::NewXxx(...) 186 | ``` 187 | 188 | - 如果你不希望Point这个词在上面代码中出现两次,你可以使用 `typedef PointFactory Factory`,这样就能写成`Point::Factory::NewXxx(...)`的形式。这可能是人们能想到的最合理的语法了。或者干脆把内部工厂PointFactory改名为Factory,这样就一劳永逸地解决了问题,除非你决定以后把它提出来。 189 | 190 | 191 | 192 | 是否使用内部工厂很大程度上取决于个人组织代码风格。 然而,从原始对象公开工厂极大地提高了API的可用性。[:FIXME]如果我发现一个名为Point的类和一个私有构造函数,我如何能够知道这个类是要被使用的?除非Person::在代码完成列表中给我一些有意义的东西,否则我不会这么做。 193 | 194 | 195 | #### 抽象工厂 196 | 197 | 目前为止,我们一直在研究单个对象的构造。有时,你可能会涉及到一系列对象的构造。在实际中这种情况是非常少见的,同工厂方法和简单陈旧的工厂模式相比,抽象工厂模式只会出现在复杂的系统中。不管怎样,主要是由于历史的原因,我们都需要讨论它。 198 | 199 | 下面是一个简单的场景:假设你在一家只提供咖啡和茶的咖啡店里面工作。这两种热饮是通过完全不同的设备制成,我们可以把它们都建模成一个工厂。茶和饮料事实上可以分为热(hot)饮和冷(cold)饮,但是这里我们只关注热饮。首先我们把HotDrink定义成: 200 | 201 | ```c++ 202 | struct HotDrink 203 | { 204 | virtual void prepare(int volume) = 0; 205 | } 206 | ``` 207 | 208 | prepare函数的功能就是提供一定量(volume)的热饮。例如,对于茶这种类型的热饮,可以实现成这样: 209 | 210 | ```c++ 211 | struct Tea: HotDrink 212 | { 213 | void prepare(int volume) override 214 | { 215 | cout << "Take tea bag, boil water, pour " 216 | << volume 217 | << "ml, add some lemon" << endl; 218 | } 219 | }; 220 | ``` 221 | 222 | 对于咖啡类型的是实现也是相似的。现在,我们可以编写一个假想的make_drink函数,该函数通过饮料的名字来制作对应的饮料。给出一系列不同的类型,这看起来相当乏味: 223 | 224 | ```c++ 225 | unique_ptr make_drink(string type) 226 | { 227 | unique_ptr drink; 228 | if(type == "tea") 229 | { 230 | drink = make_unique(); 231 | drink->prepare(200); 232 | } 233 | else 234 | { 235 | drink = make_unique(); 236 | drink->prepare(50); 237 | } 238 | return drink; 239 | } 240 | ``` 241 | 242 | 现在请记住,不同的饮料是由不同的机器制造的。在我们的例子中,我们只对热饮感兴趣,我们可以构造一个HotDrinkfactory类来建模: 243 | 244 | ```c++ 245 | struct HotDrinkFactory 246 | { 247 | virtual unique_ptr make() const = 0; 248 | }; 249 | ``` 250 | 251 | 这种类型恰好是一个抽象工厂(Abstract Factory):它是有特定接口的一个工厂,但是它是抽象的,[FIXME]这意味着它只能作为函数参数, 例如,我们需要具体的实现来实际制作饮料。例如想要制作咖啡,我们可以写成: 252 | 253 | ```c++ 254 | struct CoffeeFactory:HotDrinkFactory 255 | { 256 | unique_ptr make() const override 257 | { 258 | return make_unique(); 259 | } 260 | } 261 | ``` 262 | 263 | TeaFactory的定义也与上面的CoffeeFactory相似。现在,假设我们想要定义更高层次的接口,用来制作不同类型的饮料,可以是热饮也可以是冷饮。我们可以创建一个名为DrinkFactory的工厂,它本身包含各种可用工厂的引用: 264 | 265 | ```c++ 266 | class DrinkFactory 267 | { 268 | map> hot_factories; 269 | DrinkFactory() 270 | { 271 | hot_factories["coffee"] = make_unique(); 272 | hot_factories["tea"] = make_unique(); 273 | } 274 | unique_ptr make_drink(const string &name) 275 | { 276 | auto drink = hot_factories[name]->make(); 277 | drink->prepare(200); // oops! 278 | return drink; 279 | } 280 | }; 281 | ``` 282 | 283 | 这里,我假设我们希望根据饮料的名称,不是某个整数或enum成员来制作饮料。我们只需创建字符串和相关工厂的映射(map):实际的工厂类型是HotDrinkFactory(我们的抽象工厂),并通过智能指针而不是直接存储它们(这很有意义,因为我们希望防止对象切片) 284 | 285 | 现在,如果你想喝一杯饮料时,你可以找到相关的工厂(想象一下咖啡店的店员走到正确的机器前),生产饮料,准备好需要的量(我在前面设置了一个常数;你可以随意将其提升为一个参数),就拿到了想要的饮料。嗯,就是这样。 286 | 287 | 288 | #### 函数工厂 289 | 290 | 我想提的最后一件事情是:当我们使用工厂(factory)这个词的时候,我们通常指下面两种情况中的一种: 291 | 292 | - 一个知道如何创建对象的类 293 | - 一个被调用时会创建对象的函数 294 | 295 | 第二种情况不仅仅是传统意义上的工厂方法。如果传递返回类型为T的std::function到某个函数中,这个被传递的函数通常被称为工厂,而不是工厂方法。这可能看起来有点奇怪,但如果考虑到方法与成员函数的含义是相同的,这样说会显得有意义点。 296 | 297 | 幸运的是,函数可以存储在变量中,这意味着不只是存储指向工厂的指针(就像我们之前在DrinkFactory中做的那样),我们可以把准备200毫升液体的过程内化到函数中。这是通过从工厂切换到简单的使用函数块来实现的,例如: 298 | 299 | ```c++ 300 | class DrinkWithVolumeFactory 301 | { 302 | map()>> factories; 303 | public: 304 | DrinkWithVolumeFactory() 305 | { 306 | factories["tea"] = []{ 307 | auto tea = make_unique(); 308 | tea->prepare(200); 309 | return tea; 310 | }; 311 | // 对应Coffee类也是类似的。 312 | } 313 | } 314 | ``` 315 | 316 | 当然,在采用了这种方法之后,我们现在只需要直接调用存储的工厂, 而不必在对象被构造出来之后再调用prepare方法。 317 | 318 | ```c++ 319 | inline unique_ptr 320 | DrinkWithVolumeFactory::make_drink(const string &name) 321 | { 322 | return factories[name]; 323 | } 324 | ``` 325 | 326 | 在使用方法上和以前是一样的。 327 | 328 | #### 总结 329 | 330 | 让我们来回顾下这章涉及到的术语: 331 | 332 | - 工厂方法(factory method)是类的成员函数,可以作为创建对象的一种方式,通常用来替代构造函数。 333 | - 工厂(factory)通常是知道如何创建对象的独立的类,尽管如果你传递构造对象的函数(std::function,函数指针或者函数对象)到某个函数里面,这个参数通常也被称为工厂。 334 | - 抽象工厂(abstract factory),顾名思义,是一个抽象类,可以被生产一系列对象的具体类所继承。抽象工厂在实际中很少见。 335 | 336 | 337 | 工厂相对于构造函数调用有下面几个关键的优势: 338 | 339 | - 工厂可以说“不”,这意味着除了选择返回一个对象外,它可以返回一个空指针(nullptr)。 340 | - 命名更有直观意义,且不受限,不像构造函数的函数名必须和类名相同。 341 | - 一个工厂能够生产出许多不同类型的对象。 342 | - 工厂能够表现出多态行为,实例化一个类并通过基类的引用或指针返回实例化后的对象。 343 | - 工厂能够实现缓存(caching)和其他存储优化,他也是其他方法,例如池或单例模式(更多参见第5章内容)实现的自然的选择。 344 | 345 | 工厂与建造者模式的不同之处在于,对于工厂,您通常一次性创建一个对象,而对于建造者,您通过部分地提供信息来分段地构造对象。 -------------------------------------------------------------------------------- /docs/chapter-04-prototype.md: -------------------------------------------------------------------------------- 1 | ### 原型模式 2 | 3 | 想想你每天都在使用的东西,比如汽车、手机。很有可能,它不是从零开始设计的; 恰恰相反,制造商选择了一个现有的设计,做了一些改进,使它在视觉上有别于旧的设计(以便人们可以炫耀),并开始销售它,淘汰了旧产品。这是事情的自然状态,在软件世界中,我们也会遇到类似的情况:有时候,不是从头创建一个完整的对象,而是您想获取一个预构造的对象,并使用它的一个副本(比如你的简历)。 4 | 5 | 这让我们想到了建立原型的想法:一个模型对象,我们可以复制这些副本,定制这些副本,然后使用它们。原型模式的挑战实际上是复制部分,剩下的事情则不是什么大问题。 6 | 7 | #### 对象的构建 8 | 9 | 大多数对象构造都是使用构造函数来完成的。但是,如果已经配置了一个对象,为什么不简单地复制该对象,而不是创建一个相同的对象呢? 10 | 11 | 来看一个例子: 12 | ```c++ 13 | Contact john{"John Doe", Address{"123 East Dr", "London", 10}}; 14 | Contact jane{"Jane Doe", Address{"123 East Dr", "London", 11}}; 15 | ``` 16 | 17 | 来看看我们要做的事情。john和jane在同一栋楼工作,但在不同的办公室。而其他的则人可能在 18 | 伦敦东德街123号工作,所以如果我们想避免重复初始化地址怎么办,我们该怎么做呢? 19 | 20 | 事实上,原型模式都是关于对象复制的。当然,我们没有一个统一的方法来复制一个对象,我们会介绍其中的一些方法。 21 | 22 | ##### 普通直接的复制方式 23 | 24 | 如果你要复制的是一个值,而你要复制的对象通过值来存储所有东西,那么问题将变得十分简单。以上述例子来说,如果你将Contact类和Address类作如下定义: 25 | 26 | ```c++ 27 | struct Address { 28 | string street, city; 29 | int suite; 30 | }; 31 | struct Contact { 32 | string name; 33 | Address address; 34 | }; 35 | ``` 36 | 37 | 那么直接进行如下的拷贝是毫无问题的: 38 | 39 | ```c++ 40 | // here is the prototype: 41 | Contact worker{"", Address{"123 East Dr", "London", 0}}; 42 | // make a copy of prototype and customize it 43 | Contact john = worker; 44 | john.name = "John Doe"; 45 | john.address.suite = 10; 46 | ``` 47 | 令人遗憾的是,这种情况在实际中很少见。例如,Address对象可以是一个指针: 48 | 49 | ```c++ 50 | struct Contact { 51 | string name; 52 | Address *address; // pointer (or e.g., shared_ptr) 53 | }; 54 | ``` 55 | 这下就有些麻烦了,因为Contact john = prototype这个赋值语句直接复制了指针,使得现在john和prototype以及原型的所有其他副本都共享相同的地址。 56 | 57 | ##### 通过拷贝构造复制对象 58 | 59 | 避免重复的最简单方法是确保在组成对象的所有组成部分(在本例中是Contact和Address)上定义拷贝构造函数。例如,如果我们通过指针来存储address,即: 60 | 61 | ```c++ 62 | struct Contact { 63 | string name; 64 | Address* address; 65 | }; 66 | ``` 67 | 然后,我们需要创建一个拷贝构造函数,实际上有两种方法可以做到这一点。 68 | 69 | 第一种方法是这样的: 70 | 71 | ```c++ 72 | Contact(const Contact& other) 73 | : name{other.name} //, address{ new Address{*other.address} } 74 | { 75 | address = new Address(other.address->street, other.address->city, 76 | other.address->suite); 77 | } 78 | ``` 79 | 80 | 不幸的是,上述方法不够通用。在这种情况下,它当然可以工作(假设Address有一个初始化其所有成员的构造函数),但如果Address决定将其街道部分分割为一个由街道名称、门牌号和其他信息组成的对象,该怎么办?(那么我们又会有同样的复制问题)。 81 | 82 | 明智的做法是在Address上也定义一个拷贝构造函数。在我们的例子中可以作如下定义: 83 | 84 | ```c++ 85 | Address(const string& street, const string& city, const int suite) 86 | : street{street}, city{city}, suite{suite} {} 87 | ``` 88 | 89 | 现在我们可以重写Contact的构造函数来解决这个问题了: 90 | ```c++ 91 | Contact(const Contact& other) 92 | : name{other.name}, address{new Address{*other.address}} {} 93 | ``` 94 | 95 | 这里还有另一个问题:假设你开始使用类似双指针的东西(例如,void**或unique_ptr)即使有ReSharper和CLion这样的功能强大的代码生成工具,此时也不太可能生成正确的代码,所以在这些类型上快速生成代码可能并不总是最优解。 96 | 97 | 通过坚持使用拷贝构造函数而不生成拷贝赋值操作符,可以在一定程度上减少混乱。另一种选择是抛弃拷贝构造函数,而采用下面的方法: 98 | 99 | ```c++ 100 | template 101 | struct Cloneable { 102 | virtual T clone() const = 0; 103 | }; 104 | ``` 105 | 106 | 然后继续实现这个接口,并在需要实际副本时调用prototype.clone()。这实际上比拷贝构造函数/赋值能更好地达到目的。 107 | 108 | #### 序列化 109 | 110 | 其他编程语言的设计者也遇到过同样的问题,必须在整个对象图上显式定义复制操作,并很快意识到一个类需要是“平凡可序列化的”——默认情况下,你应该能够获取一个类并例如,将其写入文件,而不必为类添加任何特征(好吧,最多可能是一两个属性)。 111 | 112 | 为什么这与手头的问题有关?因为如果您可以将某些内容序列化到文件或内存中,那么您就可以反序列化它,保留所有信息,包括所有依赖对象。这不是很方便吗?好... 113 | 114 | 不幸的是,与其他编程语言不同,c++在序列化方面并没有为我们提供现成的工具。例如,我们不能将一个复杂的对象图序列化到一个文件中。为什么不呢?在其他编程语言中,编译后的二进制文件不仅包括可执行代码,还包括大量的元数据,而且序列化可以通过一种称为反射的特性实现——到目前为止在c++中还没有这种特性。 115 | 116 | 如果我们想要序列化,那么就像显式复制操作一样,我们需要自己实现它。幸运的是,我们可以使用一个现成的名为 `Boost` 库,而不是胡乱摆弄和思考序列化 `std::string` 的方法。序列化可以帮我们解决一些问题。下面是一个如何向 `Address` 类型添加序列化支持的示例: 117 | 118 | ```c++ 119 | struct Address { 120 | string street; 121 | string city; 122 | int suite; 123 | 124 | private: 125 | friend class boost::serialization::access; 126 | template 127 | void serialize(Ar& ar, const unsigned int version) { 128 | ar& street; 129 | ar& city; 130 | ar& suite; 131 | } 132 | }; 133 | ``` 134 | 135 | 这可能看起来有点落后,说实话,但最终的结果是,我们使用&操作符指定了 `Address` 的所有部分,我们将需要写入到保存对象的任何地方。注意,前面的代码是用于保存和加载数据的成员函数。可以告诉`Boost`在保存和加载时执行不同的操作,但这与我们的原型需求不是特别相关。 136 | 137 | 现在我们也需要对 `Contact` 类进行相同的操作: 138 | 139 | ```c++ 140 | struct Contact { 141 | string name; 142 | Address* address = nullptr; 143 | 144 | private: 145 | friend class boost::serialization::access; 146 | template 147 | void serialize(Ar& ar, const unsigned int version) { 148 | ar& name; 149 | ar& address; // no * 150 | } 151 | }; 152 | ``` 153 | 154 | 前面的`serialize()`函数的结构或多或少是相同的,但请注意一件有趣的事情:我们仍然将其序列化为 `ar & *` 地址,而没有对指针进行解引用。`Boost`足够智能,可以发现发生了什么,即使 `address` 被设置为 `nullptr`,它也可以很好地序列化/反序列化。 155 | 156 | 因此,如果你想以这种方式实现原型模式,你需要对可能出现在对象图中的每个可能类型都实现 `serialize()`。但是如果你这样做了,你现在可以做的是定义一种通过序列化/反序列化复制对象的方法: 157 | 158 | ```c++ 159 | auto clone = [](const Contact& c) { 160 | // 1. Serialize the contact 161 | ostringstream oss; 162 | boost::archive::text_oarchive oa(oss); 163 | oa << c; 164 | string s = oss.str(); 165 | // 2. Deserialize the contact 166 | istringstream iss(oss.str()); 167 | boost::archive::text_iarchive ia(iss); 168 | Contact result; 169 | ia >> result; 170 | return result; 171 | }; 172 | ``` 173 | 174 | 现在,有一个叫 `john`的联系人,你可以简单地写成: 175 | 176 | ```c++ 177 | Contact jane = clone(john); 178 | jane.name = "Jane"; // and so on 179 | ``` 180 | 然后根据你自己的想法来定制 `jane`。 181 | 182 | 183 | #### 原型工厂 184 | 185 | 如果你有要复制的预定义对象,你打算在哪里存储它们?全局变量?也许。现在假设我们的公司设有主办公室(main offices)和辅办公室(auxiliary offices)。我们可以像这样声明全局变量: 186 | 187 | ```c++ 188 | Contact main{"", new Address{"123 East Dr", "London", 0}}; 189 | Contact aux{"", new Address{"123B East Dr", "London", 0}}; 190 | ``` 191 | 192 | 例如,我们可以将这些定义放到 `Contact.h` 中,以便任何使用 `Contact` 类的人都可以使用这些全局中的一种 193 | 变量并复制它们。但更明智的做法是要有某种专门的类来存储原型和根据需要产生上述原型的定制副本。这将为我们提供额外的灵活性:例如,我们可以创建实用程序函数并生成正确初始化的 `unique_ptrs`: 194 | 195 | ```c++ 196 | struct EmployeeFactory { 197 | static Contact main; 198 | static Contact aux; 199 | static unique_ptr NewMainOfficeEmployee(string name, int suite) { 200 | return NewEmployee(name, suite, main); 201 | } 202 | 203 | static unique_ptr NewAuxOfficeEmployee(string name, int suite) { 204 | return NewEmployee(name, suite, aux); 205 | } 206 | 207 | private: 208 | static unique_ptr NewEmployee(string name, int suite, 209 | Contact& proto) { 210 | auto result = make_unique(proto); 211 | result->name = name; 212 | result->address->suite = suite; 213 | return result; 214 | } 215 | }; 216 | ``` 217 | 218 | 可以像下面这样使用上面的代码: 219 | 220 | ```c++ 221 | auto john = EmployeeFactory::NewAuxOfficeEmployee("John Doe", 123); 222 | auto jane = EmployeeFactory::NewMainOfficeEmployee("Jane Doe", 125); 223 | ``` 224 | 225 | 为什么要使用工厂?好吧,考虑一下我们复制原型然后*忘记*定制它的情况。它将在实际数据所在的位置有一些空白字符串和零。使用我们对工厂的讨论中的方法,例如,我们可以将所有非完全初始化构造函数设为私有,将 `EmployeeFactory` 声明为友元类,然后就可以了——现在 `client` 无法获得部分构造的 `Contact` 对象。 226 | 227 | #### 总结 228 | 229 | 原型设计模式阐释了对象*深度*拷贝的概念,并不需要每次都通过构造函数完整初始化来创建一个对象,可以对创建好的对象进行复制,复制产生的对象和原来的对象互不依赖,稍加修改后就能得到想要的新对象。 230 | 231 | 在C++中实现原型模式实际上只存在两种方法,且都需要手动实现: 232 | 233 | - 在代码中正确的复制对象,即进行深拷贝。可以在拷贝构造函数/拷贝赋值运算符或单独的成员函数中实现。 234 | - 在代码中支持序列化/反序列化,序列化后再进行反序列化实现拷贝。该方法需要额外的计算开销,拷贝频率越高,开销越大。该方法相对于拷贝构造函数的唯一优点是能复用已有的序列化代码。 235 | 236 | 无论选择哪种方法,都需要做一些额外工作,使用代码生成工具(例如 ReSharper、CLion)是可以减轻这部分的工作量的。如果按值来存储数据,不需要担心什么,因为一般而言并不会出现问题。 -------------------------------------------------------------------------------- /docs/chapter-05-singleton.md: -------------------------------------------------------------------------------- 1 | ### 单例模式 2 | 3 | 单例模式可以说是面试中经常碰到的设计模式了(因为它简单且好实现),它的基本思想非常简单:应用程序中应该只有一个特定组件的实例。例如,当我们将数据库加载到内存并提供只读接口时,应当考虑使用单例模式,因为浪费内存来存储几个相同的数据集是没有意义的。实际上,你的应用程序可能存在这样的约束,即两个或多个数据库实例不能放入内存(或者导致内存不足而引起程序故障)。 4 | 5 | 6 | #### 全局单例对象 7 | 8 | 9 | 一个很简单的实现单例的方法是使用静态全局对象: 10 | 11 | ```c++ 12 | static Database database{}; 13 | ``` 14 | 15 | 全局静态对象的问题在于它们在不同编译单元中的初始化顺序是不确定的。这可能会导致糟糕的后果,比如当一个全局对象去引用另一个全局对象,而后者还未被初始化,则会出现问题。 16 | 17 | 一个解决此问题的方法是使用一个全部函数来返回这个静态对象: 18 | ```c++ 19 | Database& get_database() { 20 | static Database database; 21 | return database; 22 | }; 23 | ``` 24 | 可以调用这个函数来获得对数据库的引用。但是,我们需要知道,上述代码的线程安全只有在c++11之后才得到保证,我们应该检查编译器是否确实打算加入锁以防止静态对象初始化时的并发访问。 25 | 26 | 当然,这个场景很容易出现一些麻烦的问题:如果Database决定在其析构函数中使用其他类似的单例,程序可能会崩溃。这就引出了更多的哲学问题:单例对象可以引用其他的单例对象吗? 27 | 28 | #### 经典实现 29 | 30 | 前面的代码并不能保证只有一个对象被创建。我们可以使用一个计数器来防止多于一个的对象被创建: 31 | ```c++ 32 | struct Database { 33 | Database() { 34 | static int instance_count{0}; 35 | if (++instance_count > 1) throw std::exception("Cannot make >1 database!"); 36 | } 37 | }; 38 | ``` 39 | 对于这个问题,这是一种不太友好的方法:尽管它通过抛出异常来阻止创建多个实例,但它无法传达我们不希望任何人多次调用构造函数这个目的。防止显式构造数据库的唯一方法是再次将其构造函数设为私有,并引入上述函数作为成员函数来返回唯一的实例。在前c++11时代,你只需将复制构造函数设为私有,就可以达到大致相同的目的。 40 | 41 | ```c++ 42 | struct Database { 43 | protected: 44 | Database() { /* do what you need to do */ 45 | } 46 | 47 | public: 48 | static Database& get() { 49 | // thread-safe in C++11 50 | static Database database; 51 | return database; 52 | } 53 | Database(Database const&) = delete; 54 | Database(Database&&) = delete; 55 | Database& operator=(Database const&) = delete; 56 | Database& operator=(Database&&) = delete; 57 | }; 58 | ``` 59 | 60 | 最后,你可以将`get()`实现为一个堆分配。 61 | 62 | ```c++ 63 | static Database& get() { 64 | static Database* database = new Database(); 65 | return *database; 66 | } 67 | ``` 68 | 前面的实现依赖于假设数据库一直存在到程序结束,并且使用指针而不是引用确保了析构函数(即使创建了析构函数,也必须是公共的)永远不会被调用。前面的代码不会导致内存泄漏。 69 | 70 | #### 线程安全 71 | 72 | 正如已经提到的,从c++11开始,前面列出的单例初始化方式是线程安全的。即如果两个线程同时调用get(),我们不会遇到数据库被创建两次的情况。在c++ 11之前,我们将使用一种称为双重检查锁定的方法来构造单例。典型的实现如下所示: 73 | 74 | ```c++ 75 | struct Database { 76 | // same members as before, but then... 77 | static Database& instance(); 78 | 79 | private: 80 | static boost::atomic instance; 81 | static boost::mutex mtx; 82 | }; 83 | 84 | Database& Database::instance() { 85 | Database* db = instance.load(boost::memory_order_consume); 86 | if (!db) { 87 | boost::mutex::scoped_lock lock(mtx); 88 | db = instance.load(boost::memory_order_consume); 89 | if (!db) { 90 | db = new Database(); 91 | instance.store(db, boost::memory_order_release); 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | 当然本书讲述是以Modern c++为主,故不会在这方面再做过多讨论。 98 | 99 | 100 | #### 单例模式遇到的麻烦 101 | 102 | 假设我们的数据库包含首都城市及其人口列表。我们的单例数据库要遵循的接口是: 103 | 104 | ``` 105 | class Database { 106 | public: 107 | virtual int get_population(const std::string& name) = 0; 108 | }; 109 | ``` 110 | 111 | 我们有一个成员函数来获得给定城市的人口。现在,让我们假设这个接口被一个叫做`SingletonDatabase` 的具体实现所采用,这个实现singleton的方法和我们之前做的一样: 112 | 113 | ```c++ 114 | class SingletonDatabase : public Database { 115 | SingletonDatabase() { /* read data from database */ 116 | } 117 | std::map capitals; 118 | 119 | public: 120 | SingletonDatabase(SingletonDatabase const&) = delete; 121 | void operator=(SingletonDatabase const&) = delete; 122 | static SingletonDatabase& get() { 123 | static SingletonDatabase db; 124 | return db; 125 | } 126 | 127 | int get_population(const std::string& name) override { 128 | return capitals[name]; 129 | } 130 | }; 131 | ``` 132 | 133 | 正如我们所提到的那样,单例对象的真正问题在于它们在其他组件中的使用。比如,基于上面的例子,我们构建了一个来计算几个不同城市的总人口的函数: 134 | 135 | ```c++ 136 | struct SingletonRecordFinder { 137 | int total_population(std::vector names) { 138 | int result = 0; 139 | for (auto& name : names) 140 | result += SingletonDatabase::get().get_population(name); 141 | return result; 142 | } 143 | }; 144 | ``` 145 | 146 | 这个实现的问题在于,`SingletonRecordFinder` 现在完全依赖于`SingletonDatabase`。这为测试带来了一个问题:如果我们想要检查`SingletonRecordFinder` 是否正确工作,我们需要使用实际数据库中的数据,即: 147 | 148 | ```c++ 149 | TEST(RecordFinderTests, SingletonTotalPopulationTest) { 150 | SingletonRecordFinder rf; 151 | std::vector names{"Seoul", "Mexico City"}; 152 | int tp = rf.total_population(names); 153 | EXPECT_EQ(17500000 + 17400000, tp); 154 | } 155 | ``` 156 | 157 | 但是,如果我们不想使用实际的数据库,而是想用一些虚拟的数据进行测试,在我们目前的设计中,这是不可能的,也正是这种不灵活导致了单例模式的衰败。那么,我们应该怎么做呢?首先,我们不能再依赖显示地依赖`Singleton-Database`。因为我们所需要的只是实现数据库接口,所以我们可以创建一个新的`ConfigurableRecordFinder`来配置数据的来源。 158 | 159 | ```c++ 160 | struct ConfigurableRecordFinder { 161 | explicit ConfigurableRecordFinder(Database& db) : db{db} {} 162 | 163 | int total_population(std::vector names) { 164 | int result = 0; 165 | for (auto& name : names) result += db.get_population(name); 166 | return result; 167 | } 168 | 169 | Database& db; 170 | }; 171 | ``` 172 | 173 | 我们现在使用`db`引用,而不是显式地使用单例。这让我们可以创建一个专门用于测试`RecordFinder`的自行生成的虚拟数据库: 174 | 175 | ```c++ 176 | class DummyDatabase : public Database { 177 | std::map capitals; 178 | 179 | public: 180 | DummyDatabase() { 181 | capitals["alpha"] = 1; 182 | capitals["beta"] = 2; 183 | capitals["gamma"] = 3; 184 | } 185 | 186 | int get_population(const std::string& name) override { 187 | return capitals[name]; 188 | } 189 | }; 190 | ``` 191 | 192 | 现在,我们可以重写我们的单元测试: 193 | 194 | ```c++ 195 | TEST(RecordFinderTests, DummyTotalPopulationTest) { 196 | DummyDatabase db{}; 197 | ConfigurableRecordFinder rf{db}; 198 | EXPECT_EQ(4, rf.total_population(std::vector{"alpha", "gamma"})); 199 | } 200 | ``` 201 | 202 | #### 单例和控制反转 203 | 204 | 明确地使组件成为单例的方法明显是侵入性的,并且决定停止将类视为单例最终的代价会特别昂贵。另一种解决方案是采用约定,而不是直接强制执行类的生命周期,而是将此功能外包给 `IoC` 容器。以下是在使用 `Boost.DI` 依赖注入框架时定义单例组件的样子: 205 | 206 | ```c++ 207 | auto injector = di::make_injector(di::bind.to.in(di::singleton), 208 | // other configuration steps here 209 | ); 210 | ``` 211 | 212 | 在前面,我使用类型名称中的第一个字母 `I` 来表示接口类型。本质上,`di :: bind` 行的意思是,每当我们需要一个具有 `IFoo` 类型成员的组件时,我们就用 `Foo` 的单例实例初始化该组件。 213 | 214 | 许多人认为,在 `DI` 容器中使用单例是工程实践上唯一可接受的单例用法。至少使用这种方法,如果你需要用其他东西替换单例对象,你可以在一个中心位置进行:容器配置代码。一个额外的好处是你不必自己实现任何单例逻辑,这可以防止可能出现的错误。哦,我有没有提到 `Boost.DI` 是线程安全的? 215 | 216 | #### 单态 217 | 218 | 单态(Monostate)是单例模式的变体。它是一个行为像单例,同时看起来和普通类相同的类。 219 | 220 | ```c++ 221 | class Printer { 222 | static int id; 223 | 224 | public: 225 | int get_id() const { return id; } 226 | void set_id(int value) { id = value; } 227 | }; 228 | ``` 229 | 230 | 你能看到这里发生了什么吗?该类看起来像一个带有 getter 和 setter 的普通类,但它们实际上处理静态数据! 231 | 232 | 这似乎是一个非常巧妙的技巧:你让人们实例化`Printer`,但他们都引用相同的数据。但是,用户应该如何知道这一点?用户会很高兴地实例化两台打印机,为它们分配不同的 ID,并且当它们完全相同时会感到非常惊讶! 233 | 234 | 235 | 单态方法在某种程度上有效,并且有几个的优势。例如,它很容易继承,它可以利用多态性,并且它的生命周期被合理定义(但话说回来,你可能并不总是希望如此)。它最大的优点是你可以使用一个已经在整个系统中使用的现有对象,修补它以使其以单态方式运行,并且如果你的系统能够很好地处理非多个对象实例,你就会得到一个类似单例的对象无需重写额外代码即可实现。 236 | 237 | 这样做的缺点也很明显:它是一种侵入性方法(将普通对象转换为单态并不容易),并且它使用静态成员意味着它总是占用空间,即使不需要它。最终,单态最大的缺点是它做出了一个非常乐观的假设,即类字段总是通过 `getter` 和 `setter` 公开。如果直接访问它们,你的重构几乎注定要失败。 238 | 239 | 240 | #### 总结 241 | 242 | 单例并非完全邪恶,但如果使用不慎,它们会破坏应用程序的可测试性和可重构性。如果你确实必须使用单例,请尝试避免直接使用它(例如,编写 `SomeComponent.getInstance().foo()`),而是继续将其指定为依赖项(例如,构造函数参数),从应用程序中的单个位置满足所有依赖项(例如,控制容器的反转) 243 | 244 | -------------------------------------------------------------------------------- /docs/chapter-06-adapter.md: -------------------------------------------------------------------------------- 1 | ### 适配器 2 | 3 | 我过去经常旅行,旅行时的电源适配器使得我能够将欧洲标准的插头插在英国或美国标准的插座上,这是对适配器模式很好的类比:我们想要的接口和现有的接口不同时,我们可以在现有的接口上构造一个适配器来支持想要的接口。 4 | 5 | #### 场景 6 | 7 | 这里有一个简单的例子:假设你正在使用一个擅长绘制像素的库。另一方面,你处理的是几何物体线条,矩形,诸如此类。你想继续使用这些对象,但也需要渲染,所以你需要调整你的几何体以*适配(adpat)*基于像素的表示。 8 | 9 | 让我们从定义示例的(相当简单的)域对象开始: 10 | 11 | ```c++ 12 | struct Point 13 | { 14 | int x, y; 15 | }; 16 | 17 | struct Line 18 | { 19 | Point start, end; 20 | }; 21 | ``` 22 | 23 | 现在让我们来讨论向量几何。一个典型的向量对象可能是由直线`Line`对象的集合定义的。我们可以定义一对纯虚迭代器方法,而不是从`vector`中继承 24 | 25 | ```c++ 26 | struct VectorObject 27 | { 28 | virtual std::vector::iterator begin( ) = 0; 29 | virtual std::vector::iterator end( ) = 0; 30 | }; 31 | ``` 32 | 因此,如果你想定义一个矩形对象`Rectangle`,你可以在`vector`中保存一系列的点,并简单地暴露端点。 33 | 34 | ```c++ 35 | struct VectorRectangle : VectorObject 36 | { 37 | 38 | VectorRectangle( int x, int y, int width, int height ): 39 | width_( width ), 40 | height_( height ) 41 | { 42 | lines.emplace_back( Line{ Point{ x, y }, Point{ x + width, y } }); 43 | lines.emplace_back(Line{ Point{ x + width, y }, Point{ x + width, y + height} }); 44 | lines.emplace_back(Line{ Point{x,y}, 45 | Point{x,y+height} }); 46 | lines.emplace_back(Line{ Point{ x,y + height }, 47 | Point{ x + width, y + height } }); 48 | } 49 | std::vector::iterator begin( ) override 50 | { 51 | return lines_.begin(); 52 | } 53 | std::vector::iterator end( ) override 54 | { 55 | return lines_.end( ); 56 | } 57 | private: 58 | int width_; 59 | int height_; 60 | std::vector lines_; 61 | }; 62 | ``` 63 | 64 | 现在,设置好了。假设我们想在屏幕上画线段,甚至矩形。不幸的是,我们不能做不到,因为绘图的唯一接口是这样的: 65 | 66 | ```c++ 67 | void DrawPoints(CPaintDC& dc, std::vector::iterator start, 68 | std::vector::iterator end 69 | ) 70 | { 71 | for( auto i = start; i != end; ++i ) 72 | dc.SetPixel( i->x, i->y, 0 ); 73 | }; 74 | ``` 75 | 76 | 我在这里使用来自`MFC`(微软基础类)的`CPaintDC`类,但这不是重点。关键是我们需要像素点, 但我们只有直线。我们需要一个适配器。 77 | 78 | #### 适配器 79 | 80 | 好,假设我们要画几个矩形: 81 | 82 | ```c++ 83 | vector> vectorObjects 84 | { 85 | make_shard(10, 10, 100, 100), 86 | make_shard(10, 10, 100, 100) 87 | } 88 | ``` 89 | 90 | 为了绘制这些对象,我们需要将它们从一系列的线转换成大量的点。为此,我们创建了一个单独的类来存储这些点,并将它们作为一对迭代器公开: 91 | 92 | ```c++ 93 | struct LineToPointAdapter 94 | { 95 | using Points = vector; 96 | LineToPointAdapter(Line& line) 97 | { 98 | // TODO 99 | } 100 | virtual Points::iterator begin() 101 | { 102 | return points.begin(); 103 | } 104 | virtual Points::iterator end() 105 | { 106 | return points.end(); 107 | } 108 | private: 109 | Points points; 110 | } 111 | ``` 112 | 113 | 从线段到许多点的转换正好发生在构造函数中,因此适配器是`eager`[^2]。转换的实际代码也相当简单: 114 | 115 | ```c++ 116 | LineToPointAdapter(Line& line) 117 | { 118 | int left = min(line.start.x, line.end.x); 119 | int right = max(line.start.x, line.end.x); 120 | int top = min(line.start.y, line.end.y); 121 | int bottom = max(line.start.y, line.end.y); 122 | int dx = right - left; 123 | int dy = line.end.y - line.start.y; 124 | 125 | // only vertical or horizontal lines 126 | if (dx == 0) 127 | { 128 | // vertical 129 | for (int y = top; y <= bottom; ++y) 130 | { 131 | points.emplace_back( Point{ left, y } ); 132 | } 133 | } 134 | else if (dy == 0) 135 | { 136 | for (int x = left; x <= right; ++x) 137 | { 138 | points.emplace_back(Point{ x, top }); 139 | } 140 | } 141 | } 142 | ``` 143 | 144 | 上面的代码很简单:我们只处理完全垂直或水平的行,忽略其他所有东西。现在我们可以使用这个适配器来实际渲染一些对象。我们从示例中选取两个矩形,并简单地像这样渲染它们: 145 | 146 | ```c++ 147 | for (auto&& obj : vectorObjects) 148 | { 149 | for (auto&& line : *obj) 150 | { 151 | LineToPointAdapter lpo{ line }; 152 | DrawPoints(dc, lpo.begin(), lpo.end()); 153 | } 154 | }; 155 | ``` 156 | 157 | 漂亮!我们所要做的就是,获取每个向量对象的每条线段,为这条线构造一个`LineToPointAdapter`,然后迭代适配器生成的点集,将它们提供给`drawpoint()`。这样的确可行!(相信我,确实如此。) 158 | 159 | 160 | #### 适配器的临时变量 161 | 162 | 然而,我们的代码有一个主要的问题:`DrawPoints()`会在我们可能需要的每一次屏幕刷新时被调用,这意味着相同的行对象的相同数据会被适配器重新生成,比如,无数次。 163 | 164 | 我们能做些什么呢?一方面,我们可以在应用程序启动时预先定义所有的点: 165 | 166 | ```c++ 167 | vector points; 168 | for (auto&& o : vectorObjects) 169 | { 170 | for (auto&& l : *o) 171 | { 172 | LineToPointAdapter lpo{ l }; 173 | for (auto&& p : lpo) 174 | points.push_back(p); 175 | } 176 | } 177 | ``` 178 | 179 | 然后`drawpoint()`的实现简化为: 180 | 181 | ```c++ 182 | DrawPoints(dc, points.begin(), points.end()); 183 | ``` 184 | 185 | 但让我们暂时假设,原始的`vectorObjects`集合可以改变。缓存这些点是没有意义的,但我们仍然希望避免潜在重复数据的不断更新。我们如何处理这个问题?当然,使用缓存! 186 | 187 | 首先,为了避重复生成,我们需要独特的标识线段的方法,这也就意味着我们需要独特的标识点的方式。我们可以使用 ReSharper's 生成|哈希函数(**Generate | Hash function**)来解决这个问题: 188 | 189 | ```c++ 190 | struct Point 191 | { 192 | int x, y; 193 | friend std::size_t hash_value(const Point& obj) 194 | { 195 | std::size_t seed = 0x725C686F; 196 | boost::hash_combine(seed, obj.x); 197 | boost::hash_combine(seed, obj.y); 198 | return seed;` 199 | } 200 | }; 201 | 202 | struct Line 203 | { 204 | Point start, end; 205 | friend std::size_t hash_value(const Line& obj) 206 | { 207 | std::size_t seed = 0x719E6B16; 208 | boost::hash_combine(seed, obj.start); 209 | boost::hash_combine(seed, obj.end); 210 | return seed; 211 | } 212 | }; 213 | ``` 214 | 215 | 在前面的例子中,我选择了Boost’s散列实现。现在,我们可以构建一个新的`LineToPointCachingAdapter`,以便它缓存这些点,并仅在必要时重新生成它们。除了以下细微差别之外,实现几乎是相同的。 216 | 217 | 首先,适配器现在有一个缓存: 218 | 219 | ```c++ 220 | static map cache; 221 | ``` 222 | 223 | 这里的`size_t`类型正是`Boost's`散列函数返回的类型。现在,当涉及到迭代生成的点时,我们将生成如下所示的点: 224 | 225 | ```c++ 226 | virtual Points::iterator begin() 227 | { 228 | return cache[line_hash]. 229 | begin(); 230 | } 231 | 232 | virtual Points::iterator end() { 233 | return cache[line_hash]. 234 | end(); 235 | } 236 | ``` 237 | 238 | 这个算法的有趣之处在于:在生成这些点之前,我们要检查它们是否已经生成过了。如果有,我们就退出; 如果没有,则生成它们并添加到缓存中: 239 | 240 | ```c++ 241 | LineToPointCachingAdapter(Line& line) 242 | { 243 | static boost::hash hash; 244 | line_hash = hash(line); // note: line_hash is a field! 245 | if (cache.find(line_hash) != cache.end()) 246 | return; // we already have it 247 | Points points; 248 | // same code as before 249 | cache[line_hash] = points; 250 | } 251 | ``` 252 | 253 | 254 | 耶!多亏了散列函数和缓存,我们大大减少了转换的次数。唯一遗留的问题是当不再需要过期的点时,我们需要删除它们。这个有挑战性的问题留给读者作为练习。 255 | 256 | #### 总结 257 | 258 | 适配器是一个非常简单的概念:它允许你调整所拥有的接口以适应所需要的接口。适配器的唯一实际问题是,在适应过程中,有时需要生成临时数据,以满足数据的其他表示形式。当这种情况发生时,请使用缓存:确保只在必要时才生成新数据。哦,如果你想在缓存的对象发生变化时清理过时数据,那么你还需要做更多的工作。 259 | 260 | 我们还没有真正解决的另一个问题是延迟加载:当前适配器实现一创建就执行转换。如果你只想在实际使用适配器时完成工作,该怎么办?这很容易做到,留给读者作为练习。 -------------------------------------------------------------------------------- /docs/chapter-07-bridge.md: -------------------------------------------------------------------------------- 1 | ### 桥接模式 2 | 3 | 如果你一直关注c++编译器(特别是GCC、Clang和MSVC)的最新进展,你可能已经注意到编译速度正在提高。特别是,编译器变得越来越增量化,因此编译器实际上只能重新构建已更改的定义,并重用其余的定义,而不是重新构建整个翻译单元。 4 | 5 | 我之所以提到c++编译,是因为过去开发人员一直在使用一个奇怪的技巧(又是这个短语!)来优化编译速度。当然,我说的是... 6 | 7 | #### Pimpl编程技法 8 | 9 | 让我先解释一下在指向实现的指针(`Pimpl(Pointer to implement)`)编程技法,。假设你决定创建一个`Person`类来存储一个人的姓名并允许他们打印问候。与通常定义Person的成员不同,你继续这样定义类 10 | 11 | ```c++ 12 | struct Person 13 | { 14 | std::string name; 15 | void greet(); 16 | Person(); 17 | ~Person(); 18 | 19 | class PersonImpl; 20 | PersonImpl* impl // good place for gsl::owner 21 | } 22 | ``` 23 | 24 | 这太奇怪了。对于一个简单的类来说似乎有很多工作要做。让我们看看,我们有`name`和`greet()`函数,但为什么要费心使用构造函数和析构函数呢?这个类`PersonImpl`是什么? 25 | 26 | 27 | 你现在看到的是`Person`类,选择将其实现隐藏在另一个类(`PersonImpl`)。需要注意的是,`PersonImpl`这个类不是在头文件中定义的,而是驻留在.cpp文件(`Person. cpp`, `Person`和`PersonImpl`耦合在一起)。它的定义很简单: 28 | 29 | ```c++ 30 | struct Person::PersonImpl 31 | { 32 | void greet(Person* p); 33 | }; 34 | ``` 35 | 原始的`Person`类向前声明`PersonImpl`,并继续保留指向它的指针。在`Person`的构造函数中初始化并在析构函数中销毁的正是这个指针; 如果智能指针能让你感觉更好,请随意使用。 36 | 37 | ```c++ 38 | Person::Person() : 39 | impl(new PersonImpl) 40 | { } 41 | 42 | Person::~Person( ) 43 | { 44 | delete impl; 45 | } 46 | ``` 47 | 48 | 现在,我们要实现`Person::greet()`,正如你可能已经猜到的,它只是将控制权传递给`PersonImpl::greet()` 49 | 50 | ```c++ 51 | void Person::greet() 52 | { 53 | impl->greet(this); 54 | } 55 | 56 | Person::PersonImpl::greet(Person* p) 57 | { 58 | printf("hello %s", p->name.c_str()); 59 | } 60 | ``` 61 | 62 | 这就是Pimpl编程技法,唯一的问题是为什么?!? 为什么要这么费劲地委托`greet()`并传递`this`指针呢?这种方法有三个优点: 63 | 64 | - 更大比例的类的实现被隐藏起来。如果`Person`类的实现需要提供许多私有/受保护成员,那么你将向客户端公开所有这些细节,即使客户端由于私有/受保护访问修饰符永远无法访问这些成员。使用`Pimpl`编程技法,可以只提供公共接口。 65 | - 修改隐藏`Impl`类的数据成员不会影响二进制兼容性。 66 | - 头文件只需要包含声明所需的头文件,而不需要包含实现。例如,如果`Person`需要`vector`类型的私有成员,您将被迫在头文件`Person.h`种`#include` ``和`` (这是传递性的,所以任何使用Person.h的人也会包括他们)。利用Pimpl编程技法,可以在`.cpp`文件中`#include` ``和`` 。 67 | 68 | 你将注意到,上述几点允许我们保留一个干净的、不变的头文件。这样做的一个减少编译时间,但对于我们来说, `Pimpl`很好得揭示了桥接模式: 在我们的例子中,`Pimpl`不透明的指针(不透明的相对透明的,也就是说,你不知道它背后是什么)作为一个桥梁, 将公共接口的成员与隐藏在`.cpp`文件中的底层实现连接了起来。 69 | 70 | #### 桥接模式 71 | 72 | Pimpl编程技法是桥梁设计模式的一个非常具体的说明,现在让我们来看看一些更普遍的东西。假设我们有两个对象类(在数学意义上):几何形状和可以在屏幕上绘制它们的渲染器。 73 | 74 | 就像我们对适配器模式的演示一样,我们假设渲染可以以矢量和栅格形式进行(尽管我们在这里不会编写任何实际的绘图代码),并且,就形状而言,我们将限制为圆形。 75 | 76 | 首先,我们给出基类`Renderer`: 77 | 78 | ```c++ 79 | struct Renderer 80 | { 81 | virtual void render_circle(float x, float y, float radius) = 0; 82 | }; 83 | ``` 84 | 85 | 我们可以很容易地构造矢量和栅格实现;下面我将使用一些代码模拟实际的呈现,以便向控制台编写内容 86 | 87 | ```c++ 88 | struct VectorRenderer : Renderer 89 | { 90 | void render_circle(float x, float y, float radius) override 91 | { 92 | cout << "Rasterizing circle of radius " << radius << endl; 93 | } 94 | }; 95 | 96 | struct RasterRenderer : Renderer 97 | { 98 | void render_circle(float x, float y, float radius) override 99 | { 100 | cout << "Drawing a vector circle of radius " << radius << endl; 101 | } 102 | }; 103 | ``` 104 | 105 | 基类`Shape`持有渲染器的引用; 该形状将支持`draw()`成员函数的自渲染,也将支持`resize()`操作。 106 | 107 | ```c++ 108 | struct Shape 109 | { 110 | protected: 111 | Renderer& renderer; 112 | Shape(Renderer& renderer) : renderer { renderer } { } 113 | public: 114 | virtual void draw() = 0; 115 | virtual void resize(float factor) = 0; 116 | }; 117 | ``` 118 | 119 | 您会注意到Shape类引用了一个渲染器。这恰好是我们建造的桥梁。现在我们可以创建`Shape`类的实现,提供额外的信息,比如圆心的位置和半径。 120 | 121 | ```c++ 122 | struct Circle : Shape 123 | { 124 | float x, y, radius; 125 | void draw() override 126 | { 127 | render.render_circle(x, y, radius); 128 | } 129 | void resize(float factor) override 130 | { 131 | radius *= factor; 132 | } 133 | Circle(Renderer& renderer, float x, float y, float radius): 134 | Shape{renderer}, 135 | x{x}, 136 | y{y}, 137 | radius{radius} 138 | {} 139 | }; 140 | 141 | } 142 | ``` 143 | 144 | 好的,所以这个模式很快就写好了,当然,有趣的部分是在`draw()`中:在这里我们使用桥梁连接圆(它有关于它的位置和大小的信息)和渲染过程。这里的桥就是一个`Renderer`, 例如 145 | 146 | ```c++ 147 | RasterRenderer rr; 148 | Circle raster_circle{ rr, 5, 5, 5 }; 149 | raster_circle.draw(); 150 | raster_circle.resize(2); 151 | raster_circle.draw(); 152 | ``` 153 | 154 | 在前面的例子中,桥是`RasterRenderer`: 你创建它的对象`rr`,把`rr`的一个引用传递给`Circle`,然后调用`draw()`将把`RasterRenderer`作为桥,绘制圆圈。如果你需要微调圆,你可以调用`resize()`调整它的大小,渲染仍然会工作得很好,因为渲染器不知道或关心`Circle`。 155 | 156 | 157 | #### 总结 158 | 159 | 桥是一个相当简单的概念,作为一个连接器或胶水,连接两个部分在一起。抽象(接口)的使用允许组件在不真正了解具体实现的情况下相互交互。 160 | 161 | 也就是说,桥接模式的参与者确实需要知道彼此的存在。具体来说,一个`Circle`需要一个对`Renderer`引用,相反,渲染器知道如何具体地绘制圆(`render_circle()`成员函数的名称)。这可以与中介模式形成对比,中介模式允许对象在不直接感知对方的情况下进行通信。 162 | 163 | -------------------------------------------------------------------------------- /docs/chapter-08-composite.md: -------------------------------------------------------------------------------- 1 | ### 组合 2 | 3 | #### 多个属性 4 | 5 | 组合设计模式通常适用于整个类,一个对象通常由多个对象构成。举个例子,方便理解。在一个游戏中,每个生物都有不同的强度值、敏捷值、智力值等,这就很容易定义: 6 | ```c++ 7 | class Creature{ 8 | int strength, agility, intelligence; 9 | public: 10 | int get_strength() const 11 | { 12 | return strength; 13 | } 14 | 15 | void set_strength(int strength){ 16 | Creature::strength = strength; 17 | } 18 | 19 | int get_agility() const 20 | { 21 | return agility; 22 | } 23 | 24 | void set_agility(int agility){ 25 | Creature::agility = agility; 26 | } 27 | 28 | int get_intelligence() const 29 | { 30 | return intelligence; 31 | } 32 | 33 | void set_intelligence(int intelligence){ 34 | Creature::intelligence = intelligence; 35 | } 36 | }; 37 | ``` 38 | 接下来我们想要对这些属性进行操作,例如求多个属性的最大值、平均值、总和,如下: 39 | 40 | 41 | ```c++ 42 | class Creature{ 43 | //其他的数据成员 44 | int sum() const{ 45 | return strength + agility + intelligence; 46 | } 47 | 48 | double average const{ 49 | return sum() / 3.0; 50 | } 51 | int max() const{ 52 | return ::max(::max(strength, agility),intelligence); 53 | } 54 | } 55 | ``` 56 | 57 | 然而这样并不理想,原因如下: 58 | 59 | 1)在计算所有统计数据总和时候,我们容易犯错并且忘记其中一个 60 | 61 | 2)3.0是代表属性的数目,在这里被设计成一个固定值 62 | 63 | 3)计算最大值时,我们必须构建一对std::max() 64 | 65 | 想象一下如果再增加一个新的属性,这个时候我们需要对sum(),average(),max()重构,这是十分糟糕的。 66 | 67 | 如何避免?如下: 68 | 69 | ```c++ 70 | class Creature{ 71 | enum Abilities {str, agl, intl, count}; 72 | array abilities; 73 | } 74 | ``` 75 | 76 | 上面的枚举定义了一个名为count的额外值,标记着有多少个属性。现在我们这样定义属性的get和set方法: 77 | ```c++ 78 | int get_strength() const { return abilities[str];} 79 | 80 | void set_strength(int value){ 81 | abilities[str]=value; 82 | } 83 | //对于其他属性同样 84 | ``` 85 | 现在再让我们看看对sum(),average(),max()的计算,看看有什么改进: 86 | 87 | ```c++ 88 | int sum() const{ 89 | return accumulate(abilities.begin(), abilities.end(),0); 90 | } 91 | 92 | double average() const{ 93 | return sum() / (double)count; 94 | } 95 | 96 | int max() const{ 97 | return *max_element(abilities.begin(), abilities.end()); 98 | } 99 | ``` 100 | 101 | 是不是更棒了,不仅使代码更容易编写和维护,而且添加新属性时候,十分简单,总量根本不需要去改变,并不会影响sum(),average(),max()。 102 | 103 | 104 | #### 组合图形对象 105 | 106 | 想想诸如PowerPoint等应用程序,在哪里您可以选择多个不同的对象并将其作为一个拖动。然而如果要选一个一个对象,您也可以抓取该对象。渲染也是相同的:您可以呈现单个图形对象,或者您可以将多个形状组合在一起,并将其绘制为一个组。这种方法的实现相当容易,因为它只是依赖于单个接口,如下所示: 107 | 108 | ```c++ 109 | struct GraphicObject{ 110 | virtual void draw() = 0; 111 | }; 112 | ``` 113 | 114 | 现在从名字来看,你可能认为它总是代表一个单独的项目。然而,想想看:几个矩形和圆形组合在一起代表一个组合图形对象。正如我可以定义的,比如说,一个圆: 115 | 116 | ```c++ 117 | struct Circle : GraphicObject 118 | { 119 | void draw() override 120 | { 121 | std::cout << "Circle" << std::endl; 122 | 123 | } 124 | }; 125 | ``` 126 | 同样,我们可以定义一个由几个其他图形对象组成的图形对象。是的,关系可以无限递归: 127 | 128 | ```c++ 129 | struct Group : GraphicObject 130 | { 131 | std::string name; 132 | explicit Group(const std::string& name) : name(name){} 133 | 134 | void draw() override 135 | { 136 | std::cout << "Group" << name.c_str() << " contains:" << std::endl; 137 | for(auto&& o:obejct) 138 | o->draw(); 139 | } 140 | 141 | std::vector objects; 142 | } 143 | ``` 144 | 单个圆和任意组都可以绘制,只要他们实现了draw()函数。组中有一个指向其他图形对象的指针数组,通过其访问多个对象的draw()方法,来渲染自身。 145 | 以下是编程接口的使用方法: 146 | ```c++ 147 | Group root("root"); 148 | Circle c1, c2; 149 | root.obejects.push_back(&c1); 150 | 151 | Group subgroup("sub"); 152 | subgroup.objects.push_back(&c2); 153 | 154 | root.obejcts.push_back(&subgroup); 155 | 156 | root.draw(); 157 | ``` 158 | 前面的代码生成以下输出: 159 | ```c++ 160 | Group root contains: 161 | Circle 162 | Group sub contains: 163 | Circle 164 | ``` 165 | 这是组合设计模式最简单的实现,尽管我们自己已经定义了一个定制接口。现在,如果我们尝试采用其他一些更标准化的迭代对象的方法,这个模式会是什么样子呢? 166 | 167 | #### 神经网络 168 | 机器学习是热门的新事物。机器学习中的一部分是使用人工神经网络:试图模仿神经元在我们大脑中工作方式的软件结构。 169 | 神经网络的核心概念当然是神经元。神经元可以根据其输入产生(通常是数字)输出,我们可以将该值反馈给网络中的其他连接。我们将只关注连接,所以我们将这样对神经元建模: 170 | 171 | ```c++ 172 | 1 struct Neuron 173 | 2 { 174 | 3 vector in, out; 175 | 4 unsigned int id; 176 | 5 177 | 6 Neuron() 178 | 7 { 179 | 8 static int id = 1; 180 | 9 this->id = id++; 181 | 10 } 182 | 11 }; 183 | ``` 184 | 我在id字段输入了身份。现在,你可能想做的是把一个神经元连接到另一个神经元上,这可以用 185 | ```c++ 186 | 1 template<> void connect_to(Neuron& other) 187 | 2 { 188 | 3 out.push_back(&other); 189 | 4 other.in.push_back(this); 190 | 5 } 191 | ``` 192 | 这个函数造当前神经元和另一个神经元之间建立了联系。目前为止,一切顺利。现在,假设我们也想创建神经元层。一个层很简单,就是特定数量的神经元组合再一起。 193 | ```c++ 194 | 1 struct NeuronLayer : vector 195 | 2 { 196 | 3 NeuronLayer(int count) 197 | 4 { 198 | 5 while (count --> 0) 199 | 6 emplace_back(Neuron{}); 200 | 7 } 201 | 8 }; 202 | ``` 203 | 看起来不错。但是现在有一个小问题。问题是这样的:我们希望神经元能够连接到神经元层。总的来说,我们希望像这样能够奏效: 204 | ```c++ 205 | 1 Neuron n1, n2; 206 | 2 NeuronLayer layer1, layer2; 207 | 3 n1.connect_to(n2); 208 | 4 n1.connect_to(layer1); 209 | 5 layer1.connect_to(n1); 210 | 6 layer1.connect_to(layer2); 211 | ``` 212 | 如您所见,我们有四个不同的案例需要处理: 213 | 1、神经元连接到另一个神经元 214 | 2、神经元连接到神经元层 215 | 3、神经元层连接到神经元 216 | 4、神经元层连接到另一个神经元层 217 | 218 | 正如你所猜到的,我们不可能对connect_to()函数进行四次重载。如果有三个不同的类,你会考虑创建九个函数吗?我不这么认为。相反,我们要做的是在基类中插入槽。由于多重继承,我们完全可以做到这一点。那么,下面呢? 219 | 220 | ```c++ 221 | 1 template 222 | 2 struct SomeNeurons 223 | 3 { 224 | 4 template void connect_to(T& other) 225 | 5 { 226 | 6 for (Neuron& from : *static_cast(this)) 227 | 7 { 228 | 8 for (Neuron& to : other) 229 | 9 { 230 | 10 from.out.push_back(&to); 231 | 11 to.in.push_back(&from); 232 | 12 } 233 | 13 } 234 | 14 } 235 | 15 }; 236 | ``` 237 | connect_to的实现绝对值得探讨。如您所见,它是一个模板成员函数,接受T,然后成对地迭代*this和T&的神经元,互相连接每个。但是有一个警告,我们不能只迭代*this,因为这会给我们一个SomeNeurons&和我们真正要找的类型。 238 | 这就是我们为什么被迫让一些神经元成为一个模板类,其中模板参数Self指的是继承类。然后我们在取消引用和迭代内容之前,将this指针转换为Self*。SomeNeurons是为了实现方便而付出的小小代价。 239 | 剩下的就是在Neuron和NeuronLayer中实现SomeNeurons::begin()和end(),让基于范围的循环真正工作。 240 | 由于NeuronLayer继承自vector,因此不用显示实现begin()/end(),它已经自动存在。但是神经元本身确实需要一种迭代的方法。它需要让自己成为唯一可重复的元素。这可以通过以下方式完成: 241 | ```c++ 242 | 1 Neuron* begin() override { return this; } 243 | 2 Neuron* end() override { return this + 1; } 244 | ``` 245 | 正是这个神奇的东西让SomeNeurons::connect_to()成为可能。简单来说,我们使得单个对象的行为像一个可迭代的对象集合。这允许以下所有用途: 246 | 247 | ```c++ 248 | 1 Neuron neuron, neuron2; 249 | 2 NeuronLayer layer, layer2; 250 | 3 251 | 4 neuron.connect_to(neuron2); 252 | 5 neuron.connect_to(layer); 253 | 6 layer.connect_to(neuron); 254 | 7 layer.connect_to(layer2); 255 | ``` 256 | 更不用说,如果您要引入一个新的容器(比如NeuronsRing),您所要做的就是从SomeNeurons继承,实现begin()/end(),新的类将立即连接到所有的神经元和神经元层。 257 | 258 | #### 总结 259 | 复合设计模式允许我们为单个对象和对象集合提供相同的接口。这可以通过显式使用接口成员来完成,也可以通过duck typing(在程序设计中是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定)来完成。例如基于范围的for循环并不需要您继承任何东西,而是通过实现begin()和end()。 260 | 正是这些begin()/end()成员允许标量类型伪装成“集合”。同样有趣的是,我们的connect_to()函数的嵌套for循环能够将这两个构造连接在一起,尽管它们具有不同的迭代器类型:Neuron返回Neuron*而NeuronLayer返回vector::iterator——这两者并不完全相同。哈哈,这就是模板的魅力。 261 | 最后,我必须承认,只有当你想拥有一个单一成员函数时,所有这些跳跃才有必要。如果您可以调用一个全局函数,或者如果您对有多个connect_to()实现感到满意,那么基类SomeNeurons并不是必要的。 262 | -------------------------------------------------------------------------------- /docs/chapter-09-decorator.md: -------------------------------------------------------------------------------- 1 | ### 第九章 装饰器 2 | 3 | 假设你正在使用同事编写的类,并且希望扩展该类的功能。如果不修改原始代码,你会怎么做呢?一种方法使用是继承:你可以创建一个派生类,添加你需要的功能,甚至可能重写(override)一些东西,然后就可以了。 4 | 5 | 但这并不总是有效,原因有很多。例如,您通常不希望从std::vector继承,因为它没有虚析构函数,或者从int继承(这是不可能的)。继承不起作用的最关键原因是,当你需要添加多个功能的时候,你希望遵循单一职责原则,将这些功能分开。 6 | 7 | 装饰器模式允许我们在不修改原始类型(开闭原则)或导致派生类型数量激增的情况下增加现有类的功能。 8 | 9 | #### 场景 10 | 11 | 假设我们定义一个名为Shape的抽象类: 12 | 13 | ```c++ 14 | struct Shape 15 | { 16 | virtual string str() const = 0; 17 | }; 18 | ``` 19 | 20 | 在Shape类中,`str()`是一个虚函数,我们将使用它来提供表示特定形状的字符串。现在我们可以用该接口实现Circle类或Square类: 21 | 22 | ```c++ 23 | struct Circle : Shape 24 | { 25 | float radius; 26 | explict: Circle(const float radius) : radius{radius} {}; 27 | void resize(float factor) { radius *= factor; } 28 | string str() const override 29 | { 30 | ostringstream oss; 31 | oss << "A circle of radius " << radius; 32 | return oss.str(); 33 | } 34 | }; 35 | // 下面省略了Square 类的实现 36 | ``` 37 | 38 | #### 动态装饰器 39 | 40 | 假设我们想要给形状增加一些颜色。我们可以使用组合的方式替代继承实现ColoredShape类,简单地引用一个已经构造好的Shape对象并增强它: 41 | 42 | ```c++ 43 | struct ColoredShape : Shape 44 | { 45 | Shape &shape; 46 | string color; 47 | ColoredShape(Shape &shape, const string &color) : shape(shape), color(color) {}; 48 | string str() const override 49 | { 50 | ostringstream oss; 51 | oss << shape.str() << " has the color " << color; 52 | return oss.str() 53 | } 54 | }; 55 | ``` 56 | 57 | 正如你所看到的,ColoredShape它本身是`Shape`的一种。我们可以像这样使用它: 58 | 59 | ```c++ 60 | Circle circle{0.5f}; 61 | ColoredShape redCircle{circle, "red"}; 62 | cout << redCircle.str(); 63 | // prints "A circle of radius 0.5 has the color red" 64 | ``` 65 | 66 | 如果我们现在想要增加形状的透明度,这也很简单: 67 | 68 | ```c++ 69 | struct TransparentShape : Shape 70 | { 71 | Shape& shape; 72 | uint8_t transparency; 73 | 74 | TransparentShape(Shape& shape, const uint8_t transparency) 75 | : shape{shape}, transparency{transparency} {} 76 | 77 | string str() const override 78 | { 79 | ostringstream oss; 80 | oss << shape.str() << " has " 81 | << static_cast(transparency) / 255.f*100.f 82 | << "% transparency"; 83 | return oss.str(); 84 | } 85 | }; 86 | ``` 87 | 88 | 但最重要的是我们可以把ColoredShape和TransparentShape组合起来,使得一个形状既有颜色又有透明度: 89 | 90 | ```c++ 91 | 92 | TransparentShape myCircle {ColoredShape{ Circle{23}, "green"}, 64 }; 93 | cout << myCircle.str(); 94 | // A circle of radius 23 has the color green has 25.098% transparency 95 | 96 | ``` 97 | 98 | #### 静态装饰器 99 | 100 | 你是否注意到,在之前的讨论的场景中,我们给`Circle`提供了一个名为`resize()`的函数,不过它并不在`Shape`接口中。你可能已经猜到的,因为它不是`Shape`成员函数,所以不能从装饰器中调用它。 101 | 102 | ```c++ 103 | Circle circle{3}; 104 | ColoredShape redCircle{circle, "red"}; 105 | redCircle.resize(2); // 编译不能通过 106 | ``` 107 | 108 | 假设你并不真正关心是否可以在运行时组合对象,你真正关心的是:能否访问修饰对象的所有字段和成员函数。有可能建造这样一个装饰器吗? 109 | 110 | 的确有办法实现,而且它是通过模板和继承完成的——但不是那种会导致状态空间爆炸的继承。相反,我们使用一种叫做`Mixin`继承的方法,类从它自己的模板参数继承。 111 | 112 | 为此,我们创建一个新的`ColoredShape`,它继承自一个模板参数。我们没有办法将模板形参限制为任何特定类型,因此我们将使`static_assert`用进行类型检查。 113 | 114 | ```c++ 115 | template 116 | struct ColoredShape : T 117 | { 118 | static_assert(is_base_of::value, "Template argument must be a Shape"); 119 | string color; 120 | string str() const override 121 | { 122 | ostringstream oss; 123 | oss << T::str() << "has the color" << color; 124 | return oss.str(); 125 | } 126 | }; 127 | ``` 128 | 129 | 有了`ColorredShape`和`TransparentShape> square{"bule"}; 133 | square.size = 2; 134 | square.transparency = 0.5; 135 | cout << square.str(); 136 | square.size(); 137 | ``` 138 | 139 | 这的确很棒,但并不完美:我们似乎失去了对构造函数的充分使用:即使我们能够初始化最外层的类,我们也不能在一行代码中完全构造具有特定大小、颜色和透明度的形状。 140 | 141 | 为了锦上添(即装饰!)花,我们给出`Colordshape`和`TransparentShape`转发构造函数。这些构造函数将接受两个参数:第一个参数作用于当前模板类,第二个是传递给基类的泛型形参包。 142 | 143 | ```c++ 144 | template 145 | struct TransparentShape : T 146 | { 147 | uint8_t transparency; 148 | template 149 | TransparentShape(const uint8_t transparency, Args ...args): 150 | T(std::forward(args)...), 151 | transparency{transparency} {} 152 | }; 153 | // ColoredShape也类似 154 | ``` 155 | 156 | 只是重申一下,前面的构造函数可以接受任意数量的参数,其中第一个参数用于初始化透明值,其余的只是转发给基类的构造函数。 157 | 158 | 构造函数的数目必须保证是正确的,如果构造函数的数目或值的类型不正确,程序将无法编译。如果开始向类型中添加默认构造函数,那么整体参数集的使用就会变得灵活得多,但也会引入歧义和混淆。 159 | 160 | 161 | 哦,还要确保永远不要显式地使用这些构造函数,否则在组合这些装饰器时,就会违反c++的复制列表初始化规则。现在,如何真正利用这些好处? 162 | 163 | ```c++ 164 | ColoredShape2> sq = { "red", 51, 5 }; 165 | cout << sq.str() << endl; 166 | // A square with side 5 has 20% transparency has the color red 167 | ``` 168 | 169 | 漂亮!这正是我们想要的。这就完成了静态装饰器的实现。同样,你可以对它进行增强,以避免重复类型,如`ColorredShape>`,或循环 `ColorredShape>>`;但在静态环境中,这感觉像是浪费时间。不过,多亏了各种形式的模板魔法,这是完全可行的。 170 | 171 | #### 函数装饰器 172 | 173 | 虽然装饰器模式通常应用于类,但也同样可以应用于函数。假设你想在现有的代码中实现一个额外的功能: 你想记录一个函数被调用的情况,并在Excel中分析统计数据。当然,这可以通过在调用之前和之后添加一些代码来实现。 174 | 175 | ```c++ 176 | cout << "Entering function\n"; 177 | // do the work 178 | cout << "Exiting funcion\n"; 179 | ``` 180 | 181 | 这工作得很好,但就关注点分离而言并不好:我们希望将日志记录功能存储在某个地方,以便我们可以重用它,并在必要时增强它。可以使用不同的方法来实现。一种方法是将整个工作单元作为`lambda`表达式提供给类似下面的日志组件: 182 | 183 | ```c++ 184 | struct Logger{ 185 | function func; 186 | string name; 187 | Logger(const function& func, const string& name): 188 | func{func}, 189 | name{name} 190 | { 191 | 192 | } 193 | void operator()()const 194 | { 195 | cout << "Entering" << name << endl; 196 | func(); 197 | cout << "Exiting" << name << endl; 198 | } 199 | }; 200 | ``` 201 | 202 | 使用这种方法,你可以编写以下内容: 203 | 204 | ```c++ 205 | Logger([]() {cout << "Hello" << endl; }, "HelloFunction")(); 206 | \\ output: 207 | \\ Entering HelloFunction 208 | \\ Hello 209 | \\ Exiting HelloFunction 210 | ``` 211 | 212 | 我们也可以将函数作为模板参数而不是`std::function`传入,这只需要在前面的代码中稍微改动下即可: 213 | 214 | ```c++ 215 | template 216 | struct Logger2{ 217 | Func func; 218 | string name; 219 | Logger2(const Func& func, const string& name): 220 | func{func}, 221 | name{name} 222 | { 223 | 224 | } 225 | void operator()() const 226 | { 227 | cout << "Entering" << name << endl; 228 | func(); 229 | cout << "Exiting" << name << endl; 230 | } 231 | }; 232 | ``` 233 | 与之前用法完全相同, 我们可以创建一个实用函数来日志对象: 234 | 235 | ```c++ 236 | template auto make_logger2(Func func, const string& name) 237 | { 238 | return Logger2{ func, name }; // () = call now 239 | } 240 | ``` 241 | 242 | 然后像这样使用它: 243 | 244 | ```c++ 245 | auto call = make_logger2([]() {cout << "Hello!" << endl; }, "HelloFunction"); 246 | call(); 247 | ``` 248 | 249 | 你可能会问这样做有什么意义呢?意义在于,我们现在有能力创建一个装饰器(其中包含被装饰的函数)并在我们选择的时候调用它。 250 | 251 | 前面定义的`function func`没有函数参数和返回值,如果现在你想要实现带有返回值和函数参数的`add()`函数的调用(定义如下),该怎么办: 252 | 253 | ```c++ 254 | double add(double a, double b) 255 | { 256 | cout << a << "+" << b << "=" << (a + b) << endl; 257 | return a + b; 258 | } 259 | ``` 260 | 261 | 不是那么容易!但当然也不是不可能。让我们再实现一个`Logger`版本吧: 262 | 263 | ```c++ 264 | template 265 | struct Logger3{ 266 | function func; 267 | string name; 268 | 269 | Logger3(const function& func, const string& name): 270 | func{func}, 271 | name{name} 272 | { 273 | 274 | } 275 | R operator()(Args... args) const 276 | { 277 | cout << "Entering" << name << endl; 278 | R result = func(args...); 279 | cout << "Exiting" << name << endl; 280 | return R; 281 | } 282 | }; 283 | ``` 284 | 285 | 在前面,模板参数`R`指的是返回值的类型,而`Args`,你肯定已经猜到了。装饰器保留该函数并在必要时调用它,唯一的区别是`operator()`返回一个`R`,因此不会丢失返回值。我们可以构造另一个实用函数`make_function` 286 | 287 | ```c++ 288 | template 289 | auto make_logger3(R (*func)(Args...), const string& name) 290 | { 291 | return Logger3{function(func), name }; // () = call now 292 | } 293 | ``` 294 | 295 | 注意,我没有使用`std::function`,而是将第一个参数定义为普通函数指针。我们现在可以使用这个函数来实例化带有日志记录的函数调用并使用它 296 | 297 | ```c++ 298 | auto logged_add = make_logger3(add, "Add"); 299 | auto result = log_add(2, 3); 300 | ``` 301 | 302 | 当然,可以用依赖注入( Dependency Injection)代替`make_logger3`。这种方法的好处是: 303 | 304 | - 通过提供空的对象(`Null Object`)来动态打开和关闭日志记录,而不是实际的日志对象 305 | - 禁用被记录的代码的实际调用(同样,通过替换不同的日志对象) 306 | 307 | 总之,这是开发人员工具箱中的另一个有用的工具。[FIXME:]我把这种方法放入到依赖项注入中留给读者作为练习。 308 | 309 | #### 总结 310 | 311 | 在遵循开闭原则(OCP)的同时,装饰器为类提供了额外的功能。它的特点是可组合性:几个装饰器可以以任何顺序应用到一个对象上。我们已经研究了以下类型的装饰器: 312 | 313 | - **动态装饰器** 可以存储修饰对象的引用(甚至存储整个值,如果你想的话!),并提供动态(运行时)可组合性,但代价是不能访问底层对象自己的成员。 314 | - **静态装饰器** 使用`mixin`继承(从模板参数继承)在编译时组合装饰器。这失去了任何类型的运行时灵活性(您不能重新组合对象),但允许你访问底层对象的成员。这些对象也可以通过构造函数转发完全初始化。 315 | - **函数装饰器** 可以包装代码块或特定的函数,以允许 行为的组合 316 | 317 | 318 | 值得一提的是,在不允许多重继承的语言中,装饰器也用于模拟多重继承,方法是聚合多个对象,然后提供一个接口,该接口是聚合对象的接口的集合并。 -------------------------------------------------------------------------------- /docs/chapter-10-facade.md: -------------------------------------------------------------------------------- 1 | ### 第十章:外观模式 2 | 3 | facade 4 | 5 | 首先,让我们解决一下语言上的问题:字母Ç中的小曲线被称为“cedilla”,而字母本身被读成“S”,因此单词“façade”被读成“fah-saad”。欢迎在您的代码中使用字母ç,因为大多数编译器都能很好地处理它。 6 | 7 | 好了,现在,我们开始讨论这个模式…… 8 | 9 | 我花了很多时间在量化金融和算法交易领域工作。正如您可能猜到的那样,一个好的交易终端所需要的是将信息快速地传递到交易员的大脑中:您希望尽可能快地呈现东西,没有任何延迟。 10 | 11 | 大多数财务数据(除了图表)实际上都以纯文本形式呈现:在黑屏上显示白色字符。在某种程度上,这类似于terminal/console/命令行界面在您自己的操作系统中的工作方式,但是有一个细微的区别 12 | 13 | #### 终端如何工作 14 | 15 | 终端窗口的第一部分是*缓冲区(buffer)*。这是存储呈现的字符的地方。缓冲区是一个矩形的内存区域,通常是一个1D[^1]或2D `char`或`wchar_t`数组。缓冲区可以比终端窗口的可见区域大得多,因此它可以存储一些可以回滚的历史输出。 16 | 17 | 通常,缓冲区有一个指针(例如整数)来指定当前的输入行。这样,一个已满的缓冲区不会重新分配所有行;它只是覆盖了最老的那一行。 18 | 19 | 然后是*视窗(viewport)*的概念。视窗呈现特定缓冲区的一部分。一个缓冲区可以是巨大的,所以一个视窗只是从缓冲区中取出一个矩形区域并呈现它。当然,视窗的不能超过等于缓冲区的大小。 20 | 21 | 最后,还有控制台(终端窗口)本身。控制台显示视口,允许上下滚动,甚至接受用户输入。控制台实际上是一个外观:它是对幕后复杂设置的简化表示。 22 | 23 | 通常,大多数用户与单个缓冲区和视窗交互。但是,有可能有一个控制台窗口区域被垂直分割为两个视图,每个视图都有相应的缓冲区。这可以使用Linux下的`screen` 命令来实现。 24 | 25 | 注1: 大多数缓冲区通常是一维的。这样做的原因是,传递一个一维指针比传递一个二维指针更容易,而且当结构的大小是确定的和不可变的时,使用数组或向量没有多大意义。一维方法的另一个优点是,当涉及到GPU处理时,像CUDA这样的系统最多使用6维来寻址,所以过一段时间,从n维的块/网格位置计算一维索引就成了第二天性。 26 | 27 | #### 高级的终端 28 | 29 | 典型操作系统终端的一个问题是,如果你将大量数据输送到它里面,它的速度会非常慢。例如,Windows终端窗口(cmd.exe)使用GDI来渲染字符,这是完全不必要的。在一个快节奏的交易环境中,你希望渲染是硬件加速的:应该使用API(如OpenGL)字符让作为预渲染的纹理呈现在表面上。 30 | 31 | 一个交易终端由多个缓冲区和视窗组成。在典型的设置中,不同的缓冲区可能会与来自不同交易所或交易机器人的数据同时更新,所有这些信息都需要显示在单个屏幕上。 32 | 33 | 缓冲区还提供了比一维或二维线性存储更令人兴奋的功能。例如,`TableBuffer`可以定义为: 34 | 35 | ```c++ 36 | struct TableBuffer : IBuffer 37 | { 38 | TableBuffer( vector spec, int totalHeight ) 39 | { 40 | 41 | } 42 | struct TableColumnSpec 43 | { 44 | string header; 45 | int width; 46 | enum class TableColumnAlignment 47 | { 48 | Left, Center, Right 49 | } alignment; 50 | }; 51 | } ; 52 | ``` 53 | 54 | 换句话说,缓冲区可以接受某些参数并构建一个表(是的,一个很好的老式ascii格式的表!)并将其显示在屏幕上。 55 | 56 | 视窗负责从缓冲区获取数据。它具有以下特点: 57 | 58 | - 待显示缓冲区的引用 59 | - 大小 60 | - 如果视窗小于缓冲区,它需要指定它将显示缓冲区的哪一部分。它用绝对的x-y坐标表示 61 | - 视窗在整个控制终端的位置 62 | - 游标的位置,假设当前视窗在接收用户的输入, 63 | 64 | 65 | #### 外观在那里? 66 | 67 | 在这个特定的系统中,控制台本身就是外观。在内部,控制台必须管理大量不同的对象。 68 | 69 | ```c++ 70 | struct Console 71 | { 72 | vector viewPorts; 73 | Size charSize, girdSize; 74 | }; 75 | ``` 76 | 77 | 通常情况下,控制台初始化也是一件非常棘手的事情。由于它是外观,它实际上试图提供一个真正可访问的API。这可能需要一些合理的参数来初始化所有的内部成员。 78 | 79 | ```c++ 80 | Console::Console(bool fullscreen, int char_width, int char_height, 81 | int width, int height, optional client_size) 82 | { 83 | // 创建单个缓冲和视窗 84 | // 把缓冲和视图结合在一起并放入合适的集合中 85 | // 生成图像纹理 86 | // 网格大小的计算取决于我们是否需要全屏模式 87 | } 88 | ``` 89 | 90 | 或者,也可以将所有这些参数打包到一个对象中,这个对象同样有一些合理的默认值。 91 | 92 | ```c++ 93 | Console::Console(const ConsoleCreationParameters& ccp) 94 | { 95 | 96 | } 97 | struct ConsoleCreationParameters 98 | { 99 | optional client_size; 100 | int character_width{ 10 }; 101 | int character_height{ 14 }; 102 | int width{ 20 }; 103 | int height{ 30 }; 104 | bool fullscreen{ false }; 105 | bool create_default_view_and_buffer{ true }; 106 | }; 107 | ``` 108 | 109 | #### 总结 110 | 111 | 外观设计模式是一种将简单接口放在一个或多个复杂子系统前面的方法。在我们的例子中,可以直接使用涉及许多缓冲区和视窗的复杂设置,或者,如果你只想要一个带有单个缓冲区和相关视视窗的简单控制台,你可以通过一个非常容易访问和直观的API获得它。 -------------------------------------------------------------------------------- /docs/chapter-11-flyweight.md: -------------------------------------------------------------------------------- 1 | ### 享元模式 2 | 3 | 享元(有时也称为token或cookie)是一种临时组件,可以看作是对某个对象的智能引用。通常,享元适用于拥有大量非常相似的对象情况,并且希望最小化存储这些对象的内存量。 4 | 5 | 让我们看一下与此模式相关的一些场景。 6 | 7 | #### 用户名字 8 | 9 | 想象一个大型多人在线游戏。我跟你赌20美元,不止一个叫`John Smith`的用户,因为这是一个流行的名字。因此,如果我们一遍又一遍地存储这个名称(以ASCII格式),我们将为每个这样的用户花费11个字节。相反,我们可以只存储一次名称,然后存储一个指向该名称的每个用户的指针(只有8个字节)。真是省了不少空间。 10 | 11 | 也许将名字分割成姓和名更有意义:这样,`Fitzgerald Smith`将由两个指针(16字节)表示,分别指向名和姓。事实上,如果我们使用索引而不是名称,我们可以大大减少字节数。你不会指望有2^64个不同的名字,对吗? 12 | 13 | 我们可以对它进行类型定义,以便稍后进行调整 14 | 15 | ```c++ 16 | typedef uint32_t key; 17 | ``` 18 | 19 | 根据这个定义,我们可以将用户定义为: 20 | 21 | ```c++ 22 | struct User 23 | { 24 | User( const string& first_name, const string& last_name ): 25 | first_name{ add(first_name) }, 26 | last_name{ add(last_name) } 27 | { } 28 | 29 | protected: 30 | key first_name, last_name; 31 | static bimap names; 32 | static key seed; 33 | static key add (const string& s) { } 34 | }; 35 | ``` 36 | 37 | 如你所见,构造函数利用`add()`函数的返回值来初始化成员`first_name`和`last_name`。该函数根据需要将键-值对(键是从`seed`中生成的)插入到`names`中。我在这里使用`boost::bimap`(一个双向映射),因为它更容易搜索副本。记住,如果姓或名已经在bimap中,我们只返回它的索引。 38 | 39 | 下面是`add()`函数的实现: 40 | 41 | ```c++ 42 | static key add( const string& s ) 43 | { 44 | auto it = names.right.find( s ); 45 | if( it == names.right.end() ) 46 | { 47 | names.insert( {++seed, s} ); 48 | return seed; 49 | } 50 | return it->second; 51 | } 52 | ``` 53 | 54 | 这是`get-or-add`机制的标准执行。如果你以前没有接触过`bimap`,你可能想要查阅`bimap`的文档,了解更多关于它如何工作的信息。 55 | 56 | 如果我们想把姓和名给暴露出来(该成员对象位于保护段,类型是`key`,其实不是很有用!),我们可以提供适当的`getter`和`setter`。 57 | 58 | ```c++ 59 | const string& get_first_name() const 60 | { 61 | return name.left.find(first_name)->second; 62 | } 63 | 64 | const string& get_last_name const 65 | { 66 | return name.left.find(last_name)->second; 67 | } 68 | ``` 69 | 70 | 例如,要定义用户的`operator<<`,你可以简单地编写 71 | 72 | ```c++ 73 | friend ostream& operator<<(ostream& os, const User& obj) 74 | { 75 | return os << "first_name: " << obj.get_first_name() 76 | << " last_name: " << obj.get_last_name(); 77 | } 78 | ``` 79 | 80 | 就是这样的,我不打算提供节约了多少空间的统计数据(这个真的取决于你的样本大小),但是当有大量重复的用户名的时候,这样做的确显著的节约了空间。如果你还打算进一步节约空间,可以通过改变`key`的类型定义来进一步调整`sizeof(key)`的大小。 81 | 82 | 83 | 84 | 85 | #### Boost.Flyweight 86 | 87 | 在前面的示例中,我们动手实现了一个享元,其实我们可以直接使用Boost库中提供的`boost::flyweight`,这使得`User`类的实现变得非常简单。 88 | 89 | ```c++ 90 | struct User2 91 | { 92 | flyweight first_name, last_name; 93 | User2(const string& first_name, const string& last_name) : 94 | first_name { first_name }, 95 | last_name { last_name } 96 | { } 97 | }; 98 | ``` 99 | 你可以通过运行以下代码来验证它实际上是享元: 100 | 101 | ```c++ 102 | User2 john_doe{ "John", "Doe" }; 103 | User2 jane_doe{ "Jane", "Doe" }; 104 | cout << boolalpha << 105 | (&jane_doe.last_name.get() == &john_doe.last_name.get()); 106 | // true 107 | ``` 108 | 109 | #### String Ranges 110 | 111 | 如果你调用`std::string::substring()`,是否会返回一个全新构造的字符串?最后的结论是:如果你想操纵它,那当然,但是如果你想改变字串来修改原始对象呢?一些编程语言(例如`Swift、Rust`)显式地将子字符串作为范围返回,这同样是享元模式的实现,它节省了所使用的内存量,此外还允许我们通过指定区间(range)来操作底层对象。 112 | 113 | c++中与字符串区间操作等价的是`string_view`,对于数组来说还有其他的变体——只要避免复制数据就行!让我们试着构建我们自己的,非常简单的字符串区间操作(`string range`)。 114 | 115 | 让我们假设我们在一个类中存储了一堆文本,我们想获取该文本的某个区间的字符串并将其大写,有点像文字处理器或IDE可能做的事情。我们可以只大写每个单独的字母来实现,但是现在假设我们想保持底层的纯文本的原始状态,并且只在使用流输出操作符时才大写。 116 | 117 | #### 简单方法 118 | 119 | 一种非常简单明了的方法是定义一个布尔数组,它的大小与纯文本字符串相等,布尔数组中的值指示是否要大写该字符。我们可以这样实现它: 120 | 121 | ```c++ 122 | class FormattedText 123 | { 124 | string plainText; 125 | bool *cap; 126 | public: 127 | explicit FormattedText( const string& plainText ) : plainText { plainText } 128 | { 129 | caps = new bool [ plainText.length() ]; 130 | } 131 | 132 | ~FormattedText( ) 133 | { 134 | delete [ ] caps; 135 | } 136 | }; 137 | ``` 138 | 我们现在可以用一个实用方法来大写一个特定的范围: 139 | 140 | ```c++ 141 | void capitalize(int start, int end) 142 | { 143 | for( int i = start, i <= end; ++i ) 144 | caps[ i ] = true; 145 | } 146 | ``` 147 | 然后定义一个使用布尔掩码的流输出操作符: 148 | 149 | ```c++ 150 | friend std::ostream& operator << (std::ostrem& os, const Formatted& obj) 151 | { 152 | string s; 153 | for( int i = 0; i < obj.plainText.length(); ++i) 154 | { 155 | char c = obj.plainText[ i ]; 156 | s += ( obj.cap[ i ] ? toupper( c ) : c ); 157 | } 158 | return os << s; 159 | } 160 | ``` 161 | 162 | 不要误解我的意思,这种方法是有效的。在这里: 163 | 164 | ```c++ 165 | FormattedText ft("This is a brave new world"); 166 | ft.capitalize(10, 15); 167 | cout << ft << endl; 168 | // prints "This is a BRAVE new world" 169 | ``` 170 | 171 | 但是,再一次地,将每个角色定义为拥有一个 172 | 布尔标记,当只使用开始和结束标记时。 173 | 174 | #### 享元实现 175 | 176 | 让我们利用享元模式来实现一个`BetterFromattedText`类。我们将定义一个外部类和一个嵌套类,在嵌套类中实现享元。 177 | ```c++ 178 | 179 | class BetterFormattedText 180 | { 181 | public: 182 | struct TextRange 183 | { 184 | int start, end; 185 | bool capitalize; 186 | // other options here, e.g. bold, italic, etc. 187 | bool covers(int position) const 188 | { 189 | return position >= start && position <= end; 190 | } 191 | }; 192 | private: 193 | string plain_text; 194 | vector formatting; 195 | }; 196 | 197 | 如您所见,`TextRange`只存储它所应用的起始点和结束点,以及我们是否希望将文本大写的实际格式信息,以及任何其他格式选项(粗体、斜体等)。它只有一个成员函数`covers()`,用来确定是否需要将此格式应用于给定位置的字符。 198 | 199 | `BetterFormattedText`用一个`vector`来存储·TextRange`的享元,并能够根据需要构建新的享元: 200 | 201 | ```c++ 202 | TextRange& get_range(int start, int end) 203 | { 204 | formatting.emplace_back(TextRange{ start, end }); 205 | return *formatting.rbegin(); 206 | } 207 | ``` 208 | 209 | 上面的`get_range()`函数做了三件事: 210 | 211 | 1. 构建一个新的`TextRange`对象 212 | 2. 把构建的对象移动到`vector`中 213 | 3. 返回`vector`中最有一个元素的引用 214 | 215 | 在前面的实现中,我们没有检查重复的区间范围,如果是基于享元模式节约空间的精神的话也可以进一步加以改进。 216 | 217 | 现在我们来实现`BetterFormattedText`中的`operator<<`: 218 | 219 | ```c++ 220 | friend std::ostream& operator<<(std::ostream& os, 221 | const BetterFormattedText& obj) 222 | { 223 | string s; 224 | for (size_t i = 0; i < obj.plain_text.length(); i++) 225 | { 226 | auto c = obj.plain_text[i]; 227 | for (const auto& rng : obj.formatting) 228 | { 229 | if (rng.covers(i) && rng.capitalize) 230 | c = toupper(c); 231 | 232 | } 233 | s += c; 234 | } 235 | return os << s; 236 | } 237 | ``` 238 | 239 | 同样,我们所做的就是遍历每个字符并检查是否有覆盖它的范围。如果有,则应用范围指定的内容,在本例中就是大写。注意,这种设置允许范围自由重叠。 240 | 241 | 现在,我们可以使用之前构造的所有东西来将这个单词大写,尽管API稍有不同,但更灵活: 242 | 243 | ```c++ 244 | BetterFormattedText bft("This is a brave new world"); 245 | bft.get_range(10, 15).capitalize = true; 246 | cout << bft << endl; 247 | // prints "This is a BRAVE new world" 248 | ``` 249 | #### 总结 250 | 251 | 享元模式是一种节约空间的技术。它存在许多确切的变体: 252 | 有时,享元作为API `token`返回,允许你执行修改;而在其他时候,享元是隐式的,隐藏在场景后面—就像我们的`User`一样,客户端并不打算知道实际使用的享元。 -------------------------------------------------------------------------------- /docs/chapter-12-proxy.md: -------------------------------------------------------------------------------- 1 | ### 代理 2 | 3 | 当我们关注装饰器设计模式时,我们看到了增强对象功能的不同方式。代理设计模式与此类似,但其目标通常是在提供某些内部增强功能的同时准确(或尽可能接近)保留正在使用的`API`。 4 | 5 | `Proxy` 并不是真正的同质 `API`,因为人们构建的不同种类的代理数量相当多并且服务完全不同 6 | 目的。在本章中,我们将介绍一系列不同的代理对象,你可以在网上找到更多信息。 7 | 8 | 9 | #### 智能指针 10 | 11 | 代理模式最简单、最直接的例子是智能指针。智能指针对普通指针进行了包装,同时保留引用计数,重写了某些操作符,但总的来说,它提供和普通指针一样的使用接口。 12 | 13 | ```c++ 14 | struct BankAccount { 15 | void deposit(int amount) { ... } 16 | }; 17 | 18 | BankAccount *ba = new BankAccount; 19 | ba->deposit(123); 20 | auto ba2 = make_shared(); 21 | ba2->deposit(123); // 相同的 API! 22 | ``` 23 | 24 | 因此,智能指针在某些地方可以用来替代普通指针。例如,`if (ba){...}`无论`ba`是普通指针还是智能指针,`*ba`在这两种情况下可以获得底层对象。 25 | 26 | 当然,差异是存在的。最明显的一个是你不需要在智能指针上调用delete。但除此之外,它真的试图尽可能地接近一个普通的指针。 27 | 28 | 29 | 30 | #### 属性代理 31 | 32 | 在其他编程语言中,属性用于指示字段和该字段的一组`getter/setter`方法。在c++[^1]中没有属性,但是如果我们想继续使用一个字段,同时给它特定的访问/修改(`accessor/mutator`)行为,我们可以构建一个*属性代理* 33 | 34 | 本质上,属性代理是一个可以伪装成属性的类,所以我们可以这样定义它: 35 | 36 | ```c++ 37 | template 38 | struct Property { 39 | T value; 40 | Property(const T initial_value) { *this = initial_value; } 41 | operator T() { 42 | return value; 43 | // 执行一些getter操作 44 | } 45 | T operator=(T new_value) { 46 | return value = new_value; 47 | // 执行一些setter操作 48 | } 49 | }; 50 | ``` 51 | 52 | 在前面的实现中,我已经向通常要自定义(或直接替换)的位置添加了注释,如果要采用这种方法,注释大致对应于`getter /setter`的位置。 53 | 54 | 因此,我们的类`Property`本质上是`T`的替换,不管它是什么。它的工作原理是简单地允许与T的转换,并允许两者都使用value字段。现在,你可以把它用作一个字段。 55 | 56 | ```c++ 57 | struct Creature { 58 | Property strength{10}; 59 | Property agility{5}; 60 | }; 61 | ``` 62 | 63 | 一个字段上的典型操作也将在属性代理类型的字段上工作 64 | 65 | ```c++ 66 | Creature creature; 67 | creature.agility = 20; 68 | auto x = creature.strength 69 | ``` 70 | 71 | #### 虚代理 72 | 73 | 如果试图对nullptr或未初始化的指针进行解引用,就会自找麻烦。然而,在某些情况下,您只希望在访问对象时构造该对象,而不希望过早地分配它。 74 | 75 | 这种方法称为延迟实例化。如果你确切地知道哪些地方需要懒惰行为,你就可以提前计划并为之做特别的准备。如果您不这样做,那么您可以构建一个接受现有对象并使其懒惰的代理。我们称之为虚拟代理,因为底层对象可能根本不存在,所以我们访问的不是具体的东西,而是虚拟的东西。 76 | 77 | 想象一个典型的`Image`接口: 78 | 79 | ```c++ 80 | struct Image { 81 | virtual void draw() = 0; 82 | }; 83 | ``` 84 | 85 | `Bitmap`的即时(与惰性相反)实现将在构建时从文件中加载图像,即使该图像实际上并不需要。(是的,下面的代码是模拟的。) 86 | 87 | ```c++ 88 | struct Bitmap : Image { 89 | Bitmap(const string &filename) { 90 | cout << "Loading image from " << filename << endl; 91 | } 92 | void draw() override { cout << "Drawing image " << filename << endl; } 93 | }; 94 | ``` 95 | 96 | 构造这个位图的行为将触发图像的加载: 97 | 98 | ```c++ 99 | Bitmap img{ "pokemon.png" }; // 从pokemon.png加载图像 100 | ``` 101 | 102 | 那不是我们想要的。我们想要的是那种只在使用draw()方法时加载自身的位图。现在,我想我们可以跳回到位图,让它变得懒惰,但假设它是固定的,不能修改(或继承,就此而言)。 103 | 104 | 因此,我们可以构建一个虚拟代理,它将聚合原来的位图,提供一个相同的接口,并重用原来的位图功能: 105 | 106 | ```c++ 107 | struct LazyBitmap : Image { 108 | LazyBitmap(const string filename) : filename(filename) {} 109 | ~LazyBitmap() { delete bmp; } // 如果*bmp没有先被new出来,这里就会出错 110 | 111 | void draw() override { 112 | if (!bmp) bmp = new Bitmap(filename); 113 | bmp->draw(); 114 | } 115 | 116 | private: 117 | Bitmap *bmp{nullptr}; 118 | string filename; 119 | }; 120 | ``` 121 | 122 | 我们到了。正如您所看到的,这个`LazyBitmap`的构造函数要轻得多:它所做的只是存储用于加载图像的文件名,这样图像实际上就不会被加载。 123 | 124 | 所有神奇的事情都发生在`draw()`中:这是我们检查`bmp`指针的地方,以查看底层(`eager`!)位图是否已经构造好了。如果没有,我们就构造它,然后调用它的`draw()`函数来实际绘制图像。 125 | 126 | 现在假设您有一些使用`Image`类型的API: 127 | 128 | ```c++ 129 | void draw_image(Image& img) { 130 | cout << "About to draw the image" << endl; 131 | img.draw(); 132 | cout << "Done drawing the image" << endl; 133 | } 134 | ``` 135 | 136 | 我们可以将该API与一个`LazyBitmap`实例一起使用,而不是使用`Bitmap`(万岁,多态!)来渲染图像,以一种惰性的方式加载它: 137 | 138 | ```c++ 139 | LazyBitmap img{ "pokemon.png" }; 140 | draw_image(img); // image loaded here 141 | // About to draw the image 142 | // Loading image from pokemon.png 143 | // Drawing image pokemon.png 144 | // Done drawing the image 145 | ``` 146 | 147 | #### 通信代理 148 | 149 | Suppose you call a member function foo() on an object of type Bar. Your typical assumption is that Bar has been allocated on the same machine as the one running your code, and you similarly expect Bar::foo() to execute in the same process. 150 | 151 | Now imagine that you make a design decision to move Bar and all its members off to a different machine on the network. But you still want the old code to work! If you want to keep going as before, you’ll need a communication proxy—a component that proxies the calls “over the wire” and of course collects results, if necessary. 152 | 153 | Let’s implement a simple ping-pong service to illustrate this. First, we define an interface: 154 | 155 | 156 | 假设你在 `Bar` 类型的对象上调用成员函数 `foo()`。你的典型假设是 `Bar` 与运行你的代码的机器分配在同一台机器上,并且你同样希望与`Bar::foo()` 在同一进程中执行。 157 | 158 | 现在假设你做出了一个设计决定,将 `Bar` 及其所有成员移到网络上的另一台机器上。但是你仍然希望旧代码能够工作!如果你想像以前一样继续,你需要一个通信代理——一个代理“通过线路”的调用的组件,当然如果需要的话收集结果。 159 | 160 | 让我们实现一个简单的乒乓服务(ping-pong service)来说明这一点。首先,我们定义一个接口: 161 | 162 | ```c++ 163 | struct Pingable { 164 | virtual wstring ping(const wstring& message) = 0; 165 | }; 166 | ``` 167 | 168 | 如果我们正在构建乒乓进程,我们可以将 `Pong` 实现为 169 | 如下: 170 | 171 | ```c++ 172 | struct Pong : Pingable { 173 | wstring ping(const wstring& message) override { return message + L" pong"; } 174 | }; 175 | ``` 176 | 177 | 基本上,你 ping 一个 `Pong`,它会将单词 `“ pong”` 附加到消息的末尾并返回该消息。请注意,我在这里没有使用 `ostringstream&`,而是在每次都时创建一个新字符串:这个 `API` 很容易复制为 `Web` 服务。 178 | 179 | 我们现在可以尝试这个设置,看看它是如何在进程中工作的: 180 | 181 | ```c++ 182 | void tryit(Pingable& pp) { wcout << pp.ping(L"ping") << "\n"; } 183 | Pong pp; 184 | for (int i = 0; i < 3; ++i) { 185 | tryit(pp); 186 | } 187 | ``` 188 | 最终的结果是上述代码按照我们想要的方式打印了 3 次 “ping pong”。 189 | 现在,假设你决定将 `Pingable` 服务重新定位到很远很远的 `Web` 服务器。也许你甚至决定使用其他平台,例如 `ASP.NET`,而不是 `C++`: 190 | 191 | ```c# 192 | [Route("api/[controller]")] 193 | public class PingPongController : Controller { 194 | [HttpGet("{msg}")] 195 | public string Get(string msg) { return msg + " pong"; } 196 | } // achievement unlocked: use C# in a C++ book 197 | ``` 198 | 199 | 通过此设置,我们将构建一个名为 `RemotePong` 的通信代理 200 | 这将用于代替 `Pong`。微软的 `REST SDK` 在这里派上了用场。 201 | 202 | ```c# 203 | struct RemotePong : Pingable { 204 | wstring ping(const wstring& message) override { 205 | wstring result; 206 | http_client client(U("http://localhost:9149/")); 207 | uri_builder builder(U("/api/pingpong/")); 208 | builder.append(message); 209 | pplx::task task = client.request(methods::GET, builder.to_string()) 210 | .then([=](http_response r) { 211 | return r.extract_string(); 212 | }); 213 | task.wait(); 214 | return task.get(); 215 | } 216 | }; 217 | ``` 218 | 219 | 注1: Microsoft REST SDK 是一个用于处理 REST 服务的 C++ 库。它既是开源的又是跨平台的。你可以在 GitHub 上找到它:https:/ github.com/Microsoft/cpprestsdk. 220 | 221 | 如果你不习惯 `REST SDK`,前面的内容可能看起来有点令人困惑;除了 `REST` 支持之外,`SDK` 还使用了并发运行时,这是一个 `Microsoft` 库,用于并发支持。实现此功能后,我们现在可以进行一个更改: 222 | 223 | ```c++ 224 | RemotePong pp; // was Pong 225 | for (int i = 0; i < 3; ++i) { 226 | tryit(pp); 227 | } 228 | ``` 229 | 230 | 就是这样,你得到相同的输出,但实际的实现可以在地球另一端某个地方的 `Docker` 容器中的 `Kestrel` 上运行。 231 | 232 | #### 总结 233 | 234 | 本章介绍了一些代理。与装饰器模式不同,代理不会尝试通过添加新成员来扩展对象的功能(除非它无能为力)。它试图做的只是增强现有成员的潜在行为。 235 | 236 | 存在大量不同的代理: 237 | 238 | - 属性代理是替代对象,可以在分配和/或访问期间替换字段并执行附加操作。 239 | - 虚拟代理提供对底层对象的虚拟访问,并且可以实现延迟对象加载等行为。你可能觉得你正在处理一个真实的对象,但底层实现可能尚未创建,例如,可以按需加载。 240 | - 通信代理允许我们更改对象的物理位置(例如,将其移动到云端),但允许我们使用几乎相同的 `API`。当然,在这种情况下,`API` 只是远程服务(如 REST API)的一个垫片(`shim`)。 241 | - 日志代理除了调用底层函数之外,还可以执行日志记录。 242 | 243 | 注2:在程序设计领域,垫片(shim)是一种小型函数库,可以用来截取 API 调用、修改传入参数,最后自行处理对应操作或者将操作交由其它地方执行。垫片可以在新环境中支持老 API,也可以在老环境里支持新 API。一些程序并没有针对某些平台开发,也可以通过使用垫片来辅助运行。 244 | 245 | 还存在很多其他代理,而且你自己构建的代理很可能不会属于预先存在的类别,而是会执行一些特定于你专业领域内的操作。 -------------------------------------------------------------------------------- /docs/chapter-13-chain_of_responsibility.md: -------------------------------------------------------------------------------- 1 | ### 第13章 责任链 2 | 3 | 考虑一个典型的公司违规行为:内幕交易。 4 | 假设一个特定的交易员被当场抓住利用内幕消息进行交易。 5 | 这事该怪谁?如果管理层不知道,那就是交易员了。 6 | 但或许交易员的同行也参与其中,在这种情况下,集团经理可能要对此负责。 7 | 或者,这种做法是一种制度上的做法,在这种情况下,应该受到指责的是首席执行官。 8 | 9 | 这是责任链(Chain of Responsibility, CoR)的一个示例:系统中有几个不同的元素,它们都可以一个接一个地处理消息。 10 | 作为一个概念,它很容易实现,因为这意味着使用某种类型的列表。 11 | 12 | #### 场景 13 | 14 | 想象一个电脑游戏,每个生物(`creature`)都有一个名字和两个属性值攻击(`attack`)和防御(`defense`): 15 | 16 | ```c++ 17 | struct Creature 18 | { 19 | string name; 20 | int attack, defense; 21 | // 构造函数和<<运算符 22 | }; 23 | ``` 24 | 25 | 现在,随着生物在游戏中的进展,它可能会遇到一个道具(例如,一把魔法剑),或者它最终会被施魔法。 26 | 在任何一种情况下,它的攻击和防御值都将被我们称为`CreatureModifier`的东西修改。 27 | 此外,应用多个修改器的情况并不少见,所以我们需要能够在生物上堆叠修改器,允许它们按照被附加的顺序应用。 28 | 让我们看下是如何实现的。 29 | 30 | 31 | #### 指针链 32 | 33 | 在经典的责任链(CoR)方式中,我们将实现`CreatureModifier`如下: 34 | 35 | ```c++ 36 | class CreatureModifier 37 | { 38 | CreatureModifier* next {nullptr}; 39 | protected: 40 | Creature& creature; // 可选:指针或共享指针 41 | public: 42 | explicit CreatureModifier(Creature& creature) : creature(creature) {} 43 | void add(CreatureModifier* cm) 44 | { 45 | if(next) 46 | next->add(cm); 47 | else 48 | next = cm; 49 | } 50 | virtual void handle() 51 | { 52 | if(next) next->handle(); // 关键之处! 53 | } 54 | }; 55 | ``` 56 | 57 | 这里发生了很多事情,所以让我们依次讨论一下: 58 | 59 | - 该类获取并存储它计划修改的生物的引用。 60 | - 该类实际上做的不多,但它不是抽象的:它的所有成员都有实现。 61 | - `next`成员指向一个可选的`CreatureModifer`。当然,这意味着它所指向的修改器是`CreatureModifer`的继承者。 62 | - 添加另一个生物修改器到修改器链中。这是递归执行的:如果当前修饰符为`nullptr`,则将其设置为该值,否则遍历整个链并将其放到末端。 63 | - 函数`handle()`只是处理链中的下一项,如果它存在的话;它没有自己的行为。它是虚函数,这意味着它需要被覆盖。 64 | 65 | 到目前为止,我们得到的只是一个可怜的只追加的单链表的实现。但是当我们开始继承它的时候,事情会变得更清晰。例如,以下是你如何制作一个修饰器,使生物的攻击值加倍。 66 | 67 | ```c++ 68 | class DoubleAttackModifier : public CreatureModifier 69 | { 70 | public: 71 | explicit DoubleAttackModifier(Creature& creature) : CreatureModifier(creature) {} 72 | 73 | void handle() override 74 | { 75 | creature.attack *= 2; 76 | CreatureModifier::handle(); 77 | } 78 | }; 79 | ``` 80 | 81 | 好了,我们终于有进展了。因此,这个修饰器继承自`CreatureModifier`,并且在它的`handle()`方法中做两件事:使攻击值加倍,并从基类调用`handle()`。第二部分至关重要:应用修改器链的唯一方法是每个继承类都不要忘记在其自己的`handle()`实现的末尾调用基类的方法。 82 | 83 | 这是另一个更复杂的修饰器。该修饰器将攻击值为2或2以下的生物的防御能力增加1: 84 | 85 | 86 | ```c++ 87 | class DoubleAttackModifier : public CreatureModifier 88 | { 89 | public: 90 | explicit DoubleAttackModifier(Creature& creature) : CreatureModifier(creature) {} 91 | 92 | void handle() override 93 | { 94 | if(creature.attack <= 2) 95 | creature.attack += 1; 96 | CreatureModifier::handle(); 97 | } 98 | }; 99 | ``` 100 | 101 | 同样,我们在最后调用基类。把所有这些放在一起,我们现在可以创建一个生物并对它应用一个修改器的组合: 102 | 103 | ```c++ 104 | Creature goblin{ "Goblin", 1, 1 }; 105 | CreatureModifier root{ goblin }; 106 | DoubleAttackModifier r1{ goblin }; 107 | DoubleAttackModifier r1_2{ goblin }; 108 | IncreaseDefenseModifier r2{ goblin }; 109 | root.add(&r1); 110 | root.add(&r1_2); 111 | root.add(&r2); 112 | root.handle(); 113 | cout << goblin << endl; 114 | // name: Goblin attack: 4 defense: 1 115 | ``` 116 | 117 | 正如你所看到的,地精是4/1,因为它的攻击加倍了两次,并且防御调整值,虽然添加了,但并不影响它的防御值。 118 | 119 | 这里还有一个有趣的问题。假设你决定对一个生物施放一个不能加值的法术。这容易做吗?其实很简单,因为你要做的就是避免调用基类的`handle()`:这避免了执行整个链。 120 | 121 | ```c++ 122 | class NoBonusesModifier : public CreatureModifier 123 | { 124 | public: 125 | explicit NoBonusesModifier(Creature& creature) : CreatureModifier(creature) {} 126 | 127 | void handle() override 128 | { 129 | // 在这里什么也不需要做! 130 | } 131 | }; 132 | ``` 133 | 134 | 现在,如果将`NoBonusesModifier`插入链的开头,则不会再应用其他元素。 135 | 136 | #### 代理链 137 | 138 | 指针链的例子是非常人工的。在现实世界中,你会希望生物能够任意承担和失去加成,这是一个仅追加链表所不支持的。此外,你不想永久地修改底层生物的属性(就像我们做的那样),你想要保持临时修改。 139 | 140 | 实现CoR的一种方法是通过一个集中的组件。这个组件可以保存游戏中所有可用的修改器列表,并且可以通过确保所有相关的加值被应用来帮助查询特定生物的攻击或防御。 141 | 142 | 我们将要构建的组件称为*事件代理(`event broker`)*。由于它连接到每个参与组件,因此它表示中介者设计模式,而且,由于它通过事件响应查询,因此它利用了观察者设计模式。 143 | 144 | 让我们构建一个。首先,我们将定义一个名为Game的结构,它将代表正在玩的游戏: 145 | 146 | ```c++ 147 | struct Game // 中介者 148 | { 149 | signal queries; 150 | }; 151 | ``` 152 | 153 | 我们正在使用Boost.Signals2库,用于保存称为`queries`的信号。本质上,这让我们做的是发射这个信号(`signal`),并由每个插槽`solt`(监听组件)处理它。但是事件与查询生物的攻击值或防御值有什么关系呢? 154 | 155 | 好吧,假设你想要查询一个生物的统计信息。你当然可以尝试读取一个字段,但请记住:我们需要在知道最终值之前应用所有修饰器。因此,我们将把查询封装在一个单独的对象中(这是命令模式),定义如下: 156 | 157 | ```c++ 158 | struct Query 159 | { 160 | string creature_name; 161 | enum Argument { attack, defense} argument; 162 | int result; 163 | }; 164 | ``` 165 | 166 | 我们在前面提到的类中所做的一切都包含了从生物中查询特定值的概念。我们需要提供的只是生物的名称和我们感兴趣的统计信息。`Game::Query`将构建并使用这个值来应用修饰器并返回最终值。 167 | 168 | 现在,让我们来看看生物的定义。这和我们之前的很相似。在字段方面唯一的区别是`Game`的引用: 169 | 170 | ```c++ 171 | class Creature 172 | { 173 | Game& game; 174 | int attack, defense; 175 | public: 176 | string name; 177 | Creature(Game& game, ...) : game { game }, ... { ... } 178 | // 其他成员函数 179 | }; 180 | ``` 181 | 182 | 现在,注意`attack`和`defense`是私有的。这意味着,为了获得最终的(后修饰符)攻击值,你需要调用一个单独的getter函数: 183 | 184 | ```c++ 185 | int Creature::get_attack() const 186 | { 187 | Query q{name, Query::Argument::attack, attack}; 188 | game.queries(q); 189 | return q.result; 190 | } 191 | ``` 192 | 193 | 这就是奇迹发生的地方!我们不只是返回一个值或静态地应用一些基于指针的链,而是使用正确的参数创建一个`Query`,然后将查询发送给订阅`Game::queries`的任何人处理。每个订阅组件都有机会修改基线攻击值。 194 | 195 | 现在让我们来实现修改器。同样,我们将创建一个基类,但这一次它没有`handle()`方法: 196 | 197 | ```c++ 198 | class CreatureModifier: 199 | { 200 | Game& game; 201 | Creature& creature; 202 | public: 203 | CreatureModifier(Game& game, Creature& creature) : 204 | game(game), creature(creature) 205 | {} 206 | }; 207 | ``` 208 | 209 | 因此修饰器基类并不是特别有趣。实际上,你完全可以不使用它,因为它所做的只是确保使用正确的参数调用构造函数。但是由于我们已经使用了这种方法,现在让我们继承`CreatureModifier`,看看如何执行实际的修改: 210 | 211 | ```c++ 212 | class DoubleAttackModifier : public CreatureModifier 213 | { 214 | connection conn; 215 | public: 216 | DoubleAttackModifier(Game& game, Creature& creature) 217 | : CreatureModifier(game, creature) 218 | { 219 | conn = game.queries.connect([&](Query& q){ 220 | if (q.creature_name == creature.name && 221 | q.argument == Query::Argument::attack) 222 | q.result *= 2; 223 | }); 224 | } 225 | ~DoubleAttackModifier() { conn.disconnect(); } 226 | }; 227 | ``` 228 | 229 | 如您所见,所有的乐趣都发生在构造函数和析构函数中;不需要其他方法。在构造函数中,我们使用`Game`引用获取`Game::queries`信号并连接到它,指定一个`lambda`表达式使攻击加倍。当然,`lambda`表达式必须做一些检查:我们需要确保我们增加了正确的生物(我们通过名称进行比较),并且我们所追求的统计数据实际上是`attack`。这两条信息都保存在查询引用中,就像我们修改的初始结果值一样。 230 | 231 | 232 | 我们还注意存储信号连接,以便在对象被销毁时断开它。这样,我们可以暂时应用修改器,让它在修改器超出作用域时失效。 233 | 234 | 把它们放在一起,我们得到以下结果: 235 | 236 | ```c++ 237 | Game game; 238 | Creature goblin{ game, "Strong Goblin", 2, 2 }; 239 | cout << goblin << endl; 240 | // name: Strong Goblin attack: 2 defense: 2 241 | { 242 | DoubleAttackModifier dam{ game, goblin }; 243 | cout << goblin << endl; 244 | // name: Strong Goblin attack: 4 defense: 2 245 | } 246 | cout << goblin << endl; 247 | // name: Strong Goblin attack: 2 defense: 2 248 | ``` 249 | 250 | 这里发生了什么事?在被修改之前,地精是2/2。然后,我们制造一个范围,其中地精受到双重攻击修改器的影响,所以在范围内它是一个4/2的生物。一旦退出作用域,修改器的析构函数就会触发,并断开自己与代理的连接,因此在查询值时不再影响它们。因此,地精本身再次恢复为2/2的生物。 251 | 252 | #### 总结 253 | 254 | 责任链是一种非常简单的设计模式,它允许组件依次处理命令(或查询)。`CoR`最简单的实现是创建一个指针链,从理论上讲,可以用一个普通的`vector`替换它,或者,如果希望快速删除,也可以用一个`list`。更复杂的代理链实现还利用中介模式和观察者模式允许我们处理查询事件(信号),在最终的值返回给客户端之前,让每个订阅者对最初传递的对象(它是贯穿整个链的单个引用)执行修改。 -------------------------------------------------------------------------------- /docs/chapter-14-command.md: -------------------------------------------------------------------------------- 1 | ### 第24章:命令模式 2 | 3 | #### 场景 4 | 让我们试着模拟一个有余额和透支限额的银行账户。我们将实现deposit()和withdraw()功能: 5 | ```c++ 6 | 1 struct BankAccount 7 | 2 { 8 | 3 int balance = 0; 9 | 4 int overdraft_limit = -500; 10 | 5 11 | 6 void deposit(int amount) 12 | 7 { 13 | 8 balance += amount; 14 | 9 cout << "deposited " << amount << ", balance is now " <= overdraft_limit) 21 | 16 { 22 | 17 balance -= amount; 23 | 18 cout << "withdrew " << amount << ", balance is now " << balance << "\n"; 24 | 19 25 | 20 } 26 | 21 } 27 | 22 }; 28 | ``` 29 | 当然,现在我们可以直接调用成员函数,但是让我们假设,出于审计的目的,我们需要记录每一次存款和取款,我们不能在银行帐户中这样做,因为我们已经设计、实现并测试了这个类。 30 | 31 | #### 实现命令模式 32 | 我们将首先为命令定义一个接口 33 | 34 | ```c++ 35 | 1 struct Command 36 | 2 { 37 | 3 virtual void call() const = 0; 38 | 4 }; 39 | ``` 40 | 41 | 定义好接口后,我们可以使用它来定义一个银行账户命令,该命令将封装关于如何处理银行帐户的信息: 42 | 43 | ```c++ 44 | 1 struct BankAccountCommand : Command 45 | 2 { 46 | 3 BankAccount& account; 47 | 4 enum Action { deposit, withdraw } action; 48 | 5 int amount; 49 | 6 50 | 7 BankAccountCommand(BankAccount& account, const Action 51 | 8 action, const int amount) 52 | 9 : account(account), action(action), amount(amount) {} 53 | ``` 54 | 55 | 命令中的信息包含以下内容 56 | • 要操作的账户 57 | • 要采取的行动;选项的集合和存储选项的变量都在一个声明中定义 58 | • 存入或提取的金额 59 | 60 | 一旦客户提供了此信息,我们就可以获取并使用它来执行存款或取款 61 | 62 | ```c++ 63 | 1 void call() const override 64 | 2 { 65 | 3 switch (action) 66 | 4 { 67 | 5 case deposit: 68 | 6 account.deposit(amount); 69 | 7 break; 70 | 8 case withdraw: 71 | 9 account.withdraw(amount); 72 | 10 break; 73 | 11 } 74 | 12 } 75 | ``` 76 | 77 | 通过这种方法,我们可以创建命令,然后对该命令执行帐户权限的修改: 78 | 79 | ```c++ 80 | 1 BankAccount ba; 81 | 2 Command cmd{ba, BankAccountCommand::deposit, 100}; 82 | 3 cmd.call(); 83 | ``` 84 | 85 | 这将把100元存入我们的账户。轻松!如果您担心我们仍然向客户公开原始的deposit()和withdraw()成员函数,您可以将他们设为私有,只需将BankAccountCommand指定为友元类。 86 | 87 | #### 撤销操作 88 | 89 | 由于一个命令封装了关于对银行帐户的某些修改的所有信息,它同样可以回滚这个修改,并将其目标对象返回到其先前的状态。 90 | 首先,我们需要决定是否将撤销相关操作插入我们的Command接口。为了简洁起见,我们将在这里做,但总的来说,这是一个设计决策,需要尊重我们在本书开头(第1章)讨论的接口隔离原则。例如,如果您设置一些命令是final的,并且不受撤销机制的约束。比如说,将命令分为可调用的和可撤销的,是有意义的。 91 | 不管怎样,这是已经更新的命令;注意我已经有意地从函数中删除了const: 92 | 93 | ```c++ 94 | 1 struct Command 95 | 2 { 96 | 3 virtual void call() = 0; 97 | 4 virtual void undo() = 0; 98 | 5 }; 99 | ``` 100 | 101 | 下面是BankAccountCommand::undo()的一个简单实现,其动机是(不正确的)假设账户存款和取款是对称的操作。 102 | 103 | ```c++ 104 | 1 void undo() override 105 | 2 { 106 | 3 switch (action) 107 | 4 { 108 | 5 case withdraw: 109 | 6 account.deposit(amount); 110 | 7 break; 111 | 8 case deposit: 112 | 9 account.withdraw(amount); 113 | 10 break; 114 | 11 } 115 | 12 } 116 | ``` 117 | 118 | 为什么说这个实现会崩溃?因为如果你试图提取相当于一个发达国家GDP的金额,你就不会成功,但当回滚交易时,我们没有办法说它失败了!为了获得这些消息,我们修改了withdraw()去返回一个成功的标记。 119 | 120 | ```c++ 121 | 1 bool withdraw(int amount) 122 | 2 { 123 | 3 if (balance - amount >= overdraft_limit) 124 | 4 { 125 | 5 balance -= amount; 126 | 6 cout << "withdrew " << amount << ", balance now " << 127 | 7 balance << "\n"; 128 | 8 return true; 129 | 9 } 130 | 10 return false; 131 | 11 } 132 | ``` 133 | 134 | 这样就好多了!我们现在可以修改整个BankAccountCommand去做两件事: 135 | • 取款时在内部存储一个成功标志。 136 | • 当调用undo()时使用这个标志 137 | 138 | ```c++ 139 | 1 struct BankAccountCommand : Command 140 | 2 { 141 | 3 ... 142 | 4 bool withdrawal_succeeded; 143 | 5 144 | 6 BankAccountCommand(BankAccount& account, const Action action, 145 | 7 const int amount) 146 | 8 : ..., withdrawal_succeeded{false} {} 147 | 9 148 | 10 void call() override 149 | 11 { 150 | 12 switch (action) 151 | 13 { 152 | 14 ... 153 | 15 case withdraw: 154 | 16 withdrawal_succeeded = account.withdraw(amount); 155 | 17 break; 156 | 18 } 157 | 19 } 158 | ``` 159 | 160 | 你现在明白为什么我要把const从Command中移除了吗?现在我们分配了一个成员变量withdrawal_succeeded,我们不能再声称call()是const的。我想我可以把它保存在undo()上,但这没有什么好处。好了,现在我们有了标志,我们可以改进undo()的实现。 161 | 162 | ```c++ 163 | 1 void undo() override 164 | 2 { 165 | 3 switch (action) 166 | 4 { 167 | 5 case withdraw: 168 | 6 if (withdrawal_succeeded) 169 | 7 account.deposit(amount); 170 | 8 break; 171 | 9 ... 172 | 10 } 173 | 11 } 174 | ``` 175 | 176 | 我们最终可以以一致的方式撤销withdraw()命令。当然本练习的目的是为了说明,除了存储要执行的操作的信息之外,命令还可以存储一些中间信息。这些信息同样对于审计之类的事情有用:如果您检测到100次的withdraw()操作,您可以调查潜在的黑客攻击。 177 | 178 | #### 组合命令 179 | 一次从账户A到账户B的转账,可以用两个命令模拟: 180 | 181 | 1.从A中取X元 182 | 2.从B中存X元 183 | 184 | 如果我们可以创建并调用一个封装这两个命令的命令,而不是调用两个命令,那就太好了。 185 | 这就是我们再第8章讨论的组合设计模式的本质。 186 | 让我们定义一个组合命令。我们将从vector继承。这可能会有问题,因为std::vector没有虚析构函数,但在我们的情况下这不是问题,这里有一个非常简单的定义: 187 | 188 | ```c++ 189 | 1 struct CompositeBankAccountCommand : vector 190 | , Command 191 | 2 { 192 | 3 CompositeBankAccountCommand(const initializer_list 193 | & items) 194 | 4 : vector(items) {} 195 | 5 196 | 6 void call() override 197 | 7 { 198 | 8 for (auto& cmd : *this) 199 | 9 cmd.call(); 200 | 10 } 201 | 11 202 | 12 void undo() override 203 | 13 { 204 | 14 for (auto it = rbegin(); it != rend(); ++it) 205 | 15 it->undo(); 206 | 16 } 207 | 17 }; 208 | ``` 209 | 210 | 如您所见,我们所做的只是重用基类构造函数,用两个命令初始化对象。然后重用基类的call()/undo()实现。但是等等,这不对,是吗?基类实现并没有完全切断它,因为它没有包含失败的情况。例如,如果我不能从A处取钱,我就不应该把钱存到B处:整个链应该自行取消。 211 | 为了支持这个想法,我们需要更激烈的改变,我们需要: 212 | • 向Command增加成功标记 213 | • 记录每次操作的成功或失败 214 | • 确保该命令只有在最初成功时才能撤销 215 | • 引入一个新的中间类,名为DependentCompositeCommand,它在实际回滚命令时非常小心 216 | 217 | 当调用每个命令时,我们只有在前一个成功的情况下才会这样做;否则,我们只需将成功标志设置为false。 218 | 219 | ```c++ 220 | 1 void call() override 221 | 2 { 222 | 3 bool ok = true; 223 | 4 for (auto& cmd : *this) 224 | 5 { 225 | 6 if (ok) 226 | 7 { 227 | 8 cmd.call(); 228 | 9 ok = cmd.succeeded; 229 | 10 } 230 | 11 else 231 | 12 { 232 | 13 cmd.succeeded = false; 233 | 14 } 234 | 15 } 235 | 16 } 236 | ``` 237 | 238 | 没有必要覆盖undo(),因为我们的每个命令都会检查自己的成功标志,并且只有在设置为true时才撤销操作。 239 | 你可以想象一个更强的形式,一个复合命令只有在它的所有部分都成功的情况下才会成功(想想一个转账,取款成功,存款失败——你想让它通过吗?)—这有点难以实现,我再次把它留给读者作为练习。本节的全部目的是说明当考虑到现实世界的业务需求时,简单的基于命令的方法会变得多么复杂。你是否真的需要这种复杂性……嗯,这取决于你。 240 | 241 | 242 | #### 命令查询分离 243 | 244 | 命令查询分离(CQS)的概念是系统中的操作大致分为以下两类: 245 | 246 | • 命令,这是系统执行某些操作的指令,这些操作涉及状态的变化,但不产生任何值 247 | • 查询,这是对信息的请求,产生值但不改变状态 248 | 249 | 任何目前直接公开其状态供读写的对象都可以隐藏其状态(使其为私有),然后可以提供一个单独的接口,而不是提供getter和setter对。我的意思是:假设我们有一种生物具有力量和敏捷这两种属性。我们可以这样定义该生物: 250 | 251 | ```c++ 252 | 1 class Creature 253 | 2 { 254 | 3 int strength, agility; 255 | 4 public: 256 | 5 Creature(int strength, int agility) 257 | 6 : strength{strength}, agility{agility} {} 258 | 7 259 | 8 void process_command(const CreatureCommand& cc); 260 | 9 int process_query(const CreatureQuery& q) const; 261 | 10 }; 262 | ``` 263 | 264 | 如您所见,没有getter和setter,但我们有两个(只有两个!)称为process_command()和process_query()的API成员,它们将用于与生物对象的所有交互。这两个都是专用类,连同CreatureAbility枚举,定义如下: 265 | 266 | ```c++ 267 | 1 enum class CreatureAbility { strength, agility }; 268 | 2 269 | 3 struct CreatureCommand 270 | 4 { 271 | 5 enum Action { set, increaseBy, decreaseBy } action; 272 | 6 CreatureAbility ability; 273 | 7 int amount; 274 | 8 }; 275 | 9 276 | 10 struct CreatureQuery 277 | 11 { 278 | 12 CreatureAbility ability; 279 | 13 }; 280 | ``` 281 | 282 | 如您所见,该命令描述了您想要更改的成员、您想要如何更改以及更改多少。查询对象只指定要查询的内容,并且我们假定查询的结果从函数返回,而不是在查询对象本身中进行设置(如果其他对象影响这个对象,如我们已经看到的,那么您将这样做)。下面是process_command()的定义: 283 | 284 | ```c++ 285 | 1 void Creature::process_command(const CreatureCommand &cc) 286 | 2 { 287 | 3 int* ability; 288 | 4 switch (cc.ability) 289 | 5 { 290 | 6 case CreatureAbility::strength: 291 | 7 ability = &strength; 292 | 8 break; 293 | 9 case CreatureAbility::agility: 294 | 10 ability = &agility; 295 | 11 break; 296 | 12 } 297 | 13 switch (cc.action) 298 | 14 { 299 | 15 case CreatureCommand::set: 300 | 16 *ability = cc.amount; 301 | 17 break; 302 | 18 case CreatureCommand::increaseBy: 303 | 19 *ability += cc.amount; 304 | 20 break; 305 | 21 case CreatureCommand::decreaseBy: 306 | 22 *ability -= cc.amount; 307 | 23 break; 308 | 24 } 309 | 25 } 310 | ``` 311 | 312 | 下面是更简单的process_query()定义: 313 | 314 | ```c++ 315 | 1 int Creature::process_query(const CreatureQuery &q) const 316 | 2 { 317 | 3 switch (q.ability) 318 | 4 { 319 | 5 case CreatureAbility::strength: return strength; 320 | 6 case CreatureAbility::agility: return agility; 321 | 7 } 322 | 8 return 0; 323 | 9 } 324 | ``` 325 | 326 | 如果您想要记录这些命令和查询的日志或持久性,现在只有两个位置需要完成这一点。所有这些真正的问题是,对于只想以熟悉的方式操作对象的人来说,使用API有多么困难。幸运的是,只要我们愿意,我们总是可以制造getter/setter对;这些函数只需要使用适当的参数来调用process_方法: 327 | 328 | ```c++ 329 | 1 void Creature::set_strength(int value) 330 | 2 { 331 | 3 process_command(CreatureCommand{ 332 | 4 CreatureCommand::set, CreatureAbility::strength, value 333 | 5 }); 334 | 6 } 335 | 7 336 | 8 int Creature::get_strength() const 337 | 9 { 338 | 10 return process_query(CreatureQuery{CreatureAbility:: 339 | strength}); 340 | 11 } 341 | ``` 342 | 343 | 无可否认,前面的例子非常简单地说明了在执行CQS的系统中实际发生的情况,但它很有希望提供一个概念,说明如何将所有对象接口拆分为命令和查询部分。 344 | 345 | #### 总结 346 | 347 | 命令设计模式很简单:它的基本建议是,对象可以使用封装指令的特殊对象彼此通信,而不是将这些相同的指令指定为方法的参数。有时,您不希望这样的对象改变目标或使它做一些特定的事情;相反,您希望使用这样的对象从目标查询一个值,在这种情况下,我们通常将这样的对象称为查询。虽然在大多数情况下,查询是一个依赖于方法返回类型的不可变对象,但在某些情况下(例如,参见Chain of Responsibility Broker Chain的例子;)当你希望被返回的结果被其他组件修改时。但是组件本身仍然没有修改,只是结果是。命令在UI系统中被大量使用来封装典型的操作(例如,复制或粘贴),然后允许通过几种不同的方式调用单个命令。例如,您可以使用顶级应用程序菜单、工具栏上的按钮或键盘快捷键进行复制。最后,这些动作可以被组合成宏动作序列,这些宏动作序列可以被记录下来,然后随意重放。 348 | 349 | 350 | -------------------------------------------------------------------------------- /docs/chapter-15-interpreter.md: -------------------------------------------------------------------------------- 1 | ### 第15章 解释器模式 2 | 3 | 你已经猜到了,解释器设计模式的目标是解释输入,特别是文本输入,不过公平地说,这真的无关紧要。解释器的概念与编译理论和大学里教授的类似课程有很大的联系。由于我们在这里没有足够的空间来深入研究不同类型的解析器的复杂性和诸如此类的东西,这一章的目的只是简单地展示一些你可能想要解释的事情的例子。 4 | 5 | 这里有几个相当明显的例子: 6 | - 数字字面值(如42或1.234e12)需要被解释以有效地存储在二进制文件中。在c++中,这些操作通过C API(如`atof()`)和更复杂的库(如`Boost.LexicalCast`)来实现。 7 | - 正则表达式帮助我们在文本中找到模式,但您需要认识到的是,正则表达式本质上是一种独立的、嵌入式领域特定语言(DSL)。当然,在使用它们之前,必须正确地解释它们 8 | - 任何结构化数据,无论是CSV、XML、JSON,还是更复杂的数据,在使用之前都需要进行解释。 9 | - 在解释器应用的顶峰,我们已经有了成熟的编程语言。毕竟,像C或Python这样的语言的编译器或解释器在使某些东西可执行之前必须真正理解该语言。 10 | 11 | 鉴于与解释有关的挑战的扩散和多样性,我们将简单地看一些例子。这些都说明了如何构建解释器:要么从零开始,要么利用一个库,在工业规模上帮助完成这些事情。 12 | 13 | ### 数值表达式计算器 14 | 15 | 假设我们决定解析非常简单的数学表达式,例如`3+(5-4)`,也就是说,我们将把自己限制在加法、减法和方括号中。我们需要一个程序能够读取这样的表达式,当然,也能够计算表达式的最终值。 16 | 17 | 我们将手工构建这样一个计算器,不借助于任何解析框架。这应该能够突出解析文本输入所涉及的一些复杂性。 18 | 19 | 20 | 21 | 22 | ### 词法分析 23 | 24 | 解释表达式的第一步称为词法分析,它涉及到将字符序列转换为`token`序列。`token`通常是一个基本语法元素,我们应该以这些元素的平面序列结束。在我们的例子中,`token`可以是: 25 | 26 | - 整数 27 | - 操作符(加法或减法) 28 | - 一个开或闭的括号 29 | 30 | 我们可以定义如下的结构: 31 | 32 | ```c++ 33 | struct Token 34 | { 35 | enum Type { integer, plus, minus, lparen, rparen } type; 36 | string text; 37 | // 这里有问题explicit 一般只修饰单个参数的构造函数避免显式转换。 38 | explicit Token(Type type, const string& text): 39 | type{ type }, 40 | text{ text } 41 | { 42 | 43 | } 44 | friend ostream& operator<<(ostream& os, const Token& obj) 45 | { 46 | return os << "`" << obj.text << "`"; 47 | } 48 | }; 49 | ``` 50 | 51 | 你将注意到`Token`不是`enum`,因为除了类型之外,我们还希望存储与此`Token`相关的文本,因为它并不总是预定义的。 52 | 53 | 现在,给定一个包含表达式的std::string,我们可以定义一个词法分析过程,将文本转换为`vector`: 54 | 55 | ```c++ 56 | vector lex(const string& input) 57 | { 58 | vector result; 59 | for(int i = 0; i < input.size(); ++i) 60 | { 61 | switch(input[i]) 62 | { 63 | case '+': 64 | result.emplace_back( Token{Token::plus, "+"} ); 65 | break; 66 | case '-' 67 | result.emplace_back( Token{Token::plus, "-"} ); 68 | break; 69 | case '(': 70 | result.emplace_back( Token{Token::lparen, "("} ); 71 | break; 72 | case ')': 73 | result.emplace_back( Token{Token::rparen, ")"} ); 74 | default: 75 | // numer ??? 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | 解析预定义的标记很容易。事实上,我们可以把到`>`进行简化。但是,解析一个数字并不那么容易。如果我们碰到了1,我们应该等待,看看下一个字符是什么。为此我们写了一个单独的代码块: 82 | 83 | ```c++ 84 | ostringstream buffer; 85 | buffer << input[i]; 86 | for (int j = i + 1; j < input.size(); ++j) 87 | { 88 | if (isdigit(input[j])) 89 | { 90 | buffer << input[j]; 91 | ++i; 92 | } 93 | else 94 | { 95 | result.push_back(Token{ Token::integer, buffer.str() }); 96 | break; 97 | } 98 | }; 99 | ``` 100 | 101 | 当我们继续读取数字时,我们将它们添加到缓冲区中。完成后,我们从整个缓冲区中创建一个`token`,并将其添加到生成的`vector`中。 102 | 103 | 104 | ### 语法分析 105 | 106 | 语法分析(`parsing`)过程将一系列记号转换成有意义的、通常面向对象的结构。在顶部,有一个树的所有元素都需要实现的抽象父类型是很有用的: 107 | 108 | ```c++ 109 | struct Element 110 | { 111 | virtual int eval( ) const = 0; 112 | }; 113 | ``` 114 | 115 | 该类型的`eval()`函数计算该元素的数值。接下来,我们可以创建一个用于存储整数值的元素(例如1,5、42): 116 | 117 | ```c++ 118 | struct Interger : Element 119 | { 120 | int value; 121 | explicit Interger(const int value): 122 | value{value} 123 | { 124 | 125 | }; 126 | int eval( ) const override { return value; } 127 | }; 128 | ``` 129 | 130 | 如果没有整数,则必须有加法或减法之类的运算。在我们的例子中,所有的操作都是二元的,这意味着它们有两个部分。 131 | 132 | ```c++ 133 | struct BinaryOperation : Element 134 | { 135 | enum Type { addition, substraction } type; 136 | shared_ptr lhs, rhs; 137 | 138 | int eval( ) const override 139 | { 140 | if(type == addition) 141 | return lhs->eval() + rhs->eval(); 142 | return lhs->eval() - rhs->eval(); 143 | } 144 | }; 145 | ``` 146 | 147 | 注意,在上面的代码里面,我使用的是`enum`而不是`enum class`, 这样我就可以在后面写`BinaryOperation::add`。 148 | 149 | 不管怎样,我们来看看解析过程。我们所需要做的就是将一个记号序列转换为表达式的二叉树。从一开始,它可以看起来如下: 150 | 151 | ```c++ 152 | shared_ptr parse(const vector& tokens) 153 | { 154 | auto result = make_unique(); 155 | bool have_lhs = false; // this will need some explaining :) 156 | for(auto token : tokens) 157 | { 158 | switch(token.type) 159 | { 160 | // process each of the tokens in turn 161 | } 162 | } 163 | return result; 164 | } 165 | ``` 166 | 167 | 在前面的代码中,我们只需要讨论`have_lhs`变量。记住,你试图得到的是一个树,在树的根(`root`),我们期望一个二元表达式(`BinaryExpression`),根据定义,它有两棵子树。但是当我们在一个数字上时,我们怎么知道它是在表达式的左边还是右边呢?没错,我们不知道,所以我们才要追踪这个。 168 | 169 | 现在让我们逐案分析这些问题。首先,这些整数直接映射到我们的整数构造,所以我们所要做的就是将文本转换为数字。(顺便说一句,如果我们愿意,我们也可以在词法分析阶段这样做。) 170 | 171 | ```c++ 172 | case Token::interger 173 | { 174 | int value = boost::lexical_cast(token.text); 175 | auto integer = make_shared(value); 176 | if(!has_lhs) 177 | { 178 | result->lhs = integer; 179 | have_lhs = true; 180 | } 181 | else 182 | result->rhs = integer; 183 | } 184 | ``` 185 | 186 | 加号和减号只是确定我们正在处理的操作的类型,所以它们很容易 187 | 188 | ```c++ 189 | case Token::plus: 190 | result->type = BinaryOperation::addition; 191 | break; 192 | case Token::minus: 193 | result->type = BinaryOperation::subtraction; 194 | break; 195 | ``` 196 | 197 | 然后是左括号。是的,只有左括号,我们没有明确地检测到右边。这里的思想很简单:找到右括号(我现在忽略嵌套的括号),删除整个子表达式,递归地`parse()`它,并将其设置为我们当前正在处理的表达式的左边或右边: 198 | 199 | ```c++ 200 | case Token::lparen: 201 | { 202 | int j = i; 203 | for (; j < tokens.size(); ++j) 204 | if (tokens[j].type == Token::rparen) 205 | break; // found it! 206 | vector subexpression(&tokens[i + 1], &tokens[j]); 207 | auto element = parse(subexpression); // recursive call 208 | if (!have_lhs) 209 | { 210 | result->lhs = element; 211 | have_lhs = true; 212 | } 213 | else 214 | result->rhs = element; 215 | i = j; // advance 216 | } 217 | 218 | ``` 219 | 220 | 221 | 在真实的场景中,你需要更多的安全特性:不仅要处理嵌套的括号(我认为这是必须的),还要处理缺少右括号的不正确表达式。如果真的没有右括号,你会怎么处理?抛出一个异常呢?试着分析剩下的内容并假设结束在最后?别的吗?所有这些问题都留给读者作为练习。 222 | 223 | 从c++本身的经验中,我们知道为解释过程错误编写有意义的错误消息是非常困难的。事实上,你会发现一个现象叫做跳过(`skipping`),lexer或解析器将尝试跳过错误代码,直到遇到有意义的输入:这种方法被静态分析工具所采用, 当用户键入未完成的代码时,期望它能正确工作。 224 | 225 | ### 使用词法分析器和语法分析器 226 | 227 | 通过实现`lex()`和`parse()`,我们最终可以解析表达式并计算其值: 228 | 229 | ```c++ 230 | string input{ "(13-4)-(12+1)" }; 231 | auto tokens = lex(input); 232 | auto parsed = parse(tokens); 233 | cout << input << " = " << parsed->eval() << endl; 234 | // prints "(13-4)-(12+1) = -4" 235 | ``` 236 | 237 | ### 使用Boost.Spirit进行语法分析 238 | 239 | 在现实世界中,几乎没有人手工编写解析器来处理复杂的东西。当然,如果解析的是`XML`或`JSON`等简单的数据存储格式,那么手工编写解析器是很容易的。但如果你正在实现自己的DSL或编程语言,这就不是一个选项。 240 | 241 | `Boost.Spirit`是一个库,通过为构造语法分析器提供简洁(但不是特别直观)的API来帮助创建语法分析器。该库不试图显式地分离词法分析和解析阶段(除非你真的想这样做),允许你定义如何将文本构造映射到你定义的类型的对象上。 242 | 243 | 让我以`Tlön`[^1]编程语言为例,来向你展示`Boost.Spirit`的一些用法。 244 | 245 | 注1:`Tlön`是一个玩具语言,我创建它是为了演示一个想法:如果你不喜欢现有的语言,那就创建一个新的语言。`Tlön`使用`Boost.Spririt`和交叉编译(transpiles)为c++。它是开源的,可以在下面找到: [https://github.com/nesteruk/tlon](https://github.com/nesteruk/tlon) 246 | 247 | 248 | #### 抽象语法树 249 | 250 | 251 | 首先,你需要你的抽象语法树(`Abstrct Syntax Tree, AST`)。在这方面,我只创建了一个支持访问者设计模式的基类,因为遍历这些结构非常重要。 252 | 253 | ```c++ 254 | struct ast_element 255 | { 256 | virtual ~ast_element() = default; 257 | virtual void accept(ast_element_visitor& visitor) = 0; 258 | }; 259 | ``` 260 | 261 | 然后,这个接口用于在我的语言中定义不同的代码结构: 262 | 263 | ```c++ 264 | struct property : ast_element 265 | { 266 | vector names; 267 | type_specification type; 268 | bool is_constant { false }; 269 | wstring default_value; 270 | void accept(ast_element_visitor& visitor) override 271 | { 272 | visitor.visit(*this); 273 | } 274 | }; 275 | ``` 276 | 277 | 前面的属性定义有四个不同的部分,每个部分存储在一个公共可访问的字段中。注意,它使用了`type_specification`,它本身就是另一个`ast_element`。 278 | 279 | AST的每个类都需要适应`Boost.Fusion`另一个支持编译时(元编程)和运行时算法融合(因此得名)的Boost库。适应代码非常简单: 280 | 281 | ```c++ 282 | BOOST_FUSION_ADAPT_STRUCT( 283 | tlön::property, 284 | (std::vector, names), 285 | (tlön::type_specification, type), 286 | (bool, is_constant), 287 | (std::wstring, default_value) 288 | ) 289 | ``` 290 | 291 | Spirit解析为常见的数据类型,如`std::vector`或`std::optional`,没有问题。它在多态性方面确实有更多的问题:`Spirit`宁愿您使用一个`variant`,也就是说,而不是让AST类型彼此继承。 292 | 293 | ```c++ 294 | typedef variant class_member; 295 | ``` 296 | 297 | 298 | #### 语法分析器 299 | 300 | `Boost.Spirit`允许我们将解析器定义为一组规则。所使用的语法非常类似于正则表达式或`BNF (Bachus-Naur Form)`表示法,只不过操作符放在符号之前,而不是之后。下面是一个示例规则: 301 | 302 | ```c++ 303 | class_declaration_rule %= 304 | lit(L"class ") >> +(alnum) % '.' 305 | >> -(lit(L"(") >> -parameter_declaration_rule % ',' >> lit(")")) 306 | >> "{" 307 | >> *(function_body_rule | property_rule | function_signature_rule) 308 | >> "}"; 309 | ``` 310 | 311 | 前面要求类声明以单词`class`开头。然后,它需要一个或多个单词(每个单词都是一个或多个字母数字字符,因此`+(alnum))`,用句号'分隔。'这就是%操作符的用途。正如您可能已经猜到的,结果将映射到一个向量上。随后,在花括号之后,我们期望零个或多个函数、属性或函数签名的定义,这些字段将使用一个变体映射到与我们之前的定义对应的位置。 312 | 313 | 当然,有些元素是`AST`元素的整个层次结构的根。在我们的例子中,这个根被称为一个文件(惊讶吧!),这里有一个函数,它既解析文件,又对文件进行美化打印: 314 | 315 | ```c++ 316 | template 317 | wstring parse(Iterator first, Iterator last) 318 | { 319 | using spirit::qi::phrase_parse; 320 | file f; 321 | file_parser fp{}; 322 | auto b = phrase_parse(first, last, fp, space, f); 323 | if (b) 324 | { 325 | return TLanguagePrinter{}.pretty_print(f); 326 | } 327 | return wstring(L"FAIL"); 328 | } 329 | ``` 330 | 331 | `TLanguagePrinter`实际上是一个访问者,它知道如何用不同的语言(比如c++)来呈现我们的AST。 332 | 333 | 334 | #### 打印机 335 | 336 | 在解析了语言之后,我们可能想要编译它,或者在我的例子中,将它转换成其他语言。考虑到我们之前已经在整个AST层次结构中实现了`accept()`方法,这是相当容易的。 337 | 338 | 唯一的挑战是如何处理这些变体类型,因为它们需要特殊的访问者。在`std::variant`的情况下,你想要的是`std::visit()`,但是因为我们使用`boost::variant`,所以要调用的函数是`boost::accept_visitor()`。这个函数要求你为它提供从`static_visitor`继承的类的实例,并为每种可能的类型提供函数调用重载。这里有个例子: 339 | 340 | ```c++ 341 | struct default_value_visitor : static_visitor<> 342 | { 343 | cpp_printer& printer; 344 | 345 | explicit default_value_visitor(cpp_printer& printer) 346 | : printer{printer} 347 | { 348 | } 349 | 350 | void operator()(const basic_type& bt) const 351 | { 352 | // for a scalar value, we just dump its default 353 | printer.buffer << printer.default_value_for(bt.name); 354 | } 355 | 356 | void operator()(const tuple_signature& ts) const 357 | { 358 | for (auto& e : ts.elements) 359 | { 360 | this->operator()(e.type); 361 | printer.buffer << ", "; 362 | } 363 | printer.backtrack(2); 364 | } 365 | }; 366 | ``` 367 | 368 | 然后调用`accept_visitor(foo, default_value_ visitor{…})`,根据变量中实际存储的对象类型,将调用正确的重载。 369 | 370 | ### 总结 371 | 372 | 首先,需要说明的是,相对而言,解释器设计模式是不常见的,构建解析器的挑战现在被认为是不必要的,这就是为什么我看到许多英国大学(包括我自己的大学)的计算机科学课程中删除了它。此外,除非您计划从事语言设计工作,或者制作静态代码分析工具,否则您不太可能在高需求中找到构建解析器的技能。 373 | 374 | 375 | 376 | 也就是说,解释的挑战是计算机科学中一个完全独立的领域,设计模式的书中的一个章节不能合理地解释它。如果你对这个主题感兴趣,我建议你看看`Lex/Yacc`、`ANTLR`和其他许多专门针对`lexer/parse`构造的框架。我还建议为流行的`IDE`编写静态分析插件,这是了解真实的`ast`外观、遍历方式甚至修改方式的好方法。 -------------------------------------------------------------------------------- /docs/chapter-16-iterator.md: -------------------------------------------------------------------------------- 1 | ### 迭代器 2 | 3 | 在开始处理复杂的数据结构时,都会遇到 *遍历(traversal)* 的问题。这可以用不同的方法来处理,但最常见的遍历方法,比如,`vector`,是使用一种称为 *迭代器(iterator)* 的东西。 4 | 5 | 简单地说,迭代器是一个对象,它可以指向集合中的一个元素,也知道如何移动到集合中的下一个元素。因此,只需要实现`++`操作符和`!=`操作符(这样就可以比较两个迭代器并检查它们是否指向相同的东西)。 6 | 7 | c++标准库大量使用迭代器,因此我们将讨论它们的使用方式,然后我们将看看如何制作我们自己的迭代器以及迭代器的局限。 8 | 9 | #### 标准库中的迭代器 10 | 11 | 假设你有一个名字列表,例如: 12 | 13 | ```c++ 14 | vector names {"john","jane", "jill", "jack"} 15 | ``` 16 | 17 | 如果想要获得`names`中的第一个名字,可以调用`begin()`函数。这个函数不会返回第一个名字的值或引用给你; 相反,它会返回一个迭代器给你: 18 | 19 | ```c++ 20 | vector::iterator it = names.begin(); 21 | ``` 22 | 23 | `begin()`既是`vector`的成员函数,也是全局函数。全局的`begin()`对于不能包含成员函数`c`语言风格的数组(而不是`std::array`))特别有用。 24 | 25 | 你可以把`begin()`返回一个的迭代器看作指针: 对于`vector`来说,功能上是相似的。例如,可以对迭代器进行提领操作(*dereference*)来打印实际的名称: 26 | 27 | ```c++ 28 | cout << "first name is " << *it << "\n"; 29 | // first name is john 30 | ``` 31 | 32 | 迭代器`it`是可以 *前进(advance)* 的,即移动到下一个元素。迭代器上的自增操作`++`强调的是向前移动的概念,也就是说,和指针向前移动的`++`操作(即递增内存地址)是不相同的。 33 | 34 | ```c++ 35 | ++it; // now points to jane 36 | ``` 37 | 38 | 也可以使用迭代器(像指针一样)修改它所指向的元素: 39 | 40 | ```c++ 41 | it->append(" goodall"s); 42 | cout << "full name is " << *it << "\n"; 43 | // full name is jane goodall 44 | ``` 45 | 46 | `begin()`函数对应的当然是`end()`函数,但它不是指向最后一个元素,而是指向最后一个元素之后的元素: 47 | 48 | ```c++ 49 | 1 2 3 4 50 | begin() ^ ^ end() 51 | ``` 52 | 53 | 可以使用`end()`作为终止条件。例如,让我们使用`it`来打印列表中其余的名称: 54 | 55 | ```c++ 56 | while (++it != names.end()) 57 | { 58 | cout << "another name: " << *it << "\n"; 59 | } 60 | // another name: jill 61 | // another name: jack 62 | ``` 63 | 64 | 除了`begin()`和`end()`之外,还有`rbegin()`和`rend()`,它们允许我们在集合中反向移动。在本例中,你可能已经猜到,`rbegin()`指向最后一个元素,而`rend()`指向第一个之前的一个元素: 65 | 66 | ```c++ 67 | for (auto ri = rbegin(names); ri != rend(names); ++ri) 68 | { 69 | cout << *ri; 70 | if (ri + 1 != rend(names)) // iterator arithmetic 71 | cout << ", "; 72 | } 73 | cout << endl; 74 | // jack, jill, jane goodall, john 75 | ``` 76 | 77 | 上面的代码有两点需要注意。首先,即使向后遍历`vector`,我们仍然在迭代器上使用`++`操作符。其次,我们可以对`it`做算术操作: `ri + 1`指向的是`ri`前一个元素,而不是后一个元素。 78 | 79 | `STL`中也提供不允许修改对象的常量迭代器:它们通过`cbegin()/cend()`返回,与之对应的是`crbegin()/crend()`: 80 | 81 | ```c++ 82 | vector::const_reverse_iterator jack = crbegin(names); 83 | // won't work 84 | *jack += "reacher"; 85 | ``` 86 | 87 | 最后,值得一提的是, 现代c++里面*基于范围的`for`循环(range based for loop)*,从容器的`begin()`一直迭代到`end()`。 88 | 89 | ```c++ 90 | for (auto& name : names) 91 | cout << "name = " << name << "\n"; 92 | ``` 93 | 94 | 注意迭代器在这里是自动提领的: 变量名`name`是一个引用,但也可以按值进行迭代。 95 | 96 | ### 遍历二叉树 97 | 98 | 让我们回顾一下数据结构里面遍历二叉树的练习。首先,我们定义树中的的节点: 99 | 100 | ```c++ 101 | template 102 | struct Node 103 | { 104 | T value; 105 | Node* left; 106 | Node* right; 107 | Node* parent; 108 | BinaryTree* tree; 109 | }; 110 | ``` 111 | 每个节点都有指向其左孩子结点、右孩子结点,父结点(如果有的话)以及整个树的指针。可以单独构造一个叶节点,也可以使用其子节点来构造内部结点。 112 | 113 | ```c++ 114 | explicit Node(const T& value) 115 | : value(value) 116 | , left(nullptr) 117 | , right(nullptr) 118 | , parent(nullptr) 119 | , tree(nullptr) 120 | { } 121 | 122 | Node(const T& value, Node* left, Node* right) 123 | : value(value) 124 | , left(left) 125 | , right(right) 126 | , parent(nullptr) 127 | , tree(nullptr) 128 | { 129 | this->left->tree = this->right->tree = tree; 130 | this->left->parent = this->right->parent = this; 131 | } 132 | }; 133 | ``` 134 | 135 | 最后,我们引入一个通用的成员函数来设置树指针。这是通过在所有节点的子节点上递归完成的: 136 | 137 | ```c++ 138 | void set_tree(BinaryTree* t) 139 | { 140 | tree = t; 141 | if(left) 142 | left->set_tree(t); 143 | if(right) 144 | right->set_tree(t); 145 | } 146 | ``` 147 | 148 | 有了这些,我们现在可以定义一个称为`BinaryTree`的结构,它正是这个结构允许迭代: 149 | 150 | ```c++ 151 | template 152 | struct BinaryTree 153 | { 154 | Node* root = nullptr; 155 | explicit BinaryTree(Node* const root) 156 | : root{ root } 157 | { 158 | root->set_tree(this); 159 | } 160 | }; 161 | ``` 162 | 163 | 现在我们可以为树定义一个迭代器。迭代二叉树有三种常见的方法,我们首先要实现的是前序遍历`preorder`: 164 | 165 | - 一旦遇到该元素,就返回该元素。 166 | - 递归地遍历左子树 167 | - 递归地遍历右子树 168 | 169 | 让我们从一个构造函数开始: 170 | 171 | ```c++ 172 | template 173 | struct PreOrderIterator 174 | { 175 | Node* current; 176 | explicit PreOrderIterator(Node* current) 177 | : current(current) 178 | { 179 | 180 | } 181 | // 其他成员 182 | }; 183 | ``` 184 | 185 | 需要定义`operator != `来与其他迭代器进行比较。因为迭代器的相当于指针,所以这很简单: 186 | 187 | ```c++ 188 | bool operator!=(const PreOrderIterator& other) 189 | { 190 | return this->current != other.current; 191 | } 192 | ``` 193 | 194 | 我们需要定义`*`操作符来实现提领: 195 | 196 | ```c++ 197 | Node& operator*() { return *current; } 198 | ``` 199 | 200 | 201 | 现在,最后一个问题是如何从我们的二叉树中把迭代器给暴露出来。如果将前序遍历其定义为树的默认迭代器,则可以如下所示补充成员函数: 202 | 203 | ```c++ 204 | typedef PreOrderIterator iterator; 205 | 206 | iterator begin() 207 | { 208 | Node* n = root; 209 | if(n) 210 | while(n->left) 211 | n = n->left; 212 | return iterator { n }; 213 | } 214 | 215 | iterator end() 216 | { 217 | return iterator {nullptr}; 218 | } 219 | ``` 220 | 221 | 现在,困难的部分来了:遍历树。这里的挑战是我们使用递归算法,遍历发生在`++`操作符中,所以我们最终实现如下所示: 222 | 223 | ```c++ 224 | PreOrderIterator& operator++() 225 | { 226 | if(current->right) 227 | { 228 | current = current->right; 229 | while(current->left) 230 | current = current->left; 231 | } 232 | else 233 | { 234 | Node* p = current->parent; 235 | while(p && current == p->right) 236 | { 237 | current = p; 238 | p = p->parent; 239 | } 240 | current = p; 241 | } 242 | return *this; 243 | } 244 | ``` 245 | 246 | 这太乱了!而且,它看起来一点也不像树遍历的经典实现,因为我们没有递归。 247 | 248 | 值得注意的是,`begin()`迭代器并不是从整个树的根开始; 相反,它从最左边可用的节点开始。 249 | 250 | 现在所有的部分都准备好了,下面是我们如何执行遍历: 251 | 252 | ```c++ 253 | BinaryTree family{ 254 | new Node{ 255 | "me", 256 | new Node{ 257 | "mother", 258 | new Node{"mother's mother"}, 259 | new Node{"mother's father"}}, 260 | new Node{"father"} 261 | } 262 | }; 263 | 264 | for (auto it = family.begin(); it != family.end(); ++it) 265 | { 266 | cout << (*it).value << "\n"; 267 | } 268 | ``` 269 | 270 | 您也可以将这种遍历形式暴露为一个单独的对象,即: 271 | 272 | ```c++ 273 | class pre_order_traversal 274 | { 275 | BinaryTree& tree; 276 | public: 277 | pre_order_traversal(BinaryTree& tree) : tree{ tree } {} 278 | iterator begin() { return tree.begin(); } 279 | iterator end() { return tree.end(); } 280 | } pre_order; 281 | ``` 282 | 283 | 可以这样来遍历: 284 | 285 | ```c++ 286 | for (const auto& it: family.pre_order) 287 | { 288 | cout << it.value << "\n"; 289 | } 290 | ``` 291 | 292 | 类似地,可以定义`in_order`和`post_order`遍历算法来暴露合适的迭代器。 293 | 294 | ### 迭代与协程 295 | 296 | 我们遇到一个严重的问题:在遍历代码中,`operator++`难以读懂,它与你在`Wikipedia`上读到的关于树遍历的任何内容都不匹配。它能起作用,但它之所以能起作用,只是因为我们将迭代器初始化为最左边的节点,而不是从根节点开始,这样做是有问题和令人困惑的。 297 | 298 | 299 | 为什么我们会有这个问题?因为`++`操作符是不可恢复的:它不能在两次调用之间保持其堆栈,因此,不能实现递归。现在,我们是否有一种两全其美的方法:可执行正确递归的可恢复函数? 我们可以用`协程(coroutines)`来实现。 300 | 301 | 利用协程,我们可以像这样实现后序遍历: 302 | 303 | ```c++ 304 | generator<*> post_order_impl(Node* node) 305 | { 306 | if(node) 307 | { 308 | for(auto x : post_order_impl(node->left)) 309 | co_yield x; 310 | for(auto y : post_order_impl(node->right)) 311 | co_yield y; 312 | co_yield node; 313 | } 314 | } 315 | generator*> post_order() 316 | { 317 | return post_order_impl(root); 318 | } 319 | ``` 320 | 321 | 这不是很棒吗?算法终于又可读了!而且,看不到`begin()/end()`: 我们只是返回一个 *生成器(generator)* ,逐步返回`co_yield`提供生成的值。在生成每个值之后,我们可以挂起当前操作并执行其他操作(例如,打印值),然后在不丢失上下文的情况下恢复迭代。这使的递归成为可能并允许我们写出如下的代码: 322 | 323 | ```c++ 324 | for(auto it : family.post_order()) 325 | { 326 | cout << it->value << endl; 327 | } 328 | ``` 329 | 330 | 协程是c++的未来,它解决了许多传统迭代器丑陋或不合适的问题。 331 | 332 | 333 | ### 总结 334 | 335 | 迭代器设计模式在c++中无处不在,有显式的也有隐式的(例如基于范围的)形式。不同类型的迭代器可以用于迭代不同的对象:例如,反向迭代器可以应用于`vector`,但不能应用于单链表。 336 | 337 | 实现自己的迭代器就像提供`++`和`!=`操作符一样简单。大多数迭代器都是简单的指针的外观(`façades`),在它们被丢弃之前,用于遍历集合一次。 338 | 339 | 协程修复了迭代器中出现的一些问题:它们允许在调用之间保持状态,这是其他语言(如`c#`)很久以前就实现了的。因此,协程允许我们编写递归算法。 340 | 341 | 342 | -------------------------------------------------------------------------------- /docs/chapter-17-Mediator.md: -------------------------------------------------------------------------------- 1 | ### 中介者模式 2 | 3 | 我们编写的大部分代码都有不同的组件(类)通过直接引用或指针相互通信。然而,在某些情况下,你并不希望对象必须意识到对方的存在。或者,也许我们确实希望它们能够相互了解,但我们仍然不希望它们通过指针或引用进行通信,因为它们可能会过时,而我们又不想解引用一个nullptr。 4 | 5 | 因此,中介模式是一种促进组件之间通信的机制。当然,中介本身需要被参与其中的每个组件访问,这意味着它应该是一个全局静态变量,或者只是一个注入到每个组件中的引用。 6 | 7 | #### 聊天室 8 | 9 | 因特网上的聊天室是中介模式的一个典型例子,我们先讨论对它的实现。一个最简单的实现如下: 10 | 11 | ```c++ 12 | struct Person { 13 | string name; 14 | ChatRoom* room = nullptr; 15 | vector chat_log; 16 | Person(const string& name); 17 | void receive(const string& origin, const string& message); 18 | void say(const string& message) const; 19 | void pm(const string& who, const string& message) const; 20 | }; 21 | ``` 22 | 23 | 我们得到了一个有姓名(用户id)、聊天日志和指向实际聊天室的指针的人。我们有一个构造函数和三个成员函数: 24 | 25 | - receive() 用于接收信息。通常,该函数将在用户屏幕上显示消息,并将其添加到聊天日志中。 26 | - say()用于向房间里的每个人广播信息。 27 | - pm()用于传递私人消息,需要指定消息要发送的人员的名称。 28 | 29 | 让我们实际地实现一下聊天室,它并不是特别复杂: 30 | ```c++ 31 | struct ChatRoom { 32 | vector people; // assume append-only 33 | void join(Person* p); 34 | void broadcast(const string& origin, const string& message); 35 | void message(const string& origin, const string& who, const string& message); 36 | }; 37 | ``` 38 | 究竟是使用指针、引用还是shared_ptr来实际存储聊天室用户列表,最终取决于我们自己:惟一的限制是std::vector不能存储引用。所以,我决定在这里使用指针。聊天室的API非常简单: 39 | 40 | - join()让一个人加入房间。我们暂时不打算实现leave(),而是将这个想法推迟到本章的后续示例中 41 | 42 | - broadcast()将消息发送给所有人(除了发消息的人自身)。 43 | 44 | - message()发送一个私有消息。 45 | 46 | join()的实现如下: 47 | 48 | ```c++ 49 | void ChatRoom::join(Person* p) { 50 | string join_msg = p->name + " joins the chat"; 51 | broadcast("room", join_msg); 52 | p->room = this; 53 | people.push_back(p); 54 | } 55 | ``` 56 | 57 | 就像一个经典的IRC聊天室一样,我们向房间里的每个人广播有人加入的消息。然后,我们设置人的房间指针,并将它们添加到房间中的人员列表中。现在,让我们看看broadcast():这是向每个房间参与者发送消息的地方。记住,每个参与者都有自己的Person::receive()函数来处理消息,所以实现有点琐碎: 58 | ```c++ 59 | void ChatRoom::broadcast(const string& origin, const string& message) { 60 | for (auto p : people) 61 | if (p->name != origin) p->receive(origin, message); 62 | } 63 | ``` 64 | 65 | 我们是否想要阻止广播信息向我们自己传播是一个值得讨论的问题。最后,下面是使用 `message()` 实现的私有消息传递: 66 | 67 | ```c++ 68 | void ChatRoom::message(const string& origin, const string& who, 69 | const string& message) { 70 | auto target = find_if(begin(people), end(people), 71 | [&](const Person* p) { return p->name == who; }); 72 | if (target != end(people)) { 73 | (*target)->receive(origin, message); 74 | } 75 | } 76 | ``` 77 | 78 | 这将在人员列表中搜索收件人,如果找到了收件人(因为谁知道,他们可能已经离开房间了),就将消息发送给那个人。回到Person的say()和pm()实现: 79 | 80 | ```c++ 81 | void Person::say(const string& message) const { 82 | room->broadcast(name, message); 83 | } 84 | 85 | void Person::pm(const string& who, const string& message) const { 86 | room->message(name, who, message); 87 | } 88 | ``` 89 | 90 | 至于receive(),这是在屏幕上实际显示消息并将其添加到聊天日志的好地方。 91 | 92 | ```c++ 93 | void Person::receive(const string& origin, const string& message) { 94 | string s{origin + ": \"" + message + "\""}; 95 | cout << "[" << name << "'s chat session] " << s << "\n"; 96 | chat_log.emplace_back(s); 97 | } 98 | ``` 99 | 我们在这里做了更多的工作,不仅显示消息来自谁,还显示我们目前所在的聊天会话——这对于诊断谁在什么时候说了什么很有用。 100 | 101 | ```c++ 102 | ChatRoom room; 103 | Person john{"john"}; 104 | Person jane{"jane"}; 105 | room.join(&john); 106 | room.join(&jane); 107 | john.say("hi room"); 108 | jane.say("oh, hey john"); 109 | 110 | Person simon("simon"); 111 | room.join(&simon); 112 | simon.say("hi everyone!"); 113 | 114 | jane.pm("simon", "glad you could join us, simon"); 115 | ``` 116 | 117 | #### 中介与事件 118 | 119 | 在聊天室的例子中,我们遇到了一个一致的主题:每当有人发布消息时,参与者都需要通知。这是20章中讨论的Observer模式的完美场景:中介者模式拥有一个由所有参与者共享的事件;然后,参与者可以订阅事件来接收通知,他们还可以触发事件,从而触发通知。 120 | 121 | 事件并没有内置到c++中(与c#不同),所以我们将在这个演示中使用一个库解决方案。Boost.Signals2为我们提供了必要的功能。 122 | 123 | 让我们举个简单的例子:想象一款有球员和足球教练的足球游戏。当教练看到他们的球队得分时,他们自然想要祝贺球员。当然,他们需要一些关于该事件的信息,比如谁进球了,到目前为止他们已经进了多少球。我们可以为任何类型的事件数据引入基类: 124 | 125 | ```c++ 126 | struct EventData { 127 | virtual ~EventData() = default; 128 | virtual void print() const = 0; 129 | }; 130 | ``` 131 | 132 | 我们添加了print()函数,这样每个事件都可以打印到命令行,还添加了一个虚拟析构函数以使ReSharper停止处理它。现在,我们可以从这个类派生来存储一些与目标相关的数据: 133 | 134 | ```c++ 135 | struct PlayerScoredData : EventData { 136 | string player_name; 137 | int goals_scored_so_far; 138 | PlayerScoredData(const string& player_name, const int goals_scored_so_far) 139 | : player_name(player_name), goals_scored_so_far(goals_scored_so_far) {} 140 | 141 | void print() const override { 142 | cout << player_name << " has scored! (their " << goals_scored_so_far 143 | << " goal)" 144 | << "\n"; 145 | } 146 | }; 147 | ``` 148 | 149 | 我们将再次构建一个中介模式,不过,当我们有了事件驱动的基础设施,它们就不再需要了: 150 | 151 | ```c++ 152 | struct Game { 153 | signal events; // observer 154 | }; 155 | ``` 156 | 157 | 事实上,你可以只使用全局信号而不需要一个Game类,但我们在这里使用的是最小惊奇原则,如果一个Game&被注入到一个组件中,我们知道这里有一个明显的依赖性。 158 | 159 | 不管怎样,我们现在可以构造玩家类了。当然,球员有自己的名字、在比赛中进球的次数,还有一段关于调停比赛的参考: 160 | 161 | ```c++ 162 | struct Player { 163 | string name; 164 | int goals_scored = 0; 165 | Game& game; 166 | Player(const string& name, Game& game) : name(name), game(game) {} 167 | 168 | void score() { 169 | goals_scored++; 170 | PlayerScoredData ps{name, goals_scored}; 171 | game.events(&ps); 172 | } 173 | }; 174 | ``` 175 | 176 | 这里的Player::score()是一个有趣的函数:它使用事件信号创建一个PlayerScoredData,并将其发布给所有订阅者。谁得到这个事件?当然是一名教练了。 177 | 178 | ```c++ 179 | struct Coach { 180 | Game& game; 181 | explicit Coach(Game& game) : game(game) { 182 | // celebrate if player has scored <3 goals 183 | game.events.connect([](EventData* e) { 184 | PlayerScoredData* ps = dynamic_cast(e); 185 | if (ps && ps->goals_scored_so_far < 3) { 186 | cout << "coach says: well done, " << ps->player_name << "\n"; 187 | } 188 | }); 189 | } 190 | }; 191 | ``` 192 | 193 | Coach类的实现是微不足道的;我们的教练连名字都不知道。但我们确实给了他一个构造函数,用于创建游戏订阅。事件,这样,无论何时发生什么事情,coach都可以在提供的lambda (slot)中处理事件数据。 194 | 195 | 注意lambda的参数类型是EventData*——我们不知道一个球员是否已经得分或已经被发送,所以我们需要dynamic_cast(或类似的机制)来确定我们得到了正确的类型。 196 | 197 | 有趣的是,所有神奇的事情都发生在设置阶段:不需要明确地为特定信号征募插槽。客户端可以自由地使用它们的构造函数创建对象,然后,当玩家得分时,通知被发送: 198 | 199 | ```c++ 200 | Game game; 201 | Player player{ "Sam", game }; 202 | Coach coach{ game }; 203 | player.score(); 204 | player.score(); 205 | player.score(); // ignored by coach 206 | ``` 207 | 208 | 这将产生以下输出: 209 | 210 | ```c++ 211 | coach says: well done, Sam 212 | coach says: well done, Sam 213 | ``` 214 | 215 | 输出只有两行长,因为在第三个目标上,教练不再印象深刻。 216 | 217 | #### 总结 218 | 219 | 中介者设计模式本质上是提议引入一个中间组件,系统中的每个人都可以引用该组件并可以使用它来相互通信。可以通过标识符(用户名、唯一 ID 等)代替直接内存地址进行通信。 220 | 221 | 中介者的最简单实现是一个成员列表和一个函数,它遍历列表并执行预期的操作——无论是列表中的每个元素还是有选择的。 222 | 223 | 224 | 中介者的一个更复杂的实现可以使用事件来允许参与者订阅(和取消订阅)系统中发生的事情。这样,从一个组件发送到另一个组件的消息可以被视为事件。在这种设置中,如果参与者不再对某些事件感兴趣或即将完全离开系统,他们也很容易取消订阅某些事件。 -------------------------------------------------------------------------------- /docs/chapter-18-Memento.md: -------------------------------------------------------------------------------- 1 | ### 备忘录模式 2 | 3 | 首先看一个例子来认识备忘录模式。 4 | 5 | #### 银行账户 6 | 下面是一个银行账户的类, 现在我们决定只使用deposit()函数创建一个银行账户。在前面的例子中,它是void的,而现在,deposit()将返回一个Memento。然后Memento可以回滚帐户到以前的状态: 7 | ```c++ 8 | class BankAccount 9 | { 10 | int balance = 0; 11 | public: 12 | explicit BankAccount(const int balance): balance(balance) {} 13 | Memento deposit(int amount) 14 | { 15 | balance += amount; 16 | return { balance }; 17 | } 18 | 19 | void restore(const Memento& m) 20 | { 21 | balance = m.balance; 22 | } 23 | }; 24 | ``` 25 | 26 | 至于Memento本身,我们可以采用一个简单的实现方法: 27 | 28 | ```c++ 29 | class Memento 30 | { 31 | int balance; 32 | public: 33 | Memento(int balance): balance(balance) 34 | { 35 | } 36 | friend class BankAccount; 37 | }; 38 | ``` 39 | 40 | 这里有两件事需要特别注意: 41 | 42 | - Memento类是不可变的。想象一下,如果我们可以更改余额, 那么我们可以将帐户回滚到它从未处于的状态。 43 | 44 | - 备忘录将BankAccount声明为一个友元类。这允许帐户实际使用balance字段。另一个同样有效的替代方法是将Memento作为BankAccount的内部类。 45 | 46 | 下面是备忘录模式的具体使用: 47 | ```c++ 48 | void memento() 49 | { 50 | BankAccount ba{ 100 }; 51 | auto m1 = ba.deposit(50); 52 | auto m2 = ba.deposit(25); 53 | cout << ba << "\n"; // Balance: 175 54 | // undo to m1 55 | ba.restore(m1); 56 | cout << ba << "\n"; // Balance: 150 57 | 58 | // redo 59 | ba.restore(m2); 60 | cout << ba << "\n"; // Balance: 175 61 | } 62 | ``` 63 | 64 | 这个实现已经足够好了,只是缺少了一些东西。例如,您永远不会得到表示开盘余额的Memento,因为构造函数不能返回值。 65 | 66 | #### 撤销与回滚 67 | 68 | 如果我们要存储每一个由银行帐户产生的Memento,该怎么办呢? 我们将引入一个新的银行账户类BankAccount2,它将保存它生成的每一个Memento: 69 | ```c++ 70 | class BankAccount2 // supports undo/redo 71 | { 72 | int balance = 0; 73 | vector> changes; 74 | int current; 75 | public: 76 | explicit BankAccount2(const int balance) : balance(balance) 77 | { 78 | changes.emplace_back(make_shared(balance)); 79 | current = 0; 80 | } 81 | ``` 82 | 83 | 我们现在已经解决了返回初始余额的问题:初始变化的Memento也被存储。当然,这个Memento实际上并没有返回,所以为了回滚到它,我想您可以实现一些reset()函数——这完全取决于您。 84 | 85 | 我们使用shared_ptr来存储memento,也使用shared_ptr来返回它们。此外,我们使用当前字段作为进入更改列表的“指针”,如果我们决定撤消并后退一步,我们总是可以redo和恢复到我们之前的状态。 86 | 87 | 下面我们来看一下deposit()的实现: 88 | ```c++ 89 | shared_ptr deposit(int amount) 90 | { 91 | balance += amount; 92 | auto m = make_shared(balance); 93 | changes.push_back(m); 94 | ++current; 95 | return m; 96 | } 97 | ``` 98 | 我们添加了一个方法, 该方法基于一个Memento来恢复帐户状态: 99 | ```c++ 100 | void restore(const shared_ptr& m) 101 | { 102 | if (m) 103 | { 104 | balance = m->balance; 105 | changes.push_back(m); 106 | current = changes.size() - 1; 107 | } 108 | } 109 | ``` 110 | 恢复的过程与我们之前看到的明显不同。首先,我们实际上检查shared_ptr是否已初始化。此外,当我们恢复一个memento时,我们实际上是将该memento推入更改列表,以使得一个undo操作能够正确地执行。 111 | 112 | 下面是undo()的实现: 113 | ```c++ 114 | shared_ptr undo() 115 | { 116 | if (current > 0) 117 | { 118 | --current; 119 | auto m = changes[current]; 120 | balance = m->balance; 121 | return m; 122 | } 123 | return{}; 124 | } 125 | ``` 126 | 127 | 只有当current大于零时,我们才能使用undo()。此时,我们将指针后移并获取相应的changes成员,然后返回对应的balance。如果不能回滚到以前的memento,则返回一个默认构造的shared_ptr。 128 | 129 | redo()的实现如下: 130 | ```c++ 131 | shared_ptr redo() 132 | { 133 | if (current + 1 < changes.size()) 134 | { 135 | ++current; 136 | auto m = changes[current]; 137 | balance = m->balance; 138 | return m; 139 | } 140 | return{}; 141 | } 142 | ``` 143 | 同样,我们需要能够执行一些redo:如果可以,我们可以安全地redo,如果不行,我们什么都不做,并返回一个空指针。把它们结合起来,我们现在可以开始使用undo/redo功能了: 144 | 145 | ```c++ 146 | BankAccount2 ba{ 100 }; 147 | ba.deposit(50); 148 | ba.deposit(25); // 125 149 | cout << ba << "\n"; 150 | ba.undo(); 151 | cout << "Undo 1: " << ba << "\n"; // Undo 1: 150 152 | ba.undo(); 153 | cout << "Undo 2: " << ba << "\n"; // Undo 2: 100 154 | ba.redo(); 155 | cout << "Redo 2: " << ba << "\n"; // Redo 2: 150 156 | ba.undo(); // back to 100 again 157 | ``` -------------------------------------------------------------------------------- /docs/chapter-19-null_object.md: -------------------------------------------------------------------------------- 1 | ### 第19章 空对象 2 | 3 | 我们并不能总能选择自己想使用的接口。例如,我宁愿让我的车自己开车送我去目的地,而不必把100%的注意力放在道路和开车在我旁边的危险疯子身上。软件也是如此:有时你并不是真的想要某一项功能,但它是内置在接口里的。那么你会怎么做呢?创建一个空对象。 4 | 5 | #### 场景 6 | 7 | 假设继承了使用下列接口的库: 8 | 9 | ```c++ 10 | struct Logger 11 | { 12 | virtual ~Logger() = default; 13 | virtual void info(const string& s) = 0; 14 | virtual void warn(const string& s) = 0; 15 | } 16 | ``` 17 | 18 | 这个库使用下面的接口来操作银行账户: 19 | 20 | ```c++ 21 | struct BankAccount 22 | { 23 | std::shared_ptr log; 24 | string name; 25 | int balance = 0; 26 | BankAccount(const std::share_ptr& logger, const string& name, int balance): 27 | log{ logger }, 28 | name{ name }, 29 | balance {balance} 30 | { 31 | // more members here 32 | } 33 | }; 34 | ``` 35 | 36 | 事实上,`BankAccount`可以拥有如下的成员函数: 37 | 38 | ```c++ 39 | void BankAccount::deposit(int amount) 40 | { 41 | balance += amount; 42 | log->info(("Deposited $" + lexical_cast(amount) 43 | + " to " + name + ", balance is now $" 44 | + lexical_cast(balance)); 45 | } 46 | ``` 47 | 48 | 好了,这个实现有什么吗?如果你确实需要日志记录,也没有问题,你只需实现自己的日志记录类... 49 | 50 | ```c++ 51 | struct ConsoleLogger : Logger 52 | { 53 | void info(const string& s) override 54 | { 55 | cout << "INFO: " << s << endl; 56 | } 57 | void warn(const string& s) override 58 | { 59 | cout << "WARNNING!!!" << s << endl; 60 | } 61 | }; 62 | ``` 63 | 64 | 你可以直接使用它。但是,如果你根本不想要日志记录呢? 65 | 66 | ### 空对象 67 | 68 | 我们再来仔细看下`BankAccount`的构造函数 69 | 70 | ```c++ 71 | BankAccount(const shared_ptr& logger, const string& name, int balance) 72 | ``` 73 | 74 | 由于构造函数接受一个日志记录器,因此传递一个未初始化的`shared_ptr`是不安全的。`BankAccout`可以使用指针之前,在内部检查指针是否为空,但你不知道它是否这样做了,因为没有额外的文档是不可能知道的。 75 | 76 | 因此,唯一可以传入`BankAccount`的是一个空对象,一个符合接口但不包含功能的类: 77 | 78 | ```c++ 79 | struct NullLoggor : Logger 80 | { 81 | void info(const string& s) override { } 82 | void warn(const string& s) override { } 83 | }; 84 | ``` 85 | 86 | ### 共享指针不是空对象 87 | 88 | 值得注意的是,`shared_ptr`和其他智能指针类都不是空对象。空对象是保留正确操作(执行无操作)的对象。但是,使用对未初始化的智能指针会崩溃会导致程序崩溃: 89 | 90 | ```c++ 91 | shared_ptr n; 92 | int x = *n + 1; // yikes! 93 | ``` 94 | 95 | 值得注意的是,从调用的角度来看,没有办法使智能指针是安全的。换句话说,如果`foo`没有初始化,那么`foo->bar()`会神奇地变成一个空操作,那么你不能编写这样的智能指针。原因是前缀*和后缀->操作符只是代理了底层(原始)指针。没有办法对指针做无操作。 96 | 97 | #### 改进设计 98 | 99 | 停下来想一想:如果`BankAccount`在你的控制之下,你能改进接口使它更容易使用吗?这里有一些想法: 100 | 101 | - 在所有地方都进行指针检查。这就理清了`BankAccount`的正确性,但并没有消除库使用者的困惑。请记住,你仍然没有说明指针可以是空的。 102 | - 添加一个默认实参值,类似于`const shared_ptr& logger = no_logging`其中`no_logging`是`BankAccount`类的某个成员。即使是这样,你仍然必须在想要使用对象的每个位置对指针值执行检查 103 | - 使用可选(`optional`)类型。它的习惯用法是正确的,并且可以传达意图,但是会导致传入一个`optional>`以及随后检查可选项是否为空。 104 | 105 | #### 隐式空对象 106 | 107 | 这里有一个激进的想法,需要进行两步操纵。它把涉及到把日志记录过程细分为调用(我们想要一个好的日志记录器接口)和操作(日志记录器实际做的事情)。因此,请考虑以下几点: 108 | 109 | ```c++ 110 | struct OptionalLogger : Logger 111 | { 112 | shared_ptr impl; 113 | static shared_ptr no_logging; 114 | Logger(const shared_ptr& logger) : impl { logger } { } 115 | virtual void info(const string& s) override 116 | { 117 | if(impl) impl->info(s); // null check here 118 | } 119 | // and similar checks for other members 120 | }; 121 | 122 | // a static instance of a null object 123 | shared_ptr BankAccount::no_logging{}; 124 | ``` 125 | 126 | 现在我们已经从实现中抽象出了调用。我们现在要做的是像下面这样重新定义`BankAccount`构造函数: 127 | 128 | ```c++ 129 | shared_ptr logger; 130 | BankAccount(const string& name, int balance, const shared_ptr& logger = no_logging) : 131 | log{ make_shared(logger) }, 132 | name{ name }, 133 | balance{ balance } { } 134 | ``` 135 | 136 | 如您所见,这里有一个巧妙的诡计:我们使用一个`Logger`,但存储一个`OptionalLogger`(这是代理设计模式)。然后,对这个可选记录器的所有调用都是安全的-它们只有在底层对象可用时才“发生”: 137 | 138 | ```c++ 139 | BankAccount account{ "primary account", 1000 }; 140 | account.deposit(2000); // no crash 141 | ``` 142 | 143 | 上例中实现的代理对象本质上是`Pimpl`编程技法的自定义版本。 144 | 145 | #### 总结 146 | 147 | 空对象模式提出了一个API设计的问题:我们可以对我们所依赖的对象做什么样的假设?如果我们取一个指针(裸指针或智能指针),那么是否有义务在每次使用时检查该指针? 148 | 149 | 150 | 如果你觉得没有这种义务,那么用户实现空对象的唯一方法是构造所需接口的无操作实现,并将该实例传递进来。也就是说,这只适用于函数:例如,如果对象的字段也被使用,那么你就遇到了真正的麻烦。 151 | 152 | 如果你想主动支持空对象作为参数传递的想法,你需要明确:要么指定参数类型为`std::optional`,给参数一个默认值,暗示它是一个内置的空对象(例如,= no_logging),或只写文档说明什么样的值应当出现在这个位置。 -------------------------------------------------------------------------------- /docs/chapter-20-observer.md: -------------------------------------------------------------------------------- 1 | ### 观察者 2 | 3 | 观察者模式是一种流行且必需的模式,但是令人惊讶的是,与其他语言(例如C#)相比,C++和标准库都没有现成的实现。然而,实现一个安全的、正确的观察者(如果存在这样的东西的话)从技术上来说是比较复杂的。在这章中我们将研究它的细节。 4 | 5 | #### 属性观察者 6 | 7 | 人都会变老,这是生活的事实。但是当一个人长大一岁的时候,我们可能想要庆祝他的生日。但是要怎么实现呢?可以给出下面这样一个定义: 8 | 9 | ```c++ 10 | struct Person { 11 | int age; 12 | Person(int age) : age{age} {} 13 | }; 14 | ``` 15 | 16 | 但是我们怎么知道一个人的年龄(age)什么时候发生改变呢?我们不知道。如果需要知道变化,我们可以尝试轮询:每100毫秒读取一个人的年龄,并将新值与之前的值进行比较。这种方法是可行的,但是很繁琐而且不能扩展,我们需要采用更聪明的方法。 17 | 18 | 当每一个改变年龄字段的写操作发生时,我们想要被通知到。捕捉这个通知的的唯一方法是创建setter。 19 | 20 | ```c++ 21 | struct Person { 22 | int get_age() const { return age; } 23 | void set_age(const int value) { age = value; } 24 | 25 | private: 26 | int age; 27 | }; 28 | ``` 29 | 30 | `setter set_age()` 可以通知任何关心年龄变化的人,但是怎么做到的呢? 31 | 32 | #### Observer\ 33 | 34 | 一种方法是定义某种类型的基类,任何对获取Person变化感兴趣的人都需要继承自这种基类。 35 | 36 | ```c++ 37 | struct PersonListener { 38 | virtual void person_changed(Person &p, const string &property_name, 39 | const any_new_value) = 0; 40 | }; 41 | ``` 42 | 43 | 然而,这种方法非常繁琐,因为属性更改可以发生在Person以外的类型上,而且我们也不希望为这些类型生成额外的类。这里使用更通用的定义: 44 | 45 | ```c++ 46 | template 47 | struct Observer { 48 | virtual void field_changed(T &source, const string &field_name) = 0; 49 | }; 50 | ``` 51 | 52 | field_changed()中的两个参数希望是自解释的。第一个是对其字段实际更改的对象的引用,第二个是字段的名称。是的,名称是作为字符串传递的,这确实损害了代码的可重构性(如果字段名称改变了怎么办?[^1] 53 | 54 | 注1:c#在连续的版本中两次明确地解决了这个问题。首先,它引入了一个名为`CallerMemberName`的属性,该属性将调用函数/属性的名称作为参数的字符串值插入。第二个版本简单地引入了nameof(Foo),它将取符号的名称并将其转换为字符串。 55 | 56 | 这个实现将允许我们观察Person类的变化,例如,将它们写到终端: 57 | 58 | ```c++ 59 | struct ConsolePersonObserver : Observer { 60 | void field_changed(Person &source, const string &field_name) override { 61 | cout << "Person's " << field_name << " has changed to " << source.get_age() 62 | << ".\n"; 63 | } 64 | }; 65 | ``` 66 | 67 | 例如,我们在场景中引入的灵活性允许我们观察多个类的属性变化。例如,如果我们把Creature类加入,现在就可以同时观察这两个类: 68 | 69 | ```c++ 70 | struct ConsolePersonObserver : Observer, Observer { 71 | void field_changed(Person &source, const string &field_name) {} 72 | void field_changed(Creature &source, const string &field_name) {} 73 | }; 74 | ``` 75 | 76 | 另一种替代方法是使用std::any,并去掉泛型实现。试一试! 77 | 78 | 79 | #### Observable 80 | 81 | 不管怎样,让我们回到Person。由于这将成为一个可观察类,它将不得不承担新的责任,即: 82 | 83 | - 把关心Person变化的所有观察者放置在自己的私有段 84 | - 让观察者可以订阅或者取消订阅 subscribe()/unsubscribe() 发生在Person上的变化。 85 | - 当Person发生改变的时候通知所有的观察者。 86 | 87 | 88 | 所有这些功能都可以移到一个单独的基类中,这样就可以避免为每个潜在的可观察对象复制它。 89 | 90 | ```c++ 91 | template 92 | struct Observable { 93 | void notify(T& source, const string& name); 94 | void subscrible(Observer* f) { observers.emplace_back(f); }; 95 | void unsubscrible(Observer* f); 96 | 97 | private: 98 | vector> observers; 99 | }; 100 | ``` 101 | 102 | 我们实现了subscribe(),它只是将一个新的观察者添加到观察者的私有列表中。观察者列表对其他类都不可用,甚至对派生类也不可用。我们不希望其他类随意操纵观察者集合。 103 | 104 | 接下来,我们需要实现`notify()`函数。想法上很简单:遍历每个观察者,并且依次调用对应的`field_changed()`函数。 105 | 106 | ```c++ 107 | void notify(T &source, const string &name) { 108 | for (auto &&obs : observers) observes->field_changed(source, name); 109 | } 110 | ``` 111 | 112 | 不过只从`Observable`类继承是不够的,我们的类还需要在其字段发生改变的时候,把自己作为参数调用`notify()`函数。 113 | 114 | 例如,考虑setter `set_age()`函数,它现在有三个职责: 115 | 116 | - 检查被观察字段是否已经实际更改。如果年龄是20岁,我们把它赋值为20岁,那么任何的赋值或通知就没有意义了。 117 | - 给被观察的字段赋合理的值。 118 | - 用正确的参数调用notify()函数 119 | 120 | 因此,set_age()的新实现可能长成这样: 121 | 122 | ```c++ 123 | struct Person : Observable { 124 | void set_age(const int age) { 125 | if (this->age != age) { 126 | // check_age(age); 127 | this->age = age; 128 | notify(*this, "age"); 129 | } 130 | } 131 | 132 | private: 133 | int age; 134 | }; 135 | ``` 136 | 137 | ### 连接观察者和被观察者 138 | 139 | 现在,我们已经准备好开始使用我们创建的基础设施,以便获得关于人员字段更改的通知(实际上,我们可以称其为属性)。下面是我们的观察者的样子: 140 | 141 | ```c++ 142 | struct ConsolePersonObserver : Observer { 143 | void field_changed(Person &source, const string &field_name) override { 144 | cout << "Person's " << field_name << " has changed to " << source.get_age() 145 | << ".\n"; 146 | } 147 | }; 148 | ``` 149 | 我们可以这样使用: 150 | 151 | ```c++ 152 | Person p{ 20 }; 153 | ConsolePersonObserver cpo; 154 | p.subscribe(&cpo); 155 | p.set_age(21); // Person's age has changed to 21. 156 | p.set_age(22); // Person's age has changed to 22. 157 | ``` 158 | 159 | 如果你不关心与属性依赖关系和线程安全性/可重入性有关的问题,就可以在这此止步,采用这个实现,并开始使用它。如果你想看到关于更复杂方法的讨论,请继续阅读。 160 | 161 | #### 依赖问题 162 | 163 | 大于16岁的人具有选举权,因此当某个人具有选举权之后我们希望被通知到。首先,我们假设Person类有如下的getter函数: 164 | 165 | ```c++ 166 | bool get_can_vote() const { return age >= 16}; 167 | ``` 168 | 169 | 注意,get_can_vote()没有支持字段和setter(我们可以引入这样的字段,但它显然是多余的),但是我们也觉得有必要在它上面通知()。但如何?我们可以试着找出是什么原因导致can_vote改变了它,是set_age()做的!因此,如果我们想要通知投票状态的变化,这些需要在set_age()中完成。准备好,你会大吃一惊的。 170 | 171 | ```c++ 172 | void set_age(const int value) const { 173 | if (age != value) { 174 | auto old_can_vote = can_vote(); // store old value 175 | age = value; 176 | notify(*this, "age"); 177 | 178 | if (old_can_vote != can_vote()) // check value has changed 179 | notify(*this, "can_vote"); 180 | } 181 | } 182 | ``` 183 | 184 | 前面的函数太多了。我们不仅检查年龄是否改变,我们也检查can_vote是否改变,并通知它!你可能会认为这种方法不能很好地扩展,对吧?想象一下can_vote依赖于两个字段,比如age和citizenship——这意味着它们的两个setter都必须处理can_vote通知。更糟糕的是,如果年龄也会以这种方式影响其他10种属性呢?这是一个不可用的解决方案,它会导致脆弱的代码无法维护,因为变量之间的关系需要手动跟踪。 185 | 186 | 坦白地说,在前一种情况下,can_vote属性依赖age属性。依赖性属性的挑战本质上是Excel等工具的挑战:给定不同单元格之间的大量依赖性,当其中一个单元格发生变化时,您如何知道哪些单元格需要重新计算。 187 | 188 | 当然,属性依赖关系可以被形式化为某种类型的`map>`。这将保留一个受属性影响的属性列表(或者相反,影响属性的所有属性)。遗憾的是,这个`map`必须手工定义,而且要与实际代码保持同步是相当棘手的。 189 | 190 | #### 取消订阅和线程安全 191 | 192 | 我忘记讨论的一件事是观察者如何从可观察对象中取消订阅。通常,您希望从观察者列表中删除自己,这在单线程场景中非常简单: 193 | 194 | ```c++ 195 | void unsubscribe(Observer* observer) { 196 | observers.erase(remove(observers.begin(), observers.end(), observer), 197 | observers.end()) 198 | } 199 | ``` 200 | 201 | 虽然`erase-remove`习惯用法在技术上是正确的,但它只在单线程场景中是正确的。vector不是线程安全的,所以同时调用subscribe()和unsubscribe()可能会导致意想不到的结果,因为这两个函数都会修改vector。 202 | 203 | 这很容易解决:只需对所有可观察对象的操作都加一个锁。这看起来很简单: 204 | 205 | ```c++ 206 | template 207 | struct Observable { 208 | void notify(T& source, const string& name) { 209 | scoped_lock lock{mtx}; 210 | ... 211 | } 212 | void subscribe(Observer* f) { 213 | scoped_lock lock{mtx}; 214 | ... 215 | } 216 | void unsubscribe(Observer* o) { 217 | scoped_lock lock{mtx}; 218 | ... 219 | } 220 | 221 | private: 222 | vector*> observers; 223 | mutex mtx; 224 | }; 225 | ``` 226 | 227 | 另一个非常可行的替代方案是使用类似`TPL/PPL`的`concurrent_ vector`。当然,您会失去排序保证(换句话说,一个接一个地添加两个对象并不能保证它们按照那个顺序得到通知),但它肯定会让您不必自己管理锁。 228 | 229 | ### 可重入 230 | 231 | 最后一种实现提供了一些线程安全性,只要有人需要,就锁定这三个关键方法中的任何一个。但是现在让我们设想以下场景:您有一个交通管理组件,它一直监视一个人,直到他到了可以开车的年龄。当他们17岁时,组件就会取消订阅: 232 | 233 | ```c++ 234 | struct TrafficAdministration : Observer { 235 | void TrafficAdministration::field_changed(Person& source, 236 | const string& field_name) override { 237 | if (field_name == "age") { 238 | if (source.get_age() < 17) 239 | cout << "Whoa there, you are not old enough to drive!\n"; 240 | else { 241 | // oh, ok, they are old enough, let's not monitor them anymore 242 | cout << "We no longer care!\n"; 243 | source.unsubscribe(this); 244 | } 245 | } 246 | } 247 | }; 248 | ``` 249 | 250 | 这将会出现一个问题,因为当某人17岁时,整个调用链将会是: 251 | 252 | > notify() --> field_changed() --> unsubscribe() 253 | 254 | 这是一个问题,因为在unsubscribe()中,我们最终试图获取一个已经被获取的锁。这是一个可重入问题。处理这件事有不同的方法: 255 | 256 | - 一种方法是简单地禁止这种情况。毕竟,至少在这个特定的例子中,很明显这里发生了可重入性 257 | - 另一种方法是放弃从集合中删除元素的想法。相反,我们可以这样写: 258 | 259 | ```c++ 260 | void unsubscribe(Observer* o) { 261 | auto it = find(observers.begin(), observers.end(), o); 262 | if (it != observers.end()) *it = nullptr; // cannot do this for a set 263 | } 264 | ``` 265 | 266 | 随后,当使用notify()时,只需要进行额外的检查: 267 | 268 | ```c++ 269 | void notify(T& source, const string& name) { 270 | for (auto&& obs : observes) 271 | if (obs) obs->field_changed(source, name); 272 | } 273 | ``` 274 | 275 | #### 通过 Boost.Signals2 来实现 Observer 276 | 277 | 观察者模式有很多预打包的实现,并且可能最著名的是 `Boost.Signals2` 库。本质上,该库提供了一种称为信号的类型,它表示 `C++` 中的信号术语(在别处称为事件)。可以通过提供函数或 `lambda` 表达式 来订阅此信号。它也可以被取消订阅,当你想通知它时,它可以被解除。 278 | 279 | ```c++ 280 | template 281 | struct Observable { 282 | signal property_changed; 283 | }; 284 | ``` 285 | 286 | 它的调用如下所示: 287 | 288 | ```c++ 289 | struct Person : Observable { 290 | void set_age(const int age) { 291 | if (this->age == age) return; 292 | this->age = age; 293 | property_changed(*this, "age"); 294 | } 295 | }; 296 | ``` 297 | 298 | 299 | API 的实际使用将直接使用信号,当然,除非你决定添加更多 API 陷阱以使其更容易: 300 | 301 | ```c++ 302 | Person p{123}; 303 | auto conn = p.property_changed.connect([](Person&, const string& prop_name) { 304 | cout << prop_name << " has been changed" << endl; 305 | }); 306 | p.set_age(20); // name has been changed 307 | // later, optionally 308 | conn.disconnect(); 309 | ``` 310 | 311 | `connect()` 调用的结果是一个连接对象,它也可以用于在你不再需要信号通知时取消订阅。 312 | 313 | #### 总结 314 | 315 | 毫无疑问,本章中提供的代码是一个明显的例子,它过度思考和过度设计了一个超出大多数人想要实现的问题的方式。 316 | 317 | 让我们回顾一下实现 Observer 时的主要设计决策: 318 | 319 | - 决定你希望你的 observable 传达什么信息。例如,如果你正在处理字段/属性更改,则可以包含属性名称。你还可以指定旧/新值,但传递类型可能会出现问题。 320 | - 你想让你的观察者成为`tire class`,还是你只需要一个虚函数列表? 321 | - 你想如何处理取消订阅的观察者? 322 | 323 | - 如果你不打算支持取消订阅——恭喜你,你将节省大量的实现观察者的工作,因为在重入场景中没有删除问题。 324 | - 如果你计划支持显式的 `unsubscribe()` 函数,你可能不想直接在函数中擦除-删除,而是将元素标记为删除并稍后删除它们。 325 | - 如果你不喜欢在(可能为空)裸指针上调度的想法,请考虑使用 `weak_ptr` 代替。 326 | - `Observer` 的函数是否有可能是 从几个不同的线程调用?如果他们是,你需要保护你的订阅列表: 327 | - 你可以在所有相关函数上放置 `scoped_lock`;或者 328 | - 你可以使用线程安全的集合,例如 `TBB/PPLcurrenct_vector`。你将失去顺序保证。 329 | - 来自同一来源的多个订阅允许吗?如果是,则不能使用 `std::set`。 330 | 331 | 332 | 遗憾的是,没有理想的 `Observer` 实现能够满足所有条件。 无论你采用哪种实现方式,都需要做出一些妥协。 333 | -------------------------------------------------------------------------------- /docs/chapter-21-state.md: -------------------------------------------------------------------------------- 1 | ### 第21章: 状态模式 2 | 3 | 我必须承认:我的行为是由我的状态支配的。如果我没有足够的睡眠,我会有点累。如果我喝了酒,我就不会开车了。所有这些都是*状态(states)*,它们支配着我的行为:我的感受,我能做什么,我不能做什。 4 | 5 | 当然,我可以从一种状态转换到另一种状态。我可以去喝杯咖啡,它能让我从瞌睡中清醒过来(我希望如此!)所以我们可以把咖啡当作触发器,让你真正从困倦过渡到清醒。这里,让我笨拙地为你解释一下: 6 | 7 | ```c++ 8 | coffee 9 | sleepy ----------> alert 10 | ``` 11 | 12 | 所以,状态设计模式是一个非常简单的想法:状态控制行为;状态可以改变;唯一的问题是*谁*引发了状态的变更。 13 | 14 | 基本上有两种方式: 15 | 16 | - 状态是带有行为的实际类,这些行为将实际状态从一个转换到另一个 17 | - 状态和转换只是枚举。我们有一个称为`状态机(state machine)`的特殊组件,它执行实际的转换。 18 | 19 | 这两种方法都是可行的,但实际上第二种方法是最常见的。这两种我们都会过一遍,但我必须承认我只会简单浏览第一个,因为这不是人们通常做事情的方式。 20 | 21 | 22 | #### 状态驱动的状态机 23 | 24 | 我们将从最简单的例子开始:一个电灯开关。它只能处于开和关的状态。我们将构建一个任何状态都能够切换到其他状态的模型:虽然这反映了状态设计模式的经典实现(根据GoF的书),但我并不推荐这样做。 25 | 26 | 27 | 首先,让我们为电灯开关建模:它只有一种状态和一些从一种状态切换到另一种状态的方法: 28 | 29 | ```c++ 30 | class LightSwitch 31 | { 32 | State* state; 33 | public: 34 | LightSwitch() 35 | { 36 | state = new OffState(); 37 | } 38 | void set_state(State* state) 39 | { 40 | this->state = state; 41 | } 42 | }; 43 | ``` 44 | 45 | 这一切看起来都很合理。我们现在可以定义状态,在这个特定的情况下,它将是一个实际的类: 46 | 47 | ```c++ 48 | struct State 49 | { 50 | virtual void on(LightSwitch* ls) 51 | { 52 | cout << "Light is already on\n"; 53 | } 54 | virtual void off(LightSwitch* ls) 55 | { 56 | cout << "Light is already off\n"; 57 | } 58 | }; 59 | ``` 60 | 61 | 这个实现很不直观,所以我们需要慢慢地仔细地讨论它,因为从一开始,关于State类的任何东西都没有意义。 62 | 63 | 首先,`State`不是抽象的!你会认为一个你没有办法(或理由)达到的状态是抽象的。但事实并非如此。 64 | 65 | 第二,状态允许从一种状态切换到另一种状态。这对一个通情达理的人来说,毫无意义。想象一下电灯开关:它是改变状态的开关。人们并不指望`State`本身会改变自己,但它似乎就是这样做的。 66 | 67 | 第三,也许是最令人困惑的,`State::on/off`的默认行为声称我们已经处于这种状态!在我们实现示例的其余部分时,这一点将在某种程度上结合在一起。 68 | 69 | 现在我们实现`On`和`Off`状态: 70 | 71 | ```c++ 72 | struct OnState : State 73 | { 74 | OnState() { cout << "Light turned on\n"; } 75 | void off(LightSwitch* ls) override; 76 | }; 77 | 78 | struct OffState : State 79 | { 80 | OffState() { cout << "Light turned off\n"; } 81 | void on(LightSwitch* ls) override; 82 | }; 83 | ``` 84 | 85 | 实现OnState::off和OffState::on允许状态本身切换到另一个状态!它看起来是这样的: 86 | 87 | ```c++ 88 | void OnState::off(LightSwitch* ls) 89 | { 90 | cout << "Switching light off...\n"; 91 | ls->set_state(new OffState()); 92 | delete this; 93 | } // same for OffState::on 94 | ``` 95 | 96 | 这就是转换发生的地方。这个实现包含了对`delete This`的奇怪调用,这在真实的c++中是不常见的。这对初始分配状态的位置做出了非常危险的假设。例如,可以使用智能指针重写这个示例,但是使用指针和堆分配清楚地表明状态在这里被积极地销毁。如果状态有析构函数,它将触发,你将在这里执行额外的清理。 97 | 98 | 当然,我们确实希望开关本身也能切换状态,就像这样: 99 | 100 | 101 | ```c++ 102 | class LightSwitch 103 | { 104 | ... 105 | void on() { state->on(this); } 106 | void off() { state->off(this); } 107 | }; 108 | ``` 109 | 110 | 因此,把所有这些放在一起,我们可以运行以下场景: 111 | 112 | ```c++ 113 | 1 LightSwitch ls; // Light turned off 114 | 2 ls.on(); // Switching light on... 115 | 3 // Light turned on 116 | 4 ls.off(); // Switching light off... 117 | 5 // Light turned off 118 | 6 ls.off(); // Light is already off 119 | ``` 120 | 121 | 我必须承认:我不喜欢这种方法,因为它不是直观的。当然,状态可以被告知(观察者模式)我们正在进入它。但是,状态转换到另一种状态的想法——根据GoF的书,这是状态模式的经典实现——似乎不是特别令人满意。 122 | 123 | 如果我们笨拙地说明从`OffState`到`OnState`的转换,则需要将其说明为 124 | 125 | ```c++ 126 | LightSwitch::on() -> OffState::on() 127 | OffState -------------------------------------> OnState 128 | ``` 129 | 130 | 另一方面,从OnState到OnState的转换使用基状态类,这个类告诉你你已经处于那个状态 131 | 132 | ```c++ 133 | LightSwitch::on() -> State::on() 134 | OnState ----------------------------------> OnState 135 | ``` 136 | 137 | 这里给出的示例可能看起来特别人为,所以我们现在将看看另一个手工创建的设置,其中的状态和转换被简化为枚举成员。 138 | 139 | #### 手工状态机 140 | 141 | 让我们尝试为一个典型的电话会话定义一个状态机。首先,我们将描述电话的状态: 142 | 143 | ```c++ 144 | enum class State 145 | { 146 | off_hook, 147 | connecting, 148 | connected, 149 | on_hold, 150 | on_hook 151 | }; 152 | ``` 153 | 154 | 我们现在还可以定义状态之间的转换,也可以定义为`enum class`: 155 | 156 | ```c++ 157 | enum class Trigger 158 | { 159 | call_dialed, 160 | hung_up, 161 | call_connected, 162 | placed_on_hold, 163 | taken_off_hold, 164 | left_message, 165 | stop_using_phone 166 | }; 167 | ``` 168 | 169 | 现在,这个状态机的确切规则,即可能的转换,需要存储在某个地方。 170 | 171 | ```c++ 172 | map>> rules; 173 | ``` 174 | 175 | 176 | 这有点笨拙,但本质上`map`的键是我们移动的状态,值是一组表示`Trigger-State`的对,在此状态下可能的触发器以及使用触发器时所进入的状态。 177 | 178 | 让我们来初始化这个数据结构: 179 | 180 | ```c++ 181 | rules[State::off_hook] = { 182 | {Trigger::call_dialed, State::connecting}, 183 | {Trigger::stop_using_phone, State::on_hook} 184 | }; 185 | 186 | rules[State::connecting] = { 187 | {Trigger::hung_up, State::off_hook}, 188 | {Trigger::call_connected, State::connected} 189 | }; 190 | // more rules here 191 | ``` 192 | 193 | 我们还需要一个启动状态,如果我们希望状态机在达到该状态后停止执行,我们还可以添加一个退出(终止)状态: 194 | 195 | ```c++ 196 | State currentState { State::off_hook }, 197 | exitState { State::on_hook }; 198 | ``` 199 | 200 | 完成这些之后,我们就不必为实际运行(我们使用`orchestrating`这个术语)状态机而构建单独的组件了。例如,如果我们想要构建电话的交互式模型,我们可以这样做: 201 | 202 | ```c++ 203 | while(true) 204 | { 205 | cout << "The phone is currently " << currentState << endl; 206 | 207 | select_trigger: 208 | cout << "Select a trigger:" << "\n"; 209 | 210 | int i = 0; 211 | for(auto &&item : rules[currentState]) 212 | { 213 | cout << i++ << ". " << item.first << "\n"; 214 | } 215 | 216 | int input; 217 | cin >> input; 218 | for(input < 0 || (input+1) > rules[currentState].size()) 219 | { 220 | goto select_trigger; 221 | } 222 | 223 | currentState = rules[currentState][input].second; 224 | if(currentState == exitState) break; 225 | } 226 | ``` 227 | 228 | 首先:是的,我确实使用`goto`,这是一个很好的例子,说明在什么地方使用`goto`是合适的(译者注:一般不建议在程序里面使用goto,这样会使得程序的控制流比较混乱)。对于算法本身,这是相当明显的:我们让用户在当前状态上选择一个可用的触发器(`operator<<`状态和触发器都在幕后实现了),并且,如果触发器是有效的,我们通过使用前面创建的规则映射转换到它。 229 | 230 | 如果我们到达的状态是退出状态,我们就跳出循环。下面是一个与程序交互的示例。 231 | 232 | ``` 233 | 1 The phone is currently off the hook 234 | 2 Select a trigger: 235 | 3 0. call dialed 236 | 4 1. putting phone on hook 237 | 5 0 238 | 6 The phone is currently connecting 239 | 7 Select a trigger: 240 | 8 0. hung up 241 | 9 1. call connected 242 | 10 1 243 | 11 The phone is currently connected 244 | 12 Select a trigger: 245 | 13 0. left message 246 | 14 1. hung up 247 | 15 2. placed on hold 248 | 16 2 249 | 17 The phone is currently on hold 250 | 18 Select a trigger: 251 | 19 0. taken off hold 252 | 20 1. hung up 253 | 21 1 254 | 22 The phone is currently off the hook 255 | 23 Select a trigger: 256 | 24 0. call dialed 257 | 25 1. putting phone on hook 258 | 26 1 259 | 27 We are done using the phone 260 | ``` 261 | 262 | 这种手工状态机的主要优点是非常容易理解:状态和转换是普通的枚举类,转换集是在一个简单的`std::map`中定义的,开始和结束状态是简单的变量 263 | 264 | 265 | #### Boost.MSM 中的状态机 266 | 267 | 在现实世界中,状态机要复杂得多。有时,你希望在达到某个状态时发生某些操作。在其他时候,你希望转换是有条件的,也就是说,你希望转换只在某些条件存在时发生。 268 | 269 | 当`Boost.MSM (Meta State Machine)`,一个状态机库,是Boost的一部分,你的状态机是一个通过`CRTP`继承自`state_ machine_def`的类: 270 | 271 | ```c++ 272 | struct PhoneStateMachine : state_machine_def 273 | { 274 | bool angry{ false }; 275 | } 276 | ``` 277 | 278 | 我添加了一个`bool`变量来指示调用者是否`angry`(例如,在被搁置时); 我们稍后会用到它。现在,每个状态也可以驻留在状态机中,并且可以从`state`类继承: 279 | 280 | ```c++ 281 | struct OffHook : state<> {}; 282 | struct Connecting : state<> 283 | { 284 | template 285 | void on_entry(Event const& evt, FSM&) 286 | { 287 | cout << "We are connecting..." << endl; 288 | } 289 | // also on_exit 290 | }; 291 | // other states omitted 292 | ``` 293 | 294 | 如你所见,状态还可以定义在进入或退出特定状态时发生的行为。你也可以定义在转换时执行的行为(而不是当你到达一个状态时):这些也是类,但它们不需要从任何东西继承;相反,它们需要提供具有特定签名的`operator()`: 295 | 296 | ```c++ 297 | struct PhoneBeingDestoryed 298 | { 299 | template 300 | void operator()(EVT const&, FSM& SourceState&, TargetState&) 301 | { 302 | cout << "Phone breaks into a million pieces" << endl; 303 | } 304 | }; 305 | ``` 306 | 307 | 正如你可能已经猜到的那样,这些参数提供了对状态机的引用,以及你将要进入和进入的状态。 308 | 309 | 最后,我们有守卫条件(`guard condition`):这些条件决定我们是否可以在第一时间使用一个转换。现在,我们的布尔变量`angry`不是`MSM`可用的形式,所以我们需要包装它: 310 | 311 | ```c++ 312 | struct CanDestoryPhone 313 | { 314 | template 315 | bool operator()(EVT const&, FSM& fsm, SourceState&, TargetState&) 316 | { 317 | return fsm.angry; 318 | } 319 | }; 320 | ``` 321 | 322 | 前面的例子创建了一个名为`CanDestroyPhone`的守卫条件,稍后我们可以在定义状态机时使用它。 323 | 324 | 325 | 为了定义状态机规则,`Boost.MSM`使用MPL(元编程库)。具体来说,转换表被定义为`mpl::vector`,每一行依次包含: 326 | 327 | - 源状态 328 | - 状态转换 329 | - 目标状态 330 | - 一个要执行的可选操作 331 | - 一个可选守卫条件 332 | 333 | 因此,有了所有这些,我们可以像下面这样定义一些电话呼叫规则: 334 | 335 | ```c++ 336 | struct transition_table : mpl::vector< 337 | Row, 338 | Row, 339 | Row, 340 | Row 341 | > 342 | {}; 343 | ``` 344 | 345 | 在前面的方法中,与状态不同,`CallDialed`之类的转换是可以在状态机类之外定义的类。它们不必继承自任何基类,而且很容易为空,但它们必须是类型。 346 | 347 | `transition_table`的最后一行是最有趣的:它指定我们只能尝试在`CanDestroyPhone`保护条件下销毁电话,并且当电话实际上被销毁时,应该执行`PhoneBeingDestroyed`操作。 348 | 349 | 现在,我们可以添加更多的东西。首先,我们添加起始条件:因为我们正在使用`Boost.MSM`,起始条件是一个类型定义,而不是一个变量: 350 | 351 | ```c++ 352 | typedef OffHook initial_state; 353 | ``` 354 | 355 | 最后,如果没有可能的转换,我们可以定义要发生的操作。它可能发生!比如,你把手机摔坏了,就不能再用了,对吧? 356 | 357 | ```c++ 358 | template 359 | void no_transition(Event const& e, FSM&, int state) 360 | { 361 | cout << "No transition from state " << state_names[state] 362 | << " on event " << typeid(e).name() << endl; 363 | } 364 | ``` 365 | 366 | `Boost MSM`将状态机分为前端(我们刚刚写的)和后端(运行它的部分)。使用后端API,我们可以根据前面的状态机定义构造状态机: 367 | 368 | ```c++ 369 | msm::back::state_machine phone; 370 | ``` 371 | 372 | 现在,假设存在`info()`函数,它只打印我们所处的状态,我们可以尝试`orchestrating`以下场景 373 | 374 | ```c++ 375 | 1 info(); // The phone is currently off hook 376 | 2 phone.process_event(CallDialed{}); // We are connecting... 377 | 3 info(); // The phone is currently connecting 378 | 4 phone.process_event(CallConnected{}); 379 | 5 info(); // The phone is currently connected 380 | 6 phone.process_event(PlacedOnHold{}); 381 | 7 info(); // The phone is currently on hold 382 | 8 9 383 | phone.process_event(PhoneThrownIntoWall{}); 384 | 10 // Phone breaks into a million pieces 385 | 11 386 | 12 info(); // The phone is currently destroyed 387 | 13 388 | 14 phone.process_event(CallDialed{}); 389 | 15 // No transition from state destroyed on event struct CallDialed 390 | ``` 391 | 392 | 因此,这就是定义更复杂、具有工业强度的状态机的方式。 393 | 394 | #### 总结 395 | 396 | 首先,这是值得强调的`Boost.MSM`是Boost中两种状态机实现之一,另一种是`Boost.statechart`。我很确定还有很多其他的状态机实现。 397 | 398 | 其次,状态机的功能远不止这些。例如,许多库支持分层状态机的思想:例如,一个`生病(Sick)`的状态可以包含许多不同的子状态,如`流感(Flu)`或`水痘(Chickenpox)`。如果你在处于感染流感的状态,你也同时处于生病的状态。 399 | 400 | 最后,有必要再次强调现代状态机与状态设计模式的原始形式之间的差异。重复api的存在(例如`LightSwitch::on/off vs. State::on/off`)以及自删除的存在在我的书中是明确的代码气味。不要误会我的方法是有效的,但它是不直观的和繁琐的。 401 | 402 | -------------------------------------------------------------------------------- /docs/chapter-22-strategy.md: -------------------------------------------------------------------------------- 1 | ### 策略模式 2 | 3 | 假设您决定使用包含多个字符串的数组或向量,并将它们作为列表输出 `["just", "like", "this"]`。 4 | 5 | 如果考虑不同的输出格式,您可能知道需要获取每个元素,并将其与一些额外的标记一起输出。但对于HTML或LaTeX这样的语言,列表还需要开始和结束标记或标记。这两种格式中的任何一种对列表的处理都是相似的(需要输出每个条目)和不同的(条目的输出方式)。这些都可以用一个单独的策略来处理。 6 | 7 | 我们可以制定一个渲染列表的策略: 8 | 9 | - 渲染开始标签或元素 10 | - 渲染每一个list元素 11 | - 渲染关闭标签或元素 12 | 13 | 可以针对不同的输出格式制定不同的策略,然后将这些策略输入一个通用的、不变的算法来生成文本。这是另一种存在于动态(运行时可替换)和静态(由模板组成的、固定的)类型中的模式。让我们一起来探讨下它们。 14 | 15 | #### 动态策略 16 | 17 | 因此,我们的目标是打印一个markdown或html格式的简单的文本项列表: 18 | 19 | ```c++ 20 | enum class OutputFormat { markdown, html }; 21 | ``` 22 | 23 | 我们可以在基类ListStrategy中定义打印策略框架: 24 | 25 | ```c++ 26 | struct ListStrategy { 27 | virtual void start(ostringstream& oss){}; 28 | virtual void end(ostringstream& oss){}; 29 | virtual void add_list_item(ostringstream& oss, const string& item){}; 30 | }; 31 | ``` 32 | 33 | 现在让我们跳到文本处理组件。这个组件将调用打印列表的成员函数, `append_list()` 34 | 35 | ```c++ 36 | struct TextProcessor { 37 | void append_list(const verctor items) { 38 | listStrategy->start(oss); 39 | 40 | for (auto &&item : items) 41 | listStrategy 42 | ->add_list_item(oss, item) 43 | 44 | listStrategy->end(oss); 45 | } 46 | 47 | private: 48 | ostringstream oss; 49 | unique_ptr listStrategy; 50 | }; 51 | ``` 52 | 53 | 在`TextProcessor`中我们定义了一个oss的输出字符串缓冲区,我们正在使用的策略是渲染列表,当然还有`append_list()`,它指定了为了实际渲染一个给定的策略列表,需要采取的一系列步骤。需要注意的是,正如前面所使用的,组合是两种可能的选项之一,可用于实现框架算法的具体实现。相反,我们可以添加像`add_list_item()`这样的函数作为虚成员,由派生类覆盖:这就是模板方法模式。 54 | 55 | 56 | 现在我们可以继续为列表实现不同的打印策略,比如`HtmlListStrategy` 57 | 58 | ```c++ 59 | struct HtmlListStrategy : ListStrategy { 60 | void start(ostringstream& oss) override { oss << "
    \n"; } 61 | 62 | void end(ostringstream& oss) override { oss << "
\n"; } 63 | 64 | void add_list_item(ostringstream& oss, const string& item) override { 65 | oss << "
  • " << item << "
  • \n" 66 | } 67 | }; 68 | ``` 69 | 70 | 我们可以用类似的方式实现`MarkdownListStrategy`,但是因为Markdown不需要开始/结束标记,所以我们将只覆盖`add_list_item()`函数。 71 | 72 | ```c++ 73 | struct MarkdownListStrategy : ListStrategy { 74 | void add_list_item(ostringstream& oss) override { oss << "*" << item; } 75 | }; 76 | ``` 77 | 78 | 现在我们可以开始使用文本处理器,为其提供不同的策略并得到不同的结果。例如: 79 | 80 | ```c++ 81 | 82 | TextProcessor tp; 83 | tp.set_output_format(OutputFormat::markdown); 84 | tp.append_list({"foo", "bar", "baz"}); 85 | cout << tp.str() << endl; 86 | 87 | // Output: 88 | // * foo 89 | // * bar 90 | // * baz 91 | 92 | ``` 93 | 94 | 我们可以为在运行时可切换的策略做准备,这正是我们称之为动态策略的原因。这是在`set_output_format()`函数中完成的,它的实现很简单 95 | 96 | ```c++ 97 | void set_output_format(OutputFormat format) { 98 | switch (format) { 99 | case OutputFormat::markdown: 100 | list_strategy = make_unique(); 101 | case OutputFormat::html: 102 | list_strategy = make_unique(); 103 | break; 104 | } 105 | } 106 | ``` 107 | 108 | 现在,从一种策略切换到另一种策略是很简单的,你可以直接看到结果: 109 | 110 | 111 | ```c++ 112 | tp.clear(); // clears the buffer 113 | tp.set_output_format(OutputFormat::Html); 114 | tp.append_list({"foo", "bar", "baz"}); 115 | cout << tp.str() << endl; 116 | // Output: 117 | //
      118 | //
    • foo
    • 119 | //
    • bar
    • 120 | //
    • baz
    • 121 | //
    122 | ``` 123 | 124 | #### 静态策略 125 | 126 | 多亏了模板的魔力,你可以将任何策略直接融入到类型中。只需对 `TextStrategy` 类进行最少的更改: 127 | 128 | ```c++ 129 | template 130 | struct TextProcessor { 131 | void append_list(const vector items) { 132 | list_strategy.start(oss); 133 | for (auto& item : items) list_strategy.add_list_item(oss, item); 134 | list_strategy.end(oss); 135 | } 136 | // other functions unchanged 137 | private: 138 | ostringstream oss; 139 | LS list_strategy; 140 | }; 141 | ``` 142 | 143 | 我们只是添加了 `LS` 模板参数,使用这种类型创建了一个成员策略,并开始使用它而不是我们之前使用的指针。 `append_list()` 的结果是相同的: 144 | 145 | ```c++ 146 | // markdown 147 | TextProcessor tpm; 148 | tpm.append_list({"foo", "bar", "baz"}); 149 | cout << tpm.str() << endl; 150 | // html 151 | TextProcessor tph; 152 | tph.append_list({"foo", "bar", "baz"}); 153 | cout << tph.str() << endl; 154 | ``` 155 | 156 | 前面示例的输出与动态策略的输出相同。请注意,我们必须创建两个 `textProcessor` 实例,每个实例都有不同的列表处理策略。 157 | 158 | #### 总结 159 | 160 | 策略设计模式允许你定义算法的框架,然后使用组合来提供与特定策略相关的缺失实现细节。这种方法存在于两种实现方式: 161 | 162 | - *动态策略* 只是保持一个指向正在使用的策略的指针/引用。想换一种不同的策略吗?只需更改引用。简单! 163 | 164 | - *静态策略* 要求您在编译时选择策略并坚持下去 - 以后没有改变主意的余地 165 | 166 | 应该使用动态策略还是静态策略?好吧,动态对象允许你在构造对象后重新配置对象。想象一个控制文本输出形式的 UI 设置:你更愿意拥有一个可切换的 `TextProcessor` 还是两个 `TextProcessor` 和 `TextProcessor` 类型的变量?这真的取决于你。 167 | 168 | 最后一点,你可以限制类型采用的策略集:而不是允许通用 `ListStrategy` 参数,可以采用 `std::variant` 列出允许传入的类型。 169 | 170 | -------------------------------------------------------------------------------- /docs/chapter-23-template_method.md: -------------------------------------------------------------------------------- 1 | ### 模板方法 2 | 3 | 策略模式和模版方法模式非常相似,就像工厂方法和工厂模式一样,我打算把这两个方法整合成一个设计模式来说明。 4 | 5 | 策略模式和模版方法的不同点在于,策略模式使用(静态或动态的)组合,然而模版方法使用继承。 6 | 但是核心的准则是在某个地方定义算法的框架,而在另外一个地方实现算法的细节,这符合开闭原则(在这里我们简单的扩展了下系统) 7 | 8 | 9 | #### 游戏模拟 10 | 11 | 大多数棋类游戏都非常相似:游戏开始(先进行一些设置), 12 | 玩家轮流游戏,直到决定了获胜者,然后便可以宣布获胜者。不管游戏是国际象棋、跳棋或其他什么, 13 | 我们都可以定义如下算法: 14 | 15 | ```c++ 16 | class Game 17 | { 18 | public: 19 | void run() 20 | { 21 | start(); 22 | while(!have_winner()) 23 | take_turn(); 24 | cout << "Player " << get_winner() << "wins.\n"; 25 | } 26 | }; 27 | ``` 28 | 29 | 正如你所看到的一样,`run()`方法将游戏运行起来,并简单的调用一系列其他方法。这些方法都被定义为纯虚的,同时声明在保护 30 | 字段,使得其他非派生类无法调用这些方法。 31 | 32 | ```c++ 33 | protected: 34 | virtual void start() = 0; 35 | virtual void bool have_winner() = 0; 36 | virtual void take_turn() = 0; 37 | virtual int get_winner() = 0; 38 | ``` 39 | 公平的说,上面的一些方法,尤其是返回值为`void`,并不是必须要定义为纯虚的。例如,如果一些游戏没有显示的`start()`方法,把`start()`方法声明为纯虚的就违法了里氏替换原则,因为子类中其实并不需要这个方法,但是也必须得实现这一接口方法。在策略模式那章中 40 | 我们故意的使用了不采用任何操作的虚函数方法,但是在模版方法里面这样的例子就显得不那么清楚。 41 | 42 | 现在我们在之前的基础上,加上一些和所有游戏相关的共有方法:玩家的个数和当玩家的索引。 43 | 44 | ```c++ 45 | class Game 46 | { 47 | public: 48 | explicit Game(int number_of_players) : 49 | number_of_players(number_of_players), 50 | current_player{0} 51 | {} 52 | protected: 53 | int current_player; 54 | int number_of_players; 55 | };// 省略其他成员 56 | ``` 57 | 58 | `Game`类可以被拓展出来实现象棋(`chess`)游戏。 59 | 60 | ```c++ 61 | class Chess : public Game 62 | { 63 | public: 64 | explicit Chess() : Game { 2 } { } 65 | protected: 66 | void start( ) override { } 67 | bool have_winner( ) override { return turns == max_turns; } 68 | void take_turn( ) override 69 | { 70 | turns++; 71 | current_player = ( current_player + 1 ) % number_of_players; 72 | } 73 | int get_winner( ) override { return curren_player; } 74 | 75 | private: 76 | int turns{ 0 }; 77 | int max_turns{ 10 }; 78 | 79 | } 80 | ``` 81 | 82 | 象棋游戏涉及两个玩家,在构造函数中把参数2传递给父类。然后我们重写了所有必要的函数,实现了简单的模拟逻辑,游戏在第10轮结束。下面是输出: 83 | 84 | ```c++ 85 | 1 Starting a game of chess with 2 players 86 | 2 Turn 0 taken by player 0 87 | 3 Turn 1 taken by player 1 88 | 4 ... 89 | 5 Turn 8 taken by player 0 90 | 6 Turn 9 taken by player 1 91 | 7 Player 0 wins. 92 | ``` 93 | 94 | #### 总结 95 | 96 | 与使用组合并分为静态和动态的策略模式不同,模板方法使用继承, 97 | 因此,它只能是静态的,因为一旦对象被构造,就没有办法操纵它的继承特性 98 | 99 | 模板方法中唯一的设计决策是,你想让模板方法使用的方法是纯虚的, 100 | 还是实际上有一个主体(即使该主体是空的)。 101 | 如果你预见到一些方法对所有的继承者来说都是不必要的,那就把它们变成无操作的方法。 -------------------------------------------------------------------------------- /docs/chapter-24-visitor.md: -------------------------------------------------------------------------------- 1 | ### 第24章:访问者模式 2 | 3 | 如果你要处理层次结构的类型,除非你能够访问源代码,否则不可能向层次结构的每个成员添加函数。这是一个需要提前规划的问题,并产生了访问者模式。 4 | 5 | 下面是一个简单的例子:假设你解析了一个由双精度值和加法操作符组成的数学表达式(当然是使用解释器模式) 6 | 7 | ```c++ 8 | ( 1.0 + (2.0 + 3.0) ) 9 | ``` 10 | 11 | 这个表达式可以用如下的层次结构来表示: 12 | 13 | 14 | ```c++ 15 | struct Expression 16 | { 17 | // 目前这里什么也没有 18 | }; 19 | 20 | struct DoubleExpression : Expression 21 | { 22 | double value; 23 | explicit DoubleExpression(const double value) : value{value} {} 24 | }; 25 | 26 | struct AdditionExpression : Expression 27 | { 28 | Expression* left; 29 | Expression* right; 30 | AdditionExpression(Exprssion* const left, Exprssion* const right) 31 | : left{left}, 32 | right{right} 33 | {}; 34 | 35 | ~AdditionExpression() 36 | { 37 | delete left; 38 | delete right; 39 | } 40 | }; 41 | ``` 42 | 43 | 因此,给定这个对象的层次结构,假设你想给`Expression`的各种继承类添加一些行为(好吧,目前我们只有两个,但这个数字可能会增加)。你会怎么做? 44 | 45 | ### 入侵式的访问者 46 | 47 | 我们将从最直接的方法开始,这将会打破开闭原则(Open-Closed Principle, OCP)实际上,我们将跳转到已经编写好的代码中,并修改`Expression`的接口(以及通过关联,对派生类): 48 | 49 | ```c++ 50 | struct Expression 51 | { 52 | virtual void print(ostringstrem& oss) = 0; 53 | }; 54 | ``` 55 | 56 | 这种方法除了破坏`OCP`之外,还依赖于一个假设,即你实际上可以访问层次结构的源代码,而这并不总是得到保证。 57 | 58 | 59 | 但是我们总得从某处开始吧?因此,有了这个改变,我们需要在`DoubleExpression`(这很简单,所以我在这里省略它)和`AdditionExpression`中实现`print()`: 60 | 61 | ```c++ 62 | struct AdditionExpression : Expression 63 | { 64 | Expression* left; 65 | Expression* right; 66 | ... 67 | void print(ostringstream& oss) override 68 | { 69 | oss << "("; 70 | left->print(oss); 71 | oss << "+"; 72 | right->print(oss); 73 | oss << ")"; 74 | } 75 | }; 76 | ``` 77 | 78 | 哦,这真有趣!我们在子表达式上多态地和递归地调用`print()`。妙啊!让我们来测试一下: 79 | 80 | ```c++ 81 | auto e = new AdditionExpression 82 | { 83 | new DoubleExpresion{1}, 84 | new AdditionExpression 85 | { 86 | new DoubleExpression{2}, 87 | new DoubleExpression{3} 88 | } 89 | }; 90 | ostringstream oss; 91 | e->print(oss); 92 | cout << oss.str() << endl; // print (1 + (2 + 3) ) 93 | ``` 94 | 95 | 这很简单。但是现在,假设你在层次结构中有10个继承者(顺便说一下,这在现实场景中并不少见),你需要添加一些新的`eval()`操作。这十个修改需要在十个不同的类里完成。但`OCP`并不是真正的问题。 96 | 97 | 真正的问题是接口隔离(`Interface Segregation Princile, ISP`)。你看,像打印这样的问题是一个特别关注的问题。与其声明每个表达式都应该打印自己,为什么不引入一个知道如何打印表达式的表达式打印机`ExpressionPrinter`呢?稍后,你可以引入一个表达式求值器`ExpressionEvaluator`,它知道如何执行实际的计算,所有这些都不会以任何方式影响表达式层次结构。 98 | 99 | ### 反射式的Printer 100 | 101 | 既然我们已经决定创建一个单独的打印机组件,那么让我们去掉`print()`成员函数(但当然保留基类)。这里有一个警告:不能让表达式类为空。为什么?因为只有当你有一些虚拟的东西在里面的时候你才会得到多态行为。所以,现在,我们在这里插入一个虚拟析构函数: 102 | 103 | ```c++ 104 | struct Expression 105 | { 106 | virtual ~Expression() = default; 107 | }; 108 | ``` 109 | 110 | 现在让我们尝试实现一个`ExpressionPrinter`。我的第一反应是写这样的东西: 111 | 112 | ```c++ 113 | struct ExpressionPrinter 114 | { 115 | void print(DoubleExpression* de, ostringstream& oss) const 116 | { 117 | oss << de->value; 118 | } 119 | void print(AdditionExpression* ae, ostringstream& oss) const 120 | { 121 | oss << "("; 122 | print(ae->left, oss); 123 | oss << "+"; 124 | print(ae->right, oss); 125 | oss << ")"; 126 | } 127 | }; 128 | ``` 129 | 130 | 前面的代码几乎不能通过编译。C++知道`ae->left`是`Expression`类型,但是它在运行时不能检查类型(不像各种动态类型的语言), 它不知道应该调用哪个重载函数。这也太糟糕了! 131 | 132 | 我们能做些什么呢?我们只能做一件事-移除重载并且在运行时进行类型检查。 133 | 134 | ```c++ 135 | struct ExpressionPrinter 136 | { 137 | void print(Expressoin* e) 138 | { 139 | if(auto de dynamic_cast(e)) 140 | { 141 | oss << de->value; 142 | } 143 | else if(auto ae = dynamic_cast(e)) 144 | { 145 | oss << "("; 146 | print(ae->left, oss); 147 | oss << "+"; 148 | print(ae->right, oss); 149 | oss << ")"; 150 | } 151 | string str() const { return oss.str(); } 152 | private: 153 | ostringstream oss; 154 | } 155 | }; 156 | ``` 157 | 158 | 前面的方法实际上是一个可行的解决方案: 159 | 160 | ```c++ 161 | auto e = new AdditionExpression 162 | { 163 | new DoubleExpresion{1}, 164 | new AdditionExpression 165 | { 166 | new DoubleExpression{2}, 167 | new DoubleExpression{3} 168 | } 169 | }; 170 | ExpressionPrinter ep; 171 | ep.print(e); 172 | cout << ep.str() << endl; // print (1 + (2 + 3) ) 173 | ``` 174 | 175 | 这种方法有一个相当显著的缺点:实际上,您没有为层次结构中的每个元素实现打印的编译器检查。 176 | 177 | 添加新元素时,可以继续使用`ExpressionPrinter`而不需要修改,它会跳过新类型的任何元素。 178 | 179 | 但这是一个可行的解决方案。认真地说,很有可能在这里就停止了,而不再进一步使用访问者模式:`dynamic_cast`开销并不那么昂贵,而且我认为许多开发人员会记得在`if`语句中涵盖每一种类型的对象。 180 | 181 | #### 什么是分派? 182 | 183 | 每当人们谈论访问者模式时,就会提到分派(`dispatch`)这个词。它是什么?简单地说,分派就是确定具体要调用哪个函数,为了进行分派需要多少条信息。 184 | 185 | 下面是一个简单的例子: 186 | 187 | ```c++ 188 | struct Stuff {}; 189 | struct Foo : Stuff {}; 190 | struct Bar : Stuff {} ; 191 | 192 | void func(Foo* foo) {} 193 | void func(Bar* bar) {} 194 | ``` 195 | 196 | 现在,如果我创建一个普通的Foo对象,那么调用它的func()就没有问题了 197 | 198 | ```c++ 199 | Foo* foo = new Foo; 200 | func(foo); // ok 201 | ``` 202 | 203 | 但如果我决定将它转换为基类指针,那么编译器将不知道要调用哪个重载函数: 204 | 205 | ```c++ 206 | Stuff* stuff = new Foo; 207 | func(stuff); 208 | ``` 209 | 210 | 现在,让我们从多态的角度来考虑这个问题:有没有办法强制系统调用正确的重载函数,而不需要任何运行时检查(比如`dynamic_cast`和类似的方法)。的确存在这种方法。 211 | 212 | 看,当你在一个`Stuff`上调用某个方法时,这个调用可以是多态的(多亏了虚函数表),它可以被分派到必要的组件。这样就可以调用必要的重载函数。这被称为双分派(`double dispatch`),因为: 213 | 214 | 1. 首先,你对实际对象做一个多态调用 215 | 2. 在多态调用中,调用重载。因为在对象内部,`this`有一个明确的类型(例如,`Foo*`或`Bar*`),所以会触发正确的重载。 216 | 217 | 218 | ```c++ 219 | struct Stuff 220 | { 221 | virtual void call() = 0; 222 | }; 223 | struct Foo : Stuff 224 | { 225 | void call() override 226 | { 227 | func(this); 228 | } 229 | }; 230 | struct Bar : Stuff 231 | { 232 | void call() override 233 | { 234 | func(this); 235 | } 236 | } 237 | void func(Foo* foo) { } 238 | void func(Bar* bar) { } 239 | ``` 240 | 241 | 你能看到这里发生了什么吗?我们不能只是将一个通用的`call()`实现插入到某个东西中:不同的实现必须在各自的类中,这样`this`指针才具有合适的类型。 242 | 243 | 这个实现允许你写以下代码: 244 | 245 | ```c++ 246 | Stuff* stuff = new Foo; 247 | stuff->call(); // effectively calls func(stuff) 248 | ``` 249 | 250 | #### 经典的访问者 251 | 252 | 访问者设计模式的经典实现使用了双重分派。访问者成员函数的调用是有约定的: 253 | 254 | - 访问者的成员函数通常叫做`visit()` 255 | - 在整个层次结构中实现的成员函数通常称为`accept()` 256 | 257 | 我们现在可以从我们的`Expression`基类中扔掉虚析构函数,因为我们实际上有一些东西要放进去: `accept()`函数: 258 | 259 | ```c++ 260 | struct Expression 261 | { 262 | virtual void accept(ExpressionVisitor* visitor) = 0; 263 | }; 264 | ``` 265 | 266 | 如你所见,上述代码引用了一个名为`ExpressionVisitor`的(抽象)类,它可以作为各种访问者(如`ExpressionPrinter`、`ExpressionEvaluator`等)的基类。我在这里选择了一个指针,但是你也可以选择使用一个引用。 267 | 268 | 现在,现在从`Expression`继承的每一个派生类都需要以相同的方式实现accept(),即: 269 | 270 | ```c++ 271 | void accept(ExpressionVisitor* visitor) override 272 | { 273 | visitor->visit(this); 274 | } 275 | ``` 276 | 277 | 另一方面,我们可以像下面这样定义`ExpressionVisitor`: 278 | 279 | ```c++ 280 | struct ExpressionVisitor 281 | { 282 | virtual void visit(DoubleExpression* de) = 0; 283 | virtual void visit(AdditionExpression* ae) = 0; 284 | }; 285 | ``` 286 | 287 | 注意,我们必须为所有对象定义重载;否则,在实现相应的`accept()`时将会出现编译错误。现在我们可以继承这个类来定义我们的`ExpressionPrinter`: 288 | 289 | ```c++ 290 | struct ExpressionPrinter : ExpressionVisitor 291 | { 292 | ostringstream oss; 293 | string str() const { return oss.str(); } 294 | void visit(DoubleExpression* de) override; 295 | void visit(AdditionExpression* ae) override; 296 | }; 297 | ``` 298 | 299 | `visit()`函数的实现应该是相当明显的,因为我们已经不止一次地看到了它,但我将再次展示它: 300 | 301 | ```c++ 302 | void visit(AdditionExpression* ae) 303 | { 304 | oss << "("; 305 | ae->left->accept(this); 306 | oss << "+"; 307 | ae->right->accept(this); 308 | oss << ")"; 309 | } 310 | ``` 311 | 312 | 313 | 请注意,调用现在是如何在子表达式本身上发生的,再次利用了双重分派。至于使用新的双分派访问者,它是这样的: 314 | 315 | ```c++ 316 | void main() 317 | { 318 | auto e = new AdditionExpression 319 | { 320 | // as before 321 | }; 322 | ostringstream oss; 323 | ExpressionPrinter ep; 324 | ep.visit(e); 325 | cout << ep.str() << endl; //(1+(2+3)) 326 | } 327 | ``` 328 | 329 | #### 实现一个额外的访问者 330 | 331 | 332 | 那么,这种方法的优点是什么呢?这样做的好处是你只需要通过层次结构实现一次`accept()`成员。你再也不用碰层级中的成员了。例如:假设你现在想要一种方法来计算表达式的结果?这是很容易的: 333 | 334 | ```c++ 335 | struct ExpressionEvaluator : ExpressionVisitor 336 | { 337 | double result; 338 | void visit(DoubleExpression* de) override; 339 | void visit(AdditionExpression* ae) override; 340 | }; 341 | ``` 342 | 343 | 但需要记住的是,visit()目前声明为void方法,因此实现可能看起来有点奇怪: 344 | 345 | ```c++ 346 | void ExpressionEvaluator::visitor(DoubleExpression* de) 347 | { 348 | result = de->value; 349 | } 350 | 351 | void ExpressionEvaluator::visitor(AdditionExpression* ae) 352 | { 353 | ae->left->accept(this); 354 | auto temp = result; 355 | ae->right->accept(this); 356 | result += temp; 357 | } 358 | ``` 359 | 360 | 前面的情况是无法从`accept()`返回结果的,有点棘手。本质上,我们计算左边的部分并缓存值。然后对右边的部分求值(因此设置了`result`),然后将其增加缓存的值,从而生成求和。不是很直观的代码。 361 | 362 | 尽管如此,它仍然工作良好: 363 | 364 | ```c++ 365 | auto e = new AdditionExpression{ /* as before */ }; 366 | ExpressionPrinter printer; 367 | ExpressionEvaluator evaluator; 368 | printer.visit(e); 369 | evaluator.visit(e); 370 | cout << printer.str() << " = " << evaluator.result << endl; 371 | // prints "(1+(2+3)) = 6 372 | ``` 373 | 374 | 同样,你也可以添加许多其他不同的访问者,遵循OCP,并在这个过程中玩得快乐。 375 | 376 | 377 | ### 无环访问者 378 | 379 | 现在是时候提一下,实际上,访问者设计模式有两种类型。他们是 380 | 381 | - 循环访问者,这是基于函数重载。由于层次结构(必须知道访问器类型)和访问器(必须知道层次结构中的每个类)之间的循环依赖关系,该方法的使用仅限于不经常更新的稳定层次结构。 382 | 383 | -无环访问者,基于`RTTI`。这样做的优点是对已访问的层次结构没有限制,但是,正如你可能已经猜到的那样,这对性能有影响。 384 | 385 | 实现无循环访问器的第一步是实际的访问器接口。我们没有为层次结构中的每一个类型定义一个`visit()`重载,而是尽可能地使其泛型化: 386 | 387 | ```c++ 388 | template 389 | struct Visitor 390 | { 391 | virtual void visit(Visitable& obj) = 0; 392 | } 393 | ``` 394 | 395 | 我们需要域模型中的每个元素都能够接受这样的访问者,但是,由于每个专门化都是惟一的,所以我们要做的是引入一个标记接口(`marker interface`),一个空类,只有一个虚析构函数: 396 | 397 | ```c++ 398 | struct VisitorBase // 标记接口 399 | { 400 | virtual ~VisitorBase() = default; 401 | }; 402 | ``` 403 | 404 | 前面的类没有行为,但我们将在实际访问的任何对象中将其用作`accept()`方法的参数。现在,我们可以像下面这样重新定义表达式类: 405 | 406 | ```c++ 407 | struct Expression 408 | { 409 | virtual ~Expression() = default; 410 | virtual void accept(VisitorBase& obj) 411 | { 412 | using EV = Visitor; 413 | if(auto ev = dynamic_cast(&obj)) 414 | ev->visit(*this); 415 | } 416 | } 417 | ``` 418 | 419 | 所以新的`accept()`方法是这样工作的:我们取一个`VisitorBase`,然后试着把它转换为一个`Visitor`,其中`T`是我们当前使用的类型。如果转换成功,访问者就知道如何访问我们的类型,为此它相应的`visit()`方法。如果失败了,那就没办法了。理解为什么`obj`本身没有`visit()`方法是非常关键的。如果它这样做了,它将需要为每一个有想要调用它的类型进行重载,这将会引入循环依赖。 420 | 421 | 在模型的其他部分实现`accept()`之后,我们可以通过再次定义`ExpressionPrinter`将所有东西放在一起,但这一次,它看起来如下所示: 422 | 423 | ```c++ 424 | struct ExpressionPrinter : VisitorBase, 425 | Visitor 426 | Visitor 427 | { 428 | void visit(DoubleExpression &obj) override; 429 | void visit(AdditionExpression &obj) override; 430 | string str() const { return oss.str(); } 431 | private: 432 | ostringstream oss; 433 | }; 434 | ``` 435 | 436 | 如你所见,我们为每个想要访问的地方实现了`VisitorBase`标记接口以及`Visitor` 。如果我们省略了一个特定类型的T(例如,假设我注释掉了`Visitor`;),程序仍然会编译,而相应的`accept()`调用,如果它来了,将作为一个无操作执行。在前面,`visit()`方法的实现与我们在经典访问者实现中的实现实际上是相同的,结果也是一样的。 437 | 438 | #### `Variants` 和 `std:visit` 439 | 440 | 虽然与经典访问者模式没有直接关系,但是`std::visit`还是值得一提的,因为它的名字暗示它与访问者模式有关。本质上,`std::visit`是访问`variant`类型的`correct part`的一种方法。 441 | 442 | 下面是一个例子:假设你有一个地址,该地址的部分字段是一个房屋字段。现在,一座房子可以只是一个号码(如伦敦路123号),也可以有一个名字,如Montefiore Castle。因此,你可以如下定义该`variant` 443 | 444 | ```c++ 445 | variant house; 446 | // house = "Montefiore Castle" 447 | house = 221; 448 | ``` 449 | 450 | 这两个赋值都是有效的。现在,假设你决定打印房子的名字或号码。为此,你可以首先定义一个类型,在该类型里实现对`variant`的函数调用符重载。 451 | 452 | ```c++ 453 | struct AddressPrinter 454 | { 455 | void operator()(const string& house_name) const 456 | { 457 | cout << "A house called" << house_name << "\n"; 458 | } 459 | void operator()(const int house_number) const 460 | { 461 | cout << "House number" << house_number << "\n"; 462 | } 463 | } ; 464 | ``` 465 | 466 | 现在,该类型可以与`std::visit()`一起使用,这是一个库函数,它将这个访问者应用到`variant`类型: 467 | 468 | ```c++ 469 | AddressPrinter ap; 470 | std::visit(ap, house); // House number 221 471 | ``` 472 | 可以在适当的地方定义一组访问者函数,这要感谢一些现代c++的魔力。我们需要做的是构造一个类型为`auto&`的`lambda`表达式,获取底层类型,使用`if constexpr`比较它,并相应地处理: 473 | 474 | ```c++ 475 | std::visit([](auto& arg){ 476 | using T = decay_t; 477 | if consyexpr(is_same_v) 478 | { 479 | cout << "A house called " << arg.c_str() << "\n"; 480 | } 481 | else 482 | { 483 | cout << "House number " << arg << "\n"; 484 | } 485 | }, house); 486 | ``` 487 | 488 | #### 总结 489 | 490 | 访问者设计模式允许我们向对象层次结构中的每个元素添加一些行为。我们已经看到的方法包括: 491 | 492 | - 入侵式:给层次结构里面的每个对象增加一个虚函数。可行(但是你必须获取到源代码)但打破了开闭原则。 493 | - 反射式:增加一个单独的访问者,这样就不需要改变被访问的对象;当需要动分派时使用`dynamic_cast` 494 | - 经典(双分派):被访问者整个的层次结构只会以一种通用的方式被修改一次。层次结构里面的每个元素实现`accpet()`方法来接收访问者。然后我们将`visitor`子类化,以在各个方向增强层次结构的功能。 495 | 496 | 访问者模式经常与解释器模式一起出现:在解析了一些文本输入并将其转换为面向对象的结构之后,我们需要,例如,以特定的方式呈现抽象语法树。`Visitor`帮助在整个层次结构中传播`ostringstream`(或类似的对象),并将数据整理在一起, 497 | 498 | 499 | -------------------------------------------------------------------------------- /docs/pics/ch02_composite_builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuzengh/design-pattern/d2b299088ee87df9508ad11f37798a3b919f5815/docs/pics/ch02_composite_builder.png -------------------------------------------------------------------------------- /src/singleton.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | /* 4 | class Singleton 5 | { 6 | protected: 7 | Singleton() 8 | { 9 | //do what you need to do 10 | } 11 | public: 12 | static Singleton& getSingleton() 13 | { 14 | static Singleton instance; 15 | return instance; 16 | }; 17 | Singleton(const Singleton&) = delete; 18 | Singleton& operator=(const Singleton&) = delete; 19 | Singleton(Singleton&&) = delete; 20 | Singleton& operator=(Singleton&&) = delete; 21 | }; 22 | */ 23 | class Singleton 24 | { 25 | protected: 26 | Singleton(); 27 | private: 28 | static std::mutex m_mutex; 29 | static std::atomic m_instance = nullptr; 30 | public: 31 | static Singleton* Singleton::getInstance() 32 | { 33 | Singleton* tmp = m_instance.load(std::memory_order_acquire); 34 | if (tmp == nullptr) 35 | { 36 | //std::scoped_lock(m_mutex); 37 | std::lock_guard lock(m_mutex); 38 | tmp = m_instance.load(std::memory_order_relaxed); 39 | if (tmp == nullptr) 40 | { 41 | tmp = new Singleton; 42 | m_instance.store(tmp, std::memory_order_release); 43 | } 44 | } 45 | return tmp; 46 | } 47 | Singleton(const Singleton&) = delete; 48 | Singleton& operator=(const Singleton&) = delete; 49 | Singleton(Singleton&&) = delete; 50 | Singleton& operator=(Singleton&&) = delete; 51 | 52 | }; 53 | 54 | 55 | 56 | --------------------------------------------------------------------------------