CSC/ECE 517 Fall 2007/wiki1 8 s1
This page is a response to the following assignment:
Devise one or more practical examples of Ruby's extend method--that is, examples that serve a real need and are not contrived. (If you can find existing examples on the Web, critique their practicality.) For inspiration, you might want to read up on prototype-based languages.
The objective is to educate through example the proper use of Ruby's extend method and in doing so briefly introduce modules and the include method to illustrate how it differs from extend. The design described in this solution is based on a software system implemented by the author. As the assignment asks for originality of example, external links are minimized. However, other examples are referenced at the end of the document.
Introduction
The extend method in Ruby is designed to mix in methods of a module to a class definition at the class level or to an instantiated object dynamically. The term ‘class level’ is synonymous with the concept of static in other popular object oriented languages such as Java or C# where static behaviors and attributes exist independently of any instance of the class.
Module
Before presenting an example of the extend method, it is necessary to briefly define modules and introduce a small example.
A module is a collection of functionality that cannot be used to create objects, which is to say that alone it does not define a type. It exists to group functionality that will be used by objects either at the implementation level or the class level, depending on the mechanism by which it was added.
Below is an example of a module in Ruby. Its purpose is to group benefit calculation functionality for procedures performed at a general treatment facility. It has been simplified for illustration purposes – in practice benefit calculation is immensely complicated.
# module for encapsulating benefit calculation functionality module BenefitCalculation # call to get the appropriate charge for a procedure # assume the appropriate arguments are sent in def getPatientCharge puts “getPatientCharge from BenefitCalculation” end # call to get the appropriate copay for a given procedure and insurance type # assume the appropriate arguments are sent in def getCopay puts “getCopay from BenefitCalculation” end end
Extend Example
The most effective way to explain the use of extending a module is through an example, so consider a full-service treatment center (like a hospital) that provides medical, dental, optical and homeopathic services. A software solution developed to support this type of organization may consist of four modules to encapsulate the functionality specific to their services, but there would also be functionality common to all services in a base class.
A possible high-level design of the software architecture follows:
# Service is the base class for our system. class Service # call to get the address of the service center def getAddress puts “getAddress from Service” end end class DentalService < Service # added functionality for dental services end class MedicalService < Service # added functionality for medical services end class OpticalService < Service # added functionality for optical services end class HomeopathicService < Service # added functionality for homeopathic services end
Now consider that there are some services for which insurance is an option, and there are others for which it is not. In our example, let’s consider dental and medical services to be the only services appropriate for insurance support. Where would be a good place to encode the insurance benefit calculation?
Option One
We could place it in the specific classes that accept insurance support (DentalService and MedicalService), but in doing so we would be duplicating code. Code duplication becomes a maintenance nightmare, and the difficulty grows rapidly with the number of times the code is duplicated. For this reason, it would probably be a bad idea to place the needed code directly into the class definitions of DentalService and MedicalService.
Option Two
Another option would be to define the functionality in the base class Service. This would allow us to maintain one copy of the code so that when benefit calculation changes (as it does almost weekly), changes could be made in one place and thus maintenance time is minimized. The problem with this solution is that OpticalService and HomeopathicService also inherit the functionality. Because services of their type do not support insurance, the functionality is inappropriate for their type definitions.
Option Three - Extend
Ruby provides a good solution to this problem. Using the extend method, we can place the needed benefit calculation functionality inside of a module and extend that module where it is appropriate. With this solution we solve the problems from options one and two illustrated above: 1) there is a single copy of the functionality in our solution and 2) the functionality is only added to the types for which it is appropriate.
Modifying our hierarchy above to fit this solution, we end up with the following:
class DentalService < Service # added functionality for dental services extend BenefitCalculation end class MedicalService < Service # added functionality for medical services extend BenefitCalculation end
Now that we have a clean design, let's execute some code in the IRB and see the results. Assume the module and all class definitions have been loaded.
- irb> medical = MedicalService.new
- irb> optical = OpticalService.new
- irb> optical.getAddress
- # yields 'getAddress from Service'
- irb> optical.getAddress
- irb> OpticalService.getPatientCharge
- # yields NoMethodError (we didn’t extend)
- irb> OpticalService.getPatientCharge
- irb> OpticalService.getCopay
- # yields NoMethodError (we didn’t extend)
- irb> OpticalService.getCopay
- irb> DentalService.getPatientCharge
- # yields 'getPatientCharge from BenefitCalculation'
- irb> DentalService.getPatientCharge
- irb> DentalService.getCopay
- # yields 'getCopay from BenefitCalculation'
- irb> DentalService.getCopay
Another option would be to use extend to add the functionality of the module dynamically to individual objects instead of statically to the class definitions. When doing this, the functionality can be called at the individual object level. If we return to our original definition of MedicalService (with no module extended in the class definition), we can add the module functionality to the instantiated object dynamically as follows:
- irb> medical = MedicalService.new
- irb> medical.extend(BenefitCalculation)
- irb> medical.getPatientCharge
- # yields 'getPatientCharge from BenefitCalculation
- irb> medical.getPatientCharge
The software system used in this example is being developed using C# on the Microsoft .NET platform, and modules like those in Ruby are unavailable. In order to get around the problems illustrated in options one and two above, BenefitCalculation functionality is itself implemented in a class containing static methods, and in order to perform the appropriate calculations the corresponding BenefitCalculation method is called and sent the appropriate arguments, as illustrated below:
public class Service { public virtual float getPatientCharge() { return float.MinValue; // indicates nothing was calculated } } public class MedicalService : Service { public override float getPatientCharge() { return BenefitCalculation.getPatientCharge(); } }
Notice that there is only one copy of the benefit calculation code in the C# solution, so the problem presented by option one is avoided. Likewise, HomeopathicService simply doesn't override getPatientCharge(), so the inappropriate inheritance of functionality illustrated in option two is also avoided. The difference is that objects of type MedicalService no longer contains the functionality for calculating the patient charge, but can get to it. Static languages by nature don't allow the dynamic addition of functionality as illustrated above, so that was not an option for this project.
Caution
Again take note that the use of extend lets us call module functionality at the class level if used in the context of the class definition. In this same context, were we to attempt to call extended functionality at the object level we would get a NoMethodError.
- irb> medical.getCopay
- # yields NoMethodError
- irb> medical.getCopay
If you need to add module functionality at the object level, use include instead of extend. Consider the following new definition of the MedicalService class:
class MedicalService < Service # added functionality for medical services include BenefitCalculation end
Observe the behavior of the following calls executed in the IRB and the results. Compare the output listed here with the output above.
- irb> medical = MedicalService.new
- irb> medical.getPatientCharge
- # yields 'getPatientCharge from BenefitCalculation'
- irb> medical.getPatientCharge
- irb> medical.getCopay
- # yields 'getCopay from BenefitCalculation'
- irb> medical.getCopay
- irb> MedicalService.getPatientCharge
- # yields NoMethodError (we used include)
- irb> MedicalService.getPatientCharge
- irb> MedicalService.getCopay
- # yields NoMethodError (we used include)
- irb> MedicalService.getCopay
References
A Ruby module tutorial illustrating of the uses of extend and include.
Another Ruby module tutorial again illustrating the uses of extend and include.