tag:world.hey.com,2005:/jorge/feedJorge Manrubia2024-02-10T12:13:55Ztag:world.hey.com,2005:World::Post/341662024-01-08T00:06:17Z2024-02-10T12:13:55Z2023 X was a better Twitter (for me)<div class="trix-content">
<div>I wouldn't have expected to say this, but X (Twitter) worked well for me in 2023. I got good things out of it. In the previous years, I had a <a href="https://world.hey.com/jorge/silence-48cc695d">tormented</a> <a href="https://world.hey.com/jorge/social-media-evilness-a32be7c7">relationship</a> with <a href="https://world.hey.com/jorge/social-media-evilness-ii-f7f83126">the platform</a>, so, what happened?</div><div><br></div><div>First, Mastodon happened. Elon Musk's acquisition of Twitter provoked a stampede of the most politically radicalized folks to Mastodon. That was a blessing. With the negativity magnets gone, vibes significantly improved because <a href="https://www.amazon.com/Emotional-Contagion-Hardback-Studies-Interaction/dp/0521444985/ref=sr_1_1?__mk_es_ES=%C3%85M%C3%85%C5%BD%C3%95%C3%91&crid=3CKOXV685Z38U&keywords=emotional+contagion&qid=1704670629&sprefix=emotional+contagion%252Caps%252C90&sr=8-1&ufe=app_do%253Aamzn1.fos.5e544547-1f8e-4072-8c08-ed563e39fc7d">energy is contagious</a>. Suddenly, the discussion in the Rails community became about technical stuff again, go figure!</div><div><br></div><div>Second, Twitter stopped <a href="https://telecom.economictimes.indiatimes.com/news/twitter-to-stop-forcing-users-onto-for-you-timeline/97192598">pushing the algorithmically-curated "For you" timeline through your throat</a>. If you select "Following" to see stuff from the people you follow, it will remember the selection. This was a game changer for me, as I got complete control of what entered my timeline.</div><div><br></div><div>Third, I engaged more with the platform. In previous years, I mostly used Twitter as an output channel to link my blog posts. Now, the conversation feels more inviting to participate, so I do.</div><div><br></div><div>Between 2020 and 2022, I felt a tremendous disconnection from the Rails community. I saw haters and <a href="https://world.hey.com/jorge/social-media-evilness-ii-f7f83126">folks being mean towards others</a> dominating the conversation. Now, it feels as if the storm has passed. Here's what I want from X: interesting conversations, positive vibes, no drama/politics, and a focus on Rails and software development. That's my X timeline right now. This seemed absolutely impossible just a bit over one year ago.</div><div><br></div><div>I don't even want to say it too loud.<br><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/337542023-12-23T20:13:10Z2023-12-24T07:15:45ZWhen everyone has a say<div class="trix-content">
<div>I don't think creative processes benefit from democracy — quite the opposite.</div><div><br></div><div>After presenting <a href="https://dev.37signals.com/a-happier-happy-path-in-turbo-with-morphing/">the new Turbo 8 stuff</a> in Rails World, some people opined that Rails needed to improve collaboration in the frontend space. I got a variant of this question in a couple of podcasts, which also happened when we released <a href="https://guides.rubyonrails.org/active_record_encryption.html">Active Record Encryption</a>. The underlying idea is that you should collaborate with other authors in the same space when building new things. Someone is using morphing. Someone else is doing database encryption. Why not reach out and join efforts?</div><div><br></div><div>To me, that's a fallacious idea.</div><div><br></div><div>First, worthy creations have a vision, a soul. You can't build that one vote at a time. That's how you end up <a href="https://www.jorgemanrubia.com/2017/05/09/indexed-db-camels-and-committees/">with a camel when aiming for a horse</a>. That's CORBA, IndexedDB, EJB, or SOAP. That's pain. Instead, you start with an idea for a concrete need, explore it, discuss it with a few people you trust, and, hopefully, land at a solution for the original problem. Then you share. <br><br>If by <em>better collaboration </em>you mean homogenizing criteria, unifying efforts, and getting a synergic combination of each approach, that might sound great, but that's not how software development works.</div><div><br></div><div>Second, open source comes with the ultimate collaboration capabilities built-in. Once the seed is out there, everyone can propose, discuss, get inspired, fork, modify, and contribute. That's how Stimulus got <a href="https://stimulus.hotwired.dev/reference/outlets">outlets</a> or <a href="https://marcoroth.dev/posts/guide-to-custom-turbo-stream-actions">custom Turbo stream actions</a>. That's how <a href="https://github.com/bigskysoftware/idiomorph">Turbo will use a library that originated in htmx</a> or how Laravel will offer <a href="https://github.com/hotwired-laravel/turbo-laravel/tree/cbc273d1d10512ac600b2551118d1436fe1468ab">Turbo 8 using similar helpers to Rails</a>. Those are recent examples of collaboration of the best kind. Not to say we can't improve things, but collaboration is working: Turbo receives a healthy stream of regular contributions that, ironically, seem to have ramped up after <a href="https://world.hey.com/dhh/turbo-8-is-dropping-typescript-70165c01">dropping TypeScript</a>.</div><div><br></div><div>Furthermore, the Rails frontend space includes great alternatives like <a href="https://www.phlex.fun">Phlex</a>, <a href="https://viewcomponent.org">ViewComponent</a>, <a href="https://docs.stimulusreflex.com">StimulusReflex</a> or <a href="https://github.com/hopsoft/turbo_boost-streams">TurboBoost</a>. I am sure they all originated the same way: a tiny crew with a vision building something they needed before sharing it with the world. And that's how it should be. Everyone is free to chime in on whatever project they want. Everyone <a href="https://rubyonrails.org/doctrine#big-tent">can use the library that better fits their taste</a>.<br><br>So, collaboration in the Rails front-end space, for sure! But mind democracy when building things. It can cause more harm than good.</div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/329912023-11-27T13:41:14Z2023-11-27T13:41:14ZDemo of how page refreshes with morphing work<div class="trix-content">
<div><em>This article was originally published in the </em><a href="https://dev.37signals.com/page-refreshes-with-morphing-demo/"><em>37signals dev blog</em></a><em>.<br><br></em>We published a <a href="https://www.youtube.com/watch?v=hKKycPLN-sk">demo showing how Page Refreshes with morphing work in Turbo 8</a>.<br><br></div><div>Comparing code <a href="https://dev.37signals.com/compared-to-what/">helps a lot in software discussions</a>, so I thought it would be valuable to show how the new feature compares to Turbo stream actions for performing partial updates and broadcasts. Notice that page refreshes don’t deprecate stream actions – which remain Turbo’s most responsive mechanism, but they should reduce the need to use those. And this is a good thing because stream actions are costly.<br><br></div><div>There is a <a href="https://github.com/basecamp/turbo-8-morphing-demo">companion GitHub repository for the demo</a>. You can check:<br><br></div><ul><li><a href="https://github.com/basecamp/turbo-8-morphing-demo/pull/3">The demo app code using vanilla Rails</a>.</li><li><a href="https://github.com/basecamp/turbo-8-morphing-demo/pull/6">Modification using stream actions to enhance and broadcast updating tasks</a>.</li><li><a href="https://github.com/basecamp/turbo-8-morphing-demo/pull/4">Modification using page refreshes to enhance and broadcast all the interactions</a>.</li></ul><div><br>Last week, we released <a href="https://github.com/hotwired/turbo/releases/tag/v8.0.0-beta1">the first beta</a> of Turbo 8 featuring this new system. We want to make this robust, so please give it a try and report issues away.</div><div><br>You can learn more about the new feature in the <a href="https://dev.37signals.com/a-happier-happy-path-in-turbo-with-morphing/">announcement post</a>. We will document the new system in the official docs soon.</div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/328022023-11-20T08:46:30Z2023-11-20T08:48:46ZThe radiating programmer<div class="trix-content">
<div><em>This article was originally published in the </em><a href="https://dev.37signals.com/the-radiating-programmer/"><em>37signals dev blog</em></a><em>.<br><br><br></em> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="the-radiating-programmer.png" title="Download the-radiating-programmer.png" data-click-proxy-target="lightbox_link_blob_1439754982" href="https://world.hey.com/jorge/2ece7066/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZtNnRCViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c2887e0a084f859a5399d3cd3e7fc4364dd7c6b6/the-radiating-programmer.png?disposition=attachment">
<img src="https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZtNnRCViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c2887e0a084f859a5399d3cd3e7fc4364dd7c6b6/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/the-radiating-programmer.png" alt="the-radiating-programmer.png" srcset="https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZtNnRCViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c2887e0a084f859a5399d3cd3e7fc4364dd7c6b6/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/the-radiating-programmer.png 2x, https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZtNnRCViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c2887e0a084f859a5399d3cd3e7fc4364dd7c6b6/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/the-radiating-programmer.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>You are an individual contributor at heart. You like writing code and solving technical problems. You dislike meetings and ceremony. Here’s what you can do to maximize what you like and minimize what you don’t: <em>radiate information</em>.<br><br></div><div>The daily standup meetings that Scrum popularized have bad press for a reason. The daily periodicity is way too much. And using a recurring meeting to give people a chance to catch up is atrociously disruptive and inefficient. But the questions themselves make sense: What did you do? What are you going to do? Are there any blockers? You should aim to communicate that information routinely, just not in a meeting.</div><div><br>When building software, everyone involved must adjust what they do as they learn about the problem. And that learning happens — in no small part — through the people doing the job. If they only offer their output, someone else needs to <em>extract</em> those lessons to make adjustments. That’s precisely when the bad kind of <a href="https://world.hey.com/jorge/ceremony-8f47e582">ceremony</a> — the intrusive, disruptive, and lousy — leaks into the process.</div><div><br>It’s indeed by comparison that <em>radiating information</em> shines: instead of having <em>someone</em> <em>pulling information</em> from you, you <em>push</em> the information out there for <em>everyone</em>. It might look subtle, but there is a significant difference: the control remains on your side, not anyone else’s.</div><div><br>One can argue that whenever you communicate, you radiate information. But I am talking about something more intentional here. In my daily job, I find three recurring scenarios:<br><br></div><ul><li>Communicate what you have been up to periodically.</li><li>Communicate progress on projects.</li><li>When making decisions, to give others a chance to intervene without being blocked.</li></ul><div><br>Next, I will show examples from our own <a href="https://basecamp.com/">Basecamp</a>. The underlying idea is not tied to any tool, but if you are familiar with <a href="https://37signals.com/how-we-communicate/">37signals’ communication philosophy</a>, you won’t find it surprising that Basecamp comes with first-class support for it.<br><br></div><h1>What have you worked on?</h1><div>This is the backbone. We use a <a href="https://basecamp.com/features/automatic-check-ins">check-in</a> so that everyone can answer “What have you worked on?” at least twice per week. <a href="https://world.hey.com/jorge/what-did-you-work-on-today-b153fba3">I wrote about this question some time ago</a>. You answer these asynchronously, whenever you want, with the style and level of detail you prefer, and everyone in the company can read everyone else’s answers.<br><br></div><div>Here’s the last answer I wrote:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="what-have-you-worked-on.png" title="Download what-have-you-worked-on.png" data-click-proxy-target="lightbox_link_blob_1439758374" href="https://world.hey.com/jorge/2ece7066/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NtK05CViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--399a02e79bf7eafa67374638e11948fd299cb304/what-have-you-worked-on.png?disposition=attachment">
<img src="https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NtK05CViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--399a02e79bf7eafa67374638e11948fd299cb304/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/what-have-you-worked-on.png" alt="what-have-you-worked-on.png" srcset="https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NtK05CViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--399a02e79bf7eafa67374638e11948fd299cb304/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/what-have-you-worked-on.png 2x, https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NtK05CViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--399a02e79bf7eafa67374638e11948fd299cb304/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/what-have-you-worked-on.png 3x" decoding="async" loading="lazy">
</a>
</figure></div><div><br>I have highlighted three sections to illustrate how others could benefit from the information I shared:<br><br></div><ol><li>An unexpected problem that affected what I could accomplish this week.</li><li>Something I learned that other programmers might find useful.</li><li>Some <a href="https://basecamp.com/shapeup/3.5-chapter-14#scope-hammering">scope hammering</a> that could be interesting to folks making product calls.</li></ol><div><br></div><h1>Communicating progress on projects</h1><div>Tools can help, but it is very hard to get a high-level picture of how a project is progressing unless another human — preferably the one doing the job — helps. And no, I don’t think AI will change that anytime soon.</div><div><br>Our preferred way to show progress in Basecamp is through <a href="https://basecamp.com/features/hill-charts">Hill Charts</a> updates. They combine a high-level completion gauge with a textual description. Here’s the last one I wrote:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--webp">
<a download="hill-chart.webp" title="Download hill-chart.webp" data-click-proxy-target="lightbox_link_blob_1439756410" href="https://world.hey.com/jorge/2ece7066/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2Q2OE5CViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--fe4b591a7d9745c5e774215b088a91c7ad75ffd6/hill-chart.webp?disposition=attachment">
<img src="https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2Q2OE5CViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--fe4b591a7d9745c5e774215b088a91c7ad75ffd6/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lKZDJWaWNBWTZCa1ZVT2hSeVpYTnBlbVZmZEc5ZmJHbHRhWFJiQjJrQ2dBZHBBZ0FGT2d4eGRXRnNhWFI1YVVzNkMyeHZZV1JsY25zR09nbHdZV2RsTURvTlkyOWhiR1Z6WTJWVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--274cde5a3b479746e860a2e73bd31ee3ea7b4021/hill-chart.webp" alt="hill-chart.webp" srcset="https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2Q2OE5CViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--fe4b591a7d9745c5e774215b088a91c7ad75ffd6/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lKZDJWaWNBWTZCa1ZVT2hSeVpYTnBlbVZmZEc5ZmJHbHRhWFJiQjJrQ0FBOXBBZ0FLT2d4eGRXRnNhWFI1YVVFNkMyeHZZV1JsY25zR09nbHdZV2RsTURvTlkyOWhiR1Z6WTJWVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--f5f94436fec7e4885506b934764719976db0e0f6/hill-chart.webp 2x, https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2Q2OE5CViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--fe4b591a7d9745c5e774215b088a91c7ad75ffd6/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lKZDJWaWNBWTZCa1ZVT2hSeVpYTnBlbVZmZEc5ZmJHbHRhWFJiQjJrQ2dCWnBBZ0FQT2d4eGRXRnNhWFI1YVR3NkMyeHZZV1JsY25zR09nbHdZV2RsTURvTlkyOWhiR1Z6WTJWVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0fcc203ac715918f3edbf715a664c78abb9ba78/hill-chart.webp 3x" decoding="async" loading="lazy">
</a>
</figure></div><div><br>Again, different people in the company can get different things from the update:<br><br></div><ol><li>Progress since the last update.</li><li>A problem that resulted in recovering some API we had removed.</li><li>Something we discovered that implies additional work.</li><li>How does this affect, or doesn’t, the Calendar launch?</li></ol><div><br></div><h1><strong>Non-blocking decisions</strong></h1><div>I have written about how <a href="https://world.hey.com/jorge/don-t-block-yourself-a-remote-worker-super-power-7322c679">you should avoid blocking yourself when working remotely</a>. When you need input from others to make a decision, you can often make the decision yourself while informing about it. Others can intervene if they want, or you can move forward if they don’t.</div><div><br>Here’s an example from last week. I had a question about the mobile API I wanted to check with the team. I used my best judgement to make a call and looped them in. I was happy to change the approach based on the answer, but I didn’t put myself in a self-blocking situation waiting for it.<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--webp">
<a download="non-blocking-decisions-2.webp" title="Download non-blocking-decisions-2.webp" data-click-proxy-target="lightbox_link_blob_1439756555" href="https://world.hey.com/jorge/2ece7066/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NMOGRCViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c1b04abf092809945da68b847190360c1cb4d6ec/non-blocking-decisions-2.webp?disposition=attachment">
<img src="https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NMOGRCViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c1b04abf092809945da68b847190360c1cb4d6ec/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lKZDJWaWNBWTZCa1ZVT2hSeVpYTnBlbVZmZEc5ZmJHbHRhWFJiQjJrQ2dBZHBBZ0FGT2d4eGRXRnNhWFI1YVVzNkMyeHZZV1JsY25zR09nbHdZV2RsTURvTlkyOWhiR1Z6WTJWVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--274cde5a3b479746e860a2e73bd31ee3ea7b4021/non-blocking-decisions-2.webp" alt="non-blocking-decisions-2.webp" srcset="https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NMOGRCViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c1b04abf092809945da68b847190360c1cb4d6ec/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lKZDJWaWNBWTZCa1ZVT2hSeVpYTnBlbVZmZEc5ZmJHbHRhWFJiQjJrQ0FBOXBBZ0FLT2d4eGRXRnNhWFI1YVVFNkMyeHZZV1JsY25zR09nbHdZV2RsTURvTlkyOWhiR1Z6WTJWVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--f5f94436fec7e4885506b934764719976db0e0f6/non-blocking-decisions-2.webp 2x, https://world.hey.com/jorge/2ece7066/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NMOGRCViIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c1b04abf092809945da68b847190360c1cb4d6ec/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lKZDJWaWNBWTZCa1ZVT2hSeVpYTnBlbVZmZEc5ZmJHbHRhWFJiQjJrQ2dCWnBBZ0FQT2d4eGRXRnNhWFI1YVR3NkMyeHZZV1JsY25zR09nbHdZV2RsTURvTlkyOWhiR1Z6WTJWVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0fcc203ac715918f3edbf715a664c78abb9ba78/non-blocking-decisions-2.webp 3x" decoding="async" loading="lazy">
</a>
</figure></div><div><br></div><h1><strong>Conclusion</strong></h1><div>Information radiation can look like a bureaucratic practice on the surface, but it is actually a protection against that. Think of the alternatives for the examples I referenced:<br><br></div><ul><li>Catchup meeting to see what’s left for launching Turbo 8.</li><li>Chat mention to someone in the mobile team to synchronously discuss the API response format.</li><li>1-1 with a manager to communicate how the stuff we discovered with Turbo affects the Calendar launch.</li><li>Quick call with a designer to decide whether to implement the feature we decided to nix.</li></ul><div><br>Exaggerated? That’s exactly how work looks in a lot of places. If you need a meeting, make sure it’s not one you could have avoided by radiating information. Think of <em>this meeting could have been an email</em> but with a process where <em>emails </em>flow proactively, by design.</div><div><br>As a programmer, you can add much value by summarizing your work. You can share lessons learned, struggles, unexpected turns, motivations, and, more broadly, how things move forward. No tool or automatic report can do that. Many people can benefit; you can help others and receive help. You favor making decisions based on how reality looks, which is an essential trait for any non-predictable endeavor such as software development. And ultimately, it brings clarity to what you do, which counterweights the intrinsic opacity of spending your days writing code.</div><div><br>Radiating information is not free; it takes time. You need to find a balance that makes sense. I estimate I spend one or two hours per week writing what I did or project updates. I usually leave it for the last part of my day. It doesn’t interrupt my main focused work, and it doesn’t happen every day. It’s also a muscle to train. The more you do it, the easier it gets.</div><div><br>And whenever it feels like a chore, I remind myself that it is a chore that lets me spend most of my time doing what I like.<br><br>---<br><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a></div><div><em><br></em><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/320752023-10-25T07:48:12Z2023-10-25T09:47:57ZTowards smoother page updates in Turbo<div class="trix-content">
<div>I recently published two articles in the <a href="https://dev.37signals.com/">37signals dev blog</a> related to how we will improve page updates in Turbo 8.<br><br></div><ul><li><a href="https://dev.37signals.com/a-happier-happy-path-in-turbo-with-morphing/">A happier happy path in Turbo with morphing</a>. What we will do.</li><li><a href="https://dev.37signals.com/exploring-server-side-diffing-in-turbo/">Exploring server-side diffing in Turbo</a>. An investigation of an alternative approach to fulfill the same vision.</li></ul><div><br>I hope you like them!</div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/314192023-09-26T08:26:06Z2023-09-26T08:28:29ZNavigating personal information with care<div class="trix-content">
<div><em>This article was originally published in the </em><a href="https://dev.37signals.com/navigating-personal-information-with-care/"><em>37signals dev blog</em></a><em>.<br></em><br><br>Accessing personal information from customers is a serious matter. With the launch of HEY in 2020, we developed some technology and processes to support a very simple principle: employees shouldn’t have access — intentionally or unintentionally — to personal information from our users without their explicit consent. In this post, I’ll show what this looks like in practice.<br><br></div><h1><strong>The programmer side</strong></h1><div>A few weeks ago, I was On-call and worked on a tricky HEY support ticket that involved email aliases. After some back and forth, I realized I needed access to some email addresses we stored in the database. Those are encrypted since they are personal information, so I reached out to the customer to ask for access permission:<br><br></div><blockquote>My name is Jorge. I am a programmer in the HEY team. Thanks for your help troubleshooting this one so far. I have one last request: could you please share the link to that test email showing the problem you linked. Also I need your consent to get decrypted access to that email (and only that email). We store everything encrypted, and this would be very helpful for understanding why we aren’t setting the from header correctly.Thanks!</blockquote><div><br>Once the customer replied with affirmative consent, I started a Rails console session to investigate the issue. When starting the session, our system asks for the reason. If it’s an On-call case, there is usually an associated <a href="https://basecamp.com/features/card-table">Card Table</a> card, so we enter a link to it:</div><div><br><img src="https://gopher.hey.com/0x0,q85,sWa8kn-IvjVVnqi8Zr4_JOSpoXwrUxtgCp3oLyjm1CsU=/https://dev.37signals.com/assets/images/navigating-personal-information-with-care/console-1.png" width="300" height="150" loading="lazy" decoding="async"><br><br>By default, in console sessions, personal information is encrypted. This is fine for most problems, and it prevents programmers from seeing personal information unintentionally while troubleshooting. For example, this is what I would see right now if I printed the last Todo created in Basecamp 4:</div><div><br></div><div><img src="https://gopher.hey.com/0x0,q85,szWLS7gUJk27SegooHT_9xyv5Y5iVIvgZBgQwOtBcCus=/https://dev.37signals.com/assets/images/navigating-personal-information-with-care/console-2.png" width="300" height="150" loading="lazy" decoding="async"><br><br>Back to the HEY case, with the explicit customer consent received, I started a decrypted session in the console. It asked me for a link justifying the consent, for which I entered the link to the message from the customer in Help Scout.<br><br><img src="https://gopher.hey.com/0x0,q85,s9wF-9lpEchzfpX5U4dFO1THuaWVWA0SZmRLTjKNUfxY=/https://dev.37signals.com/assets/images/navigating-personal-information-with-care/console-3.png" width="300" height="150" loading="lazy" decoding="async"><br><br></div><h1><strong>The console auditor side</strong></h1><div>All the commands we enter in production Rails consoles are stored and audited later. When those happen in a decrypted session, we flag them as sensitive and receive special attention.<br><br></div><div>At 37signals, the members of the SIP team (Security, Infrastructure, and Performance) take shifts to audit console sessions. In this case, Donal was in charge of doing the audit. This is what he saw in our auditing tool where he validated my console session:</div><div><br><img src="https://gopher.hey.com/0x0,q85,sFBwCkgYXKbRgC5WQni2FswcbVioxpjO9BaAFTHHgdB8=/https://dev.37signals.com/assets/images/navigating-personal-information-with-care/console-audit-screen.png" width="300" height="150" loading="lazy" decoding="async"><br><br>There, Donal could check that I accessed only the relevant email I got consent for and review the commands I ran. Then, he approved the session using the same tool.<br><br><img src="https://gopher.hey.com/0x0,q85,s5PlyItPvoToqnbuceIBtoMZV66qPQBzg592mXCLn3Yw=/https://dev.37signals.com/assets/images/navigating-personal-information-with-care/console-audit-consent.png" width="300" height="150" loading="lazy" decoding="async"><br><br>Every two weeks, console auditors summarize how console audits went in a Basecamp project called SIP: Console user audits. This is <a href="https://public.3.basecamp.com/p/L3MjDx5bUYhDHQHUNVVhgw3N"><strong>the one that Donal wrote</strong></a>. My session appears in the group of three HEY sessions that accessed decrypted data during that period.<br><br><img src="https://gopher.hey.com/0x0,q85,sElltpv4wI8_rxWnDiPkK4vSibyUz20Um57AX-V9YbD8=/https://dev.37signals.com/assets/images/navigating-personal-information-with-care/report-in-basecamp.png" width="300" height="150" loading="lazy" decoding="async"><br><br><br>While the process is quite streamlined now, these summaries have served to drive the development of internal tooling to reduce the need for decrypted sessions.<br><br></div><h1><strong>Everyone can do this</strong></h1><div>We have released all the technology we developed to support this workflow. If you run Rails, you can use the very same tools we do. They are free and open source:<br><br></div><ul><li><a href="https://guides.rubyonrails.org/active_record_encryption.html">The database encryption system</a>, which is part of Rails 7.</li><li><a href="https://github.com/basecamp/console1984">console1984</a>, to protect Rails console sessions and record the commands entered.</li><li><a href="https://github.com/basecamp/audits1984">audits1984</a>, the auditing tool we use to audit console sessions.</li><li><a href="https://github.com/basecamp/mass_encryption">mass_encryption</a>, to encrypt data in mass. We used this tool to <a href="https://updates.37signals.com/post/new-new-home-screen-the-lineup-doors-and-more">encrypt billions of records in Basecamp 4</a>.</li></ul><div><br>***<br><br>Everyone agrees that passwords or secret access tokens require special attention and should never appear in logs or be easily accessible by anyone. We believe that personal information deserves the same consideration. And so does the private content users create using your software.<br><br></div><div>Without your consent, we can’t see the message you wrote in Basecamp when you report a bug about it. And this is a big relief for us: it’s none of our business.<br><br></div><div>I hope this article encourages you to embrace a similar approach. The tools we released answer the technical needs, so you only need organizational commitment. I guarantee it won’t take long before you start feeling that the previous way of working was off.</div><div><br></div><div>---<br><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/309682023-08-31T07:18:56Z2023-08-31T07:20:55ZMinding the small stuff in pull request reviews<div class="trix-content">
<div><em>This article was originally published in the </em><a href="https://dev.37signals.com/minding-the-small-stuff-in-pr-reviews/"><em>37signals dev blog</em></a><em>.<br></em><br><br><em>Nitpicking</em> in pull requests reviews means offering insight that looks excessive, pedantic, and unimportant. It’s a pejorative term. I don’t question there are cases where the term applies, but many folks assimilate it to paying attention to the <em>small stuff</em>. By projecting all these negative connotations, this practice suddenly becomes undesirable: mind the big picture or leave the pull request alone. With that, we disagree. <em>Small stuff</em> when writing code matters, a lot.</div><div><br></div><h1><strong>Names matter</strong></h1><div>Naming things is a usual suspect when illustrating nitpicking. It’s also the example I have the hardest time swallowing. With the importance of names to make code understandable, how can anyone consider discussing those superfluous?<br><br></div><div>Check what <a href="https://github.com/packagethief">Jeffrey Hardy</a> told me in this recent pull request:<br><br><img src="https://gopher.hey.com/0x0,q85,sfIQidm7EQVilengda0_krXf04QbtPTYlcgWw81rbXLI=/https://dev.37signals.com/assets/images/minding-the-small-stuff-in-pr-reviews/review-1.png" width="300" height="150" loading="lazy" decoding="async"><br><br>Or what I told <a href="https://github.com/matthutchinson">Matt Hutchinson</a> in another one:<br><br><img src="https://gopher.hey.com/0x0,q85,sncBWOtPseOwQZea5b9-QRYZXAXlDhPuLqROYcrx6mwg=/https://dev.37signals.com/assets/images/minding-the-small-stuff-in-pr-reviews/review-2.png" width="300" height="150" loading="lazy" decoding="async"><br><br></div><h1><strong>Code style matters</strong></h1><div>Great code aesthetics is a distinctive trait of Ruby, and one of the <a href="https://rubyonrails.org/doctrine#beautiful-code">Rails pillars</a>. Compare:</div><div><br>In Javascript:<br><br></div><pre><strong>if </strong>(<strong>this</strong>.hasParamTarget) { <strong>this</strong>.paramTarget.value <strong>=</strong> id }</pre><div><br>In Ruby:<br><br></div><pre>param_target.<strong>value</strong> <strong>=</strong> id <strong>if</strong> has_param_target?</pre><div><br>In Python:<br><br></div><pre>datetime.<strong>now</strong>() <strong>-</strong> <strong>timedelta</strong>(days<strong>=</strong>5)</pre><div><br>In Rails:<br><br></div><pre>5.<strong>days</strong>.<strong>ago</strong></pre><div><br>Does this attention to aesthetics make you a happier programmer? And this is not a rhetorical question. Tons of outstanding programmers couldn’t care less about this stuff. But if you do, paying attention to code style and aesthetics in pull request is logical and desirable. This includes suggesting code that could be formatted more cleanly, minor logic simplifications, or slightly more idiomatic alternatives. Precisely the kind of stuff that is often labeled as <em>nitpicking</em>.</div><div><br>Now, check these examples:</div><div><br>Jeff points out a conditional that could be formatted more cleanly:</div><div><br></div><div><img src="https://gopher.hey.com/0x0,q85,sxzDxQsdD96hEBTkJu9eDtLNJ_tnksb5Xn6xvsv_hbxU=/https://dev.37signals.com/assets/images/minding-the-small-stuff-in-pr-reviews/review-3.png" width="300" height="150" loading="lazy" decoding="async"><br><br>And, in the same review, he suggests a more idiomatic way of using <em>enum</em>:<br><br><img src="https://gopher.hey.com/0x0,q85,siVd2Iss-CO3fg_lEsrwGKJEUur97_n5RccfijdH9w_I=/https://dev.37signals.com/assets/images/minding-the-small-stuff-in-pr-reviews/review-4.png" width="300" height="150" loading="lazy" decoding="async"><br><br>Here, I suggest Jeff to use <em>.ids</em> instead of <em>pluck(:id)</em>. I learned about this one in another pull request review by <a href="https://github.com/lewispb">Lewis Buckley</a> back in the day. Nitpicking is transitive.<br><br><img src="https://gopher.hey.com/0x0,q85,suBqQ_VfFO3qYmi_44ghXhE7Xz6_X1K5ztc3V-ld1OG8=/https://dev.37signals.com/assets/images/minding-the-small-stuff-in-pr-reviews/review-5.png" width="300" height="150" loading="lazy" decoding="async"><br><br>In this other example, <a href="https://dhh.dk/">David</a> suggests me some minor simplification of logic:<br><br><img src="https://gopher.hey.com/0x0,q85,sf9dq4GvQAG6o8KKizRJIc9zsOQdFqtV0ksTZZF4ECBo=/https://dev.37signals.com/assets/images/minding-the-small-stuff-in-pr-reviews/review-6.png" width="300" height="150" loading="lazy" decoding="async"><br><br>Here, Jeff suggests me to get rid of a guard clause:<br><br><img src="https://gopher.hey.com/0x0,q85,s64Oyw_KucSRJDEpvVb9AXj6kWLud_7YlzCv3zM3djKA=/https://dev.37signals.com/assets/images/minding-the-small-stuff-in-pr-reviews/review-7.png" width="300" height="150" loading="lazy" decoding="async"><br><br>And I suggest something similar to Matt here:<br><br><img src="https://gopher.hey.com/0x0,q85,sCQBNjkEhIsP5yHdYd-PFC2ao_MYedlMB_m34wiQi-xc=/https://dev.37signals.com/assets/images/minding-the-small-stuff-in-pr-reviews/review-8.png" width="300" height="150" loading="lazy" decoding="async"><br><br></div><h1><strong>Consistency matters</strong></h1><div>Consistency helps to make code easier to understand and, thus, more maintainable. In other words, it’s priceless. Consistency is elusive, though, and very hard to achieve in large codebases as time passes and different people work on them. By paying attention to the big picture you may achieve consistency at the architectural level, and by using linters, you can expect consistency at the code-formatting level. But what about the very thick middle ground? This is where coding standards live.</div><div><br>Thorough pull requests that pay attention to the <em>minutia</em> are essential to enforce coding standards and transmit them through a team. For sure that documentation can help, and that nothing replaces skills and experience, but I can’t imagine how a codebase can keep a high level of consistency unless eyes very familiar with the in-house style review changes with a magnifier.</div><div><br>A good example is the preference to avoid guard clauses unless they save you from a long sequence ahead – as showed in the previous section. Whether you agree with a guideline or not, the value of conventions resides in having them in the first place. They help with consistency; and the most interesting and juicy ones can’t be validated automatically.</div><div><br></div><h1><strong>Conclusions</strong></h1><div>To consider the small detail in code as unimportant and even pedantic reminds me of the vilified <em>architect</em> role in our industry: the one that doesn’t really code, just cares about architectural concerns and patterns, the stuff that matters. The problems with this role are apparent to most but, at the same time, many <a href="https://blog.danlew.net/2021/02/23/stop-nitpicking-in-code-reviews/">teams</a><strong> </strong><a href="https://www.reddit.com/r/cscareerquestions/comments/58n84u/how_do_you_handle_nitpicky_codereviewers/">consider</a> <a href="https://www.steveonstuff.com/2022/02/09/nitpicky-code-reviews-are-are-drag">nitpicking</a> <a href="https://blog.joetr.com/avoid-nitpicking-in-code-reviews">something</a> to <a href="https://www.mattlayman.com/blog/2017/no-nitpicking-code-reviews/">avoid</a>.<br><br></div><div>The big picture matters, for sure, but computers don’t execute big pictures, they execute lines of code, so how you compose those matters too. And code-aesthetics lover or not, everyone benefits from consistency.</div><div><br>I just had to check a few recent pull requests I was involved in to find many examples to pick from. Isolated and out of context, any of those suggestions is objectively trivial. But their aggregation is not. This approach is part of our development culture. This is how new people learn the in-house coding style, how we keep a high-quality bar in our code, and how we favor consistency.</div><div><br>I have ended up assimilating <em>nitpicking</em> in code reviews with being meticulous, thorough, and rigorous. I encourage you to review the threshold beyond which you consider some review advice as nitpicking. Maybe it is not aligned with what you value as a programmer.</div><div><br></div><div>---<br><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a><br><br></div><div><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/306842023-08-11T11:10:12Z2023-08-12T11:07:12ZDifficult and complex<div class="trix-content">
<div>There is <em>complex</em> and there is <em>difficult</em>, they are not the same. </div><div><br></div><div>Being able to run a marathon is very difficult. But what you have to do to prepare is simple. A small sheet of paper is enough to describe the steps. You will need tremendous effort and discipline - so it’s hard - but it is simple.</div><div><br></div><div>Software development is different: it is also complex.</div><div><br></div><div>A common theme in <a href="https://dev.37signals.com/series/code-i-like/">my articles on how we do Rails at 37signals</a> is how facing software development with rigid recipes is naive and counterproductive. A good example is services: using a service for everything is a simple idea; applying <a href="https://dev.37signals.com/domain-driven-boldness/">domain-driven design</a> and <a href="https://dev.37signals.com/vanilla-rails-is-plenty/">object orientation</a> <a href="https://dev.37signals.com/fractal-journeys/">is not</a>. Another manifestation is banning techniques because they can be harmful: <a href="https://dev.37signals.com/good-concerns/">concerns</a>, <a href="https://dev.37signals.com/active-record-nice-and-blended/">accessing the database from models</a>, <a href="https://dev.37signals.com/globals-callbacks-and-other-sacrileges/">callbacks, globals</a>, etc. Ironically, the alternative critics usually recommend for those is using services.</div><div><br></div><div>In software, you must take a Babylonian body of knowledge, combine it with your own experience, and apply the result to make countless daily decisions. And of all this for <a href="https://world.hey.com/jorge/erase-and-rewind-2ff46b55">a problem you can’t understand without solving it first</a>. Rigidity doesn’t work, and – because this is a game of intricate complexity – it yields more harm than benefit. You need to embrace nuance.</div><div><br></div><div>Mind the recipes you follow.</div><div><br><br>---<br><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a><br><br><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/305122023-08-01T07:18:20Z2023-08-01T07:19:50ZGlobals, callbacks and other sacrileges<div class="trix-content">
<div><em>This article was originally published in the </em><a href="https://dev.37signals.com/globals-callbacks-and-other-sacrileges/"><em>37signals dev blog</em></a><em>.<br></em><br><br><a href="https://rubyonrails.org/doctrine#no-one-paradigm">Sacrificing purity for convenience is one of the Rails pillars</a>. This principle informs several Rails features that some recommend avoiding but that we happily use in our apps. In this post, I’ll discuss how we use three of those in <a href="https://basecamp.com/">Basecamp</a>.<br><br></div><div>I’ll show some examples involving projects. In Basecamp, a <em>Bucket</em> represents a top-level container of data, and there are several types of buckets, one of which is a Project. Internally, we implement this with <a href="https://api.rubyonrails.org/classes/ActiveRecord/DelegatedType.html">delegated type</a> attribute, where a <em>Bucket</em> has an associated <em>Bucketable</em>. A Bucket can contain many <em>Events</em>, and an <em>Event</em> is a holder for information on the related request, such as the IP or the transaction id (<em>Event::Request</em>) and some additional information (<em>Event::Detail</em>).</div><div><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-08-01 at 09.18.53@2x.png" title="Download CleanShot 2023-08-01 at 09.18.53@2x.png" data-click-proxy-target="lightbox_link_blob_1309555702" href="https://world.hey.com/jorge/d9832c35/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2YyT3c1TyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--4ab7d06f3d845b2692f05bfaaee944187585be01/CleanShot%202023-08-01%20at%2009.18.53@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/d9832c35/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2YyT3c1TyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--4ab7d06f3d845b2692f05bfaaee944187585be01/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-08-01%20at%2009.18.53@2x.png" alt="CleanShot 2023-08-01 at 09.18.53@2x.png" srcset="https://world.hey.com/jorge/d9832c35/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2YyT3c1TyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--4ab7d06f3d845b2692f05bfaaee944187585be01/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-08-01%20at%2009.18.53@2x.png 2x, https://world.hey.com/jorge/d9832c35/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2YyT3c1TyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--4ab7d06f3d845b2692f05bfaaee944187585be01/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-08-01%20at%2009.18.53@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br></div><h1><strong>Callbacks</strong></h1><div>A project exists associated with the bucket that contains it. This is the relevant code that creates projects in Basecamp’s <em>ProjectsController</em>:<br><br></div><pre><strong>class</strong> <strong>ProjectsController</strong> <strong><</strong> ApplicationController
<strong>def</strong> <strong>create</strong>
@template ? create_from_template : create_without_template
<strong>end</strong>
<strong>private</strong>
<strong>def</strong> <strong>create_without_template</strong>
@project <strong>=</strong> Current.<strong>account</strong>.<strong>projects</strong>.<strong>create!</strong> create_project_params
<em># ...</em>
<strong>end</strong>
<strong>end</strong></pre><div><br>As you can see, the controller just creates a <em>Project</em> record. There is no reference to the associated bucket or anything related to tracking events or the project’s creator.<br><br></div><div>Let’s start with the <em>Bucket</em>. How is created? A <a href="https://guides.rubyonrails.org/active_record_callbacks.html">callback</a> in the <em>Bucketable</em> concern takes care of that:<br><br>This shows a common scenario for callbacks: hook additional bits of logic into the lifecycle of objects. I will show a more juicy example later, but this is good enough to discuss the pros and cons.<br><br></div><div>A common critique of callbacks is that they bring indirection, making code difficult to follow. And, indeed, you don’t want to orchestrate complex flows using callbacks. But, here, the operation is quite simple – create a bucket along its project if it does not exist – and this is not a primary Project responsibility either, so the indirection is a good fit: you’re plugging in a secondary function in a declarative way. Callbacks work great in such scenarios.<br><br></div><div>This approach lets us create any <em>buckletable</em> without caring about its companion bucket. Assuming you don’t want to place such responsibility on the caller side, the alternative here would be to encapsulate this logic in some factory. Let me show you the contrast.<br><br></div><div>In Basecamp, elements such as todos, messages, or calendar events are recordings. A <em>Recording</em> hosts a <a href="https://api.rubyonrails.org/classes/ActiveRecord/DelegatedType.html">delegated type</a> relationship with the specific underlying entity (<em>Todo</em>, <em>Message</em>, etc.). Some day we’ll explain this pattern in detail, but – for the purpose of this article – it happens that creating recordings is a more involved operation. In this case, we use a factory because the complexity justifies the benefits of a class encapsulating the details away. We expose that factory through <em>Bucket</em>, which is a container for recordings:</div><div><br><br></div><pre><strong>class</strong> <strong>Bucket</strong> <strong><</strong> ApplicationRecord
<strong>def</strong> <strong>record</strong>(<strong>...</strong>)
Recorder.<strong>new</strong>(self).<strong>record</strong>(<strong>...</strong>)
<strong>end</strong>
<strong>end</strong>
<strong>class</strong> <strong>Bucket::Recorder</strong>
<strong>def</strong> <strong>initialize</strong>(bucket)
@bucket <strong>=</strong> bucket
<strong>end</strong>
<strong>def</strong> <strong>record</strong>(recordable, children: <strong>nil</strong>, parent: <strong>nil</strong>, position: <strong>nil</strong>, scheduled_posting_at: <strong>nil</strong>, <strong>**</strong>options)
<em># ...</em>
<strong>end</strong>
<strong>end</strong></pre><div><br>We create all the recordings in Basecamp through this factory. For example, this is the controller that creates messages in Basecamp:<br><br></div><pre><strong>class</strong> <strong>MessagesController</strong> <strong><</strong> ApplicationController
<strong>def</strong> <strong>create</strong>
@recording <strong>=</strong> @bucket.<strong>record</strong> new_message,
parent: @parent_recording,
category: find_category,
subscribers: find_subscribers,
status: status_param,
visible_to_clients: visible_to_clients_param,
scheduled_posting_at: scheduled_posting_at_param
<em># ...</em>
<strong>end</strong>
<strong>end</strong></pre><div><br>While the factory is justified, the tradeoff is that we now have to use it whenever we want to create a recording. It’s a new dependency and element you need to know of. It makes things a bit more complex. Using a callback for creating buckets, we can create projects seamlessly as regular records. Different choices with different tradeoffs for different scenarios.<br><br></div><h1>CurrentAttributes</h1><div>When Rails introduced <a href="https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html">a thread-safe attributes singleton</a> – another extraction from <a href="https://basecamp.com/">Basecamp</a> – there was no shortage of detractors. Sharing global variables between controllers and models? Seriously? Seriously.<br><br></div><div>CurrentAttributes offered an alternative to the <em>#current_user</em> method most apps used to access the currently authenticated user. You could now do <em>Current.user</em>, and use this pattern to access similar request-level attributes uniformly. But let’s focus on the controversial aspect of having models accessing such global attributes.<br><br></div><div>Our full <em>Current</em> class in Basecamp is:<br><br></div><pre><strong>class</strong> <strong>Current</strong> <strong><</strong> ActiveSupport<strong>::</strong>CurrentAttributes
attribute :account, :person
attribute :http_method, :request_id, :user_agent, :ip_address, :referrer
delegate :user, :integration, to: :person, allow_nil: <strong>true</strong>
delegate :signal_identity, to: :user, allow_nil: <strong>true</strong>
resets { Time.<strong>zone</strong> <strong>=</strong> <strong>nil</strong> }
<strong>def</strong> <strong>person=</strong>(person)
<strong>super</strong>
self.<strong>account</strong> <strong>=</strong> person.<strong>try</strong>(:account)
Time.<strong>zone</strong> <strong>=</strong> person.<strong>try</strong>(:time_zone)
<strong>end</strong>
<strong>end</strong></pre><div><br>At the controller level, our authentication system sets <em>Current.person</em> as the currently authenticated <em>Person</em>. You can access that person as <em>Current.person</em> from the execution context of controllers handling authenticated requests.<br><br></div><div>This is how we associate a project with the authenticated person that creates it in Basecamp.</div><div><br></div><pre><strong>class</strong> <strong>Project</strong> <strong><</strong> ApplicationRecord
belongs_to :creator, class_name: "Person", default: <strong>-></strong> { Current.<strong>person</strong> }
<strong>end</strong></pre><div><br>The alternative here would be to pass the creator in the controller invocation. For example:<br><br></div><pre>@project <strong>=</strong> Current.<strong>account</strong>.<strong>projects</strong>.<strong>create!</strong> create_project_params.<strong>merge</strong>(creator: Current.<strong>user)</strong></pre><div><br>Here, we prefer the callback form. It captures quite succinctly that, when creating a project, a <em>creator</em> <em>Person</em> is mandatory and that, if not provided, it will be the currently authenticated person. Notice that the creator of a project doesn’t contain the project, structurally speaking. It’s more like an audit trait unrelated to what’s involved in creating a project, so it’s good to discharge the controller from knowing about it. The declarative callback approach with <em>Current.user</em> captures this whole idea in a way that an explicit controller invocation doesn’t.</div><div><br>We use this pattern in many places for tracking who creates records in Basecamp and HEY.</div><div><br></div><h1><strong>Callbacks + CurrentAttributes</strong></h1><div>Tracking changes and request-level information for certain domain entities is handy at many levels. For example, to debug issues reported by customers. I’ll show how we track events for buckets next; we use a similar system for recordings in Basecamp and a very similar approach in HEY.<br><br></div><div>First, at the controller level, we collect several essential request information in <em>Current</em>.<br><br></div><pre><strong>class</strong> <strong>ApplicationController</strong> <strong><</strong> ActionController<strong>::</strong>Base
<strong>include</strong> SetCurrentRequestDetails
<strong>end</strong>
<strong>module</strong> SetCurrentRequestDetails
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
included <strong>do</strong>
before_action <strong>do</strong>
Current.<strong>http_method</strong> <strong>=</strong> request.<strong>method</strong>
Current.<strong>request_id</strong> <strong>=</strong> request.<strong>uuid</strong>
Current.<strong>user_agent</strong> <strong>=</strong> request.<strong>user_agent</strong>
Current.<strong>ip_address</strong> <strong>=</strong> request.<strong>ip</strong>
Current.<strong>referrer</strong> <strong>=</strong> request.<strong>referrer</strong>
<strong>end</strong>
<strong>end</strong>
<strong>end</strong></pre><div><br>The <a href="https://dev.37signals.com/good-concerns/">concern</a> <em>Bucket::Eventable</em> contains the relevant code for handling events. It defines the relationship with Event, and uses an <em>after_create</em> callback to create an event when a new <em>Bucket</em> is created. Notice how the <em>creator:</em> param in <em>#track_event</em> defaults to <em>Current.person</em>.<br><br></div><pre><strong>class</strong> <strong>Bucket</strong> <strong><</strong> ApplicationRecord
<strong>include</strong> Eventable
<strong>end</strong>
<strong>module</strong> Bucket::Eventable
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
included <strong>do</strong>
has_many :events, dependent: :destroy
after_create :track_created
<strong>end</strong>
<strong>def</strong> <strong>track_event</strong>(action, creator: Current.<strong>person</strong>, <strong>**</strong>particulars)
Event.<strong>create!</strong> bucket: self, creator: creator, action: action, detail: Event<strong>::</strong>Detail.<strong>new</strong>(particulars)
<strong>end</strong>
<strong>private</strong>
<strong>def</strong> <strong>track_created</strong>
track_event :created
<strong>end</strong>
<strong>end</strong>
<br></pre><div><br>Creating an <em>Event</em> will auto-build an <em>Event::Request</em> that will, finally, grab the request details from <em>Current</em>. Here is the relevant code:<br><br></div><pre><strong>class</strong> <strong>Event</strong> <strong><</strong> ApplicationRecord
<strong>include</strong> Requested
<strong>end</strong>
<strong>module</strong> Event::Requested
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
included <strong>do</strong>
has_one :request, dependent: :delete, required: <strong>true</strong>
before_validation :build_request, on: :create
<strong>end</strong>
<strong>end</strong>
<strong>class</strong> <strong>Event::Request</strong> <strong><</strong> ApplicationRecord
belongs_to :event
before_create :set_from_current
<strong>private</strong>
<strong>def</strong> <strong>set_from_current</strong>
self.<strong>guid</strong> <strong>||=</strong> Current.<strong>request_id</strong>
self.<strong>user_agent</strong> <strong>||=</strong> Current.<strong>user_agent</strong>
self.<strong>ip_address</strong> <strong>||=</strong> Current.<strong>ip_address</strong>
self.<strong>email_message_id</strong> <strong>||=</strong> Current.<strong>email_message_id</strong>
<strong>end</strong>
<strong>end</strong>
<br></pre><div><br>On the tactical side, the combination of callbacks with <em>CurrentAttributes </em>achieves something powerful: you can create a project normally, and the operation gets seamlessly audited.<br><br></div><pre>@project <strong>=</strong> Current.<strong>account</strong>.<strong>projects</strong>.<strong>create!</strong> create_project_params</pre><div><br>Think about the alternative code where you must carry the request information from the controller layer to <em>Event::Request</em>, the last internal piece of the auditing system. You would need, again, a place to contain the creation logic, like a service or factory, and you would need to prepare all the parts to push down the request information. The resulting code could look something like:<br><br></div><pre><strong>class</strong> <strong>ProjectsController</strong> <strong><</strong> ApplicationController
<strong>def</strong> <strong>create</strong>
ProjectRecorder.<strong>new</strong>(account).<strong>record</strong>(create_project_params, Current.<strong>person</strong>, request)
<strong>end</strong>
<strong>end</strong>
<strong>class</strong> <strong>ProjectRecorder</strong>
<strong>def</strong> <strong>initialize</strong>(account)
@account <strong>=</strong> account
<strong>end</strong>
<strong>def</strong> <strong>record</strong>(params, creator, request)
Project.<strong>transaction</strong> <strong>do</strong>
@account.<strong>projects</strong>.<strong>create!</strong>(params, request).<strong>tap</strong> <strong>do</strong> <strong>|</strong>project<strong>|</strong>
project.<strong>bucket</strong>.<strong>events</strong>.<strong>create!</strong> creator: creator, action: :create, request: Event<strong>::</strong>Request.<strong>create_from</strong>(request)
<strong>end</strong>
<strong>end</strong>
<strong>end</strong>
<strong>end</strong>
<br></pre><div><br>I think this approach is worse, and for reasons that go beyond the mere aesthetics of it. An auditing concern is orthogonal to creating projects. Even if you need one, a factory that creates projects should care about about how to compose the structural parts of those. Auditing doesn’t sound like a factory responsibility.</div><div><br>By mixing an orthogonal concern into the main path, you are coupling parts that don’t belong together. This is a scenario where indirection actually helps. And callbacks provide precisely an answer to this need.</div><div><br>Back in the day, a paradigm called <a href="https://en.wikipedia.org/wiki/Aspect-oriented_programming">Aspect Oriented Programming</a> (AoP) gained some traction in academic circles. Rails’ callbacks offer pragmatic support for AoP ideas. If you want to see another example, check <a href="https://www.youtube.com/watch?v=m1jOWu7woKM">this video by DHH about how Basecamp uses callbacks to detect message mentions</a>. Same idea: extracting mentions is orthogonal to posting messages; callbacks offer direct support for expressing this without polluting the primary responsibilities of Message as a domain entity.<br><br></div><h1><strong>Suppress</strong></h1><div><br>Rails introduced <a href="https://api.rubyonrails.org/classes/ActiveRecord/Suppressor.html">support for suppressing ActiveRecord save operations in arbitrary blocks</a>. As you would expect, the idea of creating some local context that hijacked persistence meant to be used with callbacks got <em>some</em> pushback.<br><br></div><div>Here’s an example where we use this mechanism. In Basecamp, you can copy any element to a new destination. Internally, this complex system involves several pieces, including a <em>Recording::Copier</em> class in charge of copying trees of recordings. When copying a recording, we don’t want the regular event tracking system to trigger, so we suppress it:<br><br></div><pre><strong>class</strong> <strong>Recording::Copier</strong>
<em># ...</em>
<strong>private</strong>
<strong>def</strong> <strong>copy_recording</strong>
Event.<strong>suppress</strong> <strong>do</strong>
@destination_recording <strong>=</strong> destination_bucket.<strong>record</strong>(source_recording.<strong>recordable</strong>, parent: destination_parent, <strong>**</strong>copyable_attributes)
<strong>end</strong>
<strong>end</strong>
<strong>end</strong>
<br></pre><div><br>The key to using .suppress is exceptionality. You normally want the default behavior to kick in – tracking events in this case. Exceptionally, you want to suppress it. This mechanism allows you to do it without altering the original system to accommodate the conditional logic.</div><div><br>This aligns greatly with callbacks-powered systems that implement orthogonal concerns. As discussed, indirection is a feature for those. .suppress represents a succinct mechanism to introduce conditionality while keeping that indirection in place.</div><div><br></div><h1>Conclusions</h1><div>There is a strong subjectivity component in these discussions. The same code can look fantastic to some and terrible to others. But here’s something objective: we use the discussed techniques in our applications, which <a href="https://updates.37signals.com/">we actively maintain</a> while millions of people use them.</div><div><br>Maximalist positions are a thing in our industry. Take a technique, outline its drawbacks, extrapolate that you can’t use it under any circumstance, and ban it forever. We are lucky that Rails embraces <a href="https://rubyonrails.org/doctrine#provide-sharp-knives">exactly the opposite mindset</a> as one if its pillars.</div><div><br>And I think we are lucky because software development is so complex that it benefits from nuance. When balancing convenience and purity, there is a rigidity threshold that causes more harm than good. The advice of the kind <em>never-ever do this, always do that</em> should put you on alert. Software development is a game of tradeoffs, and any choice you make comes with them. You should at least understand the tradeoffs you are accepting when you reject techniques on behalf of paradigm purity.</div><div><br>There is no question that callbacks, CurrentAttributes and .suppress are <a href="https://rubyonrails.org/doctrine#provide-sharp-knives">sharp knives</a>. You can put yourself in a deep hole if you misuse or abuse them. But, in our experience, there are scenarios where they are <a href="https://dev.37signals.com/compared-to-what/">the best alternative at hand</a>, and they don’t cause any maintenance burden. So please, consider this post as a counterweight whenever you hear that using one of these Rails features will make hell break loose.<br><br>---<br><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a><br><em>This article belongs to a series of posts on Rails design techniques called </em><a href="https://dev.37signals.com/series/code-i-like/"><em>Code I like</em></a>.</div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/301942023-07-10T15:20:49Z2023-07-10T15:20:49ZLet people be<div class="trix-content">
<ul><li>Have side-projects / don’t have them.</li><li>Do open source / don’t do it.</li><li>Have tech-related hobbies / have completely unrelated ones.</li><li>Have children / don’t have them. </li><li>Work long hours / never put in an extra hour.</li><li>Small and risky / large and secure.</li><li>Work for others / work for yourself.</li><li>Work out of passion / work to pay the bills. </li></ul><div><br></div><div>Those are examples of dichotomies that enter my radar from time to time. Usually, someone criticizes someone else’s position after performing some <a href="https://world.hey.com/jorge/social-media-evilness-ii-f7f83126">intellectual judo</a> to vilify it.</div><div><br></div><div>Whenever I see people telling others how to live, I can’t help wondering: isn’t figuring out what works for you already difficult enough?</div><div><br></div><div>Let people be.</div><div><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/300532023-07-02T15:43:40Z2023-07-03T09:42:10ZSocial media evilness (II)<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="mike-yukhtenko-wfh8dDlNFOk-unsplash.jpg" title="Download mike-yukhtenko-wfh8dDlNFOk-unsplash.jpg" data-click-proxy-target="lightbox_link_blob_1276108122" href="https://world.hey.com/jorge/f7f83126/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RhM1E5TSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--d7d7ce9a3b4a039ad88bc38d027bf2f8a173fd9e/mike-yukhtenko-wfh8dDlNFOk-unsplash.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/f7f83126/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RhM1E5TSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--d7d7ce9a3b4a039ad88bc38d027bf2f8a173fd9e/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/mike-yukhtenko-wfh8dDlNFOk-unsplash.jpg" alt="mike-yukhtenko-wfh8dDlNFOk-unsplash.jpg" srcset="https://world.hey.com/jorge/f7f83126/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RhM1E5TSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--d7d7ce9a3b4a039ad88bc38d027bf2f8a173fd9e/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/mike-yukhtenko-wfh8dDlNFOk-unsplash.jpg 2x, https://world.hey.com/jorge/f7f83126/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RhM1E5TSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--d7d7ce9a3b4a039ad88bc38d027bf2f8a173fd9e/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/mike-yukhtenko-wfh8dDlNFOk-unsplash.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>Back in the day, I wrote some notes on the puzzling question of <a href="https://world.hey.com/jorge/social-media-evilness-a32be7c7">why so many people act so maliciously toward others on social media</a>. A <a href="https://www.amazon.com/Social-Warming-Dangerous-Polarising-Effects-ebook/dp/B095PV2X3M">book</a> introduced me to the concept of <a href="https://www.nature.com/articles/s41562-017-0213-3"><em>moral outrage</em></a>, which brought some answers to my questions.</div><div><br></div><div>A recent <a href="https://twitter.com/jonathanshedler/status/1667950409787527168?s=61&t=AJQ9jt2h7CAKWcUDyFfuow">tweet by Dr. Jonathan Shedler</a> referred an <a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4359608">article</a> that suggests a pretty different angle: the primary driver for people participating in public shaming is not justice but primarily hedonic – it makes participants feel good. A sense of justice plays a role, though: it increases the pleasure of witnessing the offender receiving punishment. I also learned a term for this form of malicious pleasure at another's misfortune: <a href="https://en.wikipedia.org/wiki/Schadenfreude"><em>schadenfreude</em></a>. This article states that the main driver for online shaming is <em>schadenfreude</em>.</div><div><br></div><div>As a completely ignorant regarding social psychology, I can't assess how well-conducted the three studies cited in the article were or how solid their conclusions are. But this article resonated with my anecdotal observation of these public shaming episodes. </div><div><br></div><div>I can't think of a more <a href="https://world.hey.com/jorge/energy-fd7c84d8">energy-draining</a> activity than spending your days insulting and canceling others. My Occam's razor explanation for such people was that they somehow got their energy that way. Imagine my interest – and confirmation bias – when I found a scientific paper articulating precisely this.</div><div><br></div><div>You can argue that any victim can attack an aggressor in self-defense, which is obviously fair. The problem is when you have to perform several intellectual jumps to assign the victim and aggressor roles. Then you enter a slippery slope: anyone can qualify as a victim with the right to shame whoever disagrees with their beliefs, and, in the same way, anyone can become an aggressor deserving an exemplary punishment. You just need the right intellectual contortions. The irony is that, not so long ago, people used to make fun of <a href="https://en.wikipedia.org/wiki/Godwin%27s_law">such illogicality in online discussions</a>.<br> <br>Feelings are personal and unquestionable, but everyone is responsible for their actions. Considering your feelings a wildcard to treat others awfully is a manifestation of narcissism which, again, is aligned with what the article states.</div><div><br></div><div>If moral outrage was the only reason for public shaming, the outlook would be more optimistic. After all, we can fight our tribal instincts with education and civility. But this article puts a more concerning ingredient on the table: some cruel people thrive with an excuse to project their bitterness and resentment towards others, so they do. I still believe civility will play a significant factor in isolating such actors and dramatically reducing their reach. Hopefully, we'll see that happening in the coming years.<br><br>If you are interested, I recommend you to check this <a href="https://twitter.com/JonathanShedler/status/1441450827546169347">thread</a> by Dr. Jonathan Shedler with a deeper psychological dive on this subject. Some food for thought there:<br><br></div><blockquote>The most toxic and hateful people in the world are 100% convinced they fight for what is true and right.</blockquote><div><br><br>---<br><a href="https://www.jorgemanrubia.com">jorgemanrubia.com</a><br><em>Photo by </em><a href="https://unsplash.com/@yamaicle?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Mike Yukhtenko</em></a><em> on </em><a href="https://unsplash.com/wallpapers/colors/dark?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Unsplash</em></a><em><br></em><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/296242023-06-12T02:37:39Z2023-06-12T03:41:59ZRemote struggles<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="alex-padurariu-VxtWBOQjGdI-unsplash.jpg" title="Download alex-padurariu-VxtWBOQjGdI-unsplash.jpg" data-click-proxy-target="lightbox_link_blob_1251605944" href="https://world.hey.com/jorge/43c4ee20/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U0L1psSyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--22821561fec9279fec2513eb6caf9ce177d17654/alex-padurariu-VxtWBOQjGdI-unsplash.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/43c4ee20/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U0L1psSyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--22821561fec9279fec2513eb6caf9ce177d17654/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/alex-padurariu-VxtWBOQjGdI-unsplash.jpg" alt="alex-padurariu-VxtWBOQjGdI-unsplash.jpg" srcset="https://world.hey.com/jorge/43c4ee20/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U0L1psSyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--22821561fec9279fec2513eb6caf9ce177d17654/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/alex-padurariu-VxtWBOQjGdI-unsplash.jpg 2x, https://world.hey.com/jorge/43c4ee20/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U0L1psSyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--22821561fec9279fec2513eb6caf9ce177d17654/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/alex-padurariu-VxtWBOQjGdI-unsplash.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>I've worked fully remotely for the last ten years. I find the discussion about remote work fascinating. Is remote better or a lesser evil? Is a decision full of tradeoffs? Something that works in some circumstances, but that is not a good default? </div><div><br></div><div>As with every complex subject, people – me included – love to reduce it to a dichotomy. A binary choice where you can either be right or wrong. I'm in the camp of remote-work-is-the-best, but I can't deny that many folks and companies thrive with on-site work. Plenty of evidence backing up any side you want to take here.</div><div><br></div><div>What always surprises me is that most assume that existing companies have a choice. Generally speaking, I don't think they do. If a company obtaining good results isn't working remotely already, it will likely fail to switch. The bigger the company, the greater the challenge, but I'd say the problem remains for small shops or even startups that operate on-site early on.</div><div><br></div><div>When putting any substantial change in motion, organizational gravity is a thing, but I think the problem is more profound here. Any company that functions is an incredibly complex system, and communication is what glues everything together. After somehow finding a way of communicating that works, going remote means replacing this magical glue with a wholly new and different one¹. That is a hell of a challenge.</div><div><br></div><div>When I hear of a company like Apple going remote, I imagine someone trying to transform a house into a boat, all without disrupting the people living inside. I'm sure it's doable, but <a href="https://fortune.com/2023/03/24/remote-work-3-days-apple-discipline-terminates-tracks-tim-cook/">the odds aren't great</a>. Corporate titans are certainly extreme cases, but I don't think the challenge is substantially different for smaller companies. Overhauling how a company works while keeping the operation going is quite a feat. I can understand how many companies will fail to do that.</div><div><br></div><div>I believe this is the real reason behind the recent wave of companies returning to the office. I disagree with <a href="https://fortune.com/2023/05/05/openai-ceo-sam-altman-remote-work-mistake-return-to-office/">the conclusion most seem to reach</a>, though. The failed experiment wasn't remote work but rewiring your company from the ground up. That makes for a less clickable headline, though, and is probably a harder pill to swallow for the people running the show.</div><div><br></div><div>I've been lucky enough to work for two very successful fully-remote companies during the last decade. For both, remote was never an afterthought but a foundational ingredient. I think remote work discussions don't emphasize this enough: making an on-site company remote is a tremendous challenge for any established company because it requires a restatement of how communication happens. I would keep this present when extracting conclusions from either success or failure stories.</div><div><br></div><div>And just because a binary take is necessary for these discussions: remote work rules, always 🤘!</div><div><br>***<br><br>¹<a href="https://37signals.com/how-we-communicate/">This is the best piece I know</a> on how to communicate effectively in a remote environment, and, equally important, <a href="https://37signals.com/group-chat-problems/">this is the how not to counterpart</a>. <br><br><a href="https://www.jorgemanrubia.com">jorgemanrubia.com</a><br><em>Photo by </em><a href="https://unsplash.com/@alexpadurariu?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Alex Padurariu</em></a><em> on </em><a href="https://unsplash.com/photos/VxtWBOQjGdI?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Unsplash</em></a></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/294602023-06-04T06:14:28Z2023-06-04T06:44:18ZErase and rewind<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="closeup-computer-keyboard.jpg" title="Download closeup-computer-keyboard.jpg" data-click-proxy-target="lightbox_link_blob_1243021448" href="https://world.hey.com/jorge/2ff46b55/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VJQUJkSyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--d8668a77ee6c586c4d0353738469e8d8e08054f3/closeup-computer-keyboard.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/2ff46b55/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VJQUJkSyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--d8668a77ee6c586c4d0353738469e8d8e08054f3/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/closeup-computer-keyboard.jpg" alt="closeup-computer-keyboard.jpg" srcset="https://world.hey.com/jorge/2ff46b55/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VJQUJkSyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--d8668a77ee6c586c4d0353738469e8d8e08054f3/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/closeup-computer-keyboard.jpg 2x, https://world.hey.com/jorge/2ff46b55/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VJQUJkSyIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--d8668a77ee6c586c4d0353738469e8d8e08054f3/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/closeup-computer-keyboard.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>Imagine a problem you can't understand without solving it first. The first time I saw software development described as a <a href="http://jeffsutherland.com/2002/05/agile-software-development-wicked.html"><em>wicked problem</em></a> was in <a href="https://www.amazon.com/Complete-Microsoft-Programming-Steve-McConnell/dp/1556154844">Code Complete</a> by Steve McConnell, and I haven't seen anything but a constant validation of this proposition throughout my career. And it's a paradoxical one with several counterintuitive corollaries. Here's one: if you don't understand what you need to build, you should start building it. And here's another: sweating to build something just to hit the delete button is sometimes part of the process.</div><div><br></div><div>Writing and making code work is costly; thinking and discussing things in the abstract is cheap. Everything holds up on paper, but computers are picky. I don’t believe that LLMs – as revolutionary as they are – will change this imbalance soon. Spending time building something that ends in the trash seems wasteful. How much time I could have saved if I had thought of this or that first!</div><div><br></div><div>This is a natural but flawed way of looking at things. After all, if building the right software was a matter of putting enough upfront thinking, we could make the whole thing predictable, which is <a href="https://www.martinfowler.com/articles/newMethodology.html">an idea the industry abandoned more than two decades ago</a>.</div><div><br></div><div>I think up-front thinking, designing, and discussing are good and necessary; you just need to understand their place and value. With complex systems, validating whether an idea is good, feasible, and worthy in the abstract is impossible. Too much upfront discussion with too many people can even be counterproductive. Because things are not concrete, it is easy to end up with a blurry list of interpretations, opinions, and hypotheses. All of them can be valid and well-thought. And that is precisely the problem I referred to: everything holds up on paper.</div><div><br></div><div>Now, when you write some actual code in a prototype or proof of concept, everything lands in the realm of concrete. Suddenly, you understand the problem, how costly it is, what alternatives you have, and the tradeoffs of each. Other people can see things working; opinions, questions, and answers become real. Contributing value to the discussion becomes way easier. Everyone wins.</div><div><br></div><div>Time is finite, though. The best favor you can do yourself with exploratory projects is to <a href="https://world.hey.com/jorge/timeboxing-away-f5324439">timebox them</a>. If you are thinking of dropping the code you wrote, the odds are that the problem is complex and uncertain. In such scenarios, you need protection to avoid an endless rabbit hole. Remember that you are validating ideas; set a deadline for the validation and <a href="https://basecamp.com/shapeup/3.5-chapter-14#scope-hammering">scope-hammer</a> your way through it.</div><div><br></div><div>I started last week by deleting thousands of lines for a prototype I had built for an exciting idea we’re exploring, and I didn’t feel any remorse —quite the opposite. The prototype allowed us to test the idea in different scenarios, discuss its flaws, get key feedback, and clarify what we wanted. What I built didn’t pan out, but it was very valuable to me. After it, I felt we finally knew what we wanted to build. It brought clarity.</div><div><br>If you have to drop code you sweated to write, don’t console yourself with, “<em>at least I learned something</em>”. That’s your subconscious wanting to make software development predictive. Instead, cheers with “<em>I finally understood!</em>”.<br><br>Software development is a wicked problem. Ignore at your own peril.<br><br>***</div><div><br><a href="https://www.jorgemanrubia.com">jorgemanrubia.com</a><br><a href="https://www.freepik.com/free-photo/closeup-computer-keyboard_2768428.htm#query=delete%20key&position=0&from_view=keyword&track=ais"><em>Image by rawpixel.com</em></a><em> on Freepik<br></em><br><br><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/284052023-04-23T18:49:48Z2023-04-23T19:36:05ZEnergy<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="roland-larsson-k9e4KXs6AGQ-unsplash.jpg" title="Download roland-larsson-k9e4KXs6AGQ-unsplash.jpg" data-click-proxy-target="lightbox_link_blob_1195817846" href="https://world.hey.com/jorge/fd7c84d8/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2QydTBaSCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--41658909d6b1af01da17efa7774cd803ffabfb99/roland-larsson-k9e4KXs6AGQ-unsplash.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/fd7c84d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2QydTBaSCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--41658909d6b1af01da17efa7774cd803ffabfb99/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/roland-larsson-k9e4KXs6AGQ-unsplash.jpg" alt="roland-larsson-k9e4KXs6AGQ-unsplash.jpg" srcset="https://world.hey.com/jorge/fd7c84d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2QydTBaSCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--41658909d6b1af01da17efa7774cd803ffabfb99/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/roland-larsson-k9e4KXs6AGQ-unsplash.jpg 2x, https://world.hey.com/jorge/fd7c84d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2QydTBaSCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--41658909d6b1af01da17efa7774cd803ffabfb99/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/roland-larsson-k9e4KXs6AGQ-unsplash.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>Internal energy is tricky. </div><div><br></div><div>On the one hand, energy is more reliable than happiness. I liked this reflection by <a href="https://youtu.be/__RAXBLt1iM">Jocko Willink in the Huberman podacast</a>. Everyone experiences joy and sadness as part of their daily lives: keeping a certain energy level is more feasible than a certain level of happiness.</div><div><br></div><div>On the other hand, as with anything related to habits, it's way easier to preach than practice. Many years ago, someone I love dearly told me that he tried to do <em>more of what he liked and less of what he didn't</em>. I remember thinking, "Doh, what a naive approach to life". Now I think there is deep wisdom there. Your internal energy is the result of a balance. You can have more money by either making more or spending less; you need to pay attention to both.</div><div><br></div><div>In my case, I know pretty well what energizes me. Spending time with my beloved ones, exercising, learning and building things, reading, or walking on the beach are simple activities that raise my energy levels. Except for building things, which I'm lucky to do for a living, I haven't had nearly enough of the others in the last months. As I said, easier said than done.</div><div><br></div><div>I recently learned in <a href="https://www.amazon.com/Emotional-Contagion-Studies-Emotion-Interaction/dp/0521449480">Emotional contagion</a> that introverts are way more likely to be affected by others' emotions than extroverts.</div><div><br></div><blockquote><em>…One important implication of this constitutional difference is that introverts are easier to condition than extraverts… Extraverts should also be better to resit other’s emotions.</em></blockquote><div><br></div><div>As a hard-core introvert myself, this bit resonated. Good to be mindful about the impact of interacting with <a href="https://world.hey.com/jorge/social-media-evilness-a32be7c7">negative content</a> — or negative people — on your energy levels. Those can drain your energy without you even noticing they are the culprit. </div><div><br></div><div>Finally, there is the <em>internal engaging</em> component. When you experience something that triggers you, how do you react internally? I've naturally sucked at this. Even if it is rare for me to publicly engage with things I disagree with, I tend to ruminate over them, which can be really energy-draining. Yes, <a href="https://xkcd.com/386/">someone is wrong on the Internet</a> and all that. Letting things go and saving the fire for that one percent of worthy internal battles is a superpower regarding your energy levels.</div><div><br></div><div>Be aware of your energy credit and debit. And, like with most of my writing, this advice is mostly for myself.<br><br>---<br><a href="https://www.jorgemanrubia.com">jorgemanrubia.com</a><br><em>Photo by </em><a href="https://unsplash.com/@becomingsilence?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Roland Larsson</em></a><em> on </em><a href="https://unsplash.com/photos/k9e4KXs6AGQ?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Unsplash</em></a></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/269092023-03-02T09:22:10Z2023-03-02T09:22:52ZPending tests<div class="trix-content">
<div>I recently started working on a new thing at 37signals. We have a blank slate in front of us, and nothing is set in stone, which means we are moving fast. I find myself creating meaty pull requests every day. This is how every single pull request I open ends:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="pending-tests.png" title="Download pending-tests.png" data-click-proxy-target="lightbox_link_blob_1136070169" href="https://world.hey.com/jorge/425a1da6/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NaRHJkRCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f929f96a92070136371abb1cb816e37dfad6f3cc/pending-tests.png?disposition=attachment">
<img src="https://world.hey.com/jorge/425a1da6/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NaRHJkRCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f929f96a92070136371abb1cb816e37dfad6f3cc/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/pending-tests.png" alt="pending-tests.png" srcset="https://world.hey.com/jorge/425a1da6/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NaRHJkRCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f929f96a92070136371abb1cb816e37dfad6f3cc/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/pending-tests.png 2x, https://world.hey.com/jorge/425a1da6/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NaRHJkRCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f929f96a92070136371abb1cb816e37dfad6f3cc/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/pending-tests.png 3x" decoding="async" loading="lazy">
</a>
</figure></div><div><br></div><div>This is just a reflection of how, most of the time, I write tests at the end. An exception is when a test offers me the shortest feedback loop, which, in my experience, happens more frequently in infrastructure work than in product work. But even then, I don’t look for tests to help me design systems. I don’t practice TDD (Test-driven development).</div><div><br></div><div>I am well-versed in TDD and what it has to offer. I fully embraced the paradigm when it exploded back in the day and I stopped using it at some point, first with a sense of guilt, then <a href="https://dhh.dk/2014/tdd-is-dead-long-live-testing.html/">with a sense of relief</a>.</div><div><br></div><div>There is something I like about TDD: it encourages observing a system from the outside, as a black box that hides complexity and offers an intelligible interface. As I’ve written in the past, I think <a href="https://dev.37signals.com/fractal-journeys">this is a key design principle you need to apply at every level of abstraction</a>, from outside your app boundaries until the last internal method it contains. I don’t think you need TDD to do this at all, but thinking of interfaces from their consumer’s point of view is positive.</div><div><br></div><div>On the negative side, TDD encourages a testing style I am very wary of: building very small and fast tests by mocking slow dependencies out. In my experience, this testing approach has a bad cost/benefit: it’s both expensive and ineffective when it comes to <a href="https://stackoverflow.com/questions/153234/how-deep-are-your-unit-tests/153565#153565/">reaching a given level of confidence</a>. Years ago, I wrote <a href="https://www.jorgemanrubia.com/2018/05/19/on-rails-testing/">some thoughts about my testing preferences</a>. I can sum them up as <em>test the real thing</em> as much as possible.</div><div><br></div><div>Another danger of TDD is the companion belief that the emphasis on building blocks that can be tested independently produces well-designed systems. I can buy that a well-designed system must be – to some extent — testable, but I don’t think the opposite is true or that every single part must be testable in isolation. You can build a terrible design out of perfectly testable units with a thousand tests that executes in less than a second. Then, there is the concern of making every module testable without dependencies, which comes with a tax to pay in terms of making those injectable and indirection. I believe you can build great or terrible designs with or without TDD; I just don’t see the solid cause-effect relationship many defend here.</div><div><br></div><div>But more than anything, what I like the least about TDD is the lack of pragmatism you often see associated with it. It’s rarely presented as a tool to use under certain circumstances but as a design technique that should drive how you build software. This letter of introduction, combined with the ceremony TDD brings, is a magnet for dogmatism and for all the negative things that come with it.</div><div><br></div><div>TDD is a topic that produces strong reactions from both practitioners and detractors. I never do TDD, but I know it works well for many. As long as it doesn’t smell like dogma or is used as a weapon to attack others’ professionalism, I have no problems with it. I just see it as a tool I don’t use.</div><div><br>---<br><em>This article was originally published in the </em><a href="https://dev.37signals.com/pending-tests/"><em>37signals dev blog</em></a><em>.</em></div><div>jorgemanrubia.com</div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/267732023-02-26T22:06:07Z2023-02-27T08:01:41ZSensations<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="yoann-boyer-i14h2xyPr18-unsplash.jpg" title="Download yoann-boyer-i14h2xyPr18-unsplash.jpg" data-click-proxy-target="lightbox_link_blob_1131861081" href="https://world.hey.com/jorge/c2085547/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RaMUhaRCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f9770dc2163212116a16af33522c327e93a2e555/yoann-boyer-i14h2xyPr18-unsplash.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/c2085547/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RaMUhaRCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f9770dc2163212116a16af33522c327e93a2e555/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/yoann-boyer-i14h2xyPr18-unsplash.jpg" alt="yoann-boyer-i14h2xyPr18-unsplash.jpg" srcset="https://world.hey.com/jorge/c2085547/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RaMUhaRCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f9770dc2163212116a16af33522c327e93a2e555/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/yoann-boyer-i14h2xyPr18-unsplash.jpg 2x, https://world.hey.com/jorge/c2085547/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RaMUhaRCIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f9770dc2163212116a16af33522c327e93a2e555/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/yoann-boyer-i14h2xyPr18-unsplash.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>I’m fascinated by the <em>sensations</em> that the things we use induce in us. Not their usefulness, suitability, ergonomics, or aesthetics, but sensations. I don’t have a better term to capture something as substantial as difficult to describe, but I can illustrate with some very personal sensation-driven opinions:</div><div><br></div><ul><li>Zelda Breath of the Wild’s open world from 2017 feels way more immersive than the technically stunning one in Horizon Forbidden West 2, a next-gen game from 2022.</li><li>Ruby and Python are two programming languages very similar in features and intent, but the sensations they produce in me are entirely different. Python does the job. Ruby puts a smile on my face. </li><li>Regarding personal task managers, I always go back to <a href="https://www.taskpaper.com/">Taskpaper</a>. I miss basic stuff such as support for rich text and images all the time, but I love the feeling of writing text with the right amount of todo goodies. It feels just right.</li><li>For note management, I gave a serious shot to <a href="https://www.craft.do">Craft</a> last year, but I eventually went back to <a href="https://bear.app">Bear</a>. Craft is excellent, but Bear’s editor and the overall environment felt like home to me, and I could never shake that feeling off.</li><li>I’m a sucker for markdown, editors, and self-publishing pipelines. I had a sweet combo in place with my site, but when I started using <a href="https://www.hey.com/world/">Hey World</a>, I began to write way more. I have published my favorite pieces using the very simple composer of HEY, sending an email to the world. <a href="https://world.hey.com/jorge/what-do-we-want-people-to-feel-e80d0a5a">A matter of how it makes me feel?</a></li></ul><div><br>It might seem that you can rationalize sensations, but you really can't. Building the right sensations requires the right high-level intent combined with getting a thousands of low-level details right. You need synergy – sorry for the dreaded enterprisey word. And the more complex the system, the more things to get right. I can't even imagine the work implied in getting a modern $200M video game feel right or the stress of not managing to do it.<br><br></div><div>Sensations are never part of the distilled list of features we love when comparing products, and yet, they are the essential factor that makes something stand out.<br><br>--<br><a href="https://www.jorgemanrubia.com">jorgemanrubia.com</a><br><em>Photo by </em><a href="https://unsplash.com/@yoannboyer?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Yoann Boyer</em></a><em> on </em><a href="https://unsplash.com/photos/i14h2xyPr18?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Unsplash</em></a><em><br></em><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/256372023-01-22T22:56:29Z2023-01-23T23:49:06ZWriting is thinking<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="lilartsy-333oj7zFsdg-unsplash.jpg" title="Download lilartsy-333oj7zFsdg-unsplash.jpg" data-click-proxy-target="lightbox_link_blob_1091346372" href="https://world.hey.com/jorge/95dee8cb/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZFbnd4QiIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--17cd8cc9a7fe7147e280225f39e6bba7a7c42769/lilartsy-333oj7zFsdg-unsplash.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/95dee8cb/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZFbnd4QiIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--17cd8cc9a7fe7147e280225f39e6bba7a7c42769/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/lilartsy-333oj7zFsdg-unsplash.jpg" alt="lilartsy-333oj7zFsdg-unsplash.jpg" srcset="https://world.hey.com/jorge/95dee8cb/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZFbnd4QiIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--17cd8cc9a7fe7147e280225f39e6bba7a7c42769/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/lilartsy-333oj7zFsdg-unsplash.jpg 2x, https://world.hey.com/jorge/95dee8cb/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZFbnd4QiIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--17cd8cc9a7fe7147e280225f39e6bba7a7c42769/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/lilartsy-333oj7zFsdg-unsplash.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br><a href="https://world.hey.com/jorge/writing-for-yourself-c25a5a13">You don’t need an audience</a> to put writing work for yourself:<br><br></div><ul><li>Writing can let you access things you know but can’t access deliberately. This is a powerful and utterly counter-intuitive insight.</li><li>Writing down assorted ideas as they pop up in your mind will bring all the benefits you are looking for here. In my experience, formalities such as templates or formal notations are counterproductive when it comes to thinking.</li><li>Text is the best design tool for approaching a problem when uncertain. Once you have learned as much as possible about the problem, try writing down a high-level description of the alternatives for yourself</li><li>When it comes to modeling software, compared to plain English, every other notation usage is marginal in practice.</li><li>When stuck debugging a tricky problem, write a note articulating it. <a href="https://en.wikipedia.org/wiki/Rubber_duck_debugging">Rubber duck debugging</a> is a thing, after all.</li><li>An organization that favors long-form writing favors thinking by design. </li><li>If something bugs you, write all your thoughts about it down. More often than not, this brings an immediate relief. </li><li>Authenticity concerns aside, a powerful reason to NOT use ChatGPT to write stuff on your behalf is that it will prevent you from thinking. </li></ul><div><br>---<br><em>Photo by </em><a href="https://unsplash.com/@lilartsy?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>lilartsy</em></a><em> on </em><a href="https://unsplash.com/photos/333oj7zFsdg?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Unsplash</em></a><em><br></em><a href="https://jorgemanrubia.com">jorgemanrubia.com</a></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/254452023-01-18T15:36:43Z2023-01-18T15:51:44ZThe best presenter ever<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="image.png" title="Download image.png" data-click-proxy-target="lightbox_link_blob_1086702563" href="https://world.hey.com/jorge/b4a920a7/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZqdzhWQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--1327f2db9a8b3cf450593e3ab496cf43187f171a/image.png?disposition=attachment">
<img src="https://world.hey.com/jorge/b4a920a7/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZqdzhWQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--1327f2db9a8b3cf450593e3ab496cf43187f171a/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/image.png" alt="image.png" srcset="https://world.hey.com/jorge/b4a920a7/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZqdzhWQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--1327f2db9a8b3cf450593e3ab496cf43187f171a/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/image.png 2x, https://world.hey.com/jorge/b4a920a7/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2ZqdzhWQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--1327f2db9a8b3cf450593e3ab496cf43187f171a/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/image.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>I learned about <a href="https://www.bostonphil.org/about/benjamin-zander">Benjamin Zander</a> in 2011. I was reading <a href="https://www.duarte.com/resources/books/resonate/">Resonate</a> by Nancy Duarte, and she used <a href="https://www.youtube.com/watch?v=r9LCwI5iErE&t=5s">his Ted Talk from 2008</a> as a case study in one of the chapters, to illustrate several concepts on how to deliver presentations. Nancy Duarte knows one thing or two about presenting, so her description of him as a "master communicator" caught my attention. I recommend you watch <a href="https://www.youtube.com/watch?v=r9LCwI5iErE&t=5s">the presentation</a> if you haven't.</div><div><br></div><div>I have watched that talk several times over the years. It makes me feel good. I find it entertaining, educative, and touching. His premise – recurring in his presentations – is that anyone can understand and appreciate classical music. I would identify myself as <em>tone-deaf</em> – something Benjamin says doesn't exist – so that message resonated with me, it sounded hopeful.</div><div><br></div><div>I didn't take action for many years, but I watched several more presentations by him. Something he always tries to show is the connection between emotions and music. For example, in <a href="https://www.youtube.com/watch?v=hZIC2f0fMKY">this video</a> he observes a great pianist perform and tries to coach her by talking about emotions and about what the original composer intended. In <a href="https://www.youtube.com/watch?v=rB_G7CkPDes&t=775s">this other video</a>, he does the same with a master violinist. As a totally ignorant person, I won't pretend I appreciate everything going there at the musical level, but I find the whole process both fascinating and mesmerizing. </div><div><br></div><div>The thing I love the most about his presentations is his contagious optimism. Humans are emotional magnets, and Benjamin's vibes are fantastic, so it's a pleasure exposing yourself to those. He describes his mindset as the <em>art of possibility</em> – which is, in his words, quite different from positive thinking. I recently learned that he co-authored <a href="https://www.amazon.com/Art-Possibility-Transforming-Professional-Personal/dp/0142001104">a book</a> with that same title, which is now next in my reading queue.</div><div><br></div><div>I'm currently enjoying a sabbatical month at work. This looked like an excellent opportunity to scratch the itch I've had for years, so I <a href="https://twitter.com/jorgemanru/status/1615115400873758720">got myself a digital piano</a>, and I'm starting to learn how to play it. No idea how this will end, but I am doing it because of these presentations, so thanks, Benjamin.<br><br>---<br><em>Picture from </em><a href="https://www.bostonphil.org/about/benjamin-zander"><em>bostonph.org</em></a><br><a href="https://www.jorgemanrubia.com">jorgemanrubia.com</a></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/253762023-01-16T07:21:56Z2023-01-16T07:21:57ZThe augmented programmer<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="IMG_8433.jpg" title="Download IMG_8433.jpg" data-click-proxy-target="lightbox_link_blob_1083650973" href="https://world.hey.com/jorge/d848711a/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VkTTVkQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--35ad4f2398d28f6690f1134a6fd5e52cca5a24b5/IMG_8433.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VkTTVkQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--35ad4f2398d28f6690f1134a6fd5e52cca5a24b5/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/IMG_8433.jpg" alt="IMG_8433.jpg" srcset="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VkTTVkQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--35ad4f2398d28f6690f1134a6fd5e52cca5a24b5/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/IMG_8433.jpg 2x, https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VkTTVkQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--35ad4f2398d28f6690f1134a6fd5e52cca5a24b5/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/IMG_8433.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>If you are a programmer and have tried <a href="https://openai.com/blog/chatgpt/">ChatGPT</a>, you have probably realized it’s a landscape-changing technology. The history of programming is a history of raising the abstraction level, and with all the cautions and disclaimers you want, the jump that ChatGTP represents is monumental.</div><div><br></div><div>Right after college, I got a research grant to explore <a href="https://en.wikipedia.org/wiki/Model-driven_engineering">Model Driven Engineering</a>. I was intrigued by the idea of creating software based on high-level models and domain-specific languages. Eventually, I realized my research line was, in practical terms, a dead end, and my interest dried out. I remember this question by Martin Fowler from <a href="https://www.martinfowler.com/articles/newMethodology.html">this article</a> strongly resonated with what I was seeing:<br><br></div><blockquote>But here lies the crucial question. Can you get a design that is capable of turning the coding into a predictable construction activity? And if so, is cost of doing this sufficiently small to make this approach worthwhile?</blockquote><div><br></div><div>Last week, I wanted to create a Python script to split a large CSV file into smaller ones, with a slight transformation of the dates in the process. <a href="https://gist.github.com/jorgemanrubia/2abe273acbdcb31da30cde44f295cff0">This is what I entered in Chat GPT</a>:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-01-15 at 16.48.42@2x.png" title="Download CleanShot 2023-01-15 at 16.48.42@2x.png" data-click-proxy-target="lightbox_link_blob_1083630346" href="https://world.hey.com/jorge/d848711a/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NLNDVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--bc71a16ebee79c1d475e1a18c115c886ce42a67f/CleanShot%202023-01-15%20at%2016.48.42@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NLNDVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--bc71a16ebee79c1d475e1a18c115c886ce42a67f/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-01-15%20at%2016.48.42@2x.png" alt="CleanShot 2023-01-15 at 16.48.42@2x.png" srcset="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NLNDVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--bc71a16ebee79c1d475e1a18c115c886ce42a67f/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-01-15%20at%2016.48.42@2x.png 2x, https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NLNDVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--bc71a16ebee79c1d475e1a18c115c886ce42a67f/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-01-15%20at%2016.48.42@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br>And ChatGPT nicely complied:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-01-15 at 16.49.04@2x.png" title="Download CleanShot 2023-01-15 at 16.49.04@2x.png" data-click-proxy-target="lightbox_link_blob_1083632117" href="https://world.hey.com/jorge/d848711a/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2YxNlpaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--5cf211bbadd3af80da7a119656586f40c1a2f806/CleanShot%202023-01-15%20at%2016.49.04@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2YxNlpaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--5cf211bbadd3af80da7a119656586f40c1a2f806/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-01-15%20at%2016.49.04@2x.png" alt="CleanShot 2023-01-15 at 16.49.04@2x.png" srcset="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2YxNlpaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--5cf211bbadd3af80da7a119656586f40c1a2f806/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-01-15%20at%2016.49.04@2x.png 2x, https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2YxNlpaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--5cf211bbadd3af80da7a119656586f40c1a2f806/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-01-15%20at%2016.49.04@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>As a Python newbie myself, I was curious about doing this without pandas, so I <a href="https://gist.github.com/jorgemanrubia/a2abf7d79a4a4ba31ba833631d1ee292">asked</a>:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-01-15 at 17.03.19@2x.png" title="Download CleanShot 2023-01-15 at 17.03.19@2x.png" data-click-proxy-target="lightbox_link_blob_1083632247" href="https://world.hey.com/jorge/d848711a/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2QzNnBaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--22138f17ddc74387756b4222e58d1b403f887f34/CleanShot%202023-01-15%20at%2017.03.19@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2QzNnBaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--22138f17ddc74387756b4222e58d1b403f887f34/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-01-15%20at%2017.03.19@2x.png" alt="CleanShot 2023-01-15 at 17.03.19@2x.png" srcset="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2QzNnBaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--22138f17ddc74387756b4222e58d1b403f887f34/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-01-15%20at%2017.03.19@2x.png 2x, https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2QzNnBaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--22138f17ddc74387756b4222e58d1b403f887f34/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-01-15%20at%2017.03.19@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>And ChatGPT delivered:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-01-15 at 17.04.05@2x.png" title="Download CleanShot 2023-01-15 at 17.04.05@2x.png" data-click-proxy-target="lightbox_link_blob_1083632313" href="https://world.hey.com/jorge/d848711a/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U1NnBaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--bf40b6a0c1664fe53b6e3f9fd4f048c265980955/CleanShot%202023-01-15%20at%2017.04.05@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U1NnBaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--bf40b6a0c1664fe53b6e3f9fd4f048c265980955/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-01-15%20at%2017.04.05@2x.png" alt="CleanShot 2023-01-15 at 17.04.05@2x.png" srcset="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U1NnBaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--bf40b6a0c1664fe53b6e3f9fd4f048c265980955/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-01-15%20at%2017.04.05@2x.png 2x, https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U1NnBaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--bf40b6a0c1664fe53b6e3f9fd4f048c265980955/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-01-15%20at%2017.04.05@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>That looked like a great starting point for what I wanted. I adapted the code a bit and got it working in no time.</div><div><br></div><div>Then, I wanted a little bash script to keep only the most recent subdirectories in a given folder that I kept populating with some tests. <a href="https://gist.github.com/jorgemanrubia/3f6c0706a8fb22f3f66d53ac2dbca3ae">This was the interaction</a>:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-01-15 at 17.17.28@2x.png" title="Download CleanShot 2023-01-15 at 17.17.28@2x.png" data-click-proxy-target="lightbox_link_blob_1083632459" href="https://world.hey.com/jorge/d848711a/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RMNjVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f44099e9ad9ac7e00f4948f60d380f84cf804628/CleanShot%202023-01-15%20at%2017.17.28@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RMNjVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f44099e9ad9ac7e00f4948f60d380f84cf804628/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-01-15%20at%2017.17.28@2x.png" alt="CleanShot 2023-01-15 at 17.17.28@2x.png" srcset="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RMNjVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f44099e9ad9ac7e00f4948f60d380f84cf804628/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-01-15%20at%2017.17.28@2x.png 2x, https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RMNjVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f44099e9ad9ac7e00f4948f60d380f84cf804628/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-01-15%20at%2017.17.28@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>And then I <a href="https://gist.github.com/jorgemanrubia/faea40d1a0b9f5218e7974d06a785f13">asked</a> for a more compact version I could run in a single line:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-01-15 at 17.17.35@2x.png" title="Download CleanShot 2023-01-15 at 17.17.35@2x.png" data-click-proxy-target="lightbox_link_blob_1083632547" href="https://world.hey.com/jorge/d848711a/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VqNjVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--e3ebd889bafd57dcf73ed79a5f808d7e32783766/CleanShot%202023-01-15%20at%2017.17.35@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VqNjVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--e3ebd889bafd57dcf73ed79a5f808d7e32783766/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-01-15%20at%2017.17.35@2x.png" alt="CleanShot 2023-01-15 at 17.17.35@2x.png" srcset="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VqNjVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--e3ebd889bafd57dcf73ed79a5f808d7e32783766/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-01-15%20at%2017.17.35@2x.png 2x, https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2VqNjVaQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--e3ebd889bafd57dcf73ed79a5f808d7e32783766/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-01-15%20at%2017.17.35@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>I then got to work on a little Rails tools to visualize <a href="https://www.quantconnect.com">Quantconnect</a> backtests results locally. I wanted to read some compressed CSV files containing the data to render the corresponding candlesticks and filled orders, which I always miss in the built-in reporting system. <a href="https://gist.github.com/jorgemanrubia/b9c7463cb22ebc01143e09310d841234">This is what I entered</a>:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-01-15 at 22.07.05@2x.png" title="Download CleanShot 2023-01-15 at 22.07.05@2x.png" data-click-proxy-target="lightbox_link_blob_1083632647" href="https://world.hey.com/jorge/d848711a/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NIN0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--17f7b984bb6e9c7653ea1e61791c41d6eaff7e43/CleanShot%202023-01-15%20at%2022.07.05@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NIN0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--17f7b984bb6e9c7653ea1e61791c41d6eaff7e43/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-01-15%20at%2022.07.05@2x.png" alt="CleanShot 2023-01-15 at 22.07.05@2x.png" srcset="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NIN0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--17f7b984bb6e9c7653ea1e61791c41d6eaff7e43/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-01-15%20at%2022.07.05@2x.png 2x, https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2NIN0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--17f7b984bb6e9c7653ea1e61791c41d6eaff7e43/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-01-15%20at%2022.07.05@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br>And this is what I got:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-01-15 at 09.44.29@2x.png" title="Download CleanShot 2023-01-15 at 09.44.29@2x.png" data-click-proxy-target="lightbox_link_blob_1083632713" href="https://world.hey.com/jorge/d848711a/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RKN0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--64c9aeb079d092c15d25ffa3639bfbc82e72f245/CleanShot%202023-01-15%20at%2009.44.29@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RKN0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--64c9aeb079d092c15d25ffa3639bfbc82e72f245/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-01-15%20at%2009.44.29@2x.png" alt="CleanShot 2023-01-15 at 09.44.29@2x.png" srcset="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RKN0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--64c9aeb079d092c15d25ffa3639bfbc82e72f245/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-01-15%20at%2009.44.29@2x.png 2x, https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2RKN0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--64c9aeb079d092c15d25ffa3639bfbc82e72f245/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-01-15%20at%2009.44.29@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>I then <a href="https://gist.github.com/jorgemanrubia/39a9bc011a5803c56ad6a1f1c230019b">asked</a> for a little refinement:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-01-16 at 07.56.23@2x.png" title="Download CleanShot 2023-01-16 at 07.56.23@2x.png" data-click-proxy-target="lightbox_link_blob_1083878583" href="https://world.hey.com/jorge/d848711a/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2UzckpwQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--6c667dd23ebd9d7c484f250baf66efb546c777da/CleanShot%202023-01-16%20at%2007.56.23@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2UzckpwQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--6c667dd23ebd9d7c484f250baf66efb546c777da/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-01-16%20at%2007.56.23@2x.png" alt="CleanShot 2023-01-16 at 07.56.23@2x.png" srcset="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2UzckpwQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--6c667dd23ebd9d7c484f250baf66efb546c777da/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-01-16%20at%2007.56.23@2x.png 2x, https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2UzckpwQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--6c667dd23ebd9d7c484f250baf66efb546c777da/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-01-16%20at%2007.56.23@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br>And it did:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-01-15 at 09.46.08@2x.png" title="Download CleanShot 2023-01-15 at 09.46.08@2x.png" data-click-proxy-target="lightbox_link_blob_1083632825" href="https://world.hey.com/jorge/d848711a/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U1N0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--844370538720ce147b4b54bd53e8b7e4cbecf594/CleanShot%202023-01-15%20at%2009.46.08@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U1N0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--844370538720ce147b4b54bd53e8b7e4cbecf594/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-01-15%20at%2009.46.08@2x.png" alt="CleanShot 2023-01-15 at 09.46.08@2x.png" srcset="https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U1N0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--844370538720ce147b4b54bd53e8b7e4cbecf594/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-01-15%20at%2009.46.08@2x.png 2x, https://world.hey.com/jorge/d848711a/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHNLd2U1N0paQSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--844370538720ce147b4b54bd53e8b7e4cbecf594/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-01-15%20at%2009.46.08@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>These are certainly trivial examples; I’m sure you have seen way more mind-blowing ChatGPT prompts elsewhere. They were meaningful to me because I was trying to get value out of ChatGPT when building something real. And I did. I’ll let others speculate about how software development will look in the future, but here are three thoughts I have right now:</div><div><br></div><div>First, ChatGPT lets you move faster when programming. It saves you a lot of time compared to checking APIs and googling how to do things. And this is something that any programmer, senior or not, does all the time.</div><div><br></div><div>Second, using natural language to express what you need, keeping a conversation with a machine to shape the final result, and getting functional code you can copy and paste, is a mind-blowing flow. I’ve been using it for a few weeks, and I still have a hard time wrapping my head around the fact that it works. I am certain it’s a workflow that will become mainstream. This is here to stay. </div><div><br></div><div>And third, ChatGPT doesn’t remove the need to know what you are doing. Obviating the fact that the code it generates doesn’t always work, you still need to decide when to use it, define what you need, understand the code it generates, and know how to integrate it. You still need to keep things fast, the code clean, and the system well-designed and maintainable. In this incarnation, I don’t think this technology belongs to the no-code realm but to the code assistant one. It’s like an IDE on steroids, but an IDE, nevertheless. </div><div><br></div><div>This way of using ChatGPT can save you a lot of time googling and typing code, but that is a relatively small part of the software development equation. I haven’t explored other scenarios, such as pondering alternative solutions to a problem, analyzing existing code, or generating more involved systems. I certainly intend to keep trying things here.</div><div><br></div><div>Regarding the initial quote by Martin Fowler, I don’t think ChatGPT-like systems will make software development a predictable activity, but the abstraction jump they represent is unprecedented. Natural language for generating code sounded like science fiction yesterday and is a reality today. Do you know what’s better than a fancy DSL that is very costly to create? Plain English working out of the box! With all its ambiguity and fuzziness, human language is the ultimate abstraction. And ChatGPT proves we can make it work, at least for certain scenarios.</div><div><br></div><div>For the particular case of augmenting a programmer writing code, I think ChatGPT is a game-changing tool, even in its current very embryonic vessel, with a throttled web interface that keeps going down and can’t keep your past sessions for demand reasons. </div><div><br></div><div>I intend to keep playing with it. Eager to see the next chapters here 🍿.<br><br>---<br><em>Picture: Lieutenant Ripley wearing a </em><a href="https://avp.fandom.com/wiki/P-5000_Powered_Work_Loader"><em>P-5000 Powered Work Loader</em></a><em> from </em><a href="https://en.wikipedia.org/wiki/Aliens_(film)"><em>Aliens</em></a><em>.</em><br><a href="https://www.jorgemanrubia.com">jorgemanrubia.com</a><br><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/250742023-01-01T16:32:17Z2023-01-01T18:38:19ZLearning with ChatGPT<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="drew-beamer-xU5Mqq0Chck-unsplash.jpg" title="Download drew-beamer-xU5Mqq0Chck-unsplash.jpg" data-click-proxy-target="lightbox_link_blob_1068551648" href="https://world.hey.com/jorge/aae3a4d8/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCT0ROc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--b3a9bd5a380434fadf371e6a8668b4dddfdd58d2/drew-beamer-xU5Mqq0Chck-unsplash.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/aae3a4d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCT0ROc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--b3a9bd5a380434fadf371e6a8668b4dddfdd58d2/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/drew-beamer-xU5Mqq0Chck-unsplash.jpg" alt="drew-beamer-xU5Mqq0Chck-unsplash.jpg" srcset="https://world.hey.com/jorge/aae3a4d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCT0ROc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--b3a9bd5a380434fadf371e6a8668b4dddfdd58d2/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/drew-beamer-xU5Mqq0Chck-unsplash.jpg 2x, https://world.hey.com/jorge/aae3a4d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCT0ROc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--b3a9bd5a380434fadf371e6a8668b4dddfdd58d2/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/drew-beamer-xU5Mqq0Chck-unsplash.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>I’m learning Python these days. The reason is that there are two domains I want to explore and play with — finance and artificial intelligence — and everything I want to use is built in Python. Being Ruby my go-to language, a recurring problem I’ve suffered for years is building my own infrastructure for most things I wanted to do in this space. This strategy has been fun but not quite productive, so I decided to become a Python-native for these experiments. </div><div><br></div><div>Coming from Ruby, I knew there was a path where I could get comfortable enough with Python pretty quickly. I briefly explored available books, courses, and online resources and decided to go with the <a href="https://docs.python.org/3/tutorial/">official tutorial</a>. As an exercise, I decided to go exclusively with <a href="https://openai.com/blog/chatgpt/">ChatGPT</a> to clarify further questions I had. Here are some of the questions I asked:</div><div><br></div><ul><li>can you write this python line in a more idiomatic way:</li></ul><pre>squares = list(map(lambda x: x**2, range(10)))</pre><div>I just wanted to see if it was smart enough to suggest using list comprehensions, as the tutorial did. It was.<br><br></div><ul><li>can you create multiline strings indicating the language in Python?</li><li>is this idiomatic in Python?</li></ul><pre>text = (‘Put several strings within parentheses ‘
‘to have them joined together.’)</pre><div><br></div><ul><li>how to show the type of an identifier in Python?</li><li>what is the difference between dict and dict_items?</li><li>how to iterate in Python with index</li><li>how to convert object to string?</li><li>how do you document code in Python?</li><li>can you print a table with the most common methods for a list in Python</li><li>how can I define equality logic for set elements?</li><li>can you use objects in boolean expressions?</li><li>how to convert object to boolean?</li><li>naming convention for files?</li><li>how can I print values with string interpolation?</li><li>do you need to pass self to every method definition?</li><li>python conventions for naming constants in classes</li><li>can you declare private methods in Python</li><li>but can you provide custom implementations for operators in a custom class?</li><li>Generate code for a class that encapsulates a list of ingredients. It must include a custom iterator. Also, please include an example of how to use it.</li><li>basic examples of regular expressions in Python</li><li>how to convert string to date?</li><li>what’s the equivalent of Ruby’s Gemfile in Python?</li></ul><div><br></div><div>The results were mind-blowing. It <strong>always</strong> provided great responses. Answers both directly addressed my questions and elaborated on the reasons, including examples of code clarifying the answer. For example: <br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2023-01-01 at 17.38.30@2x.png" title="Download CleanShot 2023-01-01 at 17.38.30@2x.png" data-click-proxy-target="lightbox_link_blob_1068547852" href="https://world.hey.com/jorge/aae3a4d8/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQXkvc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--3b5450269cd240b1a6a048a183101e6a7d6ee13b/CleanShot%202023-01-01%20at%2017.38.30@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/aae3a4d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQXkvc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--3b5450269cd240b1a6a048a183101e6a7d6ee13b/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202023-01-01%20at%2017.38.30@2x.png" alt="CleanShot 2023-01-01 at 17.38.30@2x.png" srcset="https://world.hey.com/jorge/aae3a4d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQXkvc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--3b5450269cd240b1a6a048a183101e6a7d6ee13b/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202023-01-01%20at%2017.38.30@2x.png 2x, https://world.hey.com/jorge/aae3a4d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQXkvc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--3b5450269cd240b1a6a048a183101e6a7d6ee13b/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202023-01-01%20at%2017.38.30@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure></div><div><br></div><div>My previous workflow would have been relying on Google, which would have found a good article or Stack Overflow answer. I didn’t miss Google a single time. Actually, the Google path would have slowed me down compared to just asking ChatGPT.</div><div><br></div><div>I’m very wary of making predictions in tech, but you don’t have to be Nostradamus to realize that a major change will happen, and it will be fast. </div><div><br></div><div>Many years ago, I was puzzled by the “<em>what can I help you with?</em>” prompt from the infamous Microsoft Office Clippit. Of course, it couldn’t help you with anything — the free-text input would just trigger a quite terrible documentation search — but the initial idea where you could ask a computer for help by just speaking human was as intriguing as appealing. With all the advancements since then, ChatGPT is the first system I see that doesn’t feel like a thin facade on top of a search engine.</div><div><br></div><div> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="File-Clippy.png" title="Download File-Clippy.png" data-click-proxy-target="lightbox_link_blob_1068537793" href="https://world.hey.com/jorge/aae3a4d8/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTUdYc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--054c49ec86b07b7ee8f98343d4a0b542d684c204/File-Clippy.png?disposition=attachment">
<img src="https://world.hey.com/jorge/aae3a4d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTUdYc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--054c49ec86b07b7ee8f98343d4a0b542d684c204/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/File-Clippy.png" alt="File-Clippy.png" srcset="https://world.hey.com/jorge/aae3a4d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTUdYc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--054c49ec86b07b7ee8f98343d4a0b542d684c204/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/File-Clippy.png 2x, https://world.hey.com/jorge/aae3a4d8/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTUdYc0Q4PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--054c49ec86b07b7ee8f98343d4a0b542d684c204/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/File-Clippy.png 3x" decoding="async" loading="lazy">
</a>
</figure></div><div><br></div><div>Because the scene felt somehow mature, it’s fascinating to see a disruptive technology appear out of nowhere. I think lots of well-established actors in the space will either have to adapt fast or face the consequences, because their moats have been demolished. ChatGPT makes current Google, Alexa, or Siri look like seriously outdated products. And it’s hard not to think of all sorts of new products leveraging <a href="https://en.wikipedia.org/wiki/Artificial_general_intelligence">AGI</a> in ways we can’t imagine.</div><div><br></div><div>The next years should be fascinating.</div><div><br>---<br><a href="https://www.jorgemanrubia.com">jorgemanrubia.com</a><br><em>Photo by </em><a href="https://unsplash.com/@dbeamer_jpg?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Drew Beamer</em></a><em> on </em><a href="https://unsplash.com/photos/xU5Mqq0Chck?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Unsplash</em></a></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/249662022-12-26T22:14:20Z2022-12-26T22:23:16Z2022<div class="trix-content">
<div>Some assorted reflections on my 2022: <br><br></div><ul><li>I <a href="https://www.jorgemanrubia.com/posts/">wrote a lot</a>, more than ever. <a href="https://world.hey.com/jorge/balance-76417d97">This was my favorite piece</a>. <a href="https://world.hey.com/jorge/aging-programmer-d448bdec">This was my biggest hit</a>. And I’m proud of <a href="https://www.jorgemanrubia.com/code-i-like/">this series</a>, into which I put quite a bit of effort. I had many interesting conversations with people with shared interests, which I enjoyed a lot. I intend to continue <a href="https://world.hey.com/jorge/writing-for-yourself-c25a5a13">writing for myself</a> in 2023.</li><li>Finally, after three years of total inactivity, I succeeded at exercising regularly. </li><li>I failed at not engaging with junk information in general and worthless dramas in particular. A 180-degree turn here is my only serious new year resolution.</li><li>I made three years at <a href="https://37signals.com">37signals</a>. The sensation of being <em>very</em> lucky I had on my first day has only intensified during this time.</li><li>GitHub Copilot and ChatGPT made my jaw drop multiple times. I look forward to diving into them further next year. </li><li>Something I relearn periodically is to listen to gut feelings. Another is to choose carefully where to spend my energy and with whom. Aiming not to forget either in 2023.</li><li>I have lots of plans for the next year. I won’t probably do most. As long as the few things truly matter to me are there, I think I’ll be fine.</li></ul><div><br></div><div>Happy 2023 🥂.<br><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/249122022-12-23T16:24:03Z2022-12-23T16:24:39ZStuck<div class="trix-content">
<div>I have referenced <a href="https://tidyfirst.substack.com/p/cohesion">this article by Kent Beck</a> in my last <a href="https://dev.37signals.com/compared-to-what/">two</a> <a href="https://dev.37signals.com/active-record-nice-and-blended/">pieces</a>. Here goes the last one. When discussing whether coupling was good or bad, Beck says:</div><div><br></div><blockquote>This is where I got stuck for a long time (years).</blockquote><div><br></div><div>I can’t think of a more influential author than Kent Beck. His <a href="https://en.wikipedia.org/wiki/Kent_Beck">contributions</a> have defined what modern software development is. This struggle around an apparently elementary question should make anyone believing to have figured this game out think twice.<br><br>Inspiring.</div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/248372022-12-19T11:13:52Z2022-12-23T22:15:35ZCode I like (V): Active Record, nice and blended<div class="trix-content">
<div><em>This article was originally published in the </em><a href="https://dev.37signals.com/active-record-nice-and-blended/"><em>37signals dev blog</em></a><em>.<br></em><br><br>Persisting objects in relational databases is an intricate problem. Two decades ago, it looked like the ultimate orthogonal problem to solve: abstract persistence out so that programmers don’t have to care about it. Many years later, we can affirm that… it’s not that simple. Persistence is indeed a crosscutting concern, but one with many tentacles.<br><br></div><div>Because it can’t be fully abstracted, many patterns aim to isolate database access on its own layer to keep the domain model persistence-free. For example <a href="https://www.oreilly.com/library/view/domain-driven-design-tackling/0321125215/">repositories</a>, <a href="https://en.wikipedia.org/wiki/Data_mapper_pattern">Data Mappers</a> or <a href="https://en.wikipedia.org/wiki/Data_access_object">DAOs (Data Access Objects)</a>. Rails, however, went with a different approach — <a href="https://guides.rubyonrails.org/active_record_basics.html#the-active-record-pattern">Active Record</a> — a pattern introduced by Martin Fowler in <a href="https://martinfowler.com/books/eaa.html">Patterns of EAA</a>:<br><br></div><blockquote>An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.</blockquote><div><br>The distinctive trait of the Active Record pattern is combining domain logic and persistence in the same class, and that’s how we use it here at <a href="https://37signals.com/">37signals</a>. At first glance, this might not look like a good idea. Shouldn’t we keep separated things, well, separated? Even Fowler mentions that the pattern is a good choice for <em>domain logic that isn’t too complex</em>. It’s our experience, however, that Rails’ Active Record keeps code elegant and maintainable in large and complex codebases. In this article, I will try to articulate why.<br><br></div><h1><strong>An impedance-less match</strong></h1><div><a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_impedance_mismatch">Object–relational impedance mismatch</a> is a fancy way of saying that Object-Oriented languages and relational databases are distinct worlds, and this results in friction when translating concepts between them.</div><div>I believe Active Record — the Rails framework, not the pattern — works so well in practice because it reduces this impedance mismatch to a minimum. There are two main reasons:<br><br></div><ul><li>It looks and feels like Ruby, even when you need to go lower level to fine-tune things.</li><li>It comes with fantastic and innovative answers for recurring needs when dealing with objects and relational persistence.</li></ul><div><br></div><div><strong>A perfect Ruby Companion<br></strong><br></div><div>Let me show you an example from <a href="https://www.hey.com/">HEY</a>. It shows the internals from the <span style="background-color: rgb(242, 242, 242);">Contact#designate_to(box)</span> method I referenced in the <a href="https://dev.37signals.com/vanilla-rails-is-plenty/">Vanilla Rails article</a>. This method handles the logic when you select a box as the destination for emails from a given contact. I’m highlighting the lines involving Active Record:<br><br></div><pre><strong>module</strong> Contact::Designatable
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
included <strong>do</strong>
<span style="background-color: rgb(250, 247, 133);"> has_many :designations, class_name: "Box::Designation", dependent: :destroy</span>
<strong>end</strong>
<strong>def</strong> <strong>designate_to</strong>(box)
<strong>if</strong> box.<strong>imbox?</strong>
<em># Skip designating to Imbox since it’s the default.</em>
undesignate_from(box.<strong>identity</strong>.<strong>boxes</strong>)
<strong>else</strong>
update_or_create_designation_to(box)
<strong>end</strong>
<strong>end</strong>
<strong>def</strong> <strong>undesignate_from</strong>(box)
<span style="background-color: rgb(250, 247, 133);"> designations.</span><strong style="background-color: rgb(250, 247, 133);">destroy_by</strong><span style="background-color: rgb(250, 247, 133);"> box: box</span>
<strong>end</strong>
<strong>def</strong> <strong>designation_within</strong>(boxes)
<span style="background-color: rgb(250, 247, 133);"> designations.</span><strong style="background-color: rgb(250, 247, 133);">find_by</strong><span style="background-color: rgb(250, 247, 133);"> box: boxes</span>
<strong>end</strong>
<strong>def</strong> <strong>designated?</strong>(by:)
designation_within(by.<strong>boxes</strong>).<strong>present?</strong>
<strong>end</strong>
<strong>private</strong>
<strong>def</strong> <strong>update_or_create_designation_to</strong>(box)
<strong>if</strong> designation <strong>=</strong> designation_within(box.<strong>identity</strong>.<strong>boxes</strong>)
<span style="background-color: rgb(250, 247, 133);"> designation.</span><strong style="background-color: rgb(250, 247, 133);">update!</strong><span style="background-color: rgb(250, 247, 133);">(box: box)</span>
<strong>else</strong>
<span style="background-color: rgb(250, 247, 133);"> designations.</span><strong style="background-color: rgb(250, 247, 133);">create!</strong><span style="background-color: rgb(250, 247, 133);">(box: box)</span>
<strong>end</strong>
<strong>end</strong>
<strong>end</strong></pre><div><br>The persistence bits look natural and easy to follow. The code is eloquent and concise, and it reads like Ruby. It doesn’t feel like a mix of concerns that don’t belong together; you don’t see a cognitive jump between “business logic” and “persistence duties”. To me, this trait is a game-changer.<br><br></div><div><strong>Answers for persistence needs<br></strong><br></div><div>Active Record offers many options to persist object-oriented models into tables. When presenting the original pattern, Fowler argues that:<br><br></div><blockquote>If your business logic is complex, you’ll soon want to use your object’s direct relationships, collections, inheritance, and so forth. These don’t map easily onto Active Record, and adding them piecemeal gets very messy.<br><br></blockquote><div>Rails’ Active Record offers answers for those and more. To enum a few: <a href="https://guides.rubyonrails.org/association_basics.html">associations</a>, <a href="https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html">single table inheritance</a>, <a href="https://api.rubyonrails.org/v7.0.4/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html">serialized attributes</a> or <a href="https://api.rubyonrails.org/classes/ActiveRecord/DelegatedType.html">delegated types</a>.<br><br></div><div>Some people recommend avoiding Rails associations at all costs. I have a hard time understanding this one: I think associations are one of the best features of Active Record, and we use them extensively in our apps. When studying object-oriented programming, <em>associations</em> between objects are a fundamental construct, just like inheritance. Same with relationships between tables in the relational world. What’s not to like about direct support for translating those into code and getting the framework to do all the heavy lifting for you?</div><div><br>Let me show you an example of associations. In <a href="https://www.hey.com/">HEY</a>, an email thread internally looks. like a <span style="background-color: rgb(242, 242, 242);">Topic</span> model with many entries (<span style="background-color: rgb(242, 242, 242);">Entry</span>). In some scenarios, the system needs to access aggregated data at the topic level based on the contained entries, such as the addressed contacts in the thread or the blocked trackers. We implement the bulk of this with associations:<br><br><br></div><pre><strong>class</strong> <strong>Topic</strong>
<strong>include</strong> Entries
<em>#...</em>
<strong>end</strong>
<strong>module</strong> Topic::Entries
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
included <strong>do</strong>
has_many :entries, dependent: :destroy
has_many :entry_attachments, through: :entries, source: :attachments
has_many :receipts, through: :entries
has_many :addressed_contacts, <strong>-></strong> { distinct }, through: :entries
has_many :entry_creators, <strong>-></strong> { distinct }, through: :entries, source: :creator
has_many :blocked_trackers, through: :entries, class_name: "Entry::BlockedTracker"
has_many :clips, through: :entries
<strong>end</strong>
<em>#...</em>
<strong>end</strong></pre><div><br>We use other Active Record features profusely to get direct persistence support for rich Ruby object models. For example, <a href="https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html">single table inheritance</a> to model the different kinds of boxes in HEY:<br><br></div><pre><strong>class</strong> <strong>Box</strong> <strong><</strong> ApplicationRecord
<strong>end</strong>
<strong>class</strong> <strong>Box::Imbox</strong> <strong><</strong> Box
<strong>end</strong>
<strong>class</strong> <strong>Box::Trailbox</strong> <strong><</strong> Box
<strong>end</strong>
<strong>class</strong> <strong>Box::Feedbox</strong> <strong><</strong> Box
<strong>end</strong>
<br></pre><div><br>Or <a href="https://api.rubyonrails.org/v7.0.4/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html">serialized attributes</a> to store the recurrence schedule details for Basecamp checkins:<br><br><br></div><pre><strong>class</strong> <strong>Question</strong> <strong><</strong> ApplicationRecord
serialize :schedule, RecurrenceSchedule
<strong>end</strong></pre><div><br>And we use <a href="https://api.rubyonrails.org/classes/ActiveRecord/DelegatedType.html">delegated types</a> to model the different kinds of contacts in HEY.</div><div><br></div><pre><strong>class</strong> <strong>Contact</strong> <strong><</strong> ApplicationRecord
<strong>include</strong> Contactables
<strong>end</strong>
<strong>module</strong> Contact::Contactables
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
included <strong>do</strong>
delegated_type :contactable, types: Contactable<strong>::</strong>TYPES, inverse_of: :contact, dependent: :destroy
<strong>end</strong>
<strong>end</strong>
<strong>module</strong> Contactable
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
TYPES <strong>=</strong> %w[ User Extenzion Alias Person Service Tombstone ]
included <strong>do</strong>
has_one :contact, as: :contactable, inverse_of: :contactable, touch: <strong>true</strong>
belongs_to :account, default: <strong>-></strong> { contact<strong>&</strong>.<strong>account</strong> }
<strong>end</strong>
<strong>end</strong>
<strong>class</strong> <strong>User</strong> <strong><</strong> ApplicationRecord
<strong>include</strong> Contactable
<strong>end</strong>
<strong>class</strong> <strong>Person</strong> <strong><</strong> ApplicationRecord
<strong>include</strong> Contactable
<strong>end</strong>
<strong>class</strong> <strong>Service</strong> <strong><</strong> ApplicationRecord
<strong>include</strong> Contactable
<strong>end</strong></pre><div><br>The caveat here is that Active Record requires you to control the database schema to leverage what it offers fully. Assuming this is the case, the ability to seamlessly persist rich and complex object models is key to making the pattern work in large and complex codebases.<br><br></div><h1><strong>Encapsulation-friendly</strong></h1><div>Because it blends so well with Ruby, Active Record plays great with using regular Ruby goodies to hide details away. This allows you to write code that looks natural and encapsulates persistence concerns without paying the ceremony tax of a separate data-access layer.</div><div><br>For example, you can check the previous <span style="background-color: rgb(242, 242, 242);">Contact::Designatable</span> code. The Active Record code is wrapped in plain private methods. You can also notice how everything — domain logic and persistence — is hidden behind a method <span style="background-color: rgb(242, 242, 242);">#designate_to</span>, which is part of that natural interface we want to see from the system boundaries, as explained in <a href="https://dev.37signals.com/vanilla-rails-is-plenty/">this article</a>. So persistence is mixed in but well organized and encapsulated.</div><div><br>In more complex scenarios, nothing prevents you from creating objects to hide complexity away. For example, in <a href="https://basecamp.com/">Basecamp</a>, to render the activity timeline for a given user, it uses a class <span style="background-color: rgb(242, 242, 242);">Timeline::Aggregator</span>, which is a PORO in charge of serving the relevant events. This class encapsulates the querying logic:<br><br><br></div><pre><strong>class</strong> <strong>Reports::Users::ProgressController</strong> <strong><</strong> ApplicationController
<strong>def</strong> <strong>show</strong>
@events <strong>=</strong> Timeline<strong>::</strong>Aggregator.<strong>new</strong>(Current.<strong>person</strong>, filter: current_page_by_creator_filter).<strong>events</strong>
<strong>end</strong>
<strong>end</strong>
<strong>class</strong> <strong>Timeline::Aggregator</strong>
<strong>def</strong> <strong>initialize</strong>(person, filter: <strong>nil</strong>)
@person <strong>=</strong> person
@filter <strong>=</strong> filter
<strong>end</strong>
<strong>def</strong> <strong>events</strong>
Event.<strong>where</strong>(id: event_ids).<strong>preload</strong>(:recording).<strong>reverse_chronologically</strong>
<strong>end</strong>
<strong>private</strong>
<strong>def</strong> <strong>event_ids</strong>
event_ids_via_optimized_query(1.<strong>week</strong>.<strong>ago</strong>) <strong>||</strong> event_ids_via_optimized_query(3.<strong>months</strong>.<strong>ago</strong>) <strong>||</strong> event_ids_via_regular_query
<strong>end</strong>
<em># Fetching the most recent recordings optimizes the query enormously for large sets of recordings</em>
<strong>def</strong> <strong>event_ids_via_optimized_query</strong>(created_since)
limit <strong>=</strong> extract_limit
event_ids <strong>=</strong> filtered_ordered_recordings.<strong>where</strong>("recordings.created_at >= ?", created_since).<strong>pluck</strong>("relays.event_id")
event_ids <strong>if</strong> event_ids.<strong>length</strong> <strong>>=</strong> limit
<strong>end</strong>
<strong>def</strong> <strong>event_ids_via_regular_query</strong>
filtered_ordered_recordings.<strong>pluck</strong>("relays.event_id")
<strong>end</strong>
<em># ...</em>
<strong>end</strong></pre><div><br>For querying, we use <a href="https://guides.rubyonrails.org/active_record_querying.html#scopes">scopes</a> extensively. Combining those with associations and other scopes lets you express complex queries with natural-looking code. For example, for rendering a <a href="https://www.hey.com/features/collections/">collection in HEY</a>, it needs to fetch all the active topics in the collection accessible to the acting contact — in HEY you can have different <em>acting</em> users depending on the selected filter. This is how the related code looks:<br><br><br></div><pre><strong>class</strong> <strong>Topic</strong> <strong><</strong> ApplicationController
<strong>include</strong> Accessible
<strong>end</strong>
<strong>module</strong> Topic::Accessible
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
included <strong>do</strong>
has_many :accesses, dependent: :destroy
scope :accessible_to, <strong>-></strong>(contact) { not_deleted.<strong>joins</strong>(:accesses).<strong>where</strong> accesses: { contact: contact } }
<strong>end</strong>
<em># ...</em>
<strong>end</strong>
<strong>class</strong> <strong>CollectionsController</strong> <strong><</strong> ApplicationController
<strong>def</strong> <strong>show</strong>
@topics <strong>=</strong> @collection.<strong>topics</strong>.<strong>active</strong>.<strong>accessible_to</strong>(Acting.<strong>contact</strong>)
<em># ...</em>
<strong>end</strong>
<strong>end</strong></pre><div><br>While this is a bit of an edge case, you can see how we also used scopes for one of the HEY performance optimizations Donal described in <a href="https://dev.37signals.com/faster-paging-in-hey/">this article</a>:<br><br></div><pre><strong>module</strong> Posting::Involving
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
DEFAULT_INVOLVEMENTS_JOIN <strong>=</strong> "INNER JOIN `involvements` USE INDEX(index_involvements_on_contact_id_and_topic_id) ON `involvements`.`topic_id` = `postings`.`postable_id`"
OPTIMIZED_FOR_USER_FILTERING_INVOLVEMENTS_JOIN <strong>=</strong> "STRAIGHT_JOIN `involvements` USE INDEX(index_involvements_on_account_id_and_topic_id_and_contact_id) ON `involvements`.`topic_id` = `postings`.`postable_id`"
included <strong>do</strong>
scope :involving, <strong>-></strong>(contacts, involvements_join: DEFAULT_INVOLVEMENTS_JOIN) <strong>do</strong>
where(postable_type: "Topic")
.<strong>joins</strong>(involvements_join)
.<strong>where</strong>(involvements: { contact_id: Array(contacts).<strong>map</strong>(<strong>&</strong>:id) })
.<strong>distinct</strong>
<strong>end</strong>
scope :involving_optimized_for_user_filtering, <strong>-></strong>(contacts) <strong>do</strong>
<em># STRAIGHT_JOIN ensures that MySQL reads topics before involvements</em>
involving(contacts, involvements_join: OPTIMIZED_FOR_USER_FILTERING_INVOLVEMENTS_JOIN)
.<strong>use_index</strong>(:index_postings_on_user_id_and_postable_and_active_at)
.<strong>joins</strong>(:user)
.<strong>where</strong>("`users`.`account_id` = `involvements`.`account_id`")
.<strong>select</strong>(:id, :active_at)
<strong>end</strong>
<strong>end</strong>
<strong>end</strong></pre><div><br></div><h1><strong>A restatement of the persistence and domain logic separation problem</strong></h1><div>In theory, a rigid separation of persistence and domain logic sounds like a good idea. However, in practice, it comes with two significant challenges.<br><br></div><div>First, whatever approach you use will imply adding and orchestrating additional data-access abstractions in multiples of the number of persisted models in your app. That translates into additional ceremony and complexity.<br><br></div><div>Second, building <a href="https://dev.37signals.com/vanilla-rails-is-plenty/">rich domain models</a> becomes harder. If domain models are free of persistence concerns, how can they implement business logic that needs to access the database? In <a href="https://en.wikipedia.org/wiki/Domain-driven_design">DDD</a>, for example, the answer is adding additional domain-level elements such as repositories and aggregates. Now you have three elements to coordinate, with plain domain entities knowing nothing about persistence. How do they access each other? How do they collaborate? This is a tempting scenario to come up with a service to orchestrate everything. It is not surprising that, ironically, many implementations that aim to embrace the best design practices end up suffering the <a href="https://martinfowler.com/bliki/AnemicDomainModel.html">anemic domain model</a> problem, where most entities are mere data holders without behavior.<br><br></div><div>The desire to separate persistence for domain logic exists for a reason. Merging both can easily result in code that doesn’t belong together or, in other words, that is difficult to maintain. This becomes obvious if you use raw SQL directly, but it is also the case if you use most ORM libraries because they only focus on the persistence side of the equation. Active Record, however, is designed on the premise that persistence and domain logic belongs together, and it comes with almost two decades of iteration around this idea.<br><br></div><div>This insight in <a href="https://tidyfirst.substack.com/p/cohesion">an article by Kent Beck</a> blew my mind quite recently:<br><br></div><blockquote>An element is cohesive to the degree that its subelements are coupled. A perfectly cohesive elements requires all subelements to change at the same time.</blockquote><div><br>In a database-powered application, domain logic is indissolubly linked to persistence. Is isolating persistence a goal or mitigation against not having the right ORM (Object Relational Mapping) technology in place? My point of view is that <em>if</em> the ORM perfectly blends with the host language, and <em>if</em> it comes with good answers for persisting your object models, and <em>if</em> it offers good encapsulation mechanisms, then the original question gets restated. Instead of <em>“how can I isolate persistence from domain logic”</em>, it becomes <em>“why would I”</em>?<br><br></div><h1>Conclusions</h1><div>I’ve known for a long time that using Active Record as I explained here works great in practice. I have never missed a standalone layer for persistence in the Rails apps I’ve worked on.<br><br></div><div>Like with <a href="https://dev.37signals.com/vanilla-rails-is-plenty/">vanilla Rails</a>, some people argue that Active Record works for quick prototypes but that, at some point, you need to embrace an alternative that isolates persistence from domain logic. This is not our experience. We use Active Record as described here in <a href="https://rubyonrails.org/doctrine#beautiful-code">Basecamp</a> and <a href="https://www.hey.com/">HEY</a>, which are two quite large Rails applications used by millions. Active Record is at the heart of everything these applications do, and <a href="https://updates.37signals.com/">we keep evolving them</a>.<br><br></div><div>There is a caveat, which is common in these articles: Active Record is a tool, and of course, you can use it to write messy and unmaintainable code. In particular, like <a href="https://world.hey.com/jorge/code-i-like-iii-good-concerns-5a1b391c">it happens with concerns</a>, it doesn’t replace the need to design your system properly and it won’t help you to do that if you don’t know how. If you are using Active Record and having code maintenance problems, it might not be the tool, but that you are not getting the other thousand of things that make code maintainable right.<br><br></div><div>I believe Active Record is so good that it eliminates the traditional reasons for a strict separation of persistence and domain logic. I consider that a feature to embrace proudly, not a choice to justify.<br><br>---<br><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a><br><em>This article belongs to a series of posts on Rails design techniques called </em><a href="https://www.jorgemanrubia.com/code-i-like/"><strong><em>Code I like</em></strong></a>.</div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/247282022-12-13T10:30:06Z2022-12-13T10:30:47ZCompared to what?<div class="trix-content">
<div><em>This article was originally published in the </em><a href="https://dev.37signals.com/compared-to-what/"><em>37signals dev blog</em></a><em>.<br></em><br><br>I loved <a href="https://tidyfirst.substack.com/p/cohesion/">this article by Kent Beck on cohesion</a>. At some point, when discussing how cohesion and coupling are opposing forces, it says:</div><div><br></div><blockquote>Wait!? Isn’t coupling bad? But cohesion is good? But cohesion is coupling of subelements? Isn’t that bad? And good?<br><br>This is where I got stuck for a long time (years). What I was missing was the “compared to what?” perspective…</blockquote><div><br></div><div>The “compared to what” reference resonated strongly with me. A recurring thought I have had lately is that most takes on software design techniques make such comparison impossible because they don’t include real code. By <em>real code</em> I mean code extracted from <em>real</em> apps.</div><div><br></div><div>I have written some pieces on Rails design techniques in the <a href="https://www.jorgemanrubia.com/code-i-like/">Code I like series</a>, and I intend to address more in upcoming posts. I’m very intentional about sharing fragments of code from our products, which are widely-used Rails applications. No synthetic or contrived examples, but the real thing.</div><div><br></div><div>I <a href="https://world.hey.com/jorge/smelling-rails-smells-0ffacec5/">loathe the term *real world*</a> when presenting software design approaches, but I’m using it here very deliberately: I believe actual code should be a mandatory ingredient in these discussions. A reality layer separates the words describing concepts in the abstract and the code that puts those in motion. It’s impossible to have an effective debate if you obviate that.</div><div><br></div><div>This reality layer takes many forms. Sometimes, it implies a significant ceremony that makes the system harder to understand and maintain. Others, it comes as a local improvement that deteriorates the system as a whole. And because reality usually brings many exceptions, answers for those often pollute the initial idea to the point it is not that appealing anymore.</div><div><br></div><div>Moreover, in practice, it’s normal to have to choose a solution that is not ideal; it’s just the best one compared to the alternatives at hand. You know, context, nuance, tradeoffs, etc. Abstract discussions rightfully ignore those to present concepts as crystal clear as possible. Code from real-world applications, however, exposes them all mercilessly.</div><div><br></div><div>I always ask for permission before sharing our code at <a href="https://37signals.com/">37signals</a>, and I always get a resounding <em>YES, PLEASE</em>. This is not surprising, considering they were talking about <a href="https://dhh.dk/2012/the-parley-letter.html/">this very same topic ten years ago</a>. I wish more companies did the same. I would love to see more takes on design techniques that showed real code from actual apps. In other words, I wish more articles made it easier to answer the opening question here:</div><div><br></div><div>Compared to what?<br><br><br><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a><br><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/243982022-11-27T10:27:13Z2022-11-27T14:14:28ZMy curse with plain code editors<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="patrick-fore-0gkw_9fy0eQ-unsplash.jpg" title="Download patrick-fore-0gkw_9fy0eQ-unsplash.jpg" data-click-proxy-target="lightbox_link_blob_1027187887" href="https://world.hey.com/jorge/03080648/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCSytrT1QwPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--ecec91eabc2792c46e1aa3edd8fafe7241fd61eb/patrick-fore-0gkw_9fy0eQ-unsplash.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/03080648/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCSytrT1QwPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--ecec91eabc2792c46e1aa3edd8fafe7241fd61eb/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/patrick-fore-0gkw_9fy0eQ-unsplash.jpg" alt="patrick-fore-0gkw_9fy0eQ-unsplash.jpg" srcset="https://world.hey.com/jorge/03080648/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCSytrT1QwPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--ecec91eabc2792c46e1aa3edd8fafe7241fd61eb/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/patrick-fore-0gkw_9fy0eQ-unsplash.jpg 2x, https://world.hey.com/jorge/03080648/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCSytrT1QwPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--ecec91eabc2792c46e1aa3edd8fafe7241fd61eb/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/patrick-fore-0gkw_9fy0eQ-unsplash.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>I think the first IDE I used was Borland Delphi in 1999. I remember being marveled by its code insight features, where the editor would suggest what properties and methods were available for a given symbol. I remember being puzzled about the programming sorcery powering that — pretty much the same feeling I have today when I play a modern video game.</div><div><br></div><div>I then got deeply involved with Java and the IDEs available back then. A little-known one, <a href="https://en.wikipedia.org/wiki/Micro_Focus_Together">Together by Togethersoft</a>, had a deep impact on me, featuring a bidirectional sync between code and UML diagrams in real-time — a feature that, in hindsight, was as cool as useless. I even went to build a toy IDE that did the same, which got me into language processors and model transformations. I eventually settled down on Eclipse, but all the Java IDEs featured excellent code assistance since the early days.</div><div><br></div><div>Rails had been on my radar since it was released, but I got serious about it around 2010. Coming from Java, everything felt better except for the editor. I chose <a href="https://macromates.com/">TextMate</a>, which Rails itself helped to popularize. While I could appreciate how Ruby being so concise played well with a simple editor, I still noticed a steep downgrade. At some point in 2011, I <a href="https://www.jorgemanrubia.com/2012/12/29/why-i-love-rubymine/">switched to RubyMine</a>, an IDE, which is the editor I use to this day. </div><div><br></div><div>The thing is that there is something in plain editors that is appealing to me. I have tried Sublime, Vim, and Visual Studio code, but my attempts don’t last because I always find the same frictions. Virtually every Rails programmer I know — a group that includes people way smarter than me — uses a plain editor, so I sometimes wonder if I am driving in the wrong way, but then I conclude that everyone is wrong but me ¯\_(ツ)_/¯. I’m kidding, editors are just a tool and all that, but I will still try to explain those frictions next.</div><div><br></div><h1><strong>Browsing symbols</strong></h1><div>Most of the time, in an editor, you are reading code. And, when reading code, the most common operation is jumping to symbol definitions — such as a method invocation — to understand how the system works. RubyMine does a fantastic job here. I want to show some examples with real-world codebases next:</div><div><br></div><div>This one from console <a href="https://github.com/basecamp/console1984">Console1984</a> shows how it can infer the right class to locate methods based on a static analysis of the code:</div><div><br><img src="https://gopher.hey.com/sIjvS-UY2gKobgJfbMMrcQ_25tNKfTjCAtPd_EtE-So4=/https://www.jorgemanrubia.com/images/plain-editors/validate.gif" width="542" height="449" loading="lazy" decoding="async"><br><br>Because Ruby is dynamically typed, it is often impossible to infer the right type. There is frequently only a matching method for a given name; in that case, it will just jump to it. When there are many, it will offer possible alternatives. This is still a qualitative improvement over having to search manually. Here’s an example from some code I recently worked on in <a href="https://www.hey.com/">HEY</a>:<br><br><img src="https://gopher.hey.com/sHTRQ6qxqATz4CuPLza-Qtd_VGhL33oauNLi7HeSj3lc=/https://www.jorgemanrubia.com/images/plain-editors/participating_contacts.gif" width="542" height="449" loading="lazy" decoding="async"><br><br>Then you have the case of external libraries. I check code from gems all the time. In RubyMine, this works seamlessly. Here’s how I can navigate to the <em>.encrypts</em> method from <a href="https://guides.rubyonrails.org/active_record_encryption.html">Active Record Encryption</a> in a class from HEY:<br><br><img src="https://gopher.hey.com/s8UwPRAQXMa46kmcpIc4UwxDxiGKcrTDB_piIRTeWqKU=/https://www.jorgemanrubia.com/images/plain-editors/encrypts.gif" width="542" height="449" loading="lazy" decoding="async"><br><br>The system works the same for Javascript (no TypeScript required), and even for CSS:<br><br><img src="https://gopher.hey.com/slcagGxoRgh2fYNq47iz3lg4ojH9loqey6vyN2bEgDYc=/https://www.jorgemanrubia.com/images/plain-editors/css.gif" width="542" height="449" loading="lazy" decoding="async"><br><br>Of course, you can locate any definition you want using simple text search, but I find that slow and inconvenient after tasting this way of browsing code.</div><div><br></div><h1><strong>Finding references</strong></h1><div>After finding symbol definitions, the next most common thing I do when reading code is searching references to symbols to understand who is invoking a piece of code. For example, who is invoking this <em style="background-color: rgb(242, 242, 242);">#file_in</em> method in HEY:</div><div><br><img src="https://gopher.hey.com/sysQEzMjSg2JQ_K7neFm5_-LxiKuDE2wOUxv0GHJQNeE=/https://www.jorgemanrubia.com/images/plain-editors/file_in.gif" width="542" height="449" loading="lazy" decoding="async"><br><br></div><h1><strong>Renaming</strong></h1><div>At writing-code time, the operation I perform more frequently is renaming things. I want to put the cursor on a symbol, select a new name, and let my editor care about the scope of the change. For example, here I’m renaming a block argument without having to care about the local variable in the method above:</div><div><br><img src="https://gopher.hey.com/s3x8Ydcp5Aqo4LZFsg164uVrLFu-rjA0NEt6ZfuAtU3c=/https://www.jorgemanrubia.com/images/plain-editors/rename.gif" width="542" height="449" loading="lazy" decoding="async"></div><div><br></div><h1><strong>Extracting things out</strong></h1><div>This is another big one for me: select a block of code and extract it to a method or extract an expression to a constant or local variable. Here is an example from HEY extracting a private method:<br><br><img src="https://gopher.hey.com/sUfbmoRIuh8hFpqpXhXSGWZRFKiBRB35G5TEvY3_A3e8=/https://www.jorgemanrubia.com/images/plain-editors/autofiling_folders.gif" width="800" height="662" loading="lazy" decoding="async"><br><br></div><h1><strong>The basics first</strong></h1><div>So those are my editing basics. I still appreciate and leverage good support for lower-level editing actions at the file, line, and character level, but my editing happiness relies on this layer of higher-level actions. In other words, I need an IDE.</div><div><br></div><h1><strong>No types tax</strong></h1><div>I know <a href="https://www.typescriptlang.org/">Typescript</a> and <a href="https://sorbet.org/">Sorbet</a> offer a fantastic editing experience with Visual Studio Code, featuring the IDE-like features I referenced above. In fact, people often include the improved editing experience among the reasons to go with those. The problem is that I have no desire to add types to my Javascript and Ruby. I wrote <a href="https://www.jorgemanrubia.com/2019/06/22/on-ruby-and-type-checkers/">some thoughts on this subject</a> back in the day, and my opinion is essentially the same today.</div><div><br></div><div>RubyMine proves there is a sweet spot where you can get 80% of the power of a types-powered IDE without paying the tax of adding types to your code. The thing is that formal type information is terrific for editors and RubyMine already offers <a href="https://www.jetbrains.com/help/ruby/rbs.html">first-class support for RBS to improve its assistance</a>, so I appreciate the effort to provide RBS descriptors for the Ruby standard library and external gems. Still, I wouldn’t like to have to create and maintain RBS files – or Sorbet annotations and RBI files – in parallel to my Ruby code.</div><div><br></div><h1><strong>Conclusions</strong></h1><div>I suspect that my exposure to fancy Java IDEs early in my career made my mental model for editing code incompatible with plain editors. I seem to be among the few suffering from this syndrome in the Rails world, so I am not entirely sure this is positive, but it is what it is.</div><div><br></div><div>Because I like it, I follow this space with interest. Stripe and Shopify have made some outstanding contributions with Sorbet and all its surrounding ecosystem of tools. I’m particularly interested in the tooling built around Visual Studio code that does not depend on Sorbet, such as <a href="https://github.com/Shopify/ruby-lsp">ruby-lsp</a>. With companies of this caliber investing in the tooling, I expect things to improve quickly in the near future. </div><div><br></div><div>If you have managed to enjoy these IDE-like features with Rails in your plain editor, I would appreciate if you <a href="mailto:jorge@hey.com">let me know</a> how. I know there are alternatives, but I haven't seen anything that can remotely compare to what RubyMine offers out of the box.</div><div><br>---<br><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a><br><em>Photo by </em><a href="https://unsplash.com/@patrickian4?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Patrick Fore</em></a><em> on </em><a href="https://unsplash.com/s/photos/typewriter?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Unsplash</em></a></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/241252022-11-15T10:58:00Z2024-01-19T23:38:50ZWhat defamation looks like<div class="trix-content">
<div>Last Sunday a friend shared with me <a href="https://twitter.com/kaspth/status/1591539139211628545">this Twitter thread by Kasper Timm Hansen</a>:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2022-11-15 at 09.15.26@2x.png" title="Download CleanShot 2022-11-15 at 09.15.26@2x.png" data-click-proxy-target="lightbox_link_blob_1011628201" href="https://world.hey.com/jorge/ea17ac2b/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCS2s0VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--fdf2202277fd951542edfdcda9667d4e2fcb8806/CleanShot%202022-11-15%20at%2009.15.26@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCS2s0VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--fdf2202277fd951542edfdcda9667d4e2fcb8806/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202022-11-15%20at%2009.15.26@2x.png" alt="CleanShot 2022-11-15 at 09.15.26@2x.png" srcset="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCS2s0VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--fdf2202277fd951542edfdcda9667d4e2fcb8806/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202022-11-15%20at%2009.15.26@2x.png 2x, https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCS2s0VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--fdf2202277fd951542edfdcda9667d4e2fcb8806/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202022-11-15%20at%2009.15.26@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>He confirmed later in the thread he was referring to me as the author of <a href="https://dev.37signals.com/vanilla-rails-is-plenty">the Vanilla Rails article</a> I recently published in the 37signals dev blog.<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2022-11-18 at 16.36.09@2x.png" title="Download CleanShot 2022-11-18 at 16.36.09@2x.png" data-click-proxy-target="lightbox_link_blob_1016319169" href="https://world.hey.com/jorge/ea17ac2b/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTUhNa3p3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--3dc471dbc352c4fece119745aee2ac1daf0044f6/CleanShot%202022-11-18%20at%2016.36.09@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTUhNa3p3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--3dc471dbc352c4fece119745aee2ac1daf0044f6/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202022-11-18%20at%2016.36.09@2x.png" alt="CleanShot 2022-11-18 at 16.36.09@2x.png" srcset="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTUhNa3p3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--3dc471dbc352c4fece119745aee2ac1daf0044f6/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202022-11-18%20at%2016.36.09@2x.png 2x, https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTUhNa3p3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--3dc471dbc352c4fece119745aee2ac1daf0044f6/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202022-11-18%20at%2016.36.09@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br>Kasper starts by presenting a history of friction during PR reviews between us, where he was careful with words, but from time to time, I called him out on tone, making him feel guilty:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2022-11-15 at 09.18.19@2x.png" title="Download CleanShot 2022-11-15 at 09.18.19@2x.png" data-click-proxy-target="lightbox_link_blob_1011628398" href="https://world.hey.com/jorge/ea17ac2b/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCRzQ1VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--b700f2aeb38eac17f0af3ac1c3f07c5fca865ebf/CleanShot%202022-11-15%20at%2009.18.19@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCRzQ1VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--b700f2aeb38eac17f0af3ac1c3f07c5fca865ebf/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202022-11-15%20at%2009.18.19@2x.png" alt="CleanShot 2022-11-15 at 09.18.19@2x.png" srcset="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCRzQ1VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--b700f2aeb38eac17f0af3ac1c3f07c5fca865ebf/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202022-11-15%20at%2009.18.19@2x.png 2x, https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCRzQ1VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--b700f2aeb38eac17f0af3ac1c3f07c5fca865ebf/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202022-11-15%20at%2009.18.19@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>What really happened was a single episode. I was pretty new in the company and was working on a very early Proof of Concept for <a href="https://guides.rubyonrails.org/active_record_encryption.html">Active Record Encryption</a>. Kasper did a review; I took action on some advice and pushed back on some other. Kasper replied to my pushback in a rude and very condescending way. I didn’t say anything to anyone, but my manager noticed. He recommended that I talk to Kasper, so we didn’t send the message that this was an acceptable way of communicating in the company.</div><div><br></div><div>I agreed with his advice and sent this message to Kasper via a private ping. I am only sharing my words here out of respect for these being private conversations:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2022-11-15 at 09.24.59@2x.png" title="Download CleanShot 2022-11-15 at 09.24.59@2x.png" data-click-proxy-target="lightbox_link_blob_1011628551" href="https://world.hey.com/jorge/ea17ac2b/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQWM2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--cebfd431b9d9738ae8795fee92acb846f6134af1/CleanShot%202022-11-15%20at%2009.24.59@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQWM2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--cebfd431b9d9738ae8795fee92acb846f6134af1/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202022-11-15%20at%2009.24.59@2x.png" alt="CleanShot 2022-11-15 at 09.24.59@2x.png" srcset="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQWM2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--cebfd431b9d9738ae8795fee92acb846f6134af1/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202022-11-15%20at%2009.24.59@2x.png 2x, https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQWM2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--cebfd431b9d9738ae8795fee92acb846f6134af1/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202022-11-15%20at%2009.24.59@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>I presented the problem with his tone privately, ensuring I showed my appreciation for his help, making it clear I didn’t consider this a big deal, and offering to talk about it if he preferred. The irony here is that I was a newcomer in the company, and I was treated harshly by him. I would never use <em>abuse</em> to refer to the way Kasper treated me in that PR, but, if there was one, I was the victim there. In over three years working at 37signals, this has been the only problem of this nature I have had with him or anyone. Again, it was a single episode; we went to work without any similar problem for over one year until he left.</div><div><br></div><div>The next episode he narrates to show how I was abusive is even more bizarre:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2022-11-15 at 09.30.57@2x.png" title="Download CleanShot 2022-11-15 at 09.30.57@2x.png" data-click-proxy-target="lightbox_link_blob_1011628649" href="https://world.hey.com/jorge/ea17ac2b/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCR2s2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f4888895020acc9b365a3d80cce951373bc1e591/CleanShot%202022-11-15%20at%2009.30.57@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCR2s2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f4888895020acc9b365a3d80cce951373bc1e591/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202022-11-15%20at%2009.30.57@2x.png" alt="CleanShot 2022-11-15 at 09.30.57@2x.png" srcset="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCR2s2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f4888895020acc9b365a3d80cce951373bc1e591/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202022-11-15%20at%2009.30.57@2x.png 2x, https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCR2s2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f4888895020acc9b365a3d80cce951373bc1e591/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202022-11-15%20at%2009.30.57@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br><br></div><div>This is what really happened: one day, Kasper shared privately <a href="https://github.com/rails/rails/pull/39929">this PR</a> with me, pointing out he hoped it would help me with the encryption work. I didn’t reply with 👍, but with <em>oh wonderful, thanks for the heads up</em>, not that I think that matters much. <br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2022-11-15 at 09.32.36@2x.png" title="Download CleanShot 2022-11-15 at 09.32.36@2x.png" data-click-proxy-target="lightbox_link_blob_1011628728" href="https://world.hey.com/jorge/ea17ac2b/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTGc2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c6ed509139e0abf29babbd0cc38a5000a5e84511/CleanShot%202022-11-15%20at%2009.32.36@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTGc2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c6ed509139e0abf29babbd0cc38a5000a5e84511/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202022-11-15%20at%2009.32.36@2x.png" alt="CleanShot 2022-11-15 at 09.32.36@2x.png" srcset="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTGc2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c6ed509139e0abf29babbd0cc38a5000a5e84511/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202022-11-15%20at%2009.32.36@2x.png 2x, https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTGc2VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--c6ed509139e0abf29babbd0cc38a5000a5e84511/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202022-11-15%20at%2009.32.36@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>I didn’t see a direct application for the PR at that moment. Four months later, I found a scenario where I could leverage that new system for attribute types to solve a problem I had. This is the <a href="https://github.com/basecamp/rails/commit/75929dfb1c0464eabb6e79bd326a6edde4ed1def">commit where I used the system</a>, which included a mention to Kasper. I then offered my appreciation to Kasper via private ping:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2022-11-15 at 11.55.29@2x.png" title="Download CleanShot 2022-11-15 at 11.55.29@2x.png" data-click-proxy-target="lightbox_link_blob_1011643863" href="https://world.hey.com/jorge/ea17ac2b/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTmQxVER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--0cf0ca5e39ea8ede7e448e82b6ddd5ad622e561a/CleanShot%202022-11-15%20at%2011.55.29@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTmQxVER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--0cf0ca5e39ea8ede7e448e82b6ddd5ad622e561a/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202022-11-15%20at%2011.55.29@2x.png" alt="CleanShot 2022-11-15 at 11.55.29@2x.png" srcset="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTmQxVER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--0cf0ca5e39ea8ede7e448e82b6ddd5ad622e561a/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202022-11-15%20at%2011.55.29@2x.png 2x, https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTmQxVER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--0cf0ca5e39ea8ede7e448e82b6ddd5ad622e561a/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202022-11-15%20at%2011.55.29@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>That’s where the reference to Obi-Wan comes from: a metaphor from Obi-Wan’s spirit telling Luke to use the force. </div><div><br></div><div>And I also showed gratitude to him in my public checkin in the company:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2022-11-15 at 09.40.01@2x.png" title="Download CleanShot 2022-11-15 at 09.40.01@2x.png" data-click-proxy-target="lightbox_link_blob_1011628940" href="https://world.hey.com/jorge/ea17ac2b/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCSXc3VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--800f646c9b0b8c24e1d492162e1c1ec439ea5c97/CleanShot%202022-11-15%20at%2009.40.01@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCSXc3VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--800f646c9b0b8c24e1d492162e1c1ec439ea5c97/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202022-11-15%20at%2009.40.01@2x.png" alt="CleanShot 2022-11-15 at 09.40.01@2x.png" srcset="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCSXc3VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--800f646c9b0b8c24e1d492162e1c1ec439ea5c97/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202022-11-15%20at%2009.40.01@2x.png 2x, https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCSXc3VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--800f646c9b0b8c24e1d492162e1c1ec439ea5c97/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202022-11-15%20at%2009.40.01@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>To my perplexity, Kasper considers this interaction, where I only showed gratitude towards him, proof of my abusive behavior.</div><div><br></div><div>He then goes to link this older tweet with my persona:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="CleanShot 2022-11-15 at 09.44.46@2x.png" title="Download CleanShot 2022-11-15 at 09.44.46@2x.png" data-click-proxy-target="lightbox_link_blob_1011629083" href="https://world.hey.com/jorge/ea17ac2b/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQnM4VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--591261987a00e3855808d06ba9e4331a8b8f81da/CleanShot%202022-11-15%20at%2009.44.46@2x.png?disposition=attachment">
<img src="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQnM4VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--591261987a00e3855808d06ba9e4331a8b8f81da/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/CleanShot%202022-11-15%20at%2009.44.46@2x.png" alt="CleanShot 2022-11-15 at 09.44.46@2x.png" srcset="https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQnM4VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--591261987a00e3855808d06ba9e4331a8b8f81da/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/CleanShot%202022-11-15%20at%2009.44.46@2x.png 2x, https://world.hey.com/jorge/ea17ac2b/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQnM4VER3PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--591261987a00e3855808d06ba9e4331a8b8f81da/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/CleanShot%202022-11-15%20at%2009.44.46@2x.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>I think that tweet was referring to other person and other project, and that he recycled the tweet for me, to sustain the deceptive narrative. I never got to work with Kasper side by side on any project. He worked on product, I worked on infrastructure, and never got to collaborate closely on anything meaningful. He did a bunch of PR reviewing for me and for other folks. I always found those valuable and showed appreciation publicly and privately.</div><div><br></div><div>Through the rest of the Twitter thread he goes to refer to me as <a href="https://twitter.com/kaspth/status/1591539155913347072"><em>this asshole</em></a>, calling me a <a href="https://twitter.com/kaspth/status/1591539157612261377">mediocre programmer without ideas of my own</a>, a <em>parrot of stuff that comes from the top</em>, and <a href="https://twitter.com/kaspth/status/1592167684195733505">someone whose writings make him feel <em>creeped out</em></a>. All these are regular hateful Twitter comments and I can tolerate them without problem. But that’s not the case with being accused of being abusive towards a colleague. That's a quite different story.</div><div><br></div><div>There are a lot of actual victims of abuse in the working place. Abuse can happen in a thousand of subtle ways, so while I can debunk specific episodes easily, being accused of being abusive in public causes a tremendous damage to my reputation. A damage I can’t fully undo. Someone that doesn’t know me, even reading what I have to say, can be left with fair concerns about how is my behavior when working with others. There is a reason why defamation is a crime in many countries. </div><div><br></div><div>I would like to say that I am OK with all this, but the reality is that it has affected me deeply. I know it’s a false accusation sustained on blatant lies, and I have already received unconditional support from people who know me, but I am far from being OK right now. </div><div><br></div><div>It’s evident that Kasper is in a bad place, and I can be compassionate about that. I hope he finds a way out of it, and I would be more than willing to have a private conversation if that could help in any way. I was very close to letting this slide, which was the advice I received from everyone. But seeing Kasper's behavior, I'm concerned that this will become a thing where he refers to that thread about how abusive I am whenever I do something relevant (the trigger here was writing a technical post).</div><div><br></div><div>My only desire right now is to put this behind me, but I won’t stay silent or quiet at this level of defaming. </div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/240082022-11-08T22:06:54Z2022-12-23T22:15:26ZCode I like (IV): Vanilla Rails is Plenty<div class="trix-content">
<div><em>This article was originally published in the </em><a href="https://dev.37signals.com"><em>new 37signals dev blog</em></a><em>. </em><a href="https://dev.37signals.com/vanilla-rails-is-plenty"><em>Read it there</em></a><em>. <br></em><br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="image.png" title="Download image.png" data-click-proxy-target="lightbox_link_blob_1004354249" href="https://world.hey.com/jorge/71d0465c/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTWs2M1RzPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--1f0b6bbd7ce54d90d3d1b57a0e7e8590fe7405fb/image.png?disposition=attachment">
<img src="https://world.hey.com/jorge/71d0465c/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTWs2M1RzPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--1f0b6bbd7ce54d90d3d1b57a0e7e8590fe7405fb/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/image.png" alt="image.png" srcset="https://world.hey.com/jorge/71d0465c/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTWs2M1RzPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--1f0b6bbd7ce54d90d3d1b57a0e7e8590fe7405fb/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/image.png 2x, https://world.hey.com/jorge/71d0465c/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTWs2M1RzPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--1f0b6bbd7ce54d90d3d1b57a0e7e8590fe7405fb/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/image.png 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>I have often heard this: vanilla Rails can only get you so far. At some point, apps become unmaintainable, and you need a different approach that brings the missing pieces, because Rails encourages a poor separation of concerns at the architectural level.<br><br></div><div>The seminal <a href="https://www.oreilly.com/library/view/domain-driven-design-tackling/0321125215/">Domain Driven Design (DDD) book</a> discusses these four conceptual layers: presentation, application, domain, and infrastructure. The application layer implements business tasks by coordinating work with the domain layer. But Rails only offers <em>controllers</em> and <em>models</em>: models include persistence via Active Record, and Rails encourages direct access to them from controllers. Critics argue that the application, domain, and infrastructure layers inevitably merge into a single mess of fat models. Indeed, alternatives always include additional buckets, such as services or use case interactors, at the application layer, or repositories, at the infrastructure layer.<br><br></div><div>I find this discussion fascinating because here at <a href="https://37signals.com/">37signals</a> we are big fans of both vanilla Rails and Domain Driven Design. We don’t run into the alleged maintenance problems when evolving our apps, so here I would like to discuss how we organize our application code.</div><div><br></div><h1><strong>We don’t distinguish application and domain layers</strong></h1><div>We don’t separate application-level and domain-level artifacts. Instead, we have a set of domain models (both Active Records and POROs) exposing public interfaces to be invoked from the system boundaries, typically controllers or jobs. We don’t separate that API from the domain model, architecturally speaking.</div><div><br>We care a lot about how we design these models and the API they expose, we just find little value in an additional layer to orchestrate the access to them.<br><br></div><div>In other words, we don’t default to create services, actions, commands, or interactors to implement controller actions.</div><div><br></div><h1><strong>Controllers access domain models directly</strong></h1><div>We are fine with plain CRUD accesses from controllers for simple scenarios. For example, this is how we create boosts for messages and comments in Basecamp:<br><br></div><pre><strong>class</strong> <strong>BoostsController</strong> <strong><</strong> ApplicationController
<strong>def</strong> <strong>create</strong>
@boost <strong>=</strong> @boostable.<strong>boosts</strong>.<strong>create!</strong>(content: params[:boost][:content])
<strong>end</strong></pre><div><br>But more often, we perform these accesses through methods exposed by domain models. For example, this is the controller to select the desired box for a given contact in HEY:<br><br></div><pre><strong>class</strong> <strong>Boxes::DesignationsController</strong> <strong><</strong> ApplicationController
<strong>include</strong> BoxScoped
before_action :set_contact, only: :create
<strong>def</strong> <strong>create</strong>
@contact.<strong>designate_to</strong>(@box)
respond_to <strong>do</strong> <strong>|</strong>format<strong>|</strong>
format.<strong>html</strong> { refresh_or_redirect_back_or_to @contact, notice: "Changes saved. This might take a few minutes to complete." }
format.<strong>json</strong> { head :created }
<strong>end</strong>
<strong>end</strong></pre><div><br>Most of our controllers use this approach of accessing models directly: a model exposes a method, and the controller invokes it.</div><div><br></div><h1><strong>Rich domain models</strong></h1><div>As opposed to <a href="https://martinfowler.com/bliki/AnemicDomainModel.html">anemic domain models</a>, our approach encourages building <em>rich</em>domain models. We think of domain models as our application API and, as a guiding design principle, we want that one to feel as natural as possible.</div><div><br>Because we like to access business logic via domain models, some core domain entities end up offering much functionality. How do we avoid the issues related to the dreaded <em>fat model</em> problem? With two tactics:<br><br></div><ul><li>Using concerns to organize model’s code.</li><li>Delegating functionality to additional systems of objects (AKA using plain object-oriented programming).</li></ul><div><br>I’ll clarify with an example. A core domain entity in Basecamp is a <em>Recording</em>. Most elements a user manages in Basecamp are recordings — the original use case that motivated <a href="https://api.rubyonrails.org/classes/ActiveRecord/DelegatedType.html">Rails’ delegated types</a>.</div><div><br>You can do many things with recordings, including copying them to other places or incinerating them. <em>Incineration</em> is the <a href="https://world.hey.com/jorge/code-i-like-i-domain-driven-boldness-71456476">term we use</a> for “deleting data for good”. For the caller — e.g., controller or job — we want to offer a natural API:</div><div><br></div><pre>recording.<strong>incinerate</strong>
recording.<strong>copy_to</strong>(destination_bucket)</pre><div><br>But, on the inside, incinerating data and copying are pretty different responsibilities. So we use concerns to capture each:</div><div><br></div><pre><strong>class</strong> <strong>Recording</strong> <strong><</strong> ApplicationRecord
<strong>include</strong> Incineratable, Copyable
<strong>end</strong>
<strong>module</strong> Recording::Incineratable
<strong>def</strong> <strong>incinerate</strong>
<em># ...</em>
<strong>end</strong>
<strong>end</strong>
<strong>module</strong> Recording::Copyable
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
included <strong>do</strong>
has_many :copies, foreign_key: :source_recording_id
<strong>end</strong>
<strong>def</strong> <strong>copy_to</strong>(bucket, parent: <strong>nil</strong>)
<em># ...</em>
<strong>end</strong>
<strong>end</strong></pre><div><br>I wrote an article about <a href="https://world.hey.com/jorge/code-i-like-iii-good-concerns-5a1b391c">how we use concerns here</a>, if you are interested.</div><div><br>Now, incineration and copying are involved operations. <em>Recording</em> wouldn’t be a good spot to implement those. Instead, it delegates the work itself to additional systems of objects.</div><div><br>For incinerating, <em>Recording::Incineratable</em> creates and executes a Recording::Incineration, which encapsulates the logic of incinerating a recording:</div><div><br></div><pre><strong>module</strong> Recording::Incineratable
<strong>def</strong> <strong>incinerate</strong>
Incineration.<strong>new</strong>(self).<strong>run</strong>
<strong>end</strong>
<strong>end</strong></pre><div><br>For copying,<em> Recording::Copyable</em> creates a new <em>Copy</em> record.<br><br></div><pre><strong>module</strong> Recording::Copyable
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
included <strong>do</strong>
has_many :copies, foreign_key: :source_recording_id
<strong>end</strong>
<strong>def</strong> <strong>copy_to</strong>(bucket, parent: <strong>nil</strong>)
copies.<strong>create!</strong> destination_bucket: bucket, destination_parent: parent
<strong>end</strong>
<strong>end</strong></pre><div><br>Here, things are more complex: Copy is a child of Filing. <em>Filing</em> is a common parent class for both the <em>copy</em> and <em>move</em> operations. When a filing is created, it enqueues a job that will eventually invoke its <em>#process</em> method. That method invokes <em>file_recording</em>, a <a href="https://en.wikipedia.org/wiki/Template_method_pattern">template method</a> to be implemented by child classes. When implementing that method, Copy creates a <em>Recording::Copier</em> instance to perform the copy.<br><br></div><pre><strong>module</strong> Recording::Copyable
<strong>extend</strong> ActiveSupport<strong>::</strong>Concern
included <strong>do</strong>
has_many :copies, foreign_key: :source_recording_id
<strong>end</strong>
<strong>def</strong> <strong>copy_to</strong>(bucket, parent: <strong>nil</strong>)
copies.<strong>create!</strong> destination_bucket: bucket, destination_parent: parent
<strong>end</strong>
<strong>end</strong>
<strong>class</strong> <strong>Copy</strong> <strong><</strong> Filing
<strong>private</strong>
<strong>def</strong> <strong>file_recording</strong>
Current.<strong>set</strong>(person: creator) <strong>do</strong>
Recording<strong>::</strong>Copier.<strong>new</strong>(
source_recording: source_recording,
destination_bucket: destination_bucket,
destination_parent: destination_parent,
filing: self
).<strong>copy</strong>
<strong>end</strong>
<strong>end</strong>
<strong>end</strong>
<strong>class</strong> <strong>Filing</strong> <strong><</strong> ApplicationRecord
after_create_commit :process_later, unless: :completed?
<strong>def</strong> <strong>process_later</strong>
FilingJob.<strong>perform_later</strong> self
<strong>end</strong>
<strong>def</strong> <strong>process</strong>
<em># ...</em>
file_recording
<em># ...</em>
<strong>end</strong>
<strong>end</strong></pre><div><br>This example is not as simple as the incineration one, but the principle is the same: rich internal object models hidden behind high-level APIs on domain models. This doesn’t mean we always create additional classes to implement concerns, far from it, but we do when complexity justifies it.<br><br></div><div>Along with concerns, this makes the approach of classes with a large API surface work. If you are thinking about the Single Responsibility Principle (SRP), as Michael Feathers says in <a href="https://www.oreilly.com/library/view/working-effectively-with/0131177052/">Working effectively with Legacy code</a> you must differentiate between SRP violations at the interface level or the implementation level:<br><br></div><blockquote><em>The SRP violation we care more about is violation at the implementation level. Plainly put, we care whether the class really does all of that stuff or whether it just delegates to a couple of other classes. If it delegates, we don’t have a large monolithic class; we just have a class that is a facade, a front end for a bunch of little classes and that can be easier to manage.</em></blockquote><div><br>In our example, there are no fat models in charge of doing too many things. <em>Recording::Incineration</em> or <em>Recording::Copier</em> are cohesive classes that do one thing. <em>Recording::Copyable</em> adds a high-level <em>#copy_to</em> method to <em>Recording</em>’s public API and keeps the related code and data definitions separated from other <em>Recording</em> responsibilities. Also, notice how this is just good old object orientation with Ruby: inheritance, object composition, and a simple design pattern.<br><br>On a final note, one could argue that these three are equivalent:<br><br></div><pre>recording.<strong>incinerate</strong>
Recording<strong>::</strong>Incineration.<strong>new</strong>(recording).<strong>run</strong>
Recording<strong>::</strong>IncinerationService.<strong>execute</strong>(recording)</pre><div><br>We don’t believe they are: we strongly prefer the first form. On one side, it does a better job of hiding complexity, as it doesn’t shift the burden of composition to the caller of the code. On the other, it feels more natural, like plain English. It feels more Ruby.<br><br></div><h1><strong>What about services?</strong></h1><div>One of the building blocks of DDD is <em>services</em>, which are meant to <em>“capture important domain operations that can’t find a natural home in a domain entity or value object”</em>.<br><br></div><div>We don’t use services as first-class architectural artifacts in the DDD sense (stateless, named after a verb), but we have many classes that exist to encapsulate operations. We don’t call those <em>services</em> and they don’t receive special treatment. We usually prefer to present them as domain models that expose the needed functionality instead of using a mere procedural syntax to invoke the operation.<br><br></div><div>For example, this is the code for signing up a new user in Basecamp via invitation tokens:<br><br></div><pre><strong>class</strong> <strong>Projects::InvitationTokens::SignupsController</strong> <strong><</strong> Projects<strong>::</strong>InvitationTokens<strong>::</strong>BaseController
<strong>def</strong> <strong>create</strong>
@signup <strong>=</strong> Project<strong>::</strong>InvitationToken<strong>::</strong>Signup.<strong>new</strong>(signup_params)
<strong>if</strong> @signup.<strong>valid?</strong>
claim_invitation @signup.<strong>create_identity!</strong>
<strong>else</strong>
redirect_to invitation_token_join_url(@invitation_token), alert: @signup.<strong>errors</strong>.<strong>first</strong>.<strong>message</strong>
<strong>end</strong>
<strong>end</strong>
<strong>end</strong>
<strong>class</strong> <strong>Project::InvitationToken::Signup</strong>
<strong>include</strong> ActiveModel<strong>::</strong>Model
<strong>include</strong> ActiveModel<strong>::</strong>Validations<strong>::</strong>Callbacks
attr_accessor :name, :email_address, :password, :time_zone_name, :account
validate :validate_email_address, :validate_identity, :validate_account_within_user_limits
<strong>def</strong> <strong>create_identity!</strong>
<em># ...</em>
<strong>end</strong>
<strong>end</strong></pre><div><br>So instead of having a <em>SigningUpService</em> in charge of the “<em>signing up</em>” domain operation, we have a <em>Signup</em> class that lets you validate and create an identity in the app. One can argue this is just a bit of syntax sugar away from being a service or even a form object. But, as I see it, it’s just plain object orientation with Ruby to give a domain concept a proper representation in code.<br><br></div><div>Also, we don’t make a big deal of distinguishing whether a domain model is persisted or not (Active record or PORO). From the business logic consumer’s point of view, that’s irrelevant, so we don’t capture the distinction between domain entities and value objects in code. They are both domain models to us. You can find many POROs in our <em>app/models</em> folders.<br><br></div><h1><strong>The dangers of isolating the application layer</strong></h1><div>My main problem with the idea of an isolated application layer is that people often take it way too far.<br><br></div><div>The original DDD book warns about the problem of abusing services:<br><br></div><blockquote>Now, the more common mistake is to give up too easily on fitting the behavior into an appropriate object, gradually slipping towards procedural programming.</blockquote><div><br>And you can find the same advice in <a href="https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/ref=sr_1_1?keywords=implementing+domain+driven+design&qid=1667229833&qu=eyJxc2MiOiIxLjM4IiwicXNhIjoiMC45OCIsInFzcCI6IjAuOTcifQ%3D%3D&sr=8-1&ufe=app_do%3Aamzn1.fos.006c50ae-5d4c-4777-9bc0-4513d670b6bc">Implementing Domain Driven Design</a>:<br><br></div><blockquote>Don’t lean too heavily toward modeling a domain concept as a Service. Do so only if the circumstances fit. If we aren’t careful, we might start to treat Services as our modeling “silver bullet.” Using Services overzealously will usually result in the negative consequences of creating an Anemic Domain Model, where all the domain logic resides in Services rather than mostly spread across Entities and Value Objects.</blockquote><div><br>Both books discuss the challenges of isolating the application layer, starting with the nuances of differentiating domain and application services. Furthermore, they acknowledge that most layered DDD architectures are <em>relaxed</em>, with the presentation layer sometimes accessing the domain layer directly. The original DDD book states that what enables DDD is the <em>crucial separation of the domain layer</em>, noting that some projects <em>don’t make a sharp distinction between the user interface and the application layers</em>.</div><div><br>However, in the Rails world, you often see dogmatic takes that advocate against controllers directly talking to models with tremendous conviction. Instead, there should be an intermediary object to mediate between both — e.g. an application service from DDD or an interactor from <a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html">Clean Architecture</a>. I believe such nuance-free recommendations favor the appearance of either:<br><br></div><ul><li>Tons of boilerplate code because many of these application-level elements simply delegate the operation to some domain entity. Remember that the application layer should not contain business rules. It just coordinates and delegates work to domain objects in the layer below.</li><li>An <a href="https://martinfowler.com/bliki/AnemicDomainModel.html">anemic domain model</a>, where the application-level elements are the ones implementing the business rules, and domain models become empty shells carrying data around.</li></ul><div><br>These approaches are often presented as a <a href="https://world.hey.com/jorge/no-silver-buckets-84d249d5">tradeoff-free answer</a> to a very complex problem: how to design software properly. They often imply that good architecture happens as a consequence of using a discrete set of archetypes, which is not only very naive but incredibly misleading for the inexperienced audience. I hope that the alternative I presented here resonates with people looking for more pragmatic alternatives.<br><br></div><h1><strong>Conclusions</strong></h1><div>In our experience, this approach with vanilla Rails results in maintainable large Rails applications. As a recent example, we just launched <a href="https://basecamp.com/new">Basecamp 4</a> built on top of Basecamp 3, the codebase of which is almost 9 years old, includes 400 controllers and 500 models, and serves millions of users every day. I don’t know if our approach would work at Shopify scale, but I am sure it would for most businesses using Rails out there.<br><br></div><div>Our approach reflects one of the Rails’ doctrine pillars: <a href="https://rubyonrails.org/doctrine#no-one-paradigm">No one paradigm</a>. I love architectural patterns, but a recurring problem in our industry is that people get very dogmatic when translating those into code. I think the reason is that simple strict recipes are very appealing when tackling a problem as complex as software development. 37signals’ code is the best I’ve seen in my career, and I say that as a spectator since I haven’t written most of it. In particular, it is the best incarnation of DDD principles I’ve seen, even if it doesn’t use most of its building blocks.<br><br></div><div>So if you abandoned the vanilla Rails path and now you are wondering if you really need those additional boilerplate classes whenever you need to handle some screen interaction, be sure that there is an alternative path that won’t compromise the maintainability of your application. It won’t prevent you from having to know how to write software — no alternative will — but it might bring you back to happy territory again.</div><div><br>---<br><br><em>Thanks to </em><a href="http://quotedprintable.com/"><em>Jeffrey Hardy</em></a><em> for his valuable feedback as I wrote this article. He’s one of the main contributors to this approach of architecting vanilla Rails applications I’ve come to learn and love.<br><br>This article belongs to a series of posts on Rails design techniques called </em><a href="https://www.jorgemanrubia.com/code-i-like/"><strong><em>Code I like</em></strong></a>.<br><br>Other articles in this series:<br><br></div><ul><li><a href="https://world.hey.com/jorge/code-i-like-i-domain-driven-boldness-71456476">Domain-driven boldness</a></li><li><a href="https://world.hey.com/jorge/code-i-like-ii-fractal-journeys-b7688f93">Fractal journeys</a></li><li><a href="https://world.hey.com/jorge/code-i-like-iii-good-concerns-5a1b391c">Good concerns</a></li></ul><div><br><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a> </div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/235212022-10-15T08:58:40Z2022-11-27T00:12:03ZOnline discussions, the good parts<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="discussions.jpg" title="Download discussions.jpg" data-click-proxy-target="lightbox_link_blob_975922712" href="https://world.hey.com/jorge/8502cc44/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQmhtS3pvPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--edd1c44c69855a500c4456fd0b40c53e2529b6aa/discussions.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/8502cc44/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQmhtS3pvPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--edd1c44c69855a500c4456fd0b40c53e2529b6aa/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/discussions.jpg" alt="discussions.jpg" srcset="https://world.hey.com/jorge/8502cc44/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQmhtS3pvPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--edd1c44c69855a500c4456fd0b40c53e2529b6aa/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/discussions.jpg 2x, https://world.hey.com/jorge/8502cc44/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQmhtS3pvPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--edd1c44c69855a500c4456fd0b40c53e2529b6aa/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/discussions.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>Some recent positive experiences made me think about the source of my distaste for online discussions. The blame is on me: I assimilated <em>online</em> to social media or online comments in the wild.</div><div><br></div><div>I can’t imagine a worse discussion platform than Twitter. Any meaningful discussion requires nuance, which Twitter eliminates by design. Making points via headlines while everyone is watching and scoring is precisely the recipe for the disaster Twitter is as a conversation platform.</div><div><br></div><div><a href="https://www.hey.com/world/">HEY World</a> was the main culprit behind my mind shift. Many people have recently reached out via email regarding my recent posts. Some people wanted to know more, others wanted to share a different point of view, and others just wanted to show appreciation. The common denominator for these interactions is that they were friendly and enriching.</div><div><br></div><div>Email gives you plenty of space to interchange arguments and nuance, which is the mandatory ingredient for a productive conversation. And email is private. When nobody is watching, I think most humans default to behave as they would in real life with a stranger, regulating tone and manners accordingly. What’s the point in showing yourself outraged, ironic, harsh, or as a virtue-signal dispenser when there is no crowd to applaud you or to boo the other part? These days, when someone reaches out via social media with a juicy question, I always ask for an email privately. I never regret doing this.</div><div><br></div><div>I’ve recently had positive experiences on Reddit. Reddit discussions are usually nice and on-topic, with most people sharing different points of view respectfully. I think a key factor is that the platform is strongly moderated both implicitly, via up and down votes, and explicitly. <a href="https://www.reddit.com/r/rails/comments/y094y6/comment/irqpy9m/?utm_source=reddit&utm_medium=web2x&context=3">Harsh tweet-like comments</a> quickly disappear via downvotes, and <a href="https://www.reddit.com/r/programming/comments/xmsklw/comment/ipqjg9g/?utm_source=reddit&utm_medium=web2x&context=3">good questions</a> and <a href="https://www.reddit.com/r/programming/comments/xmsklw/comment/ipqqw88/?utm_source=reddit&utm_medium=web2x&context=3">thoughtful</a> <a href="https://www.reddit.com/r/programming/comments/xmsklw/comment/ipqts7d/?utm_source=reddit&utm_medium=web2x&context=3">answers</a> get promoted via upvotes. Off-topic or hateful comments are often directly moderated away. I still have my doubts about public discussion spaces, but Reddit makes for a pretty decent one.</div><div><br></div><div>So good old email and Reddit, go figure! They have been around forever; I just discovered them now. Social media might look like an expanding gas when discussing things, but you can choose to go to breathe somewhere else. Internet is huge! I think David <a href="https://world.hey.com/dhh/email-is-the-antidote-d9d53c10">nails it here</a>:</div><div><br></div><blockquote>But social media is not the internet. It’s merely one distorted expression of it.</blockquote><div><br></div><div>If you ever feel like reaching out, please do: <a href="mailto:jorge@hey.com">jorge@hey.com</a>. And if you are receiving these via email, replying works too.</div><div><br></div><div>---</div><div><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a> </div><div><em>Photo by </em><a href="https://unsplash.com/@lunarts?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Volodymyr Hryshchenko</em></a><em> on </em><a href="https://unsplash.com/s/photos/discussion?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Unsplash</em></a></div><div><br></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/234562022-10-10T07:55:10Z2022-12-17T17:28:15ZCode I like (III): Good concerns<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="vardan-papikyan-JzE1dHEaAew-unsplash.jpg" title="Download vardan-papikyan-JzE1dHEaAew-unsplash.jpg" data-click-proxy-target="lightbox_link_blob_968301004" href="https://world.hey.com/jorge/5a1b391c/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTXdadHprPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--6aaa787a1e5748d498d0af173b63006dce23e362/vardan-papikyan-JzE1dHEaAew-unsplash.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/5a1b391c/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTXdadHprPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--6aaa787a1e5748d498d0af173b63006dce23e362/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/vardan-papikyan-JzE1dHEaAew-unsplash.jpg" alt="vardan-papikyan-JzE1dHEaAew-unsplash.jpg" srcset="https://world.hey.com/jorge/5a1b391c/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTXdadHprPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--6aaa787a1e5748d498d0af173b63006dce23e362/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/vardan-papikyan-JzE1dHEaAew-unsplash.jpg 2x, https://world.hey.com/jorge/5a1b391c/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCTXdadHprPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--6aaa787a1e5748d498d0af173b63006dce23e362/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/vardan-papikyan-JzE1dHEaAew-unsplash.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br><a href="https://api.rubyonrails.org/classes/ActiveSupport/Concern.html">Rails concerns</a> have received much criticism over the years. Are they the solution to all the problems or something to avoid at all costs? I think a problem with concerns is that you can use them however you want, so no surprise you can shoot yourself in the foot when doing it. After all, concerns are just <a href="https://ruby-doc.com/docs/ProgrammingRuby/html/tut_modules.html">Ruby mixins</a> with some syntax sugar to remove common boilerplate code.<br><br><a href="https://37signals.com">37signals</a> has years of experience using them in large Rails codebases, so I would like to share some of the design principles we use in this post.<br><br></div><h1>Where to put concerns</h1><div>Ruby mixins are often presented as an alternative to multiple inheritance: a code-reuse mechanism across classes. We use some concerns this way, but the most common scenario where we use them is to organize code within a single model. We use different conventions for each case:</div><div><br></div><ul><li>For common model concerns: we place them in “<em>app/models/concerns”</em>.</li><li>For model-specific concerns: we place them in a folder matching the model name: “<em>app/models/<model_name></em>”.</li></ul><div><br></div><div>For example, this is an example of a model-specific concern from Basecamp:<br><br></div><pre># app/models/recording.rb
class Recording < ApplicationRecord
include Completable
end
# app/models/recording/completable.rb
module Recording::Completable
extend ActiveSupport::Concern
end</pre><div><br>This convention removes the need to repeat the namespace when including the concern.<br><br>For controllers, the situation is inverted. We place most concerns in the "<em>controllers/concerns</em>" folder, with some concerns that only apply to a certain subsystem placed in a subfolder named after that: "<em>controller/concerns/<subsystem></em>". I would like to explore how we do controllers in another post.</div><div><br></div><h1>Improve <strong>readability</strong></h1><div>A common criticism of concerns is that <a href="https://www.cloudbees.com/blog/when-to-be-concerned-about-concerns">they worsen readability</a>. I think the opposite is true. When used right, they improve readability in two ways:</div><div><br></div><div>First, <strong>they help to manage complexity</strong>. The essence of dealing with complex systems is to <a href="https://world.hey.com/jorge/code-i-like-ii-fractal-journeys-b7688f93">divide them into smaller pieces over and over so we can focus on one thing at a time</a>. A concern is another tool in your toolbox to achieve precisely that.</div><div><br></div><div>The key here is that each concern should be a cohesive unit that captures a trait of the host model. In other words, they should only contain things that belong together. You should not treat concerns as arbitrary containers of behavior and structure to split a large model into smaller parts. They need to feature a genuine “<em>has trait</em>” or “<em>acts as</em>” semantics to work, just like class inheritance needs the “<em>is a</em>” relationship. They will cause more harm than good otherwise. </div><div><br></div><div>Check this example from the HEY screener I <a href="https://world.hey.com/jorge/code-i-like-i-domain-driven-boldness-71456476">talked about in the past</a>. Users in HEY act as examiners of clearance petitions from other contacts that want to send them an email:</div><div><br></div><pre>class User < ApplicationRecord
include Examiner
end
module User::Examiner
extend ActiveSupport::Concern
included do
has_many :clearances, foreign_key: "examiner_id", class_name: "Clearance", dependent: :destroy
end
def approve(contacts)
...
end
def has_approved?(contact)
...
end
def has_denied?(contact)
...
end
...
end</pre><div><br>The concern matches the domain role of an examiner of clearance petitions, and it only contains code related to that role. This enhances maintainability: the fewer concepts you need to manage at any moment, the easier things are to understand.</div><div><br></div><div>And second, <strong>concerns offer an additional abstraction to reflect domain concepts</strong>.</div><div><br></div><div>Below are the concerns included by the <em>Topic</em> model in HEY. Just like the examiner example, notice how most names capture domain concepts that are easy to grasp. They offer an additional opportunity to resemble the domain, which is a net positive regarding readability.</div><div><br></div><pre>class Topic< ApplicationRecord
include Accessible, Breakoutable, Deletable, Entries, Incineratable, Indexed, Involvable, Journal, Mergeable, Named, Nettable, Notifiable, Postable, Publishable, Preapproved, Collectionable, Recycled, Redeliverable, Replyable, Restorable, Sortable, Spam, Spanning
...</pre><div><br></div><h1><strong>Enhance, but not replace, rich object models</strong></h1><div>A common misconception with Rails concerns is that they represent an alternative to traditional object-oriented techniques, such as class inheritance or composition. Take <a href="https://medium.com/@carlescliment/about-rails-concerns-a6b2f1776d7d">this</a>:</div><div><br></div><blockquote>Business logic is better modeled as abstractions (classes), rather than concerns. Value objects, services, repositories, aggregates or whatever artifact that fits better.</blockquote><div><br></div><div>Or <a href="https://www.cloudbees.com/blog/when-to-be-concerned-about-concerns">this</a>:</div><div><br></div><blockquote><em>Favor composition<br></em><br>I’m not saying you HAVE to put everything in one file. Please, by all means, extract some logic into a custom class and call it.</blockquote><div><br></div><div>I think this is a false dichotomy. Using concerns doesn't limit or replace the need to design systems properly. In particular, you shouldn't use concerns to enable fat and flat Active Record models nicely organized instead of proper systems of objects with a good distribution of responsibilities. I know that's a real risk with concerns because I created such messes during my first experiences with them.</div><div><br></div><div>37signals is big on good old object-oriented design, inheritance and composition, design and implementation patterns, and we have POROs all over our <em>models</em> folder. Concerns actually play great with this approach. Let me illustrate this with a simple example.</div><div><br></div><div>In HEY, paid customers keep their email address reserved forever, even if they cancel their subscription. Because of that, when the system terminates an account, it chooses between fully deleting all the data (incineration) or just keeping a minimal set, such as outbound forwarding (purging). Let me show you some relevant parts of the code:<br><br></div><pre>class Account < ApplicationRecord
include Closable
end
module Account::Closable
def terminate
purge_or_incinerate if terminable?
end
private
def purge_or_incinerate
eligible_for_purge? ? purge : incinerate
end
def purge
Account::Closing::Purging.new(self).run
end
def incinerate
Account::Closing::Incineration.new(self).run
end
end</pre><div><br>Incineration and purging are involved operations that share some common code. So guess how we solve that? With additional classes encapsulating the operations and with good old inheritance to reuse common bits:<br><br> <figure class="attachment attachment--preview attachment--lightboxable attachment--png">
<a download="image.png" title="Download image.png" data-click-proxy-target="lightbox_link_blob_969368113" href="https://world.hey.com/jorge/5a1b391c/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCREZpeHprPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--9ac9b402c3e887a4891b6200cc2f627a09dc4186/image.png?disposition=attachment">
<img src="https://world.hey.com/jorge/5a1b391c/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCREZpeHprPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--9ac9b402c3e887a4891b6200cc2f627a09dc4186/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--946116ea0c454412635aa7309bd9472bf633014c/image.png" alt="image.png" srcset="https://world.hey.com/jorge/5a1b391c/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCREZpeHprPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--9ac9b402c3e887a4891b6200cc2f627a09dc4186/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--8c0bbbd8bfe72a73b222ec1c20265f8631973dc9/image.png 2x, https://world.hey.com/jorge/5a1b391c/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCREZpeHprPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--9ac9b402c3e887a4891b6200cc2f627a09dc4186/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--d2fed6b5089e475a3013538949ec7f311ac6e345/image.png 3x" decoding="async" loading="lazy">
</a>
</figure></div><div><br>I love this approach of using concerns to offer a nice domain-oriented API on models that hides a complex subsystem from the caller’s point of view. If we want to terminate an account, we can just say:<br><br></div><pre>account.terminate</pre><div><br>Versus something more verbose and less fluid like:<br><br></div><pre>AccountTerminationService.new(account).run</pre><div><br>And notice that we don't have a fat <em>Account</em> model responsible for dealing with all the logic of incinerating or purging accounts. There is a subsystem of three classes in charge of that, and the <em>Account</em> model offers just the door to use it.<br><br>Concerns enable these more concise, and nicer-looking APIs while keeping model code organized and without sacrificing what you can do regarding system design.</div><div><br></div><h1><strong>Conclusions</strong></h1><div>Concerns are a tool. I am not sure if they qualify as <a href="https://rubyonrails.org/doctrine#provide-sharp-knives">sharp</a> or if they are just too open, but they can cause trouble when misused. However, with some simple guidelines, I think they are a fantastic resource if you are a Rails programmer.</div><div><br></div><div>Concerns combined with good object-oriented design are a sweet combo. Of course, concerns won’t remove the need for having to know how to design software. Still, they are a pragmatic mechanism to improve your code organization, making it more intelligible and maintainable.</div><div><br></div><div>You often hear that vanilla Rails will only get you so far and that you need additional constructs, harnesses, and conventions on top of it. If it serves, <a href="https://basecamp.com/">Basecamp</a> and <a href="https://www.hey.com/">HEY</a> are vanilla Rails apps using traditional object orientation and patterns, and they heavily use concerns.</div><div><br>---</div><div><br></div><div><em>This article belongs to a series of posts on Rails design techniques called </em><a href="https://www.jorgemanrubia.com/code-i-like/"><strong><em>Code I like</em></strong></a>.</div><div><br><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a> <br><em>Photo by </em><a href="https://unsplash.com/@varpap?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Vardan Papikyan</em></a><em> on </em><a href="https://unsplash.com/s/photos/puzzle?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Unsplash</em></a></div>
</div>
Jorge Manrubiajorge@hey.comtag:world.hey.com,2005:World::Post/232452022-09-24T10:54:19Z2022-12-23T22:15:27ZAging programmer<div class="trix-content">
<div> <figure class="attachment attachment--preview attachment--lightboxable attachment--jpg">
<a download="oziel-gomez-x7gz40Z9ObM-unsplash-2.jpg" title="Download oziel-gomez-x7gz40Z9ObM-unsplash-2.jpg" data-click-proxy-target="lightbox_link_blob_952594452" href="https://world.hey.com/jorge/d448bdec/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQlJ3eHpnPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f579b9de96167f6689c8040e1984a393e4a86b84/oziel-gomez-x7gz40Z9ObM-unsplash-2.jpg?disposition=attachment">
<img src="https://world.hey.com/jorge/d448bdec/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQlJ3eHpnPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f579b9de96167f6689c8040e1984a393e4a86b84/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUIya0NBQVU2REhGMVlXeHBkSGxwU3pvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--0b5e092e6240e14fab357b4c1013c9a0c881ff87/oziel-gomez-x7gz40Z9ObM-unsplash-2.jpg" alt="oziel-gomez-x7gz40Z9ObM-unsplash-2.jpg" srcset="https://world.hey.com/jorge/d448bdec/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQlJ3eHpnPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f579b9de96167f6689c8040e1984a393e4a86b84/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUQya0NBQW82REhGMVlXeHBkSGxwUVRvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--a8a2bb6a9884c661ef30854054544882621a1752/oziel-gomez-x7gz40Z9ObM-unsplash-2.jpg 2x, https://world.hey.com/jorge/d448bdec/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCQlJ3eHpnPSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--f579b9de96167f6689c8040e1984a393e4a86b84/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDam9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFLQUZta0NBQTg2REhGMVlXeHBkSGxwUERvTGJHOWhaR1Z5ZXdZNkNYQmhaMlV3T2cxamIyRnNaWE5qWlZRPSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--9b7ba00c32731d121dd7ff5706d31cc3652788dd/oziel-gomez-x7gz40Z9ObM-unsplash-2.jpg 3x" decoding="async" loading="lazy">
</a>
</figure><br><br>Back in college, they told me that I would start my career writing code, but eventually, I would move to a position where I would ask others to code my designs. To celebrate that this turned out to be completely false, here are some assorted reflections as a 40-year-old programmer that looks back:</div><div><br></div><ul><li>Compared to my younger versions, I feel at my best. Of course, acquired technical knowledge helps, but experience and knowing how to approach work make a more significant difference. I think I'm much better than 15 years ago, and I hope I am much worse than 15 years from now. That feeling of continuous learning means the world to me.</li><li>Related: working with people you can learn from is a wonderful source of motivation.</li><li>I carry many of the defects I had, but now I know myself and their impact much better, so at least I can try to counter them.</li><li>My desire to manage people is at all-time lows.</li><li>My desire to discuss technical stuff with people, both to help and be helped, is at all-time highs.</li><li>I am way more predictable with my throughput.</li><li>I used to be very sensitive to tone and manners in the working place. I still am. </li><li>I've learned to offer myself a chance to reconsider technical battles. Before, I fought them all until the end; now, I happily change course early when the smell isn't right or when I run out of appetite.</li><li>When I started, I didn't spend a second thinking about time, scope, and appetite. Now, I rarely do anything where those aren't the decision-making force.</li><li>I don't enjoy switching contexts. My perfect agenda is composed of a single meaty task I can focus on for days.</li><li>Communicating effectively is a complex skill that takes years to develop, and an essential one if you want to program professionally.</li><li>I am way more cautious when deploying things.</li><li>I have no idea about how effective pair programming is. My desire to discover it is zero.</li><li>Similarly, I don't discuss the benefits of getting people in the same room to solve a problem, but I am not super interested either. </li><li>I enjoy being challenged and the feeling of not knowing how to solve a problem at first.</li><li>I am a generalist at heart. Too much infrastructure work and I miss product development. Too much backend, and I miss frontend. This comes with positives and negatives, but I accept it's just how I am wired. I never understood why some people despise the term full-stack.</li><li>After almost 10 years of remote work, it would be close to impossible for me to go back to an office.</li><li>I have come to consider accountability an essential perk. I started my career in a place where, in general, nobody cared about anything. I need exactly the opposite environment for my own sanity. </li><li>I am skeptical by default about any hot new things in the programming space. I think this can be a double-edged sword. Younger me was the opposite.</li><li>"No matter how it looks at first, it's always a people problem", by Gerald Weinberg, is essentially true and something to have very present by technically-minded people.</li></ul><div><br></div><div>I can’t agree more with the first answer to <a href="https://www.quora.com/Do-people-lose-interest-in-programming-as-they-age-Is-it-accurate-to-expect-that-older-programmers-are-slower-make-more-mistakes-and-would-rather-be-doing-something-else-such-as-managing-programmers/answer/Jeff-Kesselman">this question in Quora</a>:</div><div><br></div><blockquote>Do people lose interest in programming as they age? Is it accurate to expect that older programmers are slower, make more mistakes, and would rather be doing something else such as managing programmers?</blockquote><div><br></div><div>No, no and no.<br><br>---<br><a href="https://www.jorgemanrubia.com/">jorgemanrubia.com</a><br><em>Photo by </em><a href="https://unsplash.com/@ozgomz?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Oziel Gómez</em></a><em> on </em><a href="https://unsplash.com/s/photos/old?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText"><em>Unsplash</em></a><br><br></div>
</div>
Jorge Manrubiajorge@hey.com