Lesson 5
Backward Compatibility in Practice with Ruby
Backward Compatibility: Practice

Welcome back! Today, we'll master what we learned about backward compatibility in practice. Get prepared to apply all the knowledge on practice tasks, but first, let's look at two examples and analyze them.

Task 1: Enhancing a Complex Data Processing Method with Default Parameters

Let's say that initially, we have a complex data processing method designed to operate on an array of hashes, applying a transformation that converts all string values within the hashes to uppercase. Here's the initial version:

Ruby
1def process_data(items) 2 processed_items = items.map do |item| 3 item.transform_values { |value| value.is_a?(String) ? value.upcase : value } 4 end 5 processed_items.first(3).each { |item| puts "Processed Item: #{item}" } # Display the first 3 items for brevity 6end

We intend to expand this method, adding capabilities to filter the items based on a condition and allow for custom transformations. The aim is to retain backward compatibility while introducing these enhancements. Here's the updated approach:

Ruby
1def process_data(items, condition: ->(x) { true }, transform: nil) 2 processed_items = items.select(&condition).map do |item| 3 if transform 4 transform.call(item) 5 else 6 # Default transformation: Convert string values to uppercase 7 item.transform_values { |value| value.is_a?(String) ? value.upcase : value } 8 end 9 end 10 processed_items.first(3).each { |item| puts "Processed Item: #{item}" } # Display the first 3 items for brevity 11end 12 13# Default behavior - convert string values to uppercase 14process_data([{ "name" => "apple", "quantity" => 10 }, { "name" => "orange", "quantity" => 5 }]) 15 16# Custom filter - select items with a quantity greater than 5 17process_data([{ "name" => "apple", "quantity" => 10 }, { "name" => "orange", "quantity" => 5 }], condition: ->(x) { x["quantity"] > 5 }) 18 19# Custom transformation - convert names to uppercase and multiply the quantity by 2 20custom_transform = ->(item) { item.transform_keys { |key| item[key].upcase! if key == "name"; key }.transform_values { |value| value.is_a?(Numeric) ? value * 2 : value } } 21process_data([{ "name" => "apple", "quantity" => 10 }, { "name" => "orange", "quantity" => 5 }], transform: custom_transform)

In this evolved version, we've introduced two optional parameters: condition, a Proc to filter the input array based on a given condition, and transform, a Proc for custom transformations of the filtered items. The default behavior processes all items, converting string values to uppercase, which ensures that the original method's behavior is maintained for existing code paths. This robust enhancement strategy facilitates adding new features to a method with significant complexity while preserving backward compatibility, showcasing an advanced application of evolving software capabilities responsively and responsibly.

Task 2: Using the Adapter Design Pattern for Backward Compatibility

Imagine now that we are building a music player, and recently, the market demands have grown. Now, users 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 or the Adapter we've already implemented for WAV support?

Let's say that we currently have a MusicPlayer class that can only play MP3 files:

Ruby
1class MusicPlayer 2 def play(file) 3 if file.end_with?(".mp3") 4 puts "Playing #{file} as mp3." 5 else 6 puts "File format not supported." 7 end 8 end 9end

Let's approach this challenge by introducing a composite adapter, a design that encapsulates multiple adapters or strategies to extend functionality in a modular and maintainable manner.

Ruby
1class MusicPlayer 2 def play(file) 3 if file.end_with?(".mp3") 4 puts "Playing #{file} as mp3." 5 else 6 puts "File format not supported." 7 end 8 end 9end 10 11class MusicPlayerAdapter 12 def initialize(player) 13 @player = player 14 @format_adapters = { 15 ".wav" => method(:convert_and_play_wav), 16 ".flac" => method(:convert_and_play_flac) 17 } 18 end 19 20 def play(file) 21 file_extension = File.extname(file).downcase 22 if (adapter_func = @format_adapters[file_extension]) 23 adapter_func.call(file) 24 else 25 @player.play(file) 26 end 27 end 28 29 def convert_and_play_wav(file) 30 # Simulate conversion 31 converted_file = file.sub(".wav", ".mp3") 32 puts "Converting #{file} to #{converted_file} and playing as mp3..." 33 @player.play(converted_file) 34 end 35 36 def convert_and_play_flac(file) 37 # Simulate conversion 38 converted_file = file.sub(".flac", ".mp3") 39 puts "Converting #{file} to #{converted_file} and playing as mp3..." 40 @player.play(converted_file) 41 end 42end 43 44# Upgraded music player with enhanced functionality through the composite adapter 45legacy_player = MusicPlayer.new 46enhanced_player = MusicPlayerAdapter.new(legacy_player) 47enhanced_player.play("song.mp3") # Supported directly 48enhanced_player.play("song.wav") # Supported through adaptation 49enhanced_player.play("song.flac") # Newly supported through additional adaptation

This sophisticated adaptation strategy ensures that we can extend the MusicPlayer to include support for additional file formats without disturbing its original code or the initial adapter pattern's implementation. The MusicPlayerAdapter thus acts as a unified interface to the legacy MusicPlayer, capable of handling various formats by determining the appropriate conversion strategy based on the file type.

Summary and Practice

Great job! You've delved into backward compatibility while learning how to utilize default parameters and the Adapter Design Pattern in Ruby. Get ready for some hands-on practice to consolidate these concepts! Remember, practice makes perfect. Happy coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.