In early day, when I programed in PL/SQL, I know that PL/SQL support keyword based parameter in procedure/function but I never used this feature, simply I have not recognized the benefit of using it that time.
Ruby does not support keyword based parameter but Ruby programmers fake it easily using hash in combination with symbol. Using this style is very popular in Ruby core lib and Rails. The following example illustrates it, suppose we want to create method
create_user
that create database user, in traditional position based parameter, we will do like that
def create_user(username,password,ignore_error,force,verbose) #.. implementation detail is ignored end #calling it create_user('scott','tiger',false,true,true)on keyword based version, the method has simply one parameter
params
def create_user(params) username = params[:username] password = params[:password] ignore_error = params[:ignore_error] verbose = params[:verbose] force= params[:force] #.. implementation detail is ignore end #calling it create_user(:username=>'scott',:password=>'tiger',:ignore_error=>false, :verbose=>true, :force=>true)The keyword based version is obviously more verbose, requires more typing, but offers several benefits.
MORE EXPRESSIVE AND LESS ERROR
The keyword based version is more expressive, just by looking at how the method is called, we know what is the intention, there is no need to look at the implementation file to figure out what it does. The position based version suffers what Joshua Bloch mentioned in his How to Design a Good API and Why it Matters "Long lists of identically typed params harmful"
GOOD DEFAULT
In the position based version, we can only assign default value for those parameters that are at the end of parameter list.
def create_user(username,password=username,ignore_error=false,force=false,verbose=false) #.. implementation detail is ignored end #calling it create_user('scott')This will not give a flexibility of using default value just for few last one. In the keyword based version, we can archive it easily as follow
def create_user(params) username = params[:username] password = params[:password] || username ignore_error = params[:ignore_error] verbose = params[:verbose] force= params[:force] #implementation detail is ignored #note that Ruby consider nil as false in condition expression, so we should design #boolean value keyword in such way that its default value is false end #calling it create_user(:username=>'scott',:verbose=>true) create_user(:username=>'scott',:force=>true) create_user(:username=>'scott',:ignore_error=>true)I also see people using
Hash::merge
to shorten assignment of default values and adding some assertion of accepted keywords e.g
#borrow from ActiveSupport class Hash def assert_valid_keys(*valid_keys) unknown_keys = keys - [valid_keys].flatten raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}\nValid key(s): #{valid_keys.join(',')}") unless unknown_keys.empty? end end class DatabaseSchemaBuilder attr_accessor :ignore_error,:verbose,:force def default_options {:ignore_error=>@ignore_error,:verbose=>@verbose,:force=>@force} end def create_user(params) params.assert_valid_keys(:ignore_error,:verbose,:force) params = default_options.merge(params) username = params[:username] password = params[:password] ignore_error = params[:ignore_error] verbose = params[:verbose] force= params[:force] #implementation detail is ignored end endEASY TO CHANGE
When we need lets say adding new parameter to the method. In position based version, we end up with changing contract of the method, which may result in looking at every line of code that use this method and make change unless you put at the end of parameter list with default value.
In keyword based version, it is obvious less painful, just adding one more keyword, setting a default value, anyway change only the method itself, e.g. we want to add parameter
:noop
, meaning no operation, just for testing.
def default_options {:ignore_error=>false,:verbose=>false,:force=>false,:noop=>false} end def create_user(params) params.assert_valid_keys(:ignore_error,:verbose,:force,:noop) params = default_options.merge(params) username = params[:username] password = params[:password] || username ignore_error = params[:ignore_error] verbose = params[:verbose] force= params[:force] noop=params[:noop] #implementation detail is ignored endHIDING DESIGN DECISION
Using position based parameter with little bit long parameter list, I have to decide if put one parameter before other or not. I do not have this problem when using keyword based parameter, so it help me do program faster.
No comments:
Post a Comment