Welcome! In this lesson, we'll dissect two main software design patterns: the Facade and Adapter patterns. We aim to unravel how these patterns ensure backward compatibility while adding new features. Backward compatibility means that new updates don't break existing systems, allowing for new functionalities without affecting the current code. Think of the Facade and Adapter patterns as cassette-shaped tape adapters for CD players, bridging the new and the old.
Design patterns are recognized solutions to frequent problems in software design and are time-tested methods resulting from the craftsmanship of seasoned developers. Among many design patterns, we're focusing on the Facade
and Adapter
patterns today. The Facade
pattern provides a simplified interface to a complex subsystem, whereas the Adapter
pattern enables classes with incompatible interfaces to work together. Let's delve deeper to unravel their use cases.
The Facade
pattern simplifies complex processes by providing a higher-level interface. Consider an online shopping application. When a user places an order, it triggers many operations. By using the Facade
pattern, we can create an OrderFacade
class to simplify these operations:
C++1#include <iostream> 2 3class Order { 4public: 5 void create() { 6 std::cout << "Order created.\n"; 7 } 8}; 9 10class Product { 11public: 12 void checkAvailability() { 13 std::cout << "Product availability checked.\n"; 14 } 15}; 16 17class Payment { 18public: 19 void processPayment() { 20 std::cout << "Payment processed.\n"; 21 } 22}; 23 24class Delivery { 25public: 26 void arrangeDelivery() { 27 std::cout << "Delivery arranged.\n"; 28 } 29}; 30 31class OrderFacade { 32public: 33 void placeOrder() { 34 order.create(); 35 product.checkAvailability(); 36 payment.processPayment(); 37 delivery.arrangeDelivery(); 38 } 39 40private: 41 Order order; 42 Product product; 43 Payment payment; 44 Delivery delivery; 45}; 46 47int main() { 48 OrderFacade orderFacade; 49 orderFacade.placeOrder(); 50 return 0; 51}
Here, assume the Order
, Product
, Payment
, and Delivery
classes are already correctly implemented in different places, with all the necessary methods.
The Facade
pattern, as demonstrated in the online shopping application example, ensures backward compatibility by consolidating complex subsystem interactions (ordering, payment, delivery) behind a simple OrderFacade
interface. This allows the underlying subsystems to evolve independently (e.g., changing the payment process or delivery options) without necessitating changes to the client code, thereby preserving the interface's constancy over time. In addition, it makes the code more decoupled, allowing all order steps to be updated independently.
The Adapter
pattern is our bridge for making otherwise incompatible interfaces work together, similar to how a travel adapter allows devices from one country to be used in the electrical outlets of another. For a more streamlined example, imagine a simple scenario where we have a legacy MusicPlayer
designed to play MP3 files alone, and we're looking to support more formats like WAV without changing its interface.
C++1#include <iostream> 2#include <string> 3 4class MusicPlayer { 5public: 6 void play(const std::string& file) { 7 if (file.substr(file.find_last_of(".") + 1) == "mp3") { 8 std::cout << "Playing " << file << " as mp3.\n"; 9 } else { 10 std::cout << "File format not supported.\n"; 11 } 12 } 13}; 14 15class MusicPlayerAdapter { 16public: 17 MusicPlayerAdapter(MusicPlayer* player) : player(player) {} 18 19 void play(const std::string& file) { 20 if (file.substr(file.find_last_of(".") + 1) == "wav") { 21 // Convert WAV file playback request into MP3 format request 22 std::string convertedFile = file; 23 convertedFile.replace(convertedFile.find_last_of(".") + 1, 3, "mp3"); 24 std::cout << "Converting " << file << " to " << convertedFile << " ...\n"; 25 player->play(convertedFile); 26 } else { 27 player->play(file); 28 } 29 } 30 31private: 32 MusicPlayer* player; 33}; 34 35int main() { 36 MusicPlayer legacyPlayer; 37 legacyPlayer.play("song.mp3"); // Directly supported 38 39 MusicPlayerAdapter adapterPlayer(&legacyPlayer); 40 adapterPlayer.play("song.wav"); // Supported through adapter 41 42 return 0; 43}
In this example, the MusicPlayerAdapter
wraps the MusicPlayer
, allowing it to play WAV files by converting them to the MP3 format it supports. This demonstrates the Adapter
pattern's core idea: facilitating backward compatibility by enabling a new feature (WAV support) without altering the original music player's code. It's a seamless way to extend functionality while preserving the old system's integrity.
Great work! We've covered two powerful design patterns: Facade and Adapter. Both serve specific needs to ensure backward compatibility when adding features to existing software. You now understand their functions, usage, and their role in ensuring backward compatibility in software development. Ready your programming hats! In our upcoming practical exercises, we'll work hands-on with these patterns!