How to write code that will be understood by everyone?


    From the translator: Kamil Lelonek's article on the meaning of code readability and “empathy for programmers” was published for you .

    Have you ever wondered who will view your code? How difficult can it be for others? Tried to determine its readability ??
    “Any fool can write code that will be understood by the machine. But the code, which is clear to people too, is written only by good programmers, ”- Martin Fowler.
    From time to time, when I see some snippets of code, I lose faith in the existence of empathy among programmers. You have to understand what I am talking about - in fact, each of us came across a code that was written just awfully and was practically unreadable.
    Skillbox recommends: A two-year hands-on course "I am a PRO Web Developer . "

    We remind: for all readers of "Habr" - a discount of 10,000 rubles when writing to any Skillbox course on the promotional code "Habr".
    I recently saw something like this:

    defmodule Util.Combinators do
      def then(a, b)do
        fn data -> b.(a.(data)) end
      end
      def a ~> b, do: a |> then(b)
    end

    In principle, everything is fine here: maybe someone just works fantasy or the author of the code has a solid mathematical education. I did not want to rewrite this code, but subconsciously it seemed to me that something was wrong here. “There must be a way to make it better, to formulate differently. I’ll see how everything works, ”I thought. Pretty quickly, I found this:

    import Util.{Reset, Combinators}
    # ...
    conn = conn!()
    Benchee.run(
      # ...
      time: 40,
      warmup: 10,
      inputs: inputs,
      before_scenario: do_reset!(conn) ~> init,
      formatter_options: %{console: %{extended_statistics: true}}
    )

    Hmmm, it looks like not only ~> is imported, but also the conn! / 0 and do_reset! / 1 functions. Ok, let's take a look at theReset module:

    defmodule Util.Reset do
      alias EventStore.{Config, Storage.Initializer}
      def conn! do
        {:ok, conn} = Config.parsed() |> Config.default_postgrex_opts() |> Postgrex.start_link()
         conn
      end
      def do_reset!(conn) do
        fn data ->
          Initializer.reset!(conn)
          data
        end
      end
    end

    As for conn !, there are a couple of ways to make this site easier. Nevertheless, it makes no sense to dwell on this moment. I'd rather focus on do_reset! / 1. This function returns a function that returns an argument and performs a reset for the Initializer; and the name itself is rather complicated.



    I decided to perform reverse engineering code. According to the benchee documentation, before_scenario accepts script input as an argument. The return value becomes the input for the next steps. This is what the author probably meant:

    • Postgrex connection initialization.
    • Reset EventStore.
    • Using input values ​​as configuration items (talking about the number of accounts).
    • Preparing data for tests (i.e. creating users and entering the application).
    • Using benchmarks.

    In general, everything is clear, such code is easy to write. I will note that in refactoring I will not show or change the init function, here it is not very important.

    The first step is to explicitly use aliasing instead of implicit import. I have never liked the “magic” functions that appear in my code, even if Ecto.Query makes the queries elegant. Now our Connection module looks like this:

    defmodule Benchmarks.Util.Connection do
      alias EventStore.{Config, Storage.Initializer}
      def init! do
        with {:ok, conn} =
               Config.parsed()
               |> Config.default_postgrex_opts()
               |> Postgrex.start_link() do
          conn
        end
      end
      def reset!(conn),
        do: Initializer.reset!(conn)
    end

    Then I decided to write a “hook”, as suggested in the documentation:

    before_scenario: fn inputs -> inputs end

    All that is left to do is to prepare the data. The final result is:

    alias Benchmarks.Util.Connection
    conn = Connection.init!()
    # ...
    Benchee.run(
      inputs: inputs,
      before_scenario: fn inputs ->
        Connection.reset!(conn)
        init.(inputs)
      end,
      formatter_options: %{console: %{extended_statistics: true}}
    )
    Connection.reset!(conn)

    Is this code perfect? Probably not yet. But is it easier to understand? I hope so. Could it have been done right away? Definitely yes.

    What is the problem?


    When I proposed a solution to the author, I heard: "Cool." However, I did not expect more.

    The problem is that the primary code worked. The only thing that led me to think about the need for refactoring is too complex code structure and its low readability.

    To convince other developers to make their code readable, you need something convincing. And the argument “I decided to redo your code because it is not very understandable” will not be accepted, the answer will be “it means you are just a bad developer that I can do ¯ \ _ () _ / ¯”.



    This is (not) a management problem.


    No one is surprised that a business expects employee results. And the sooner they are received, the better. Managers usually evaluate software and its writing in terms of deadlines, budget, speed. I'm not saying that this is bad, I just try to explain why a code that is not too high-quality and that “just works” appears. The fact is that managers are not interested in beauty and readability, they need sales, low costs and quick results.

    When they put pressure on programmers, they are looking for a way out. Most often, the solution is to create a “working code” in which there can be a bunch of “crutches”. It is created without the thought that the code needs to be serviced in the future. And the elegant code is very difficult to write quickly. It does not matter how experienced a programmer you are - when you are working under time pressure, no one thinks about beauty.

    This situation can be solved only by convincing managers that a bad (albeit working) code is threatening to increase its maintenance costs in the near future. Correction of bugs and adding new features, reviewing, writing technical documentation - in the case of poor-quality code, all this takes much more time than in a situation when it is elegant and rational.



    The important role of empathy


    If you are developing software, then you understand that it is intended for other people, and development is carried out in a team. And empathy is very important here.

    Your code is a peculiar form of communication. In the process of developing the architecture of the future software you need to think about those who will interact with your code.

    “Empathy programmers” helps to create a cleaner and more rational code, even when deadlines are tight, and the manager constantly “presses”. It helps to understand what it is - to disassemble someone else's unreadable code, which is extremely difficult to understand.



    As output


    I recently wrote code on Elixir:

    result = calculate_results()
    Connection.close(conn)
    result 

    Then I thought about the Ruby tap method, which allows you to rewrite this code:

    calculate_result().tapdo
      Connection.close(conn)end

    IMHO, it would be better to do this without an intermediate result variable. I thought about how this can be done, and came to the following conclusion:

    with result = calculate_results(),
         Connection.close(conn),
      do: result 

    The result will be the same. But using with can make it difficult for someone studying this code, since in the usual case with is used differently.

    Therefore, I decided to leave everything as it was, in order not to do better to myself at the expense of others. I ask you to do the same, without complicating the code beyond measure. Before you enter some kind of exotic function or variable, remember that your colleagues may simply not understand them when reviewing.

    In general, I recommend using the following principle: “When you write YOUR code, think about OTHERS”.

    Also popular now: