What actually matters

Started by CultLeader, August 14, 2021, 08:57:10 AM

Previous topic - Next topic

CultLeader

Today I will talk about what actually matters in software development to understand why we do what we do.

If you want to choose a coding style there are many options available, say Object oriented programming, domain driven development, test driven development, liskov's bullshit principle, choose convention over configuration and so on and so on. How to know what to use? Today, I want to simplify people to think about just one thing basically to get max correctness and solidity from their projects.

And this is the following spectrum below:



Basically, all that actually matters is how many mistakes you can eliminate before your app runs in production. I'm sure most devs would like to find out about issues early not to be sweating in production fixing issues and writing post mortems. It's that simple.

So, all of our decisions in software development, if we want to strive to make correct software, we should want to know that as many things as possible will work before app is running in the wild.

We assume that every bad thing that could happen will happen unless we enforce otherwise. With this mindset, let's evaluate some mistakes that happen during application development and how can we align these decisions with this simple curve.

Using undefined variable (like from typos) in code

Nasty error. Happens so often in dynamically typed languages it is not even funny. Dynamic languages proponents say that this is remedied with tests. But tests are written by humans. Humans make mistakes. Humans may not test all paths of code. And if they do that's a lot of extra work that needs to be done.

How to deal with this problem? Use statically typed language and you will never see this, its that simple. With this solution we slid the divide between compile time and runtime a little to the right (where we would want to).

Incorrectly written SQL queries in code

Can happen in statically typed languages. Either write tests to check every flow that uses the database, or, as I do with the pattern, have all SQL queries defined as data in meta executable, test them in meta executable against test data, and generate typesafe functions to use those queries. Again, we slid the dividing line to the right.

Incorrect REST call from frontend to backend

People use naked json's with ramls, swaggers or dynamic garbage like GraphQL. This will only get you so far, this disallows for interface changes, which encourages legacy interfaces to be maintained. How to never worry about this in production?

Again, what I do with the pattern is have my rest endpoints defined as data in meta executable and generate typesafe code for the frontend to access the REST endpoint and for the backend and I can never make a mistake. Easily slid the dividing line again.

Incorrect html formation for frontend components, like buttons etc.

Dealing with lots of HTML is hard. Doing that with plain Javascript is horrible. You have no idea if things will work just changing plain javascript, you have to test it.

What I do with the pattern, is define an enum in meta executable of a certain button

Meta executable:

let frontend_resource_types : enum_type list = [
  mk_enum_type
    ~enum_prefix:"Button"
    ~type_name:"user_buttons"
    [
      "Logout", FtUserLogin;
      "ChangePasswordShowDialog", FtUserSettings;
      "ChangePasswordComplete", FtUserSettings;
    ];

    ...
]


Generated code:

type user_buttons =
  | ButtonLogout
  | ButtonChangePasswordShowDialog
  | ButtonChangePasswordComplete

let all_user_buttons = [
  ButtonLogout;
  ButtonChangePasswordShowDialog;
  ButtonChangePasswordComplete;
]


Then I have functions that have basic labels on buttons and html generation


let user_button_to_name =
  function
  | ButtonLogout -> "Logout"
  | ButtonChangePasswordShowDialog -> "Change password"
  | ButtonChangePasswordComplete -> "Password change complete"

...

let user_button_to_html
    ?(enabled = true)
    ?(data_id = "")
    ?(custom_callback = _blank_custom_callback)
    (btn: user_buttons) =
    ...
  let default_template () =
    Printf.sprintf {|
      <a %s data-class="%s" data-tag="%s" onclick="callbacks.onButtonClick(this, '%s', '%s')"
        class="waves-effect waves-light btn %s">%s</a>
    |} maybe_id btn_id data_id btn_id data_id disabled_class name
  in
  match btn with
  | ButtonLikeMessage ->
    Printf.sprintf {|
      <a data-class="%s" data-tag="%s" href="#!" onclick="callbacks.onButtonClick(this, '%s', '%s')" class="%s">%s</a>
    |} btn_id data_id btn_id data_id disabled_class name


Now every time I add button in meta executable I have to go pick a label, I copy paste html from other button example, style html any way I want and I also have a place where I have to create implementation of the button after I add it:


let on_button_click (dom_elem: Js_of_ocaml.Dom_html.element Js.t) the_btn the_id = (
    ...
    match the_btn with
    | ButtonLogout -> (
        clear_session_cookies_and_refresh ()
      )


Then anywhere I need that button I just generate it as enum and can never make a mistake:


str_gen_basic (fun append_string ->
    user_button_to_html ButtonLogout |> append_string
  )


And it always works the first time.

Basically, for one abstraction, like button, I dig a tunnel, and I have typesafe code I have to implement for it to work. The sample of buttons I used is smaller than actual project, I use tons of them. And every time I add a new button everything, its html, its labeling, its actions work the first time. I did same with dropdowns, same with restful loading components that render loading animation until server gets queried, user inputs etc. etc. I don't deal with raw html when interacting with buttons, I deal with typesafe enums.

Again, we moved the slider to the right.

Ensure kafka topic is used by someone

In 99.9% software projects today nobody is checking that and everyone hopes and prays that everything works.

What you can do in meta executable, if you have all schemas and topics defined as data, and you generate typesafe interfaces to be implemented, where you allow everyone to interact with the topics if that's the only way topics are used in your project, then it is trivial to test in your meta executable, with few loops through your data, that topic is used nowhere and you can mark queue deleted in data, and it would be executed in your infra.

It's that simple.


Conclusion

So, basically, when any bullshiter comes to you and says "we need to implement so and so bullshit pattern because so and so PhD idiot who never worked on practical problems in his lifetime wrote a whitepaper"

Show them this spectrum and ask the simplest questions:
1. How many possible mistakes this pattern introduces into mistake spectrum
2. Does this pattern move overall slider to the compile time side (like the pattern always does) or does this pattern move it to the left, where there will appear more errors in production?

Very simple, clear, easy to understand, common sense, no need to write whitepapers about it.

Have a nice day folks!