I’m quite new to OOP and I’m concerned that this class that I’ve written is really poorly designed. It seems to disobey several principles of OOP:
- It doesn’t contain its own data, but relies on a yaml file for
values. - Its methods need to be called in a particular order
- It has a lot of instance variables and methods
It does work, however. It’s robust, but I’ll need to modify the source code to add new getter methods every time I add page elements
It’s a model of an html document used in an automated test suite. I keep thinking that some of the methods could be put in subclasses, but I’m concerned that I’d have too many classes then.
What do you think?
class BrandFlightsPage < FlightSearchPage
attr_reader :route, :date, :itinerary_type, :no_of_pax,
:no_results_error_container, :submit_button_element
def initialize(browser, page, brand)
super(browser, page)
#Get reference to config file
config_file = File.join(File.dirname(__FILE__), '..', 'config', 'site_config.yml')
#Store hash of config values in local variable
config = YAML.load_file config_file
@brand = brand #brand is specified by the customer in the features file
#Define instance variables from the hash keys
config.each do |k,v|
instance_variable_set("@#{k}",v)
end
end
def visit
@browser.goto(@start_url)
end
def set_origin(origin)
self.text_field(@route[:attribute] => @route[:origin]).set origin
end
def set_destination(destination)
self.text_field(@route[:attribute] => @route[:destination]).set destination
end
def set_departure_date(outbound)
self.text_field(@route[:attribute] => @date[:outgoing_date]).set outbound
end
def set_journey_type(type)
if type == "return"
self.radio(@route[:attribute] => @itinerary_type[:single]).set
else
self.radio(@route[:attribute] => @itinerary_type[:return]).set
end
end
def set_return_date(inbound)
self.text_field(@route[:attribute] => @date[:incoming_date]).set inbound
end
def set_number_of_adults(adults)
self.select_list(@route[:attribute] => @no_of_pax[:adults]).select adults
end
def set_no_of_children(children)
self.select_list(@route[:attribute] => @no_of_pax[:children]).select children
end
def set_no_of_seniors(seniors)
self.select_list(@route[:attribute] => @no_of_adults[:seniors]).select seniors
end
def no_flights_found_message
@browser.div(@no_results_error_container[:attribute] => @no_results_error_container[:error_element]).text
raise UserErrorNotDisplayed, "Expected user error message not displayed" unless divFlightResultErrTitle.exists?
end
def submit_search
self.link(@submit_button_element[:attribute] => @submit_button_element[:button_element]).click
end
end
If this class is designed as a Facade, then it’s not (too) bad design. It provides a coherent unified way to perform related operations that rely on a variety of un-related behavior holders.
It appears to be poor separation of concerns, in that this class essentially coupling all the various implementation details, which might turn out to be somewhat tricky to maintain.
Finally, the fact methods need to be called in a specific order may hint at the fact you’re trying to model a state machine – in which case it probably should be broken down to several classes (one per “state”). I don’t think there’s a “too many methods” or “too many classes” point you’d reach, the fact is you need the features provided by each class to be coherent and making sense. Where to draw the line is up to you and your specific implementation’s domain requirements.