We've likened it to having someone review a paper you've written: you often read what you think you wrote, not what's actually written.
This got me to questioning what others have found to be transformative in their development practices.
We've likened it to having someone review a paper you've written: you often read what you think you wrote, not what's actually written.
This got me to questioning what others have found to be transformative in their development practices.
89 comments
My second most important change was to learn how to use contract based programming, or, if the language has poor support, at least to use a ton of asserts. This, for me, feels like stabilising the code very quickly and, again, improved my bug/line ratio. It forces me to encode what I expect and the code will relentlessly and instantly tell me when my assumptions are wrong so I can go back and fix that before continuing to base the next steps on broken assumptions.
We also follow very strict style guides, but this is all defined in formatters and linters, so there's no arguing against it.
Even for my personal projects, the first thing I do is usually set up .editorconfig and TSLint (if I use Typescript).
Even for legacy projects, starting with adding regression tests for every bug you find is a great way to introduce testing. And when you add new features, you can write tests for that as well.
I find that a FP mindset is helpful mostly because it tends to reduce the amount of global or spread-around state. This in turn makes testing and quickly iterating in a REPL a lot easier. Also when later the time comes to debug, it's much simpler if you don't have a huge amount of state to set up first.
And even if a lot of state is required, having it be an explicit input to a procedure is helpful because it makes it much clearer what you need to set up when doing manual testing.
Treat code mess with the same techniques you would treat RL mess. Sometimes you sweep it under the rug. You can toss it in a closet or attic. You can buy a shelf or box and toss everything in there.
You can clean it up seasonally, like set aside a sprint for it.
Some kinds of messes are hazardous and absolutely should not be tolerated - this is similar to leaving milk out or trash piling. Many people make this mistake of assuming that because some messes are very dangerous, all of it is too.
Most messes will slow you down, but cost more to clean up. Overall, I've seen documentation do more harm than help - it's often faster to interrupt a colleague doing something than for that colleague to spend weeks documenting and updating documentation on something that gets thrown away.
Some will take more time to fix later than now - this is what people mean by tech debt. But it's less common than it seems.
With a lot of mess piles, the important parts float to the top and the less important ones sink at the bottom. God classes are mess piles.
Once the mess becomes a burden, it's a good time to clean up.
You may need a few janitors and landscapers, especially for a large code base.
Some people place much higher priority on cleanliness than others. Be respectful of them but you don't have to be like them.
Code messes are similar. Sweep them under a rug or toss them in a closet and your code will look clean, just like your counter looked clean while those two dishes were in the sink. And like the sink you can quickly get to the point where those closets are scary to open and no one wants to walk on that rug because it makes weird crunchy noises.
So how does one leave ones messy code "on the counter"? Well in my "writing C with vi" days I actually outdented any code that I didn't consider clean/good/permanent. Like all the way to the left, regardless of where it would naturally be indented at. It stuck out like a dirty mug on clean granite, and no one (importantly including me) failed to notice it.
With modern IDEs and auto-formatting and the somehow unavoidable use of Python I no longer use that technique, but I will comment any such code liberally with #HACK or #TODO or similar
If you live by yourself, you can be as messy as you want. It's only a problem if your mess becomes a biohazard for your neighbors that you have to take care of it. Otherwise, if you're happy with your mess, that's fine.
If you live with other people, your mess in the common area becomes their mess. That's a problem if other people do not like your mess. There needs to be agreed upon standard by those living together.
Eventually, the mess will inhibit your ability to work productively regardless of the situation. I guess you can always throw out dirty dishes and buy new ones, but there is a cost to bear.
You can't discount people who keep code clean, modular and simple. It's not an easy thing to do unlike cleaning your house. It just takes one's will to clean the house but when it comes to keeping your codebase clean, it's much more than just will.
Sound knowledge of software architecture and design principles is required to write and keep your code clean and unfortunately, the number of people with this knowledge is very small in most teams and sometimes there's none.
Also, it's not okay to be messy unless you are working on a school/university project.
And between say 'medicine' and 'refuse collection', standards of hygiene and priorities can and will be wildly different.
Let me put it this way. I've seen no relationship with code purity and a happy client
No? Would you say then that you don't care if your food comes out slowly or gives you food poisoning?
You care about the result, but you trust it to someone else.
It is a Chef's responsibility to maintain the mise-en-place and food safety to a degree which enables him or her to keep delivering meals quickly and without salmonella.
It is an Engineers's responsibility to maintain the code clarity and tests to a degree which enables him or her to keep delivering improvements on business needs quickly and securely.
I find a lot of a certain kind of sw person who need analogies to make their mental model work. It never did for me
This is just it. It's expensive to organize things.
Bad architecture is worse than no architecture. It take time to build, then more time to tear down.
How do you organize with no experience? A mess already holds a structure. It can be evolved into the right structure. But if you adopt the wrong structure too early, you're stuck with a bad system that has to be destroyed and rebuilt.
Software architecture isn't like that at all. Developers routinely have massive fights and disagreements over whether something is clean or not. One person's clean is another person's over-engineered monstrosity.
This changes the equation quite a bit. In any sufficiently big team either one person has to be top dog and enforce their personal perception of clean on a perhaps unwilling team. Or you have to flex and tolerate multiple competing definitions in a codebase, which if you have "clean freaks" in the team can lead to constant pointless refactoring and rewriting.
Definitely the way to go. It's not a problem until it's a problem.
I see it as a problem waiting to happen until it happens.
The dropping of standards is always going to make things a bit easier on yourself but if there isn't really much more to it than that, is proclaiming "It's okay to be messy" really a good thing?
I don't think(I might be wrong) other engineering fields can get away so lightly with such attitudes, why do you think we can? Particularly in light of so many security disasters unfolding around us. How can you justify an attitude that seems to move in the opposite direction of where many believe we should be heading, i.e. tighter control, more strongly enforced standards and a distancing from the 'move fast, break stuff' ethos
Many of us are familiar with that balance in our daily lives; e.g. we don't mop the floor so clean we can eat off it. But code is often continually polished, refactored, documented as soon as there is the slightest smell.
We do keep tidy enough. The standards for office cleanliness never qualifies to surgery standards, and yet a lot of people like to compare their code to medical device safety levels.
Besides that, there is a clear structure to mess. Too much or too little order and you lose control. The right amount will give you the correct structure.
In short, learning new programming paradigms completely changed my view of programming and these skills can translate over to more "mainstream" languages, so it is still a worthwhile effort.
For me, introducing immutability of data (functional paradigm) helped clean up many interfaces and made the code feel resilient. I realized I rarely passed the input data back to the caller (it was often a new and different structure). At the end of it all, I was somehow more confident in the 'run time' of the app and reasoning about issues is much easier. Currently I am not using any language features to enforce immutability - I just code the receiving function to not change any input structures (or in rare cases, return a new one). I suspect there are some exceptions lying around but having most of the code behave this way has helped.
I found learning Rust valuable, as it forces you think about (and specify) ownership and mutability. It has exhaustive branch matching enforced by compiler, and it has no Null.
Also ChucK (and other niche languages are often cool). "ChucK is a concurrent, strongly timed audio programming language for real-time synthesis, composition, and performance"
I did a Coursera course on digital music creation in ChucK and it was such a joy, and such an unusual language.
I've been working full time with Rust for long enough that I've acquired an intuition about ownership such that a lightweight borrow checker in my mind monitors ownership designs in Python. I use mutability sparingly. I use compositional design. Etc.
This is the best way I've seen the advantages of static type checking articulated.
E.g.:
Applied to finance: Applied to chess: As a bonus, it's clear where to enforce invariants and fail early:I'm working on extending auto reloading to all of the assets in the project because I know that tight feedback loops are that important.
I've been wanting to try that ever since I saw a video of someone developing an FPS in Common Lisp while playing it at the same time. They would modify the bullets and the way they made collision, then fire after each change to see the effect.
https://www.youtube.com/watch?v=ydyztGZnbNs
edit:
Also check Arcadia for Clojure:
https://www.youtube.com/watch?v=_p0co13WYPI&feature=emb_titl...
And developing flappy bird in clojurescript (canonical figwheel example):
https://www.youtube.com/watch?v=KZjFVdU8VLI
Thank you for the links.
REPL means Read-Eval-Print-Loop and its a place where you can run code immediately and get immediate feedback. For example F12 in your browser and use the console to do 1+1. But you can also use that console to interact with your code in the browser, the DOM and make http requests.
But I also see REPL as a principle - to get as quick feedback from the code you write as possible, and things that help this are:
* Quick compile times
* Unit tests
* Continuous integration
So that each stage is as quick as possible. I write code and within as second or two know if it failed a test. Within a few seconds maybe I can play with the product locally to test it manually. Once committed, I can see it in production or a staging environment at least pretty quickly.
You can then have bigger REPL loops where a product manager can see your initial code fairly quickly, give you feedback and you can get stated on that right away and get it out again for review quickly.
I don't think there is any excuse not to work like this given the explosion of tooling in the last 20 years to help.
2. YAGNI
Writing over elaborate code because it is fun! That's fun at first but you soon learn it's better to write what is needed now. There is a balance and writing absolutely shit code is not an excuse, but adding too generic code because of stuff that might happen is also a problem.
When I'm able to greenfield something myself, and use this from day one, I tend to very naturally end up with the "well-separated monolith" that some people are starting to talk about. I have on multiple occasions had the experience where I realize I need to pull some fairly significant chunk of my project out so it can run on its own server for some reason, and it's been a less-than-one-day project each time on these projects. It's not because I'm uniquely awesome, it's because keeping everything testable means it has to be code where I've already clearly broken out what it needs to function, and how to have it function in isolation, so when it actually has to function in real isolation, it's very clear what needs to be done and how.
Of all the changes I've made to my coding practice over my 20+ years, I think that's the biggest one. It crosses languages. At times I've written my own unit test framework when I'm in some obscure place that doesn't already have one. It crosses environments. It crosses frontend, backend, command line, batch, and real-time pipeline processing. You need to practice writing testable code, and the best way to do it is to just start doing it on anything you greenfield.
My standard "start a new greenfield project" plan now is "create git repo, set up a 'hello world' executable, install test suite and add pre-commit hook to ensure it passes". Usually I add what static analysis may be available, too, and also put it into the pre-commit hook right away. If you do it as you go along, it's cheap, and more than pays for itself. If you try to retrofit it later.... yeowch.
Their second decade they learn that simplicity and readability is always better, even if it does require more code.
It's better to have 300 lines of simple, straight-forward, easy-to-read code than to have a 30 line version of the same code that's difficult to understand, and difficult to debug.
Always code for your future-self and future-others who really don't want to spend an hour figuring out what the hell you did in that awesome looking 30 lines of entropy.
Ends up with actually more documentation text than code itself, which is a good thing. But for the majority of code it an be kept simple and self-documenting.
But with a "keep it simple" philosophy the number of lines of actual code never really matters that much, while code where the developers were clever and complex at every opportunity (often just showing off) the code is quicksand and a quagmire every every single line of the way, and is a nightmare.
http://www.linusakesson.net/programming/kernighans-lever/
> If we deliberately stay away from clever techniques when writing code, in order to avoid the need for skill when debugging, we dodge the lever and miss out on the improvement. We would then need other sources of motivation in order to grow as programmers, and if no such motivation appears, our abilities stagnate (or even deteriorate).
I'm not experienced enough to guess if this is accurate, but I found it very interesting
Based on my experience, if a coworker wanted to "avoid cleverness" I imagine we'd end up arguing over something like a for-loop versus a map, but such small code decision matter very very little in comparison to overall architecture, and where you place the "seams" in your system.
So I ask, when you have felt that code is "too clever", was it because someone used a map, but you're more comfortable with for loops, or was it bigger than that?
My first job was maintaining some PHP. The author didn't know what a function was. The code was a 5,000 line script, top to bottom, with basic control and looping logic nested up to 17 deep (I counted). It was horrible. Yet surely, the author had avoided cleverness at all costs; he had built a working system with only the most basic tools, those being all he knew.
- Too much business logic hidden behind dependency injection (using Dagger in this instance). I think DI should be used sparingly, to decouple large subsystems like the database or the network. It can feel clever to inject everything, so your system is super modular and decoupled, but that just makes it much harder to understand, with little or no real benefit.
- Somewhat related, excessive use of annotations to accomplish tasks that could be done in a more straightforward way with normal code. For example, in Android, you might have an annotation that adds some fragment to your activity as a mix-in; but is that really easier than just calling a function to do the same thing?
This stuff starts to cause real trouble when there are 1000 occurrences sprinkled through your code, and suddenly you need to step through it to debug a tricky problem. Straight-line code is vastly easier to deal with.
So I’d say “clever” is more of a problem at an architectural level, rather than inside individual functions.
At the low level, shorter is almost always better. If somebody comes up with a clever way to reduce a function from 10 lines to 4, say, that’s great -- as long as its purpose is clear and it’s testable.
Agreed about the annotations; it's one of the annoying things about Java, IMHO. The language itself is so inexpressive that people have to resort to annotations to do basic stuff. I think some level of "metaprogramming" can be useful (regardless of the language), either for boilerplate reduction or for removing aspects like logging from the main code, but it's too easy to become "clever" about it (not just in Java; Rubyists abuse metaprogramming way too often too, for example). I generally favour "explicit over implicit", which also means that I generally dislike inheritance because of all the non-local reasoning.
Same here! I think that approach works really well when you’re able to use it.
I’d love to try removing the DI framework and see what you get just rolling it all by hand, but that’s a tough sell in a large pre-existing project.
- handrolled is better and the framework stinks
- there are some disadvantages to the framework, but after doing it all by hand you also understand how it makes a lot of things easier
- it's a tradeoff that largely amounts to which kinds of problems you're personally more willing to put up with (quite likely outcome)
in any case you would learn something
One I deal with at work is someone trying to be "clever" and coming up with some annotation based tool to automatically serialize and deserialize objects to a third party system. It's got complex class heirarchies, interception points, etc and I regularly have to crawl through this code. The "not clever" way would be a function that translates the objects to the very simple csv format. In fact I've done that to test interacting with it and when the bash version is simpler to read than the c# version you know it's over complicated.
Another frequent one is "this code is repeated, better move it to a function", problem is you then need to make a change and you have to go through every code path to check it's relevency or add optional parameters, then next time it needs to change you have to check all code paths and inspect which ones are using which paramters. The "not clever" way is to leave the repetitive code, this can have it's own downsides like fixing one place and missing another, but those are much easier to deal with.
Basically use the least level of abstraction that can reasonably get the job done. Your php example probably could have used a little more abstraction like functions and structures but most "enterprisy" code I see could use a lot less.
There is a great section of John Ousterhout's "A Philosophy of Software Design" that goes into this. He mentions that moving code to functions / methods doesn't eliminate complexity, it nuat kind of shifts it: it adds complexity to the "interface" of the module.
Sometimes leaving code inline with its original context is better.
Instead I implemented some kind of ASN.1 tree parser, which parsed to lisp, then I tied that through C bindings to some kind of callback to the lisp functions, and clever reuse of datatypes and whatever. I don't even remember how it worked!
I don't remember if it did all the processing on the device or if I had (the same flavour) lisp running in the build system too. I just remember that in the end, even I didn't know exactly how it worked or how to hunt down the inevitable bugs. It looked very elegant though, with lots of autogenerated types from the ASN.1 spec.
The C code which eventually replaced my unholy mess had to handle the datatypes manually in each function, but it was easy to understand and easy to update. Fewer bugs too, I'm sure. And one less dependancy. (The lisp engine.) It didn't have the automatic integration with our vendor MIB file, you'd have to manually add or update functions in the C code whenever someone in another part of the company decided to update the MIB file.
Small price to pay. I'm not saying the "elegant" idea could have been made workable and easy for other developers to understand, but definitely not by me in that point in time. Lisp at the time was my hammer and I saw a lot of nails. (I wasn't even any good with the hammer for actual nail like things, I think.)
Code that relies on implementation details of a library that a good portion of developers wouldn't necessarily know. In C# if your code relies on LINQ being lazily evaluated to produce the correct answer it's probably too clever.
Using reflection in static languages like Java or C# when it's not necessary.
Writing code that passes functions around when you don't need to.
Chaining together a series of map/filter/reduces in order to avoid writing a simple for loop.
> Based on my experience, if a coworker wanted to "avoid cleverness" I imagine we'd end up arguing over something like a for-loop versus a map, but such small code decision matter very very little in comparison to overall architecture, and where you place the "seams" in your system.
> So I ask, when you have felt that code is "too clever", was it because someone used a map, but you're more comfortable with for loops, or was it bigger than that?
This rule basically comes down to all things being equal try to write straightforward code. If you can write two functions in 5 lines of code but one function will be more easily understood by a more novice programmer, go with the more straightforward approach.
>My first job was maintaining some PHP. The author didn't know what a function was. The code was a 5,000 line script, top to bottom, with basic control and looping logic nested up to 17 deep (I counted). It was horrible. Yet surely, the author had avoided cleverness at all costs; he had built a working system with only the most basic tools, those being all he knew.
No one is arguing that writing straightforward code is the most important code quality to strive for, just that before you implement that currying solution to reduce the number of lines of code from 40 to 35. Think twice.
-I remember being called to help rewrite a few lines of Perl (the other developer didn't manage to do it): it took me two or three hours with constantly looking at a manual to rewrite these few lines in a Perl that a beginner could understand.
The end result had the same line number than the previous version..
-Configuration files made of C++ template for dubious reasons..
I spent a whole sprint building out "the RAD" - a rooted acyclic digraph - that ensured all sorts of correct behavior and powerful options for working with the graph. It felt awesome building what I thought was this super elegant thing that, to me, made total sense since I had been working with it for 3 weeks.
The other devs were confused by it and we're afraid to touch the code. I admitted failure and worked with them to rewrite it. It ended up being an adjacency list (i.e. a graph) with breadth-first traversal, but as long as I didn't call it that, they understood it and liked it.
And in hindsight, we didn't need all the guarantees that it provided, so they were right.
Cleverness also refers to architecture, designing meta-types to encapsulate all sorts of things that just don't need the flexibility, over-designing a system in anticipation of future needs that may never come. Sometimes it's a delicate balance and often only experience can dictate how "clever" one should be.
Many developers just out of university, or still in it, in certain languages, like to run with clever things they can do with the type system to abstract away all sorts of stuff, which becomes painful later.
The concepts that influenced me the most were immutability and the power of mapping+filtering. Whenever I read a for/while loop now, I’ll attempt to convert it to the FP equivalent in my head so that I can understand it better.
SQL, Python, PHP, JavaScript, and many aspects of Unix and the C/Make toolchain all come to mind.
Mind you, I don't like all of the above. At least 2 in particular I definitely wouldn't mind seeing "just go away."
But I do recognize that they have special staying power. And they didn't get this power by accident.
The marginal cost of deprecating something or breaking something is higher than the incremental cost of workarounds. This happens all the time from the laryngeal nerve in Giraffe to the evolution of a programming language, say JavaScript.
Please don’t mix good design with popularity. It’s a correlation/causation fallacy.
Evolution doesn’t ‘design’ anything, least of all anatomy.
The ‘special power’ is that they have stuck around, for whatever reason or circumstance.
So why are we in this python 2 to 3 mess?
For many (most) systems, there are designs that make perfect sense, but will be hard to debug.
When I started designing so that programs could tell me what they are going to do, what they are doing, and why, and so that their state, wherever possible, could be expressed into a structure where I could "see" the wrongness, my development time went way down--because it was really time spent debugging, so my productivity went way up.
Rather than just make a loop and increment every so often, I made the system output it's "plan" for the user ramp, and a second component execute the ramp plan. We had a bug in the calculation which we were able to see easily by reading the "plan." Without this step, we would have been unsure if it was the calculation of when to add users, or the implementation of adding a user that had a problem.
My younger self did not truly understand the strength of modeling or treating data as the reification of a process. I saw everything through the lens of what I knew as a programmer which was, for the early 2000s java developer I was, a collection of accessors, mutators and some memorized pablum about object orientation. I treated the database with contempt (big mistake) and sought to solve all problems through libraries.
Now I can see the relational theory in unix pipes. I can see the call-backs of AWK and XSLT processing a stream of lines/tuples or tree nodes as the player-piano to the punch cards. I understand that applications come and go, but data is forever. I no longer sweat the small stuff and finally feel less the imposter.
Parse, don't validate https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
All libs/products should be pure functions, with input output documented, making libs/products predictable
Use in-app event sourcing to reduce the need for global states states
DoD https://youtu.be/yy8jQgmhbAU In non bare-metal languages, this will be useful for readability
For errors, return instead of throw
Turns out, years ago I was writing some C# code and I found myself wanting to explore something like that. However, it wasn't idiomatic for C# (especially at the time), and I talked myself out of it.
Since then, I've wondered if that's what people mean by type driven development. It's cool to see that is at least in the general ballpark.
Using TypeScript daily, I have a great time with type driven development. (C# and TS was created by the same guy)
I was never comfortable with Java community approach of automating stuff, with their heavy use of reflections, method name parsing instead of being strict in typings, use generics, macro, templating, etc.
You'd be using types in a way that pretty much no one in C# does (not idiomatic), except for maybe those who are explicitly trying to go for type driven development (for example, people doing things like railway oriented programming).
What's the difference between in-app and not?
That line serves as reminder to utilize that, not to exclude non in-app event sourcing
By module to module, do you mean between very loosely decoupled systems such as microservices, or closely associated things like queues and method calls in a single-process process?
I work with TypeScript on daily basis and I'm in the middle of building this library to help with event sourcing and other stuffs. https://github.com/Kelerchian/florcky
I'll be working with rust, C++, and C# in a few weeks due to professional demand and I might get somewhere with event-sourcing using those language.
In the long run we might be utilizing wasm and ffi to introduce universal protocol for event sourcing.
* Automated regression testing and build/deploy pipeline. The machine will do things quickly and repeatedly exactly the same way, given the same input conditions/data.
* TDD. Create tests based on requirements, get one thing working at a time, and refactor with a safety net.
* Mixing FP style and OO style programming to get the best of both worlds.
* Understanding type systems, and how to use types to catch/prevent errors and create meaningful abstractions.
* Good code organization, both in physical on-disk structure and in terms of coupling & cohesion.
* Validate all incoming data and fail fast with good error messaging.
Make your code easy to read at the expense of easy to write. Don't abstract unless you know exactly the use case. If the use case is not immediately forthcoming, YAGNI (you aint gonna need it). Related, don't be clever. Clever is cute and all. It does not belong in production code.
Any line, function, workflow, etc can fail. You need to have worked through all the failure cases and know how you are going to handle them.
Somethings don't need to scale, ever. But most things I work on do. If I don't know the story on how I can ramp up an implementation a few orders of magnitude, then I can't say I've designed it well.
Aside from that, measure everything. Metrics, telemetry, structured logging. Design for failure and design for understanding what happened to cause the failure. Data will tell you what you are doing, not what you think you are doing.
There was a post here yesterday about a neat map thing. I hit a bug and the author could only rely on their experience with it being potentially slow at times but saying it should work. If there were proper metrics in place, they would know that N% of calls exceed M seconds and cause a timeout. He could then relate this to the underlying API and its performance as the service experiences it. With proper logging, they see what the cache hit ratio is and determine if new cache entries should be added.
Build, measure, learn.
Oh, and automated tests. So much to be written on that.
I liked this article I recently came across discussing the topic: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
I'm building internally used software, and getting feedback from stakeholders in the testing environment was hard work, and often impossible.
We went from roughly monthly releases to deploy-when-it's-ready, and this tightened our feedback loops immensely.
Also, when something breaks due to a deployment, it's usually just one or two things that could be responsible, instead of 20+ that went into one monthly release. Waaaay easier to debug with small increments.
Basically it involved writing additional code to set up the app state to the state I need.
It went from changing a line of code, running the app, clicking/filling fields as needed, and then seeing the change reflected, to the app immediately being in the state that I wanted.
Now that I'm working in web-apps, hot-reloading is quite beautiful.
At the team level, probably CI/CD. It forces us to break the monolith into digestible chunks and makes regression testing easier.
Also recently did Thorsten Ball's Interpreter/Compiler books which focus heavily on unit testing the functionality (I can't recommend these books enough).
I now can't imagine going back to a pre-TDD world
However at work, I often find that I feel like I can't write unit tests before starting code because I just don't know how it's going to get built until I start poking at the code. I'm not sure how to break out of this
You'd start with shallow functions and quality asserts. Initially, you'd have broken tests, you have to write code to fix them, and that's your TDD.
Your comment is exactly what I try to get across in the book but it's not always easy.
If you cant decide how you want something to look (as that's not always easy) just take a punt on something. Make sure it's a small decision and make something useful. Sure you might have to change it, but at least you'll be basing that on some real feedback.
One thing I was uncomfortable with about TDD is the step to "just write enough to make the test pass". The danger seems to be when you have to walk away from some code and you or whoever picks up your code doesn't know what you were up to.
In a simple app and a few tests you could probably tell, but the larger an application gets, the less you can put effort into finding out "this test works because the application works how it should" or "this test works because someone wrote just enough code and hardcoded some value somewhere in order to make the test pass".
It seems "safer" to write tests that are going to stay broken until every little corner of everything works properly, even though this is less iterative.
As mentioned above I have little TDD experience, so maybe I'm missing something.
This is the _only_ point where you can safely refactor, if your tests are failing how do you know you haven't broken something? So long as you keep things "green" you know you're ok
> The danger seems to be when you have to walk away from some code and you or whoever picks up your code doesn't know what you were up to.
Just don't do this. You're not "done" with the TDD cycle until you make it pass and have refactored.
I reflect this in my git usage too, roughly it goes
- Write a test -> Make it pass -> git commit -am "made it do something" -> refactor -> run tests (if i get in a mess, revert back to safety and try again) -> git add . -> git commit --amend --no-edit
> It seems "safer" to write tests that are going to stay broken until every little corner of everything works properly, even though this is less iterative.
The problem with this is how do you safely refactor when you have potentially dozens of tests failing?
A big point of this approach is it makes refactoring a continuous process and makes it easier because you have tests proving you haven't accidentally changed behaviour.
https://github.com/quii/learn-go-with-tests
Once the code compiles I go into the implementation and make it work.
Now and then I realize during implementation that I need some additional parameter, but it's easy to add that.
1) Writing a test that calls an API and asserts that everything resulted correctly from it is more of an integration test in my opinion - in this case I can agree this can be written beforehand.
2. If I'm changing my tests as I write my code then it doesn't matter if I go test-code-test-code vs code-test-code-test. I have still changed my definition of "correct" on the fly based on something I found out as I implemented.
That is, you don't need to test that inbuilt standard library stuff is working as expected (e.g does a string reverse) but rather, does the combination of possible inputs match your expectations?
Although sometimes the domain is not clear enough before you start, that's shitty.
Why? Because I’ve tried to actually think about my underlying data structure instead of defaulting to convention but reflex.
The best example? Marcel Weiher Ch 13 in “iOS and MacOS Performance Tuning” explains how to improve lookups in a public transport app by 1000x.
What more? The description of the the data (the code) and the the application (the intended use) are probably way better because thinking about performance is similar to thinking about your data. You want answers fast. Fast answers, fast code.
Also doesn't hurt that it improves Intellisense/Autocomplete, gives better hints/help as you're calling a method, and improves on "self documenting" code.
Distributed version control is one of the greatest things to have gained popularity in the last ~20 years; if you’re not incorporating git, hg, fossil, ... start now.
You gain: concise annotated commits, ease of cloning/transporting, full history, easy branching for feature branches or other logically seperate work, tools like bisecting, blame/annotation, etc., etc.
1. Not being afraid to look at the code of the libraries that my main project depends on. It's a slow, deliberate process to develop this habit and skill. But more importantly, as you keep doing this, you will develop your own tactics of understanding a library's code and design, in a short amount of time.
2. Not worrying about deadlines all the time. Not a programming technique as such, but in a world of standups and agile, sometimes, you tend to work for the standup status. Avoiding that has been a big win.
3. (Something new I've been trying) Practicing fundamentals. I know the popular opinion is to find a project that you can learn a lot from, but that may not always happen. Good athletes work on their fundamentals all the time - Steph Curry shoots like > 100 3 point shots everyday. I'm trying to use that as an inspiration to find some time every week to work on fundamentals.
4. Writing: essays, notes. In general, I've noticed I gain more clarity and confidence when I spend some time writing about a subject. Over time, I've noticed, I've become more efficient in the process.
You could for example pick something as simple as a HashTable, and implement it from scratch. Then, you could add more complexity to it, like HTs that won't fit in memory, expanding and shrinking HTs efficiently etc.
Or, you could use one of the several practice websites like LeetCode to practice Algorithms/DS problems.
Once you start building a habit, you will also become better at organizing your practice routine and finding out more about what to work on, and where to look for study/practice materials. But mind you, this is a slow process, which you want to build as a habit. There is no end goal here (like cracking Google interview or such), this is a process to get better at the fundamental skills in your field.
Basic inline documentation. Who wrote it, when and why. What else did you consider. How does it differ from other solutions. Why is it designed the way it is. Brief history of design changes. What state did you reach in testing. What are the forward looking goals. Takes 5 minutes, pays for itself many times over in future. 90% of effort is maintenance.
I think literally every logger let's you log at a certain point as a single line that does not cover the context of the log.
This way you know how much the log comment is supposed to cover as well as take a benchmark on the said context.I can never think to just add a single line logger anymore.
This is much better than inserting comments in a code as comments can have no context except the line below.
You can add a function like,
logger.comment
and don't let it log anything to be comment syntax replacement.
You can certainly add a comment on how you don't like it.
Thankfully that was decades ago, which means I enjoyed the magic and bliss for most of my professional career.
Also interesting: I started using state machines in hardware designs before I applied them in software.
They're a super cheap way of
1. allowing feature flags
2. injecting credentials in a way the user thinks about exactly once
3. moving workstation-specific details out of your code repository
They're implemented into the core of most every language in existence (especially shell scripts) and you're probably already using them without knowing. They're (get this) _variables_ for tuning to your _environment_.
Sounds like I'm being sarcastic here (eh, maybe a bit) but it never really hit me until I really dug into the concept.
Other development practices that boosted my output significantly: Regular cardio exercise like running, strength training by lifting weights, and regularly reading source code for pleasure.
https://github.com/nikitavoloboev/dotfiles/blob/master/zsh/a...
For example once we had a project that wanted to add Role Based Access Control and some juniorish engnineer suggested adding boolean columns to the table for each of the user's roles.. Nope. Instead we created a document store w/ roles and defining new roles was as simple as adding a const to a set in the service's code. Good thing too because the number of roles grew from the initially requested ~3 to almost 100. That would have been too wide of a table.
Which would limit the scope of the "moderator" role explicitly to the tenant database.
Eg. https://martendb.io/documentation/documents/tenancy/
Fyi: The op, which i was responding too, was referring to using roles as const. Which is not dynamic :)
2. Don't get into the trap of refactoring just because you read a new cool way of doing it unless, it's reduces half the size of code or improve performance multi fold. This might waste time as you could write a new feature or plan about it in that time.
3. Write very big elaborative comments. It's for future you as you can't remember everything, sometimes you need to know why you wrote that code or condition as you might not remember why you wrote it at that time.
Try again.
In reality the code might be right for 90% of use cases, but once you multiply this over a whole project, you end up with total dogsh*t, and mainly because of laziness too. This is the biggest reason the inexperienced developers create such horrendously buggy code.
But with a better architecture and nearly zero mistakes. Through using OO correctly ( please read the word correctly)
One step at a time.
Ps. This is mostly when a code-base is detected where a part of the code is sensitive for bugs. Eg. From today: handling better payment providers and payments.
I thought it was important enough to make the change bigger, but with the intention of removing recurring/similar errors in someone else his code-base.
Afterwards, go to him and explain why. Developers can be proud of their code, although it's just shitty code ( eg. Long functions, loops, if else, switch, by reference, ... Is mostly an example of bad code) and never say it's shitty..
- Reading books (e.g. Clean Code, Refactoring: Ruby Edition, Practical Object-Oriented Design, ... these left a mark)
- Going to conferences (Rubyist? Attend a talk by Sandi Metz if you have a chance)
- Testing (TDD at least Unit Testing)
- Yeah, the usual stuff: quick deployments, pull requests, CI/CD ...
- Preferring Composition over Inheritance (in general and except exceptions :)
- Keeping on asking myself: what is the one responsibility of this class/object/component?
- Spending a bit more time on naming things
- Have a side-project! It's a fun way of learning new things (my current one is https://www.ahoymaps.com/ - I could reuse many things that I learnt also in my 9-5)
In short, I have been doing Clojure since last year.
What would end up happening sometimes later in the day I would decide revert or modify the change, so my commit history ends up flooded with bunch of small commits.
I ended up writing a script that checks my current work, and if enough changes or time has past, then a commit is recommended:
https://github.com/stvpn/sunday/blob/master/git-train/git-bo...
This makes it way easier for someone (possibly you) looking later to understand what was done and why.
(I once almost took a job where the repo was just a series of huge commits taken at more or less regular intervals, but with no logical coherence and no attempt to document what was happening. Noped right out of there.)
At this stage I don't worry much about the commit message, as it's purely local, and many of them will be squashed (discarding the message). So the trivial ones get one-liner messages like "WIP: Adder: Comment fix" or "WIP: Parser fix '...', needs testcase'. "WIP" commits are a useful tag for me: They are never allowed to be pushed to a shared repo.
Then the "git add -p" stage.
When a task, feature or bug has been dealt with, I'll start using "git add -p" to separate out parts of files, and commit them as logically independent changes. At this stage, I don't mind separately committing even very scrappy little changes, such as comment typos, as separate commits because I will merge them later. The key is to separate out logically separate kinds of things in the delta from worktree to HEAD. During this I will usually find a few things that are untidy or comments that could be worded better, and add tiny commits for those changes.
The "git add -p" stage is a great time to get some perspective on what logical units were actually needed for the main task, and what else was refactored or fixed in passing, and this "pick up the pieces later" method frees me up to fix things and do small refactors without having to switch context while doing it.
Then the "git rebase -i" stage.
When the adding is done, "git rebase -i" to reorder into a sequence of logical, coherent and explainable changes. Ideally in an order where things still work if they are partially checked out in that order (bisectability), and squash together separate "git add -p" chunks that really must be one logical unit. Also, squashing trivial fixes such as typos in comments, whitespace etc into logical commits.
I may then go back and clean up and flesh out some of the commit messages before pushing the lot upstream for review. Or, in practice, there's usually no review of my work other than testing it, but so that others can at least read through the commit messages and patches to see what was done and how; hopefully learn from it.
The above cycle is usually done about every 1-2 workdays, but it can be longer if there's a tough problem being debugged or a complex new feature (but for big new features a branch is more useful). If something turns out to be too big, though, it doesn't matter because I can always commit any work in progress to a new local branch or stash, and rewind back to a stable worktree.
The final, logically coherent and documented set of commits is very satisfying to push, and rarely contains "junk" commits or unexplained changes, even though what I commit locally at first is often like that. I guess it helps to know Git quite well.
I'm pretty much adding fuzz-testing to all my current/new projects now. It doesn't make sense in all contexts, but anything that involves reading/parsing/filtering is ideal - you throw enough random input at your script/package/library and you're almost certain to find issues.
For example I wrote a BASIC interpreter, a couple of simple virtual-machines, and similar scripting-tools recently. Throwing random input at them found crashes almost immediately.
I'd not always been the best learner and this taught me how to learn more effectively. Whilst at the sametime accepting the limitations of the brain.
It led me to find the Anki app and combined with more effective learning, it positively affected my ability to be a better programmer.
After some time I've noticed that I read the code much easier and understand flow when the indentation is similar to python or go.
Also, I keep my editor on a vertical half of the screen.
Monitors may be wide but your eye sight will never change and moving your eye sight left and right, worse, moving your neck left and right is tiring and vertically is easier to follow as the code is the one that moves around instead of you.
Same as sibling comment but I keep my apps' window size at about 70% of the display's width starting near the mid to right end, so I don't have to look way to the left when pretty much everything is leaned on the left side. This brings most of the text, be it code or web page that you read close to mid, which is easier on your neck.
1) Start with consistent naming conventions, notation and structures
2) Rely on autocomplete to simplify typing/readability/programming effort. It’s a tremendous force multiplier.
Ninja’d
3) TDD
4) Functional programming
Words stop meaning anything when you use it all the time.
Use a thesaurus. Naming everything “handler” or “node” is just naming them “function” and “object”.
Good way to make non programmers laugh too.
We enforce the architecture as much as possible through protocol conformance, which doesn’t always work, but works extraordinarily well for “Views” or “Scenes”.
It’s taken so much stress out of the development lifecycle for our team.
A broader one: writing expressions as much as possible. Basically, it means avoiding unnecessary mutations (and jumps).
Then avoiding architecture. Thinking algorithms that process data (instead of "systems") has been transformative.
2. If you can, step through any new code with the debugger. You may find the execution path isn't what you thought it was.
Greatly improved readability, even though there was a short adjustment.
2. Figuring out unit and integrations tests
3. Embracing clean code paradigm and SOLID architecture
SELECT this , that , something FROM table
takes a while to get used to
- Reduce uneccessary IO / resource strain, as you said
- Predictability; consuming program receives data in the same order, every time
- If the DBA adds additional columns to the table, it doesn't hose downstream consumer processes
- Easier to debug if some problem does arise.
- Clarity, if you are only using a few columns from a large table. I might just be getting old though ;)
Data should be served in the simplest, most robust manner.
It should be easily consumed by other services, with little extra effort from the programmer.
If you use Select *, you either accept the possibility of it breaking unexpectedly, or have to write logic within the consuming service to deal with that. If the problem can be eliminated by the DB/query itself, it should be.
> - Predictability; consuming program receives data in the same order, every time
For any joined table, you can specify table name for each '*' or add an alias last to keep the column you need instead of writing it all.
Seems specifying each column name isn't in any way crtitically bad when you can't select all but a column in SQL, which is a deficiency in the language.