Welcome back! Today, we'll master what we learned about backward compatibility in practice. Get ready to apply all the knowledge to practice tasks, but first, let's look at two examples and analyze them.
Suppose that initially, we have a complex data processing function designed to operate on a list of maps, applying a transformation that converts all string values within the maps to uppercase. Here's the initial version in C++:
C++1#include <iostream> 2#include <vector> 3#include <unordered_map> 4#include <algorithm> 5 6void process_data(const std::vector<std::unordered_map<std::string, std::string>>& items) { 7 std::vector<std::unordered_map<std::string, std::string>> processed_items; 8 for (const auto& item : items) { 9 std::unordered_map<std::string, std::string> processed_item; 10 for (const auto& pair : item) { 11 std::string upper_value = pair.second; 12 std::transform(upper_value.begin(), upper_value.end(), upper_value.begin(), ::toupper); 13 processed_item[pair.first] = upper_value; 14 } 15 processed_items.push_back(processed_item); 16 } 17 for (size_t i = 0; i < std::min(processed_items.size(), static_cast<size_t>(3)); ++i) { 18 std::cout << "Processed Item: "; 19 for (const auto& pair : processed_items[i]) { 20 std::cout << pair.first << ": " << pair.second << " "; 21 } 22 std::cout << "\n"; 23 } 24}
To enhance this function, we'll introduce an overloaded function to allow custom transformation and filtering capabilities while maintaining backward compatibility.
Function templates in C++ allow you to write generic, reusable code that can handle different data types. By using templates, you define a blueprint for a function without specifying the exact data types the function will operate on. This allows the compiler to generate a version of the function with the correct data types when the function is called.
In the code snippet below, function templates are used to define a generic version of the process_data
function. This templated function accepts custom transformation and filtering logic through lambda expressions, allowing for flexible processing while maintaining the function's default behavior to ensure backward compatibility.
C++1template <typename Condition, typename Transform> 2void process_data( 3 const std::vector<std::unordered_map<std::string, std::string>>& items, 4 Condition condition, 5 Transform transform 6) { 7 std::vector<std::unordered_map<std::string, std::string>> processed_items; 8 for (const auto& item : items) { 9 if (condition(item)) { 10 auto transformed_item = transform(item); 11 processed_items.push_back(transformed_item); 12 } 13 } 14 for (size_t i = 0; i < std::min(processed_items.size(), static_cast<size_t>(3)); ++i) { 15 std::cout << "Processed Item: "; 16 for (const auto& pair : processed_items[i]) { 17 std::cout << pair.first << ": " << pair.second << " "; 18 } 19 std::cout << "\n"; 20 } 21} 22 23// Default behavior - convert string values to uppercase 24process_data([{ {"name", "apple"}, {"quantity", "10"} }, { {"name", "orange"}, {"quantity", "5"} }]); 25 26// Custom filter - select items with a quantity greater than 5 27process_data( 28 [{ {"name", "apple"}, {"quantity", "10"} }, { {"name", "orange"}, {"quantity", "5"} }], 29 [](const auto& item) { return std::stoi(item.at("quantity")) > 5; }, 30 [](const auto& item) { return item; } 31); 32 33// Custom transformation - convert names to uppercase and multiply the quantity by 2 34process_data( 35 [{ {"name", "apple"}, {"quantity", "10"} }, { {"name", "orange"}, {"quantity", "5"} }], 36 [](const auto&) { return true; }, 37 [](const auto& item) { 38 std::unordered_map<std::string, std::string> transformed_item; 39 for (const auto& pair : item) { 40 if (pair.first == "name") { 41 std::string upper_value = pair.second; 42 std::transform(upper_value.begin(), upper_value.end(), upper_value.begin(), ::toupper); 43 transformed_item[pair.first] = upper_value; 44 } else { 45 transformed_item[pair.first] = std::to_string(std::stoi(pair.second) * 2); 46 } 47 } 48 return transformed_item; 49 } 50);
The evolved version uses function templates and lambda expressions to allow for custom transformations and filtering, ensuring the process remains backward compatible with existing code paths by maintaining default functionality.
Imagine now that we are building a music player, and recently, market demands have grown. Users now expect support not just for MP3 and WAV but also for FLAC files within our music player system. This development poses a unique challenge: How do we extend our music player's capabilities to embrace this new format without altering its established interface?
Let's say we currently have a MusicPlayer
class that can only play MP3 files:
C++1#include <iostream> 2#include <string> 3#include <unordered_map> 4 5class MusicPlayer { 6public: 7 void play(const std::string& file) { 8 if (file.ends_with(".mp3")) { 9 std::cout << "Playing " << file << " as mp3." << std::endl; 10 } else { 11 std::cout << "File format not supported." << std::endl; 12 } 13 } 14};
We can approach this challenge by introducing a composite adapter, which encapsulates multiple strategies to extend functionality modularly:
C++1class MusicPlayerAdapter { 2public: 3 MusicPlayerAdapter(MusicPlayer* player) : player_(player) { 4 format_adapters_ = { 5 {".wav", [this](const std::string& file) { convert_and_play_wav(file); }}, 6 {".flac", [this](const std::string& file) { convert_and_play_flac(file); }} 7 }; 8 } 9 10 void play(const std::string& file) { 11 std::string file_extension = file.substr(file.find_last_of(".")); 12 auto it = format_adapters_.find(file_extension); 13 if (it != format_adapters_.end()) { 14 it->second(file); 15 } else { 16 player_->play(file); 17 } 18 } 19 20private: 21 void convert_and_play_wav(const std::string& file) { 22 std::string converted_file = file.substr(0, file.size() - 4) + ".mp3"; 23 std::cout << "Converting " << file << " to " << converted_file << " and playing as mp3..." << std::endl; 24 player_->play(converted_file); 25 } 26 27 void convert_and_play_flac(const std::string& file) { 28 std::string converted_file = file.substr(0, file.size() - 4) + ".mp3"; 29 std::cout << "Converting " << file << " to " << converted_file << " and playing as mp3..." << std::endl; 30 player_->play(converted_file); 31 } 32 33 MusicPlayer* player_; 34 std::unordered_map<std::string, std::function<void(const std::string&)>> format_adapters_; 35}; 36 37int main() { 38 MusicPlayer legacy_player; 39 MusicPlayerAdapter enhanced_player(&legacy_player); 40 41 enhanced_player.play("song.mp3"); // Supported directly 42 enhanced_player.play("song.wav"); // Supported through adaptation 43 enhanced_player.play("song.flac"); // Newly supported through additional adaptation 44 45 return 0; 46}
This adaptation strategy ensures we can extend the MusicPlayer
to include support for additional file formats without altering its original code or the adapter pattern's implementation. The MusicPlayerAdapter
acts as a unified interface to the legacy MusicPlayer
, handling various formats by determining the appropriate conversion strategy based on the file type.
Great job! You've delved into backward compatibility while learning to utilize function overloading, templates, and the Adapter design pattern in C++. Get ready for some hands-on practice to consolidate these concepts! Remember, practice makes perfect. Happy coding!