dsdshcym / dsdshcym.github.io

https://yiming.dev/
1 stars 1 forks source link

Stop using Behaviour to define interfaces, use Protocol! - Yiming Chen #22

Open utterances-bot opened 3 years ago

utterances-bot commented 3 years ago

Stop using Behaviour to define interfaces, use Protocol! - Yiming Chen

I think Elixir developers (me included) often misuse Behaviour to define common interfaces. In this post, I'd propose to use Protocol for defining common interfaces and use Behaviour for sharing common logic. And you may see why I made Promox and Objext to fill the gaps I find in the current Elixir community.

https://yiming.dev/blog/2021/07/18/stop-using-behaviour-to-define-interfaces-use-protocol/

madclaws commented 3 years ago

Hi, what will be the solution for the first Weather module problem. How can we use protocol to get the same effect?. Since Protocols can only be used for data types right?.

dsdshcym commented 3 years ago

@madclaws That's a really good question! Thank you for raising that up! (I should've explained that in the article, but my ignorance made me forgot about that.)

The trick to solve this with Protocol is that you can define empty Structs:

defprotocol WeatherSource do
  @spec temperature(WeatherSource.t(), Location.t()) :: {:ok, Temperature.t()}
  def temperature(source, location)
end

defmodule WeatherSource.A do
  # The benefit of using a struct is that we can add more fields in the future, 
  # to store things like configurations
  # So we don't need to fetch them from Application.get_env or some global data sources
  defstruct []

  def new(), do: %__MODULE__{}

  defimpl WeatherSource do
    def temperature(location), do: ...
  end
end

Promox.defmock(for: WeatherSource)

Then when we use this API

source = WeatherSource.A.new()
location = Location.new("Shanghai")

WeatherSource.temperature(source, location)

(A sidenote: the code above looks very much like Ruby code 😂 )

BTW, you may find my previous blog post useful: https://yiming.dev/blog/2021/06/13/what-i-learned-from-implementing-combinators-in-3-elixir-patterns/

madclaws commented 3 years ago

@dsdshcym Thanks for the explanation, although i felt it like very Object oriented. To get a behaviour like experience, we had to add extra.

dsdshcym commented 3 years ago

@madclaws Exactly! I think that's the good part of OO and can still benefit Elixir projects

And I also feel the pain of these extra boilerplate, so I made https://github.com/dsdshcym/objext, in hope to provide a better API

Here is how to solve the above Weather problem with Objext:

defmodule Weather do
  use Objext.Interface

  definterfaces do
    def temperature(source, location)
  end
end

defmodule Weather.SourceA do
  use Objext, implements: [Weather]

  def new(), do: buildo()

  def temperature(_source, location), do: #...
end

source = Weather.SourceA.new()
location = Location.new("Shanghai")

Weather.temperature(source, location)

I think the complexity of this code is in between Behaviour and Protocol (the biggest huddle would actually be the weird buildo API which would build an encapsulated Struct under the hood)

But hopefully with Objext, we can deal with pure, impure interfaces, and everything in between smoothly :pray: