Writing easy to testable, scalable and maintainable codes

Hi

I am using protocol in hope, to write a easy testable, scalable and maintainable codes

The protocol definition and implementation looks as follow:

defmodule SapOdataService.Auth do
  alias SapOdataService.Auth.UserLogin
  alias SapOdataService.Auth.UserInfo

  defprotocol Auth do
    @moduledoc """
     Protocol for SAP authentication.
    """
    def sign_in(data)
  end

  defimpl Auth, for: UserLogin do
    def sign_in(user) do
      %UserInfo{firstname: "Foo", lastname: "Boo", basic_auth: "Basic FooBoo"}
    end
  end
end

the user login struct(user_login.ex):

defmodule SapOdataService.Auth.UserLogin do

  defstruct username: nil, password: nil
  @type t :: %__MODULE__{username: String.t, password: String.t}

end

the user info struct(user_info.ex):

defmodule SapOdataService.Auth.UserInfo do

  defstruct firstname: nil, lastname: nil, basic_auth: nil
  @type t :: %__MODULE__{firstname: String.t, lastname: String.t, basic_auth: String.t}

end

As you can see the code above, the implementation of userlogin:

defimpl Auth, for: UserLogin do
    def sign_in(user) do
      %UserInfo{firstname: "Foo", lastname: "Boo", basic_auth: "Basic FooBoo"}
    end
  end

The sign_in function will make a http post request to server, if the given authorization was correct or not.

For the request, I use HTTPoison library for make a request to the server.

Supposed, in one day HTTPoison will not exists anymore and some developers develop new HTTP client and then I have to replace HTTPoison through new HTTP client.

My goal is, to write codes easy to scale and maintain.

I read the wonderful article about mock from José, he suggests to use protocol and pass the dependency as the parameter to function. That is the point, why I use he protocol over behaviour.

In addition, how to force the protocol definition to return particular type. In my case, I would like to do like this:

defprotocol Auth do
  @moduledoc """
   Protocol for SAP authentication.
  """
  @spec sign_in(data) :: UserInfo.t
  def sign_in(data)
end

Thanks

1 Like

You’re able to, more or less… as long as you specify the UserInfo module correctly; I created a small project based on your info above;

# mix.exs
defmodule SapOdataService.Mixfile do
  use Mix.Project
  # ...
  defp deps do
    [{:dialyxir, "~> 0.4.3"}]
  end
end

# lib/sap_odata_service/auth.ex
defprotocol SapOdataService.Auth do
  alias SapOdataService.Auth.UserInfo

  @moduledoc """
  Protocol for SAP authentication.
  """
  @spec sign_in(any) :: UserInfo.t
  def sign_in(data)
end

# lib/sap_odata_service/auth/user_login.ex
defmodule SapOdataService.Auth.UserLogin do
  alias SapOdataService.Auth

  defstruct [username: nil, password: nil]
  @type t :: %__MODULE__{username: String.t, password: String.t}

  defimpl Auth do
    def sign_in(user) do
      true
      #%UserInfo{firstname: "Foo", lastname: "Boo", basic_auth: "Basic FooBoo"}
    end
  end
end

# lib/sap_odata_service/auth/user_info.ex
defmodule SapOdataService.Auth.UserInfo do
  defstruct [firstname: nil, lastname: nil, basic_auth: nil]
  @type t :: %__MODULE__{firstname: String.t, lastname: String.t, basic_auth: String.t}
end

If I run Dialyzer, I get the expected output (along with some apparent protocol-related “unknowns”, for missing implementations I guess):

$ mix do deps.get, compile
...
$ mix dialyzer
Checking PLT...
...
lib/sap_odata_service/auth/user_login.ex:8: The inferred return type of sign_in/1 ('true') has nothing in common with #{}, which is the expected return type for the callback of 'Elixir.SapOdataService.Auth' behaviour
Unknown functions:
  'Elixir.SapOdataService.Auth.Atom':'__impl__'/1
  'Elixir.SapOdataService.Auth.BitString':'__impl__'/1
  ...
Unknown types:
  prettypr:document/0
 done in 0m5.00s
done (warnings were emitted)

Not sure how you want to roll it though… in your case, you put the protocol inside the SapOdataService.Auth module, so the protocol module became SapOdataService.Auth.Auth, etc.

Just tell, how to write codes correct way.

What do you mean? Dialyzer correctly pointed out the error in the code (returning true instead of a %UserInfo{} struct)… so it will catch this type of issue (where the incorrect type is returned compared to what’s in the typespec).

Aha…ok thanks so much. I will try to do like you says and will let you know.

I’ll admit that Dialyzer’s output is a bit weird sometimes :slight_smile: But mostly… remember that when you refer to one module from within another one, you either have to alias it or use the full module name.

Some key points:

  • I split everything up into separate files
  • SapOdataService.Auth is just the protocol, I hope this was what you intended
  • The implementation of the protocol for SapOdataService.Auth.UserLogin was moved to the UserLogin module itself
    • This allows us to use defimpl without specifying for:, and also makes more sense to me; keep things together that belong together…

You can try this out with different return values from sign_in/1 if you want to, in order to satisfy your concerns that it would catch any incorrect return types.

1 Like