Is it bad practice to provide your own setter or should I use setproperty?

Viewed 100

Suppose if I had the following Employee struct:

mutable struct Employee
  _id::Int64
  _first_name::String
  _last_name::String
  
  function Employee(_id::Int64,_first_name::String,_last_name::String)
    # validation left out.
    new(_id,_first_name,_last_name)
  end
end

If I wanted to implement my own setproperty!() I can do:

function setproperty!(value::Employee,name::Symbol,x)
  if name == :_id
    if !isa(x,Int64)
      throw(ErrorException("ID type is invalid"))
    end
    setfield!(value,:_id,x)
  end
  if name == :_first_name
    if is_white_space(x)
      throw(ErrorException("First Name cannot be blank!"))
    end
    setfield!(value,:_first_name,x) 
  end
  if name == :_last_name
    if is_white_space(x)
      throw(ErrorException("Last Name cannot be blank!"))
    end 
    setfield!(value,:_last_name,x)
  end 
end

Have I implemented setproperty!() correctly?

The reason why I use setfield!() for _first_name and _last_name, is because if I do:

if name == :_first_name
  setproperty!(value,:_first_name,x) # or value._first_name = x 
end 

it causes a StackOverflowError because it's recursively using setproperty!().

I don't really like the use of setproperty!(), because as the number of parameters grows, so would setproperty!().

It also brings to mind using Enum and if statements (only we've switched Enum with Symbol).

One workaround I like, is to document that the fields are meant to be private and use the provided setter to set the field:

function set_first_name(obj::Employee,first_name::AbstractString)
    # Validate first_name before assigning it.   
    obj._first_name = first_name
end

The function is smaller and has a single purpose.

Of course this doesn't prevent someone from using setproperty!(), setfield!() or value._field_name = x, but if you're going to circumvent the provided setter then you'll have the handle the consequences for doing it.

1 Answers

Of course this doesn't prevent someone from using setproperty!(), setfield!() or value._field_name = x, but if you're going to circumvent the provided setter then you'll have the handle the consequences for doing it.

I would recommend you to do this, defining getter,setter functions, instead of overloading getproperty/setproperty!. on the wild, the main use i saw on overloading getproperty/setproperty! is when fields can be calculated from the data. for a getter/setter pattern, i recommend you to use the ! convention: getter:

function first_name(value::Employee)
  return value._first_name
end

setter:

function first_name!(value::Employee,text::String)
  #validate here 
  value._first_name = text
  return value._first_name 
end

if your struct is mutable, it could be that some fields are uninitialized. you could add a getter with default, by adding a method:

function first_name(value::Employee,default::String)
  value_stored = value._first_name
  if is_initialized(value_stored) #define is_initialized function
    return value_stored
  else
    return default
  end
end

with a setter/getter with default, the only difference between first_name(val,text) and first_name!(val,text) would be the mutability of val, but the result is the same. useful if you are doing mutable vs immutable functions. as you said it, the getproperty/setproperty! is cumbersome in comparison. If you want to disallow accessing the fields, you could do:

Base.getproperty(val::Employee,key::Symbol) = throw(error("use the getter functions instead!")

Base.setproperty!(val::Employee,key::Symbol,x) = throw(error("use the setter functions instead!")

Disallowing the syntax sugar of val.key and val.key = x. (if someone really want raw access, there is still getfield/setfield!, but they were warned.)

Finally, i found this recomendation in the julia docs, that recommends getter/setter methods over direct field access https://docs.julialang.org/en/v1/manual/style-guide/#Prefer-exported-methods-over-direct-field-access

Related