Programming like a Pirate (Alt+Shift+M)
kranglefant.tumblr.com
kranglefant.tumblr.com
if (page.hasAttribute(“Test”)) {…
is when you then decide to change boolean attribute Test to an enum attribute Environment with values Development, Test and Production.Because now you have to hunt down all the page.hasAttribute(“Test”) in your code and replace them with something different. And if your code is creepy enough you will miss one or two and something will break.
So you have to look very carefully whether you repeat the same checks. If you repeat a check in different places and it has some meaning, you should abstract it away.
Encoded strings like this are a maintenance problem lying in wait.
How do you grep/search for it? These are all equivalent:
if (page.hasAttribute("Test")) {…
if (page.hasAttribute('Test')) {…
string test = "Test";
if (page.hasAttribute(test)) {…
string cheese = "Test";
if (page.hasAttribute(cheese)) {…I hated seeing somebody argue against it because we'd be better off as developers if more people understood this and made a habit of hiding implementation to this extent.
If you're using a compiled language, the compiler will tell you all the lines that need changing.
If you're using a dynamic language, you should have test coverage for all those methods and your tests will blow up with "no such method".
Or maybe, you could special-case the 'hasAttribute(name)' method so it logs a debug warning and checks the enum attribute.
Really, there are enough ways to deal with that and you're far from the first person to have had that problem.
Please don't do this never.
With "isTestPage()" you have a method with well-specified outputs.
With "page.hasAttribute('Test')," a load of things become part of the permanent interface of the class: hasAttribute(), the datatype of 'Test,' the content of the field in the constructed argument etc...
"Does isTestPage() return what I expect? Yup, so the problem is going to be in surroundPageWithSetupsAndTeardowns()."
A well named function becomes a black-box; you can tell if it's working just based on its inputs and outputs. Why open black boxes that are working?
Maybe to modify them ?
I really don't like this attitude of ignoring inner workings of things just because they seem to work. Sometimes we really are working under constraints that make digging through the code impractical, but in my experience these constraints tend to be in the code itself - namely, someone wrote it as a 'black box' with tons of kludges and `if False:` style comments.
I don't want to exaggerate and say that every programmer should know his every tool down to it's last bit, but taking black boxes apart is what makes one a hacker, and what allows one to improve one's skills.
On the other hand, most systems these days are so complex that you have to have some black boxes. I know more about how my linux kernel works than the vast majority of people I work with, and yet I would classify the majority of it as a black box.
I have a general idea of various trade-offs in efficiency and fragmentation resistance in malloc implementations, and if I have to I can dig into one. On the other hand, jemalloc works great and I just don't have the time to take it apart, so I just use it.
Ultimately to get things done you need to trust that functions do what they say they do until you get evidence to the contrary. Taking functions apart can make you better at doing your job, but rarely fixes the problem you have Right Now.
There are simply fewer thing that can go wrong.
bool noMoreElves() {
return elfCount == 0;
}
void fireAllReindeer() {
for (int i = 0; i < reindeerCount; ++i) {
delete reindeerArray[i];
}
delete reindeerArray;
}
void checkCloseWorkshop() {
if ( noMoreElves() ) {
fireAllReindeer();
}
}
the above is certainly readable, but takes forever to parse -- the below is, I would argue, just as clear, and much quicker to read (the point the OP made) void checkCloseWorkshop() {
if ( elfCount == 0 ) {
// fire all reindeer
for (int i = 0; i < reindeerCount; ++i) {
delete reindeerArray[i];
}
delete reindeerArray;
}
}
the comment above the loop is possibly superfluous, but helpful"Good code invariably has small methods and small objects. I get lots of resistance to this idea, especially from experienced developers, but no one thing I do to systems provides as much help as breaking it into more pieces."
– Kent Beck
I dislike reading Smalltalk influenced code with many small pieces. They tend to use instance state as a way to pass parameters, and that makes me uncomfortable - I have a leaning towards functional programming.
I also distrust function names. A function like "noMoreElves" might actually be "no more elves!" and implemented as "elfCount = 100". I exaggerate a little, but an inline "elfCount == 0" is much easier to verify than having to jump elsewhere to see the code.
Kent beck also gave this amazing (and suprising) answer on SO about "how far to go with your tests?" He is in my eyes, a true pragmatist and gives some sound advice.
Uncle bob on the other hand seems to have drunk too much of his own koolaid. I saw him give a talk 2 years ago at a java conference which made him seem more like a Steven Balmeresque salestype, not an active developer.
In such cases, the golden rule is "if you do it more than once, create a new function". I don't know what kind of things you program if you have 100-line functions with no code sharing.
void checkCloseWorkshop() {
if ( noMoreElves() ) {
fireAllReindeer();
}
}
is enough, I don't need to look more precisely. But, for someone who is more "Bottom/Top", the second version is a headache because they need to jump everywhere and keep 10 different files/functions open all the time.An anecdote of this, I once worked with a programmer who was "Bottom/Top" and really hated functions. (1500+ lines function was the way to go for him). He would then refactor my code like this:
void checkCloseWorkshop() {
bool noMoreElves() {
return elfCount == 0;
}
void fireAllReindeer() {
for (int i = 0; i < reindeerCount; ++i) {
delete reindeerArray[i];
}
delete reindeerArray;
}
if ( noMoreElves() ) {
fireAllReindeer();
}
}
I.e. Pasting my functions into the other functions he needed.. rather than just calling mine or moving them in another file. Obviously, there was no tests or frameworks used by this company. This is a true example of "Bottom/Top" thinking.Last note, I understand your point but there's a way to mix both strategies by using high-level strategies. For instance, fireAllReindeer is deleting an array while noMoreElves is checking for emptiness. Good C++ programmers would use boost or the STD functors/SmartPointers rather then manually reinventing the wheel every time.
Personally, I'm in the latter camp. There are people whose names I've been told more than 5 times, but still don't remember them. I remember the person, but not the name. In school, I almost never knew any teacher's name and it took me several years to learn the names of most of the other children.
This also means that you can't have a "One True Most Readable Style". You only have several local maximums for different classes of people.
I also agree it's painful to trace through dozens of classes/methods to determine what the actual implementation is. However, I don't agree the culprit is small methods. Small, interdependent methods in a single class (or package) are great and can be easy to follow if they are consistent. Usually the culprit is overuse of inheritance and design patterns.
However, one case where I find myself being less "fancy" is functions that run/generate SQL statements. Although there are many cases where you can re-use certain portions of SQL statements and build up a query through smaller functions, this ends up being a nightmare to debug. I find it is better to be as plain and explicit as possible.
Dear lazyweb,
I want to bridge that gap in my education. Where do I go to read loads of code in different styles? With the emphasis on plurality of styles.
If you're interested in text compression, then download a few compression toolkits and see how they work. Also read a few articles on the topic. Regular expression engines? Dozens are available. Interested in networking? Take a look at ZeroMQ and other networking packages.
Databases? That's a big topic, but you don't need to understand the entire package. Pick some aspect which interests you and see if you can figure out how SQLite, PostgreSQL, and MySQL handle it.
Here's an easy one to start off with - how do Lua, Python, and C++ implement hash table-like data structures?
Personally, I don't like traditional OOP, so for me, Go and D are interesting to me (read the standard libraries if interested). Go tries to restrict you to their "idiomatic" style, and D tries to let you program in whatever style is appropriate for the problem.
My best advice is to solve a real problem in a new language. Try to make that solution as idiomatic as possible for that language. Trying a functional language like Haskell or Racket will also do you a lot of good.
When I have been left stranded without API documentation, I look at method implementations to figure out which method does the thing I want, if any. This is normally at least somewhat painful--but it's occasionally downright pleasant, when the author of the original code has written it in this "every method is written as if it is exactly as deep in the abstraction hierarchy as you'll need to go" style.
Which is to say, when you code like this, it's pleasant enough to read the source for that purpose that docs become somewhat redundant. I could imagine an editor designed to interact with code written like this, just displaying in a sidebar the implementation of any function you scroll past in autocomplete--because that's exactly as much, and as little, as you need to know to decide whether that function is the one you want.