How to properly refer to Model attributes that might be wrapped in the future? (AKA 'getters' for Ecto models)

In the project I am working on, there are many fields that might at some point need more complicated behaviour.

An example: A Subscription can have a cost. I can refer to it like subscription.cost in the controllers/templates.

But at some point, the cost to show in the interface will not always be 1:1 the same as the field that is stored in the database. For instance, there might be a dynamic default value, or the cost depends on a combination of the Subscription cost field and another attribute defined on another model that the Subscription belongs to.

At that time, I need to define a method on Subscription that implements this, and replace all locations that used subscription.cost to the new Subscription.cost(subscription).

To ensure that I won’t burn my fingers that way, I now have added the following code to most of my models:

  for attr <- ~w{cost discount_cost name some_other_attribute etc}a do
    @doc """
    Basic getter for `#{attr}`
    """
    def unquote(attr)(plan) do
      %Subscription{unquote(attr) => val} = plan
      val
    end
    quote do
      defoverridable [unquote(attr), 1]
    end
  end

I could automate this even further, by reading the list of fields from Subscription.__schema__(:fields) and define functions for all of the existing fields that way.

But: Is this the correct approach? Or is there a better way to do this?

2 Likes

I would really be grateful if someone could voice their opinion on this subject. Is this a good practice or is there a better way?

1 Like

I think the idea of getters is mistaken. For one thing, suppose cost requires some time consuming computation. In an OO world you hit the getter, compute that once, and then persist it in the object so that subsequent lookups are fast. This is not possible with immutable data, which tells us that possibly we’re thinking about the problem wrong.

Models are just data. If they aren’t the data you need, then you need to transform that data into whatever is necessary and use that. If you write code that needs a notation of cost that is no longer a simple lookup from the database, then that’s different data. Build that data and pass it in.

4 Likes

FYI a similar discussion happened on the mailing list before, and may be relevant to your question: https://groups.google.com/forum/#!searchin/elixir-lang-talk/getter/elixir-lang-talk/ybC_0c4M38k/6itFgwQfCAAJ

3 Likes

Thank you very much! That is a very interesting discussion.

‘getters’ is a term whose definition is very muddled by its OOP-background. The discussion you linked above resulted in (if I understood it correctly) people agreeing that separation-of-concerns by ‘hiding’ the internal representation of your data type from the outside world is a good idea.

It is very easy to write OOP-smelling code by relying on the field-based lookups that can be used for maps and structs. Therefore, I think it is a good idea to wrap most of the model attributes in this way, as it will make your code more suitable for change in the future.

1 Like