CSC/ECE 517 Fall 2007/wiki1 8 s1
The extend method in Ruby is designed to mix in methods of a module to a class at the class level. 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
- def getPatientCharge
- puts “getPatientCharge from BenefitCalculation”
- end
- def getPatientCharge
- def getCopay
- puts “getCopay from BenefitCalculation”
- end
- def getCopay
- 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
- def getAddress
- puts “getAddress from Service”
- end
- def getAddress
- 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
Caution
Again take note that the use of extend lets us call module functionality at the class level. 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 (us used include)
- irb> MedicalService.getCopay