How to Write Unmaintainable Code (2017)
github.com
github.com
I carry over the same principles to functions/methods. Functions are usually less than a screen which makes it rather easy to pick up the context and understand the program flow and the use of variables. I use "list" if there is there is a single list. The context usually defines what is in there. Maybe the function name, the type declarations or the fact that we are in a part of code handling lists of customers. How hard is it to guess what countCustomers(list) or incrementCustomersScore(list, n) is doing? Writing incrementCustomersScore(goodCustomerList, customerScoreDelta) is just redundant. Variable names just need to be descriptive enough to define their role within it's context. If the function is very small, the variable names can be very short.
Local variables are the smallest named things in our systems, whith a very small scope, there shouldn't be an excessive naming cult around it. Give advice how to properly name projects, packages, namespaces and classes and I start listening. We are really bad at this.
Also, by not including semantic information in the variable, it also requires this to be conveyed everywhere else e.g. your function names. You rightly point out that hopefully there's tight context around this thing (e.g. it's only about customers) and maybe you can leave that out, but over time as code changes and grows in weird ways, this may not always hold.
What do you think?
Won't other people still need to read private code?
It does help one turn a bad API into a bad API that will lead to slightly less bugs.
Using `list` is just bad. Your list (probably) holds a set of things that has a name. So name your list that. At the very least, name it after the type of things it's holding. Types are only declared once, but variable names are forever. Having to constantly look up the type of a variable because the programmer was too lazy to name the list of things as `things` instead of `list` is rather inconsiderate.
(Of course, there are times when you would use `list`. For instance, if you're writing a list sorting function, and you actually have zero context more than "yep, this thing is a list".)
As you say, list might be the best name for a variable if the use context is generic. The same with iterators and such. But you really have to think ahead to keep the names consistent, complicated names work against consistency.
List<Foo> foos;
for (fooIndex = 0; fooIndex < foos.length; ++fooIndex) {
Foo foo = foos[fooIndex];
List<Bar> bars = foo.bars;
for (barIndex = 0; barIndex < bars.length; ++barIndex) {
Bar bar = bars[barIndex];
}
}
It would be immediately, visibly obvious if you accidently use `fooIndex` in the `bars` list. Good luck getting that with `i` and `j`.That, of course, is assuming you even need the indexes. Otherwise you get this:
foreach (Foo foo in foos) {
foreach (Bar bar in foo.bars) {
}
}i and j really make more sense (as i and j) when used in their more mathematical connotation: indexes into a multidimensional array/object.
for (i = 0; i < thing.length; i++) {
for (j = 0; j < thing[i].length; j++) {
thing[i][j]; // this is very close to the mathematical use of indexes
}
}
Makes perfect sense because i and j are being used to index into the same thing.i and j would be poor fits for your first example because they don't index into the same thing anymore. You've introduced additional levels of indirection, to rewrite your first example using i and j (and illustrate why this is an awkward construct):
for (i = 0; i < foos.length; i++) {
for(j = 0; j < foos[i].bars.length; j++) {
foos[i].bars[j];
}
}
It's not wrong, but it is awkward and unclear as the code grows larger it becomes easier to misuse i, j, and subsequent index variables.=====
And of course, you're absolutely correct: foreach or equivalent is a better construct. It states the intent of the code, rather than the how of the code. This keeps your code closer to the semantics of your problem specification.
lst is actually a very good example. If you are writing small functions of less than a screen (which you arguably should be), and you can see that lst is a list of FileInfo, and the code to populate it in a function called GetAllWebConfigsUnderPath, and you have twenty of thirty other types of list generating functions, then seeing lst as the primary thing that gets filled everywhere is comforting and recognisable.
Having said that, having lstWebConfigs and lstAppleFiles and lstUnixFiles might be useful, for example.
Also, I assume you mean "type" as in "the kind of things that it is", rather than its actual type, such as integer, string, etc. lstInteger is largely pointless in this age of IDEs, but lstUserIdentifiers is demonstrably better (You shouldn't compare UserIdentifiers with StockCodeIdentifiers unless you work in H.R.)
"Your list (probably) holds a set of things that has a name. So name your list that."
"Oh, great... Here's another `list` variable, let me go see what it is this time."
Thre's strong grounds for saying that `i` is better.
At least in a lot of the code I work with, `index` is most likely to be assumed to mean something akin to a SQL index similar - a data structure or module used for fast lookup.
Whereas `i` is universally understood to be an iteration counter or a position in a sequence. That's a well-known convention that goes back for hundreds of years, and that many (if not most) of us learned before we really learned to program, by virtue of its use in math notation.
And yes, words can have many definitions. This is where you actually do rely on context for the reader to separate. And I have zero issues with local conventions overriding global conventions. So if you really do have a lot of confusion between whether an integer means an array index or an SQL index, by all means create convention to disambiguate.
(Also, while "hundreds of years" is technically accurate, it's probably not as old as you think. Euler was mid 1700's with his sigma summation notation.)
like iFoo, or listFoo, etc... When using these, I often dont know whats an Object that cannot be changed, and whats a variable.
Sure you can play around with it, but I feel like I've lost plenty of time trying to understand other's code.
When you're as jaded as I am, very.
What is list? What are its elements? Is it a list of customers? Is it a list of something that has a customer as one of its properties? I've seen far too much bullshit in my time to make any contextual assumptions about something called "list".
Same goes for "n". Is that the number of customers whose scores to increment, or is it the amount to increment for each customer in the list? Or something else? What happens if it's negative?
Any time I have to look inside a function (no matter how small) to see what it actually does, I find myself wondering why I must endure this extra cognitive load (and then endure again later when I read the same code but have forgotten the implementation details). And any time I find even mildly ambiguous names, I MUST assume the worst, because people just do stupid things, and therefore I must assume for my own survival that YOU have done stupid things, even if you haven't.
It's very little extra effort to call it modifyCustomersScores(customers, scoreDelta) and eliminate as much guesswork as possible.
Remember: Code is read FAR more often than it is written. Why wouldn't you take steps to optimize throughput on the common path?
Explicit naming is a courtesy. It's developer U/X. It's telling your reader "Yes, I've been there in the trenches. I've seen the horrors. Here's a flashlight to help you on your way."
Local type inference ("var") can reduce the friction here, but, you've probably still got to explicitly declare types for parameters and return values, so there's always an excuse to be lazy.
I'd say that full-on Hindley-Milner type inference might help, except that I suspect that the folks who do this kind of thing would find a way to create even more chaos in the kinds of languages that have one.
But even code rot can be mitigated somewhat with this discipline.
Which of these changes is easier to justify:
countCustomers(list)
incrementCustomersScore(list, n)
// where "list" is to change type from "list of customers" to "list of sales domains"
VS countCustomers(customers)
modifyCustomersScores(customers, scoreDelta)
// where "customers" is to change type from "list of customers" to "list of sales domains"
Code is a map to what the computer is supposed to do. Making things familiar and easily navigated just makes sense. Good code requires good U/X, just like good websites require good U/X. It's just something you do to make your users have a better time using your stuff.I would invest effort in making the definitions of local variables easy to read, while keeping the names short. (And keeping the functions themselves short so there aren't many locals in scope.)
The idea is to reduce cognitive load as much as possible for someone navigating your code. Every line the reader has to read increases cognitive load, and slows him down.
There are situations where text needs to stand alone (such as tweets) but I think a function call is too narrow a focus. The body of a reasonably-sized function seems like a more natural chunk for understanding a program.
The correct comparison is: 1. what is the cumulative time wasted when people guess wrong? 2. what is the cumulative time spent writing out a meaningful name?
When I'm conservative in my estimate of how good people are at guessing my intuitions, I always find that #1 seems to dwarf #2.
Are you getting paid by least amount of characters typed?
“Fewer characters” seems to be the domain of people who place little to no value on good tools. In the worst cases they make choices that are actively hostile to anyone else using better tools.
See also the Toxic 10x-er, who is really a 3x who makes everyone else 2/3rds less effective.
I realize that with time, functions like `malloc` become second nature, but why not call it `allocate_memory`? (bash suffers from this too) It's even worse when reading the source code of old video games due to the abstract nature of the operations being performed on virtual representations of visual elements.
The argument of time constraints when authoring doesn't hold much value. Java is verbose AF but it's also one of the most used languages. Second, most code is read, not written and should be written to be read by others, not just the author.
There may be a reason to be terse due to time constraints and lack of IDEs/autocomplete or memory constraints or w/e, but the need to shorten variables names and functions today only serves the whims of the author, not the 10+ future readers. tinyrant :D
I know when I was in my CS program in the early 90s it seemed like that was transitioning. The older profs would teach us every trick in the book to reduce the actual file size & visual noise in our code. The younger profs would beat in our head to not do that.
Map<K,V> drives me nuts. Especially since K has to be comparable and/or hashable.
I'd be going with `userAge` or `user_age` either way. :-P
(in fact, some of the best keyboards I've ever used were of the old mechanical-switch variety found on IBMs, teletypes, and various CRT terminals. Even the worst of the bunch were a lot better than the almost-legendarily-bad macbook keyboard I'm using at this moment.)
> I was watching videos about my favorite topic, UNIX history, and found a talk by Hendrik Jan Thomassen. He's the UNIX pioneer in the Netherlands and has worked with Thompson and Ritchie in the past. He says the real reason why UNIX commands like ls, cp, mv, etc., are short is because PDP11 console keys were super hard to press. You'd get knuckle pain if you typed too much. So to save your fingers, all the commands were designed to be as short as possible. True or not, that's a fun story.
The page includes a video.
As an undergraduate, I was relegated to the teletypes or a modem on one of the 10 dialup lines -- upperclassmen and grad students got first pick of the CRTs and there was rarely a free one available. I suppose I get the last laugh, as I didn't developer carpal tunnel syndrome.
Coding assembly on a teleterminal using greenbar paper now seems equal parts primitive, badass, and insane.
It still is; there is a reason people tended to shorten terms in the field they are deeply engaged in. Verbosity gets in the way, and there is always a tension with it (and where the optimum point for clarity is depends on the focus and expertise of the audience.)
As to why it happened in the first place: it is from an era when some compilers only looked at the first 8 characters for uniqueness of a name. Toss on top of that that a screen was only 80 characters wide and short names made a lot sense.
Now it is just tradition, I feel, more than anything else.
There are still developers that insist on code only ever being 80 characters wide, even in today's world of 24+ inch, 1920x1080+ monitors.
I understand wanting code to be narrow enough to be able to have diffs side-by-side, but you can get 120-character wide lines and still achieve that.
I've seen others argue that if a line is too long, it's harder to understand what it's doing, and I would agree with that with one caveat: In this context, "line length" should be understood as the number of operations being done, not necessarily absolute number of characters. A line that is 110 characters long but has meaningful variable and function names is going to be easier to grok than a line with only 60 characters but has terse names.
Resolution is largely irrelevant. The size of the font and screen size/distance is what matters for readability.
Think in terms of books that are pleasent to read. If the lines are too long it’s easy for some to lose track of where they are. If the font is too small people with poor eyesight will have trouble reading anything. Character limit is an accessibility issue. I am personally limited to ~80 characters for my use case.
Though the issue is largely irrelevant for people who are know of therecent AST aware formatting tools that can limit lines to whatever length you’d like.
Regarding variable names, it's a matter of taste, but for example:
NumberOfDocumentsProcessed += MaybeFindUpdatedDocuments(ClientPool->GetClient(NextClientIndex++));
n_docs += find_updated_docs(cli_pool->get(next_cli_idx++));
I find the second line much more pleasant to read, especially if these variable names repeat several times in a function, which they usually do. The problem with long lines is that the style inevitably drifts toward the first example, because nobody can say no to "Can we make the names more descriptive?" With 80-character limit there's a certain friction that acts as a counterbalance.> then... (and this is the kicker) when you write the code, for each of these paragraphs you write a corresponding global function named:
> Act1_2_4_6_3_13()
> Do not document these functions. After all, that's what the design document is for!
Ideally, your company will also have several file shares into which old and unidentifiable documents are filed, in nested layers of folders named things like "Old documents", "John's Code", "Please Clean Up", and "Maybe Delete". However, for maximum effectiveness do not file your design documents here! Instead, put various old drafts (not identified as such, of course) in a folder on the server where your code used to run, somewhere like C:\backups\aurdbcl1\C\Documents and Settings\users\jdoe\Desktop\ (where aurdbcl1 is the name of the server where your program ran 20 years ago). This will ensure that they will spend as long of a time as possible hunting for something that could be useful. If possible, also ensure that there are some multi-Gigabyte zip files scattered about which have to be unpacked just in case there's something useful inside.
A related technique for compiled languages is to arrange for various copies of the source code to be copied into similarly diverse locations. Do not use version control under any circumstances; if you must, check in at most once a month with the comment "changed code". This works best for a program which is maintained very rarely. Anyone attempting to fix it will first have to identify which copy of the source code is the right one. Make sure you also have some copies that never made it to production, where you started changing things and never finished. If possible, the production version should also be subtly different from all copies of the source code that can be found.
(The above is, unfortunately, from experience. I have many other rants which similarly belong in this document, particularly around exception handling and copy-pating, but I feel that this comment is becoming too long already.)
1. Write a CAD system. User-facing code in BASIC. Rendering engine written in assembly.
2. Run the interpreted BASIC code through an optimizer, which condensed all variable and function names to be as short as possible (one or two characters) and moved frequently-used routines to the top of the module before compiling it all into one executable chunk (no more interpreter). This optimized code is almost unreadable but executes a lot faster (25%-50%) than the human-readable interpreted code. Hurray!
3. Lose the original source code.
Build machine's hard drive died. The backups weren't. No version control -- only occasional snapshots. Source code that was on various floppy disks lying around the office was incomplete and badly outdated (like the "Old Documents/John's Code example, above).
4. Team panics and spends a month trying to piece "Humpty Dumpty" (the software) back together again. No luck.
5. Spend the next year trying to reconstruct function and variable names from "AA423()" and "aa_237" to such gems as "customerListIThink", "doesSomethingToInventory()", "NoIdea()", "BreaksWhenIChangeIt()" while struggling to add new features at 1/10th the rate before the loss.
6. Try to rewrite the whole thing in C.
7. Go out of business.
True story.
For a second there I thought i was reading about golang[1].
* Single letter var names[2] / [3]
* Underscores [4] / [5]
* extended ascii code [6] / [7]
[1]https://golang.org/doc/effective_go.html
[2] https://github.com/Droogans/unmaintainable-code#single-lette... [3] https://github.com/golang/go/wiki/CodeReviewComments#variabl...
[4] https://github.com/Droogans/unmaintainable-code#underscore-a...
[5] https://golang.org/doc/effective_go.html#blank
[6] https://github.com/Droogans/unmaintainable-code#extended-asc...
They are also a Haskellers favorite.
There's a non-sarcastic paper running on the internet named something like "descriptive variable names considered harmful". Most of the community agrees with it, including me.
There are tons of those in Android framework code as well
> It took a team of maintenance engineers nearly 3 days to figure out that this whopper variable name described a const, reference, function argument that was holding information from a database column of type Varchar[30] named "LastName" which was part of the table's primary key.
- const, reference and function argument should be pretty obvious from the code context
- LastNameCol seems like self-describing... of course you would have to know that you're debugging an application that talks to databases, but eh
I'd say it's somewhat akin to proving a negative. Sure you get that it's a const, reference and function argument which likely refers to the Last Name Column. However is that all it is? Or is there more meaning in the name? Meaning that could be crucial to understanding how the application functions? Does one of the characters mean function reference or does it refer to something else? Apparently not in this case but how can you know without investigating?
It’s like when you find a nil-guard in a function. You can’t truly refactor it until you know whether it was added defensively just-in-case or whether nils can still exist in the system at the function’s callsite, calling for further upstream examination.
There’s a certain hell in trying to follow a code base where every function is infected with ‘if (!arg) return’.
Fortunately, he stopped programming and became very successful in agriculture. Good outcome for both fields.
Well that is what agriculture is all about.
https://quoteinvestigator.com/2016/12/30/not-malice/
I can offer as corollary to the article a couple of unmaintainable simple batches:
http://reboot.pro/topic/2986-yacbfc-dec2hexcmd-and-hex2deccm...
This one is good and there's no attribution problem.
Only have one struct with a member of every type. That one struct will be the type of every variable you have.
If the language you use support implicit operator overload abuse them to make sure all types can be implicitly converted to each other.
Ex.
struct Variable { byte a; short c; int d; long e;
...
}This will make it impossible to know what type a variable is until you find its assignment, which of course isn't on the same line as the declaration and even better reuse the same variable for multiple values.
typedef union { int i; float f; double d; long l; char c; void *p; ... } var;
Now you might also get some nice float-interpreted-as-int bugs if you forget the type!
You can also kind of pretend that you have C#-style type inference:
var x = { .i = 10 }; var y = { .f = 2.0f };
Prefer APIs over data.
Start with DSLs.
Always connect (and never enqueue).
Create abstractions for information.
Use static typing across subsystem boundaries.
Put language semantics on the wire.
Write lots of unit tests.
Leverage context.
Update information in place.
C++ has member variables (which this refers to) and member functions.
"Method" is not part of the vernacular.
This doesn't make sense on so many levels. Only a recluse sitting in some dungeon who never dealt with people can come up with something so inane.
Edit:
I understand that that every developer should aspire to become someone who writes code that reads like a children's book, and they should. However, actively discouraging writing comments is so wrong on so many levels.
At the last place I worked, we used to have a small at-home coding test before interview and none of the young developers who successfully completed the test did any sort of code documentation or commenting. When I asked why, they would tell me that their code is self-documenting, which was utter nonsense.
At the last place I worked, we used to have a small at-home coding test before interview and none of the young developers who successfully completed the test did any sort of code documentation or commenting. When I asked why, they would tell me that their code is self-documenting, which was utter nonsense.
Would you really argue in favor of something like this?
``` class Account { ... /// returns the accountId public int d_nr() { ... } } ```
``` class Account { ... /// returns the accountId public int getAccountId () { ... } } ```
The first comment is obsolete when using a proper method name, the second does that and so the comment is redundant.
May I ask: What did you experience in the wild?
All you have accomplished really is to move the descriptive name from the variable's name to the variable's type, and at what cost? You now have to create a whole new datatype. The programmer must look up that datatype and see oh, it's just an int. And now you need conversion routines or worse yet casting to convert that type to a simple int for interop reasons (database, UI).
Experience has shown that it is very productive to have a small number of generally useful datatypes which are augmented by custom datatypes.
Creating a new datatype for something as simple as an int or String defeats that and makes it more cumbersome to work with. (It's especially unwarranted for a variable that is an immutable variable serving as a simple id.)
If you are using a strongly typed language, you can just right click on the accountId type and find everywhere in your codebase where the “accountId” is being used.
The “accountId” is not “just an int”. An accountId has semantics that would be different than an int. An accountId is not the same as a “customerId” that may also be represented as an int. The accountId also has different uses than an int. You’re not going to take the sum, average, etc of an accountId.
An accountId that happens to have a value of 1 is semantically not equivalent of a customerId of 1. A method that expects a list of accountIds that are just ints will just as happily take a list of customerIds.
But a method that takes as a parameter List<AccountId> will cause a compile time error if you pass in a List<CustomerId>.
Why use a strongly typed language and then ruin one of the benefits of it if you don’t use domain specific types?
And as for the "type shyness":
> Creating a new datatype for something as simple as an int or String defeats that and makes it more cumbersome to work with.
It is never "just an int". Soon you will ask questions about the int, and in a sufficiently large application you will forget the answers to the quesions easily, and scatter the same question (i.e., the same boolean logic) all over the place. Consider an application where you need to filter something by year. A year, you know? As in 1974, 2018, etc. An int is bad, because an int could be -5, which does not make sense. Ok, a uint then? Still bad, because then there is a year 0 and - if your application deals with birthdays of real people, 1800 would be an invalid birthday. The semantics (or pragmatics, linguistically speaking) emerge from how you interpret the integer, and this interpretation can happily live in that one file with that one class that encapsulates "just" that integer.
The AccountId probably shouldn't be < 0, and a specific type could be the one place where you make sure that this is always the case.
What if you start with a 32bit integer and suddenly you realize that your weird Internet-Of-Things-Sensor reading database has grown and the 32bit integer is getting too small? You could just use 64bits. With "just an int", you now get to change every single function that expects an int for the purpose of a SensorReadingId (or an account id or whatever) and change it to size_t, uint, int64 or whatever. OR, you just tell the object that its internal representation uses 64bits now, because the AccountId class is the one single place that deals with account ids. The type provides a consistent interface that allows you to infer the semantics of its use. An int doesn't do that.
This idea predates java by some ~20 years [1]. An int is not an account id. An int is not a year. An email is not a string. The result of working like that is considered a code smell [2].
Still, coming back to the example I made: It was primarily about commenting. I was taught to always comment, and StyleCop enforced comments on each and every field of a class, leading to noisy, superfluous comments.
/// Gets or Sets the Name
string Name { get; set; }
does not add anything.As for returning an AccountID: I'd like to point out that the Account is a strong and independent object, who can make his own decisions and does not need to return anything. Returning its id like that is actually breaking encapsulation. Still if you do it, it probably shouldn't be an integer.
Why are people scared of creating types and objects?
[1] https://link.springer.com/chapter/10.1007%2F978-1-4612-6315-...
[2] https://sourcemaking.com/refactoring/smells/primitive-obsess...
You deal with a lot of data that is of only a few simple types, and as I said in my post, interoperability means you can't get too crazy with your class definitions. They need to stay simple or the SYSTEM will become more complicated.
As for a variable never staying a simple type, again, see my example in my previous post. A string id is going to stay a string id and its data and behavior is dictated by being an id to remain supersimple.
I am not against defining new types; I am against defining a new type for every single variable "just because."
Most languages don't have the plethora of integer types that C has. Java, for example, has just has 2 in general use: int (signed 32 bit) and long (signed 64 bit). (Nobody considers byte a general integer type.)
In C/C++, you have to define so many things about the types you use because so much is "implementation dependent." So for C/C++, you may be correct. Most other languages define their types more stringently and include a smaller number of general purpose types.
https://docs.microsoft.com/en-us/dotnet/csharp/language-refe...
But even then, a year is an int, but it would have certain semantics you would want to enforce outside of integers.
Are you honestly suggesting making a Year class to represent years in a C++-like language? And you want to forbid Years from being -5 or 1800, because they "do not make sense"?
What if I want to interpret the integer in a way you didn't think of when you wrote your Year class? Maybe I want to know whether my friend John was older when his sister Sally was born than Jesus was when St. Paul was born?
> What if you start with a 32bit integer and suddenly you realize that your weird Internet-Of-Things-Sensor reading database has grown and the 32bit integer is getting too small? You could just use 64bits. With "just an int", you now get to change every single function that expects an int for the purpose of a SensorReadingId (or an account id or whatever) and change it to size_t, uint, int64 or whatever.
The work needed to push the buttons on your keyboard to make the changes is the least of your worries when you want to change the underlying data types used in a complex program. If you have a special SensorReadingId type and you change what it is, all your code breaks at once and you get a hell of a mess to untangle.
It may not be practical to enforce "ints that represent sensor reading ids are SensorReadingId type" across all of your code. You'll need to unpack your type to a primitive type whenever you talk to an external library or serialise SensorReadingId's to storage or the network. Even if you did enforce SensorReadingId everywhere:
- Arrays of SensorReadingId now double in size. Perhaps your code crashes at startup because some stack frame grew too large.
- If you serialise SensorReadingId's anywhere, changing the type broke binary compatibility with your old code. Or the network protocol you're supposed to be speaking. Or the library you're using.
- You probably had to make a toInt() or unpack() or similar function anyway so that people who want to get at information inside the value of the SensorReadingId can do so. You still have to look at all the call sites of these functions.
In reality, you're going to do this sort of change incrementally. You push the 64-bit integers through more and more code, taking care that the high bits of the integers make it through everywhere you've made the change.
> As for returning an AccountID: I'd like to point out that the Account is a strong and independent object, who can make his own decisions and does not need to return anything. Returning its id like that is actually breaking encapsulation. Still if you do it, it probably shouldn't be an integer.
Why is an Account making its own decisions? This sounds like the "circle that knows how to draw itself" antipattern. My friend and I want to talk about a certain Account; how do I arrange things so that the Account makes the decision to put an identifier for itself into a certain field in a certain protobuf message before I send the message to my friend?
That’s why languages don’t represent dates as just as struct with ints - there is a Date class that knows how to semantically add days to dates taking leap years into account, time zones, etc. That supports our point....
Then will your class take into account that the calendar changed in the mid 1600s(?). If you need to calculate dates and compare dates.
If you have a special SensorReadingId type and you change what it is, all your code breaks at once and you get a hell of a mess to untangle.
How so? All of your other code should be treating it like an opaque type. If I was comparing two sensor ids for equality outside of the class and I changed the underlying type, I should modify the equality comparison operator to work with the new semantics.
It may not be practical to enforce "ints that represent sensor reading ids are SensorReadingId type" across all of your code. You'll need to unpack your type to a primitive type whenever you talk to an external library or serialise SensorReadingId's to storage or the network. Even if you did enforce SensorReadingId everywhere:
You shouldn’t have to do it in but one place. The serializer would call your .ToString() method anyway. You just override it.
If you serialise SensorReadingId's anywhere, changing the type broke binary compatibility with your old code. Or the network protocol you're supposed to be speaking. Or the library you're using.
Versioning and deprecating APIs and interfaces is a solved problem. But you shouldn’t be using the same model for internal use as your external model anyway.
Arrays of SensorReadingId now double in size. Perhaps your code crashes at startup because some stack frame grew too large.
I thought we were specifically not talking about low level languages and you mentioned Java. When would that be an issue in Java?
Why is an Account making its own decisions? This sounds like the "circle that knows how to draw itself" antipattern. My friend and I want to talk about a certain Account; how do I arrange things so that the Account makes the decision to put an identifier for itself into a certain field in a certain protobuf message before I send the message to my friend?
I wouldn’t be sending my Account class that probably has business logic in it across the wire. I would be mapping it to a dumb AccountModel POCO/POJO with something like Automapper.
In C, `struct tm` is literally a struct whose fields are all `int`s. But people typically represent time in C programs as an integer of some width measuring time in some fixed unit since some fixed point in time.
> - there is a Date class that knows how to semantically add days to dates taking leap years into account, time zones, etc. That supports our point.... > > Then will your class take into account that the calendar changed in the mid 1600s(?). If you need to calculate dates and compare dates.
I can't anticipate those requirements---maybe our date class has to bomb out if you pass in, say, 1800 or -5 as a year because someone on my team insists they're invalid.
People work around these classes when they have to do date arithmetic because they're too slow, or they depend on the machine's locale and the locale isn't under control, or they take timezones into account in fundamentally broken ways, or any number of other reasons.
>> If you have a special SensorReadingId type and you change what it is, all your code breaks at once and you get a hell of a mess to untangle. > > How so? All of your other code should be treating it like an opaque type.
That's a fantasy. When you wrote your reply, you deleted all the parts of my post explaining "how so"---why not all of your other code is going to treat it like an opaque type.
> You shouldn’t have to do it in but one place. The serializer would call your .ToString() method anyway. You just override it.
I don't want to convert your boxed int to a string and then parse it as an integer just to do math on it. That creates a ton of syntactic noise, not to mention being slow.
> Versioning and deprecating APIs and interfaces is a solved problem.
Yes, and the solution is a pain in the ass! So don't create new interfaces to version and deprecate for no good reason.
> I thought we were specifically not talking about low level languages and you mentioned Java. When would that be an issue in Java?
I didn't mention Java.
> I wouldn’t be sending my Account class that probably has business logic in it across the wire. I would be mapping it to a dumb AccountModel POCO/POJO with something like Automapper.
And I'd just send an integral id over the wire, probably documenting that it's the id of an account using something like the field's name in the message. No extra tooling needed, and I don't have to make a fuss about how the id() function (that I had to add to the Account object anyway) is for the big boys and should be avoided whenever possible.
People rapidly get themselves into deep shit when they find out their overcooked codebase is too slow to be useful. The class names remain but their structure and function and any semblance of the clean abstraction they used to provide are the first things to go.
Even C has a bunch of functions to work with the date struct as an opaque type....
I can't anticipate those requirements---maybe our date class has to bomb out if you pass in, say, 1800 or -5 as a year because someone on my team insists they're invalid. People work around these classes when they have to do date arithmetic because they're too slow, or they depend on the machine's locale and the locale isn't under control, or they take timezones into account in fundamentally broken ways, or any number of other reasons.
And then you use a better class. In Java you use something like JodaTime in .Net you use NodaTime - a .Net library written and maintained by Jon Skeet (the person with the highest ranking on Stack OverFlow). If that’s not good enough you write a custom extension or mixin to handle your special snowflake handling and still let all of the consumers treat it as an opaque type.
I don't want to convert your boxed int to a string and then parse it as an integer just to do math on it. That creates a ton of syntactic noise, not to mention being slow.
We were talking about an accountId that just happens to be represented as an int. You’re not going to be doing math on an account id. Also neither .Net with generics or even C++ templates have the boxing and unboxing issue.
Yes, and the solution is a pain in the ass! So don't create new interfaces to version and deprecate for no good reason.
If there was a business case to change the underlying structure - by definition there was a “good reason”.
And I'd just send an integral id over the wire, probably documenting that it's the id of an account using something like the field's name in the message. No extra tooling needed, and I don't have to make a fuss about how the id() function (that I had to add to the Account object anyway) is for the big boys and should be avoided whenever possible.
Even so, you should still not send your internal representation that may change across the wire and do some type of mapping. Are you going to send the same model that came from your ORM that’s tied directly to your database across as the representation to the outside world?
Yes, for conversion to/from time_t, formatting, and parsing. That's my point; you use the integral type whenever you don't care whether it was a Thursday because the integral type isn't going to waste your time.
> And then you use a better class. In Java you use something like JodaTime in .Net you use NodaTime - a .Net library written and maintained by Jon Skeet (the person with the highest ranking on Stack OverFlow). If that’s not good enough you write a custom extension or mixin to handle your special snowflake handling and still let all of the consumers treat it as an opaque type.
Stop with the hero worship. You should do things that make sense in context. Recommending that everyone use a library in all contexts because it's written by Jon Skeet is a ridiculous thing to do. (BTW, it seems like the Joda-Time people recommend you use the standard java.time package instead.)
> We were talking about an accountId that just happens to be represented as an int. You’re not going to be doing math on an account id. Also neither .Net with generics or even C++ templates have the boxing and unboxing issue.
Who are you to say I won't do math on account ids?
> If there was a business case to change the underlying structure - by definition there was a “good reason”.
In the situations the two of us have discussed, there is no business case to create these interfaces in the first place. Where is the good reason?
> Even so, you should still not send your internal representation that may change across the wire and do some type of mapping. Are you going to send the same model that came from your ORM that’s tied directly to your database across as the representation to the outside world?
What ORM? What database? If I'm using an ORM and a database, high-performance time operations are likely out of the question.
What else are you going to do in C? But even most real world implementations of C have all sorts of structs and typedefs that you shouldn’t make any assumptions about and you should treat as an opaque type.
Who are you to say I won't do math on account ids?
Is that really what you are going to argue? That in a real world use case you’re going to be doing math on an accountId?
What ORM? What database? If I'm using an ORM and a database, high-performance time operations are likely out of the question.
So you’re going to pass a raw key/value record set around your code and program like it was 2006? So now you’re going back to mapping your sql resultset to an object that makes sense anyway. Going right back to my point that you’re going to have a mapping between your raw results -> domain model -> view model (where the view model is serialized for external use) and back again anyway.
Subtract two times? (BTW, struct tm isn't an opaque type.)
> > Who are you to say I won't do math on account ids?
> Is that really what you are going to argue? That in a real world use case you’re going to be doing math on an accountId?
You seem to think that's absurd? Maybe I want to store a set of accountids. So I sort them (bit-extraction or comparison!), take deltas (subtraction!), and encode the deltas sensibly. Maybe I have two lists of accountids and I want to check that none of the things in the first list show up in the second list. Some sort of hashing scheme (bit-fiddling!) is going to be my friend here.
> > What ORM? What database? If I'm using an ORM and a database, high-performance time operations are likely out of the question.
> So you’re going to pass a raw key/value record set around your code and program like it was 2006? So now you’re going back to mapping your sql resultset to an object that makes sense anyway. Going right back to my point that you’re going to have a mapping between your raw results -> domain model -> view model (where the view model is serialized for external use) and back again anyway.
What SQL? What resultset? What object? I have an integer. Why do you assume it doesn't make sense?
You’re going to do “bit extraction” to sort? If you treat accountId as an opaque type, you are going to have a comparison operator as part of the type and the rest of your code is going to just use a < or > symbol.
take deltas (subtraction!), and encode the deltas sensibly. Maybe I have two lists of accountids and I want to check that none of the things in the first list show up in the second list. Some sort of hashing scheme (bit-fiddling!) is going to be my friend here.
Or in a modern language since you have already defined equality between the two types and overridden the GetHashCode() function....
var deltas = accountList1.Except(accountlist2)
Why are you doing that all through your code instead of encapsulating the concept of equality, less than and greater than in one place?
What SQL? What resultset? What object? I have an integer. Why do you assume it doesn't make sense?
You were referring to CRUD apps and having to serialize the Account object. Either you are using an ORM, you are getting results from a database as a record set - which is usually represented by a dictionary of key value pairs and mapping it to your Account class or you are sending back a raw record set.
More than likely, you are mapping from your domain model to your externally exposed view model anyway.
You (i.e., all commenters in this thread somehow disputing the AccountId object version of the story) are completely right, it is difficult to decide up front what you need. Of course, depending on your domain, it may be perfeclty valid to have -5 years or 1800, but I am not asking you to never implement years like this, I am merely suggesting that it is useful to have one place where such decisions, invariants, roles - your understanding of a year, perhaps specific to your domain - are located.
How your understanding is structuring is a different concern, because of course, a class with 10000 LOC is horrible to work with. But you can always use means of abstraction, and compose independent modules. An int does everything, maybe in one context a year shoudln't be 1800, maybe formatted as 4 digits. A year class / type / object is the place to store these decisions. As I outlined, such decisions are both technical (how many bits?) but may also be domain specific (year must be > 1800). Maybe you have an int internally, but abstract it with a non-zero int.
It boils down to interpretation. An int allows for many operations, but they may be invalid (in the sense of "implausible", or "undefined"). For example, an account ID of 1 can be multiplied by 2 and then by 2 again, and so on. Integers form a relation this way, but not all extensions of this relation might be "meaningful.
Integers as ids are a great example for this. Technically, they are great, because they are simple numbers, can be typed on a keyboard, and be readily interpreted, but not like integers.
6 people two times as many than 3 people. In Germany, grades are marked 1 - 6, (1 = very good, 6 = very poor). But the relationship is merely ordinal. A grade of 4 is not "twice as bad" as a 2, although the numbers are relatable that way. An Account ID of 6 is not twice as "good" as an ID of 3. For ids, you want them to identify, in that they are exclusive and exhaustive - but integer IDs are not supposed to be ordinal (in that their sizes are comparable in relation to each other, 6 > 5 is an invalid statement). Of course, you can retain this interpretation, e.g. an auto increment id would indicate that id 6451 was created WAY later than ID 6, but this is difficult to interpret; because it doesn't really tell you how much later, and also deletion of integers in between a range may be reissued (id 1, 2, 3, 4, delete 3, 3 is a missing rank, 3 could be reissued). So ids aren't ints, because they are exhaustive, but they aren't ordinal, or at least interpreting them ordinally is dangerous, and some operations aren't allowed even if you interpret them to be ordnial; for example, it would not make sense to calculate an arithmetic mean from integer ids, although mathematically this operation is allowed. In statistics, this concern is discussed as the Skalenniveau (German, meaning Level Of Scale), in English it's called level of measurement [1].
In sum, it does not matter what your usecase is, an int is an int, but depending on your use case, it might be worthwhile not to pass an uninterpreted integer around, but actually wrap it in some kind of object, where you localize all your decisions how to interpret the integer.
None of should be derogated as some form modern hipster javascript, where none of us highlevel kids don't know how to bit-bang a set difference from some account ids; but rather, these ideas are really old. Even in C, data abstraction is useful, in that you don't fiddle with integers but define a set of methods, possibly in a module that interact with a hidden internal representation. Deciding on where to cut these modules appears to be a difficult task, but we all have known this for a while now [2].
As I said, nothing wrong with bitbanging, but encapsulating interpretation in types, classes, objects, modules or functions is a useful strategy to reduce the complexity, and you lose all of it when you just pass integers around and cross your fingers and hope the next developer won't calculate a mean from your integer ids.
And finally: The performance argument. Tell me how many requests/ops per second you need and let's find out why your program can't do them. Make it work, make it fast, not the other way around.
I have had too many arguments where people talked about "performance" without stating numbers. I had a colleague who argued that joins were bad, because they were slow (which conceptually, they are), but then your database is a highly optimized processor to do exactly these operations. Their fear of table-joining yielded a database with few tables, each of which had very long columns, each of which contained values separated by two semicolons, which they would then manipulate using string manipulations. Also the table grew, because the lack of normalization caused a lot of redundancy. I have seen many sins in the name of performance, and I will kindly ask about some numbers. Performance without numbers is not a good argument against data abstraction.
Also it can usually be handeled. If your high level Python program becomes too slow for some reason, feel free to implement the slow parts in a really fast language and add a clever algorithm in assembler; see for example np and scipy.
These approaches aren't mutually exclusive. "Please don't do it" and "considered harmful" doesn't get us anywhere.
Anyway, coming back to the topic of the thread: How does code become unmaintainable? By following rules of thumb without thinking on their contextual requirements, and most often by passing around integers in the name of performance, while ignoring locality of decision, locality of code, and ease of comprehension. An int is memory, and account id is an intentional interpretation of this memory (an int also being an interpretation of memory already, I get it, this is about abstraction - and finally, true comprehension comes from world-reference. The int doesn't care whether it is 5. You do, see above.)
RAM and CPU power are less expensive than two developers wasting their time trying to understand some low-level code and identifying which functions expect an int64 and which need a size_t, and whether they are equivalent, and so on, while they could just be passing around some thing with a stable interface, and a localized world-reference (you know, a name).
I wouldn't argue that all of this object/type stuff it is THE way to go, but these treatments were all invented specifically to solve the issues of raw-integers. And certainly I am quite ok with the fact that not everything uses Java; however the ideas we're talking about here are not specific to Java, and Java in particular often provides a very poor version of the story.
I would, however argue, that it is important to be congurent to the unit of expression of your programming language. Treating C as if it were object-oriented will give you a bad time, and not using objects in C# and Java will also give you a bad time. If your language natively provides an optimized iterator pattern, as does Python, coding in the C-for-loop-index idiom will give you a bad time. Most unmaintainable Java comes from not understanding Java, because you mistake it for C without & and *, but with Objects.
Unmaintainable code is about people. Code doesn't maintain itself.
</rant rel="sorry">
If you can explicitly refute those, then your position would be stronger.
Why should the developer care what the underlying type is?
It should be an opaque type. When you serialize it, your serializer should call your ToString() method. When you deserialize it, your deserializer would call the constructor that takes an int.
But why are you saving an id to a database - which would never be used as int - as an int?
But even in most CRUD apps, your domain model with rich types would be different than your view model which would probably also be different than your DB model.
You are going to be mapping back and forth regardless - hopefully using a tool like Automapper.
That's because some document generators will exclude functions that doesn't have document attached.
I see this in practice WAY too often... Heck, I've been somewhat guilty of it myself before.
(Bonus points if it's in a functional language :P )
Also, I've been led to believe that design of constrained / embedded systems follow different rules. Your code can be unreadably complex if you have to squeeze out that extra performance (however, even then, it should probably be well-commented or well-documented), and generally (though not alway) such programs will be smaller and made by smaller teams.
It's when these kind of practices are applied in enterprise software with large number of programmers, where maintainability is more important than performance, that this is an issue.
What is meant by that statement? Is "idempotent" a mistype/mistakenly selected word? If the author meant "pure" instead, should I file a PR, or consider this some sort of meta-meta-obfuscation? If the author meant "pure", is that actually true of C specifically, or just generally good programming advice (localize side effects to known locations, the fewer the better, don't sprinkle them all over and document where they occur)?
f(f(x))=x
assuming f is a function (binary operators are just functions (maps) in mathematics, too, e.g. +:NxN->N (read: "plus" is a map from the Cartesian product of N by N to N))
At the very least it should be presented in a more unmaintainable way.
Edit, apparently repo owner already checked with author: https://github.com/Droogans/unmaintainable-code/issues/3 :+1:
Grey/Gray gets me every time.
I (American) was working previously on a project with a British-spelling team for a British-spelling client. It was fine when they had named a variable and I could follow their lead, but there are so many random words with spelling differences that I’d never think of that there was invariably a bit of mix-and-matching between spelling conventions.
It’s not like I’m going to look up every word in a British English dictionary to see if I’m spelling it the British way.
GrEy for England, grAy for America.
Also, use synonyms for common function verbs. Like Grab instead of Get. GrabPageFromServer()
In a code base I dealt with, a developer did exactly those. Most "get" functions had "get" at the end of the function name. Some functions had it at the beginning, but replaced with "grab".
Also, he misspelled a class name and just went with it.
https://github.com/Droogans/unmaintainable-code#a-real-life-...
Also I noticed a mistake in my comment.
I wrote ", should read" it should of course have been ", should write"
i for an index is ok, but this is a red herring: You only ever need an i for imperative iteration and in a sufficiently high level language you are better of with selection and projection, (iteration for that matter), where you just foreach over each element and don't actually need to ask for things by index. Sure, y'all be hacking C, nothing wrong with that, it depends on what you str doing, but for comprehension, the mere existence of i is a red flag, because most of the time you want something to happen to a collection if things and foreach is perfect for that (or map, fold, filter, pick your poison).
Still, a better example than `i` for short identifier names would be x,y,z, or r,g,b These are so common and conventional that I would expect them to have semantics of their own, still they could be chunked to types such as Coordniate, Location, or Point, or Paint, or Color, respectively. But comming up for better names than x,y,z is really difficult, will bloat your code and make it hard to understand what is going on.
I am pointing this all out, because many of you seem to favor such very short identifier names - and I am with you: sometimes they make sense, see above - but empirical evidence suggests that very short identifier names, and single-letter identifier names in particular significantly worsen comprehension of code. The same is true for abbreviations (depending on how strongly you abbreviate).
[1] https://ieeexplore.ieee.org/document/7884623/ (disclosure: author)
[2] https://doi.org/10.1007/s11334-007-0031-2
[3] http://compscinet.dcs.kcl.ac.uk/JP/jp040302.abs.html
[4] https://dl.acm.org/citation.cfm?doid=382080.382628
[5] https://link.springer.com/article/10.1007%2Fs10664-018-9621-...
Preprint for [1] here: https://brains-on-code.github.io/shorter-identifier-names.pd...
[5] is the extended Version of [1] including discussion of how attention is shifted during the experiment. It turns out that, when code uses words as identifier names people don't need to read the comments as much.
I've noticed this especially true for JavaScript and Go.
In JavaScript , because the language is still evolving and there is no real standard about how to architect code with this language unless you are using a entire "platform" like Angular / Vue / React witch will highly influence your coding style.
In Go because Go developers are performances obsessed , coming from C/C++ background and do believe that naming their variable with one letter will save 1GB of memory allocation , 1000 CPU Cycles and make their code "Clear and Concise"
Ultimately , because there is very little standard in this industry in terms of code governance and a large percentage of projects are outsourced to other companies , we end up with what is described in this repository.
The problem is that you get too close to the code you write, and make assumptions about its obviousness that simply aren't true. Far better to be explicit at the cost of a few extra keystrokes.
data.reject{|e| e.value.nil?}
data.reject{|element| element.value.nil?}
I would always prefer the first one. My general rule of thumb is: If a variable has the scope of exactly one line, or if it is an incremental counter, use one letter.Go is also not performance obsessed - else it would not have opted for garbage collection by default.
As far as variable naming goes, Go convention says - the further from its declaration a variable is used, the more explicit and qualified the name should be to disambiguate it. This to me strikes as being eminently sensible.