Hacker News new | past | comments | ask | show | jobs | submit login
Attitudes That Lead to Maintainable Code (atomicobject.com)
225 points by ingve on March 8, 2017 | hide | past | favorite | 176 comments



I think you can write any program using only advanced concepts from the problem domain, not advanced concepts from programming itself. For example, I'll happily implement a difficult numerical algorithm for inverting a matrix, but I won't write a monad framework in C++ for that. IMO there are almost no projects that call for programming cleverness (as opposed to problem domain cleverness). You can get very far with boring imperative code that any beginner can read, with powerful algorithms introduced only locally and documented clearly.


With some caveats. Execute 10 network requests in parallel on background threads, then reconcile their results, after that fire off another 3 requests in parallel (again, on a background thread), reconcile all the values you got so far, and return to the main thread. Oh and repeat the whole thing every time the user session's token changes, or the user requests a manual refresh. Also don't forget to cancel all running requests when the process is interrupted before it is finished. One more thing: If one request fails, bubble the error to the main thread and skip all subsequently planned requests.

You CAN solve this with counters and locks (be careful about deadlocks though), but you will save yourself a lot of pain if you just use a FRP framework. You will also be able to easily react to the things you forgot ("Oh the requests should also be halted and retried after connectivity is lost").

So where do you draw the line? Do you spare your coworkers the agony of learning FRP or do you implement something where you suspect it will horribly crash and burn in production?


I rarely solve difficult programming challenges. I solve difficult business challenges all the time.


For the past couple of years, I have been employed as a sysadmin, but for the past ~two years, my main task has been to implement the glue that hooks up several software systems we use and write what could be reporting tools.

Programming-wise it's been kind of dull, actually. No fancy clever stuff. At the same time it's been demanding in terms of understanding the business perspective on things. How do you define "costs accumulated on a given project" or "coverage contribution of a given project"? Having no background in business management or accounting, I have little to no clue, but it sure was interesting to find out.

So, yeah, we're in the same boat, I guess. ;-)


I'll echo that sentiment, I experience it daily. Very rarely is a business itself abstract and complicated enough to merit an abstract and complicated technical solution. Meatspace is still pretty simple.


There's often a non-complicated solution to a difficult problem but spotting it takes some skill and experience and sometimes takes longer to implement than a more obvious complicated solution.


And sometimes complicated problems can really be solved only by a complicated solution, but can be half-assed in a simple way. You pay for the partial solution much later.

It is good to avoid future discounting in this way.


I usually solve difficult programming challenges by turning them into simple ones.


I rarely solve difficult programming challenges. I solve difficult business challenges all the time.

This is an expedient way of presenting difficulties encountered in software development. Suppose you need to collate information from many different processes that are distributed in space and time, possibly pursuing the information from a series of back-up sources when the primary source fails, and providing on demand, at any point along the way, a summary of results taking into account pending results and failures.

This is a straightforward problem for a human. A person does not need special training to pick up the phone when their boss calls and say, "The framer finished on Tuesday, the faucet supplier hasn't replied to any of the voice mails I left last week so I sent an email to the plumber since he's used them before, the electrician says the wiring won't pass inspection the way we asked him to do it, and the client approved a draw for $10k so there will be no problem paying for the tile on Friday." Someone who does not program computers does not see a need to analyze this problem and create a precise and detailed plan for how to pursue and summarize this kind information. The difficulty arises when attempting to do something like this with a computer.

Therefore, it's reasonable to describe it as a programming challenge. But you'd be foolish to. You're much better off calling it a business challenge, because getting it right matters to other people and the proper modifier for stuff that matters is "business." If it's "programming" then you're just amusing yourself on company time.

We've internalized this terminology to a sick degree. I took someone's dependent little brother to a diabetes appointment for them, and when I was summarizing the visit to her, I accidentally referred to the doctor's directions for taking the various medications as the "business logic." I meant to distinguish it from the things the doctor explained merely to satisfy my curiosity. As programmers we've had it drilled into us that "business" means something that matters to other people and can be discussed with them. Anything that is not "business" is at best mildly self-indulgent to mention and at worst selfish to even think about.

Therefore, we strive to describe everything we do as "business." This is how the statement

I rarely solve difficult programming challenges. I solve difficult business challenges all the time.

becomes a declaration of virtue.

As an illustration of how words affect how we think about things, replace the word "programming" with "engineering." We aren't ashamed of tackling "engineering" challenges. So there's an out when something is hard and matters but can't plausibly called a business challenge: call it an engineering challenge instead.


IMO the distinction has more to do with whether you are:

(A) Converting something that you understand and are satisfied with... into a form the computer can operate upon.

(B) Persuading other people to change their process or expectations into something that is sane and generally understandable.


There are many tasks that are easy to solve with a human brain and very hard to solve with a computer. Anyone except a programmer will see them as sane and generally understood and will be satisfied with a description of the problem that would be sufficient for a human being. These are the kinds of problems where a programmer has to be very careful about the perception of his or her work.

The naive programmer sees that all of the difficulty has been introduced by the decision to solve the problem with a computer system. Therefore it is the programmer's responsibility to solve the difficult problem and justify the difficulty to others. Concurrency is not a problem for human beings, so if concurrency is a difficulty, the programmer must take the time to solve it, and if a non-programmer asks why he is taking so long, he must do his best to explain why concurrency is hard for computer programs.

The wise programmer shields himself by exposing as much of the difficulty as possible onto non-programmers without every mentioning the computer. Never mind that the task is well enough specified for a $15 an hour temp worker to carry out -- it is ridden with underspecified "business logic!" The naive programmer assumes that the system must operate under any reasonable ordering of events or failures. The wise programmer brings obscures scenarios up in meetings, suggests that such-and-such a scenario isn't valid, furrows his brow worriedly at the answer, and then proclaims that the "business rules" need to be "more completely specified" to handle these "previously out of scope cases."

Again, never mind that in the past a dozen different human beings with no special training performed the task reliably without needing to call meetings to find out how to do their job. Never mind that no difficulty existed until the introduction of a computer that needed to be programmed. The wise programmer understands that the task is difficult and he needs to make everyone else feel that difficulty so that he doesn't appear to be struggling with an easy assignment. Therefore a task that the business has been performing successfully since its inception must be found to be ridden with previously undiscovered "business challenges."


I'm not entirely sure whether I should be reading that as an earnest explanation regarding recurring problems with automation, or a gradually increasing level of sarcasm aimed towards lazy developers.


Neither developer is lazy. They both work hard and solve a hard problem. However, the naive developer is perceived as taking a long time on an easy problem for no good reason, and might not even be allowed to finish. The wise developer is perceived as taking a disappointing but reasonable amount of time on a surprisingly difficult problem. That the wise developer achieves this by acting disingenuously and wasting a lot of other people's time tells us something about a social phenomenon we see in the comments here, namely that programmers care quite a lot about how they label the difficulties they face, and they feel most comfortable with labels that classify the difficulty outside their area of professional competence.

Obviously the absurdity bothers me. I didn't enjoy being the naive developer, and I don't think I would enjoy being the wise developer, either, if I were capable of it. That means I'm doomed never to work alone. Right now my team is eight months into a project that was supposed to produce a prototype in three months, and we are yet to produce anything beyond a handful of disconnected demos. If I was doing this project by myself, or if I was leading a team of people like me, we would have been relieved of responsibility a long time ago. But we're fine because we have a couple of wise developers who are capable of communicating the difficulties in the manner described above, by essentially teaching other parts of the company that they don't know how to do their jobs.

I dislike the reality and am intentionally painting an unflattering picture of it, but I'm not trying to assign blame or disparage any party, because I don't have a solution. I just thought I'd present it as a way of seeing things.


Give me my procedures, conditionals, and loops, and I shall move mountains.

Assembler (Prince of Persia): https://github.com/jmechner/Prince-of-Persia-Apple-II

BASIC: http://softwareengineering.stackexchange.com/questions/14945...


> Give me my procedures, conditionals, and loops, and I shall move mountains.

Isn't just MOV enough for "moving mountains"? https://www.cl.cam.ac.uk/~sd601/papers/mov.pdf


No. CNOT is enough (assuming it can access memory), not MOV.


Regarding that second link:

My career in software development started with BASIC - Pick BASIC to be exact. It was 1992, and I was 18 years old, had moved to Phoenix a week after graduating from high school to attend a local tech school (now defunct), and I needed something to pay the rent after having my hours cut as a cashier for Osco Drugs.

Seeing my other classmates at the time taking "apprenticeship" positions doing electronics work, I decided to "cold-call" nearby companies, and landed on a "mom-n-pop" shop doing software development work. I put in my application, and they took a chance on me. They didn't hire me as a software developer, though - but as an "operator" - the guy to "run reports, keep paper in the printers, and mount tapes" for the actual programmers, to free up their time. I learned how to load a open-reel 9-track tape drive with vacuum columns at that place...something I have never used since.

I was "in heaven" - being paid much more than I was as a cashier (but finding out much later that I was actually "underpaid" - oh, the ignorance of youth!), while doing less work! They gave me access to an account on their IBM RS6000 system running AIX - and one of the guys gave me a book on PICK BASIC to look over, and play around with.

The shop developed "insurance claims management" software for the "Arizona Health Care Cost Containment System" (AHCCCS) - basically Arizona's form of Medicaid; this company's software managed claims (accounting, billing, etc). They used a form of PICK called "UniVerse":

https://en.wikipedia.org/wiki/Pick_operating_system

PICK (and UniVerse) were actually very interesting systems on their own; kinda a self-contained OS that integrated a programming language (BASIC) with a relational (and multi-value!) DB, along with other stuff. It compiled to a byte-code, which some manufacturers made CPUs to run (instead of interpreters); the idea was "portable software" across multiple architectures. Basically the idea of Java before Java. It worked out about as well, of course (because every implementation of PICK was just a bit different).

Anyhow - the programming language used was BASIC - and my experience up to that point was writing bits and bobs of BASIC (plus some assembler - for the 6809 and 6502 processors - interestingly the latter was for the Apple IIe - CALL -151 was my friend; hand assembly of op-codes in the monitor - ugh). Well - they were monitoring the account, as I coded up adventure games and other diversions in PICK BASIC. The programmers and owner started feeding me with tasks ("hey, can you fix this issue" or "can you build this demo for the printer", etc) - and I proved myself. They hired me after I had graduated the tech school, and I've been here in Phoenix ever since.

As far as BASIC and businesses are concerned - it is still used today, as much as everybody rants on it. Besides Visual Basic (large following there), PICK BASIC is still used (it has a legacy almost as long and broad as COBOL), and there are many other old "business BASICs" that existed and still have users today. I also know that there is at least one industrial robot arm manufacturer who sells a "Robot BASIC" to program and control their arms.

...and BASIC lives on today, of course - both closed and open-source variants. I'm more familiar with the various open-source BASICs. You have Gambas (which looks very much like VB), then there is QB64 (multi-platform 64 bit compiled QBASIC if you will), and also something called "BaCON", which is a "BASIC-to-C" converter (just the other day I was thinking taking it, and dumping out some Web Assembly code - essentially to see if I could get a BASIC program compiled to run in a web browser). There are also more than a few interpreters in javascript and other languages that can run BASIC code of some sort or another.

Would I recommend it for new projects of large scope? Probably not, but I wouldn't outright discount it, either. I think it still has its place in the computing world, though that world is doing its best to ignore it in the hope that it is forgotten. It did teach some "bad habits" - but I think that a modern form of it, with proper OOP support, could be a very powerful teaching language for beginners - which is what it was always meant to be.

It will always have a spot near and dear to my heart, at least.


Thanks for letting us into your lovely personal computing story.

I often ruminate about the graveyard of dying software - the BASICs, the COBOLs, the xBases, the Delphis and the PowerBuilders, to name just the few I know. And the immense amount of code written and souls poured into by programmers over the years.

They're all there, there is still juice in them, but we move on because, impermanence. This was also a theme in _why's parting gift. https://news.ycombinator.com/item?id=5575707


This is exactly the attitude I try get into every developers head!

Please write maintainable code using common and broadly understood methods. There is no award for surprising me in how fancy your solution is, in fact the opposite.


"Please write maintainable code using common and broadly understood methods."

Often times, the most common methods are the most obfuscated and unclear.

Maybe this is my perspective just because I am a Java developer. Pulling in lots of poorly understood dependencies. Depending on annotations that change execution flow in complex ways that are very difficult to follow, even with a sophisticated IDE. Deploying into complex web containers. ORMs mapping method calls into who knows what SQL calls. Lots of superfluous mutable state everywhere.

(I'm looking at you Spring. Although Spring Boot is a definite improvement.)


Thank you. After 30+ years of programming, I am often completely disgusted at what a bunch of magic filth JEE/Spring is.

The guy(s) who made annotations in Java 1.5 deserves extreme punishment :-)


Nah, annotations are great when they're used for their stated purpose, that is, annotating code.

They shouldn't have been allowed to actually change runtime behavior or be used to control code generation.


Sorry, the use of the word 'common' was a poor choice [and I'm actually not sure what would be a better choice]. I agree with you wholeheartedly. I honestly find a lot of those common methods very confusing.


> There is no award for surprising me in how fancy your solution is, in fact the opposite.

While I can agree with "no award" part, I don't think "the opposite" should be true.

How do you know you got surprised because the method was "fancy" instead of because your knowledge was lacking?


While I do not go around trying to beat people with a stick when they make things I see as overly complex, I think it's good to have a conversation about it, sit down together to find the simpler solution [or accept the one that is already implemented] and hopefully reach alignment on the path forward.

It's totally possible my knowledge is lacking, which is why the conversation is critical. If I just need to be educated on why something I saw as complex was the right decision, so be it, and I look forward / enjoy those opportunities.

At the end of the day, I run the tech team and founded this company, while others might come and go, I need to feel ownership over the code, product and most importantly it's shortcomings. If I cannot own that openly, I cannot lead/direct others, and that is when technical teams really start to fall apart imo.


Doesn't matter. If your code is not understandable by most developers, it is probably worthless in practice.


The only thing that lays further ahead on that road is the lowest common denominator, copy-pasted code and lots and lots of mediocre code monkeys. I don't want to go there, although I can understand people who would like programming to work that way.


Sure, but to my mind monads are common and broadly understood, at least compared to all the alternatives for achieving the same kind of thing (e.g. AOP shudder).


> but to my mind monads are common and broadly understood

Wow, you need to talk to more developers. Not just in the sense to broaden your views, but to improve your skills as a developer. Code is written to be read by humans (and orthogonally executed by machines). If your mental model of what is broadly understood by programmers is that far off, you are likely churning out lots of unmaintainable code.

Functional programming makes up a tiny slice of the programming world and most people that aren't doing functional programming (and even some who are) don't have a strong grasp of monads.


Within FP, sure, monads are common and (at least should be) broadly understood. Outside of FP, though, monads are neither common nor well understood.

You make these broad statements about monads as if FP was the whole programming world. It's not. It's probably less than 5% of it.


But isn't typical high-quality OOP code essentially Monadic code? State is encapsulated in an object and transformed by method calls. Method calls can be chained, and state can be encapsulated and extracted using constructors and accessors, respectively. The basic operations are all the same. OOP is just less strict with how you can transform objects, i.e. function/method purity is not enforced.


> But isn't typical high-quality OOP code essentially Monadic code?

Nope.

> State is encapsulated in an object and transformed by method calls.

Monads aren't objects (which necessarily have a runtime manifestation). Monads (as used in functional programming) are abstract type constructors (which normally don't have a runtime manifestation).

> Method calls can be chained, and state can be encapsulated and extracted using constructors and accessors, respectively.

Monadic bind satisfies three concrete, clearly defined algebraic laws. The use of monads in functional programming arose from the field of denotational semantics, which gives “programs” (by which functional language semanticists mean “expressions”) precise mathematical meanings in a compositional manner.


I understand what a Monad is. My point is that high-quality OOP code typically adheres to the rules of Monads.

I didn't say Monads were objects. I simply talked through the most common properties and operations of OOP code, in order to show how they are closely related to the operations on Monads.

I understand that OOP code does not, in general, satisfy the constraints of Monads. Hence, why I specified high-quality OOP code, which, in contrast, typically does.


> My point is that high-quality OOP code typically adheres to the rules of Monads.

Last I checked, there are only three laws: left/right identity and associativity, and they are just algebraic laws that have nothing to do with your code being “good” or “bad”. Or do you have some other meaning of “rules of monads” in mind?

Furthermore, every higher-order strict language is already monadic, without you having to do anything special. Thus, monads aren't opt-in, or even opt-out; they're there whether you like it or not. This monadic structure permeates the language, regardless of whether you write “good” or “bad” programs in it. For more info, see here: https://news.ycombinator.com/item?id=13823145


Yes, those are the laws. The theorems are more interesting from a maintaining-software perspective, though.

Yes, all higher-order strict languages can be embedded in a monadic model. However, there's an important practical difference between a single Monad with the entire application or computer's state encapsulated (course grain encapsulation), and many Monads with fine-grained encapsulation. High-quality OOP code can be embedded in the latter, and that's where the maintainability comes from.

--- Perhaps, if you focused more on understanding the point I'm trying to make instead of proving me wrong, this conversation would be more beneficial to both of us.


Computation in every higher-order, strict language has an intrinsic monadic structure. That includes pretty much every object-oriented language in existence. (Ironically, it doesn't include Haskell.)


ELI5: What do you mean here by "higher-order"?

And, by "intrinsic", do you mean that "the language does that, but the programmer doesn't have to do anything to make it happen, or even know"? If so, doesn't this contradict your answer to smaddox?


I imagine the GP is talking about how code in procedural languages is equivalent to monadic code on pure languages.

If so, I don't know why the "higher-order" restriction is there. It seems unneeded. Also, it about is as much something "the programmer doesn't have to do anything or even know" as monadic code on languages that have specialized syntactic sugar, like Haskell. It creates plenty of details the programmer has to think about, the only difference is that in procedural languages all the details are always there, while in pure ones the detail set changes all the time.


I'm not talking about “pure languages” (whatever that is) at all.


> What do you mean here by "higher-order"?

Procedures as first-class values that you can store in variables, pass as arguments to other procedures, etc.

> And, by "intrinsic", do you mean that "the language does that, but the programmer doesn't have to do anything to make it happen, or even know"?

Yes. Although, IMO, programmers benefit from knowing it, for more or less the same reasons civil engineers benefit from knowing the laws of nature.

> If so, doesn't this contradict your answer to smaddox?

No. Monads, as abstract type constructors, are already hardcoded into the result type of every computation. (This is the difference between, say, an int like “2”, and a computation that produces an int like “1 + 1” or “fix (\x -> x + 1)”.) So hardcoded, in fact, that there doesn't have to be syntax for it, because there's no way you can not say it.


What's fancy to you? What's broadly understood?

Will you get upset if someone in your java codebase uses lambdas or optionals or even the ternary operator? All of the above are still considered black magic to many in our industry.


If a standard feature in your language of choice confuses or upsets you when it is not misused... You may have a problem. The problem is typically lack of willingness to learn.


It totally depends on the situation, is it something the whole team needs to work with on a regular basis? And is it really the most optimal solution (those two questions are highly interlinked)?

If that's the case then we might want to a) educate everyone else first or b) pick something we know will be less of a struggle for others to maintain.

I guess there is always option c) ask the original author never to leave and be available 24/7 to answer questions, but that, obviously, isn't realistic.

Edit: I answered the question poorly. Are you "writing art" or doing your job? I don't mind if the outcome is pretty, but your first priority is to do your best in your role within the organization. If the author or the code feels that is the case then generally I won't ever get upset with anything they do.


I work on a product that was mostly C. At the time the developers loved it! Look at how clear and explicit everything is. These days however people recoil in horror when they see parts still in that old C style.

The rest of the world can and will change their definition of what "common and broadly understood" means.


> I think you can write any program using only advanced concepts from the problem domain, not advanced concepts from programming itself.

But that is a trivial statement considering that a Turing-complete subset of most (all?) languages is relatively simple.

The point addressed by the article is maintainable code, which is something else.


OP's point is that the best way to write maintainable code is using bog standard, boring code and leaving clever solutions to the level of the problem domain.


Bog-standard meaning not testable and not contract-based?

Clever solutions and abstractions (hey, immutability is clever right?) can really help out reasoning.

And sometimes you really need clever things due to problem domain, so you really end up with totally not boring code.

For example, you wouldn't cook up your own matrix manipulation everywhere, so you use a clever solution instead. Writing operations directly opposed to composing and optimizing them.

Or in high performance world, using a much more complex but lock-free algorithm instead of the "bog-standard" locked one, sometimes the benefit is huge.

The real important part is to exactly document the clever parts and enforce contracts on them, so you do not end up with potpourri everywhere.


Attitudes that Lead to Mixing Problem Domains


+100

This took me 10 years to realize.

I find myself getting rid of everything clever, my code reads like a boring instruction manual.

I'm starting to deeply re-think how code interviews should be conducted.

Instead of asking smarty-pants questions, we should be asking mediocre questions and just asking someone to put together a straight-forward and simple solution.

Important: it's surprisingly difficult to make code that looks simple and boring :), it often takes some iteration.


I don't think you can write maintainable code without monads. Code has to be written in the language of the business domain so that it can be understood in those terms, so you can't (to take an extreme example) have every other line being "if err != nil ...". But if you leave secondary concerns completely implicit then your code also becomes unmaintainable because you can never understand what any given function is doing (at the implementation level) and so you can never maintain anything. You need the dual perspective that monads or something like them give you where you can say "this is a secondary concern in this code but it is there".

It's not about "programming cleverness" - there's nothing "clever" about monads, rather the monad is a very simple and boring interface that turns out to be a good tool for helping you write code that does what it looks like.


I might be confused, but there is ample evidence that you can write maintainable code without monads.

Take the Linux kernel, NASA rovers, or redis - all written in boring old C with no monads and great results.


What does maintainable mean? I propose a definition: we can add features at a constant rate, but the rate of bugs per line of code drops off over time. Linux certainly doesn't meet that threshold: the rate of bad bugs is pretty constant. If we decompose it, we'll see a safe maintainable section with a monadic use of goto, and a less safe section that doesn't.

NASA's projects that do hit this standard tend to be written in CL with macros that do this.

I haven't read redis; anyone else?


Any definition of maintainable that rules out the linux kernel, is... questionable. At best.

The sheer number of committers guarantees bugs. That is not a remarkable feature. The remarkable number of committers is.


Thats an interesting model, but I cant think of many projects where the features are added at a constant rate. The deluge of pull requests going to Linux are not constant, and probably grows with the number of contributors perhaps relative to population growth of programmers (also not constant).

Perhaps another metric is how long after your code is released will you be able to support it? In which case Linux and NASA do quite well.


Linux kernel is the opposite of boring old C, as in procedural code. It is nicely factored object-oriented C in most of the important places.


If you replace, "maintainable" with "easily maintained by a large group of people" the statement holds true.


Can you give an example of a project maintained by a large group of people written with monads?

The only one that comes to mind is Spark, and while I haven't used it recently, was nightmarishly buggy with many subtle corner cases :/

Even jumping outside of C, Atom, Eclipse, Chrome, FireFox all are maintained by large groups of people and dont have a single monad!


What about GHC?


I would love to learn what a nomad is. Can you explain what it is to someone who already is programming for many years? I just find the Wikipedia article very hard


A nomad is something that goes "nom!" ;)

A monad is basically an function, in the algebra sense, that will take a set of input, and map it to a different output.

That's the most simple definition I can give without getting into the math, and it's been a minute since I've really gotten into Group theory.

Maybe through a Haskell tutorial for some hands-on guidance?

http://learnyouahaskell.com/chapters

https://www.youtube.com/watch?v=9QveBbn7t_c


A nomad is a wanderer. A monad is just a particular abstract interface that turns out to come up a lot and so lead to very reusable code. It's hard to talk about monads in general because the concept is very abstract, I find the best thing is to understand a couple of specific examples first (Writer and Future are good simple cases, and then Nondeterminism or Continuation gives you some idea of the powerful things you can do) and then try to work up from there to the abstract part that unifies all these things and lets you write general-purpose functions that will work with any of them.

http://m50d.github.io/2013/01/16/generic-contexts is an explanation of a case where I found myself using the Applicative interface - Monad is very similar to Applicative (it just adds one extra possible operation).


My favorite description of monads is "decorators for function composition". You write a chunk of code to compose functions in one place.

That turns out to be extremely useful in many cases.

Even so, I don't believe that monads are the only way to write good code.


Monads are like burritos. This is explained very well at http://blog.plover.com/prog/burritos.html.

In short they are way to automate building new types out of old types in such a way that code written for the old types can immediately be turned into code written for the new types. This allows a lot of otherwise repetitive code that you might otherwise wind up writing to be pushed to your type system. Which in turn makes it feasible to build a richer type system.

Haskell takes this idea to an extreme.


Its pretty much a way to chain functions together in order to do something.

This is a good read: http://web.cecs.pdx.edu/~antoy/Courses/TPFLP/lectures/MONADS...


I hope everyone had fun because of my typo :)


The three rules that I most frequently quote:

1. Code to an interface, not an implementation. Think about the essence of the class, without which, it would be a different class. Then build an API for that essence, then implement the API.

2. Program into a language, not in a language. Work out first what you want to achieve, then figure the best way of achieving that in the language.

3. Tell, don't ask. Don't expect client systems to know how your system works. When the state of your domain has changed, tell other systems, don't wait for them to ask.

The "Factorio" game is one of the best explanations of good programming I've ever come across. Build components that do one thing well, then build components that use those components, and keep growing the system in that way. When you want to keep the relationship between two components more flexible, push values from the first component onto a 'conveyor belt' and have the second component pick up values off that belt. Service oriented architecture in game form.


I don't agree fully to that (1). I used to do it, because it sounds like a great idea... However, the implementation of your use case is often too subtle to get the API right to start with, and you end up having to play around and 'adjust' your API. ie, maintaining it even tho it doesn't even exists yet!

My method is now to implement a 'dirty' integration, without any care of structure or cleanliness, then when it's in place and working only then do I sit down and look at the API I would have liked to have, and implement this by mostly refactoring what is already working.

With that method, you end up with something that is clean, and more importantly does exactly what is needed an no more. There is no bits 'that could be useful later' and are never implemented, or used, or tested, and the model fits the job.


Yeah, this really reminds me of Sandi Metz's "Duplication is less expensive than the wrong abstraction." I know we aren't talking duplication specifically here, but I've eagerly built the wrong abstraction several times and it suuuuucks.

These days I'm all for the "dirty integration" first.


If you have proper functional tests, you can replace wrong abstractions at a cost.

The real issue is code abstracted bottom-up instead of proper design. Such abstractions end up being special cases and become annoying and worthless if requirements (incl. internal ones) change.

While sometimes you can design yourself into a bad end (typically by not taking a really critical requirement into consideration), this is not what often happens.


> The real issue is code abstracted bottom-up instead of proper design.

Exactly. We're (basically) saying the same thing here. I kept my comment short-and-sweet as not to get into the nitty-gritty of everyone's different work situations/requirements/stances on different testing practices/building an app vs a library or service/programming paradigm/etc.

To expand ever so slightly: "These days I'm all about 'dirty integration' first to help inform my ultimate design."


Make it work; make it right; only if proven necessary, make it fast.

Make it work is the dirty version, always comes first without thought of proper factoring or abstractions. Once working and under test, it's much easier to then make it right, which is factoring out duplication and fixing up the code to be clean and well factored. Then and only then if something is too slow and a profiler shows a problem do you make it faster.


This way you end up dead really soon. The model only works for small projects.

Short version rephrasing of the rule: put a prototype into production.

You can see how it is obviously bad.

Typically cleaning up a dirty version is VERY hard. Takes much more time than rewriting assuming you wrote proper functional tests.

Likewise you cannot make a truly wrong design really fast without redoing it.

I would flip the rule on the head: design so that it can be made fast, is easy to get right, therefore will work.


No, you're not understanding the context in which to apply this; this isn't something you do at the app level, it's something you do at the feature level. The app is always well structured and well factored, but when adding a feature, you start dirty, get it working, then clean it up, and then optimize it if required, then move on to the next feature. This model works for all sized projects, all the time. The point of doing things dirty is to avoid premature architecture/abstraction which is a bad habit many programmers have.

Now if you have the experience to know beforehand what abstractions you need because you've done this many times, of course you can start cleaner; you do things dirty when you're unsure of what abstractions you might need and don't want to get hung up in trying to invent an abstraction before you know you need it.


It depends on your knowledge/experience levels. A very experienced develeoper's prototype would likely be much better than a junior's production-ready version.


That is true, but mostly because said experienced developer actually has a design in mind while programming. At the very least a sketch of one.

It is usually better to just write it out and discuss instead of following through based only on your own gut, however good it is.


I think this is pretty close to "the right way" (as I see it). However, I want to add one thing:

> With that method, you end up with something that is clean, and more importantly does exactly what is needed an no more. There is no bits 'that could be useful later' and are never implemented, or used, or tested, and the model fits the job.

You want a clean abstraction that gets the job done, and cleanness is better than strict minimality. If you leave big gaps in your abstraction, then it's not a good abstraction anymore, and you'll probably have problems down the road (especially once things get transferred to a support group). It's definitely a judgement call how far you should take this, however. Generally, I want to do what I can to avoid tempting future developers to put new features at the wrong level of abstraction.


If you are writing a service or library to be consumed by other programmers, you don't have much choice but to get the interface as correct as possible up front. Once it is widely use, no matter how much you might want to get all of the consumers to use a new version that fixes design flaws, its going to be extremely difficult. So in this case it is very important to put much careful consideration into the design of the API up front.


Well, at the point where you want to make a 'public' API for any sort of service, one would hope you know enough about the problem you are trying to solve to be able to provide a nice API?

Also, for a 'public' API, I would definitely go for a bit more abstraction, and would attempt at making as future proof as practical. Of course you can ALSO fall on the other side of the fence with that by providing an API that is so 'future proofed' that they become next to unusable without another API on top! Apple was the Grand Specialist at that in the 90's.

My reply to the post two level up was more regarding /internal/ APIs, the ones you create on top of an existing library for example, to integrate into your own systems.


There's definitely a difference between what I preach, and what I frequently do in practice. It really depends what I'm building, and how well-defined are the requirements.


To ruminate on the parallels with Factorio - if there's one thing that Factorio can really highlight, it's the cost of technical debt, and thus the importance of refactoring.

If you never "refactor" your factories in Factorio, you'll end up with an undefendable and inefficient mess. Spaghetti factories indeed. But if you stop to think, spend some time refactoring how things are working, and build to your new technologies, you'll end up with a megafactory which can help you escape with no problem.

It also highlights the importance of experience and experimentation; finding the proper ratios of furnaces to miners, maximizing the bandwidth of a conveyer belt while minimizing the stagnant materials; once you learn this, your next playthrough will be both faster and require less refactoring.

As for SOA, one thing Factorio taught me was the value of back pressure. Why spend resources producing materials which aren't being consumed? Conveyer belts and movers show the value of backpressure nicely - when the belt is full, movers stop placing items on the belt. The factories stop producing, and thus stop consuming intermediate materials... all the way back to your miners. But the moment that belt starts to clear up, the entire mechanism swings back into work, with enough queued materials to ensure that you are not suddenly starved.

The parallels with programming are a bit more indirect than with actual material production, but they do exist.


>As for SOA, one thing Factorio taught me was the value of back pressure. Why spend resources producing materials which aren't being consumed? Conveyer belts and movers show the value of backpressure nicely - when the belt is full, movers stop placing items on the belt. The factories stop producing, and thus stop consuming intermediate materials... all the way back to your miners. But the moment that belt starts to clear up, the entire mechanism swings back into work, with enough queued materials to ensure that you are not suddenly starved.

This bit of your comment reminded me of The Goal: A Process of Ongoing Improvement[1] by Eliyahu Goldratt. It's a great fictional story/book that does an excellent job of teaching about bottlenecks and the Theory of Constraints. Specifically how to identify them and eliminate them.

[1] http://amzn.to/2miLjsH


I'm guessing you know it, but for those that don't, The Phoenix Project is also an excellent book for understanding the similarities between manufacturing and software development.


I did not know that. Adding that to my reading list. Thanks!


I loved discovering the main bus strategy in factorio (everything you produce goes on a very wide main bus, so that others can easily pick it off) at the same time I was implementing an Event Bus (post events to one bus, subscribers listen and react) in some Java code. Perfect parallel, even called the same thing.


The second point sounds a lot like "fight the language", which doesn't seem right.

We had a coworker implement a custom Either object in Java instead of just using exceptions, and it made the interface significantly less ergonomic. I really like functional return values like that, but I've found that it makes things easier to use the paradigms the language provides.


It's not about fighting the language at all, it's about not letting the language dictate the business rules. The functionality exposed by the app should be the same whether I code it in JavaScript, Go or Assembly. How I implement it in those languages is a detail that comes after I've decided what to implement.


this is a very lenghty way of saying program top-down, not bottom-up, unless your are targeting the metal :)


The number one attitude that leads to maintainable code for me, is to not worry about problems I don't actually have.

That is, solve the problem I am trying to solve. Do not go out of the way to solve other problems. They will have their time, or they were not important.

Obviously, it took some time to get so that I could write code that wasn't a disaster on its own. I would argue I haven't gotten past that point, actually. Which is all the more reason that the less I touch to solve a problem, the better.


I'm not sure how I feel about this.

What has worked for me is embracing change.

Requirements will change, third party integrations will change, front end frameworks, use cases, service providers etc.

Everything will change so write (really build) code that tolerates change (IOC, interfaces, etc. Any form of loose coupling you can get away with cleanly).

There are of course situations when the one line fix is the right one. But in general, this outlook on short term vs long term changes (knowing when the concrete should become the abstract) is one of the differences between being a programmer and a software engineer (or being a senior and a junior).


> Everything will change so write (really build) code that tolerates change (IOC, interfaces, etc.

Everything might change. Not everything will change.

I think the problem I have with your principle is that many of the techniques to accommodate change (the "IOC, interfaces, etc" you mention) can have a real and immediate cost when it comes to understanding a codebase, and that's the fundamental sin when it comes to maintainability.

Abstraction and indirection have value, obviously, but on balance I'm with GP; don't add 'em until you actually need 'em. In particular, don't add an interface and a factory and whatnot until you actually have more than one implementation.

This may depend on your languages and dev environment; something like IDEA where large-scale but mechanically trivial refactorings can be accomplished quickly and safely are more suited to KISS than e.g. dynamic languages where tooling is much much weaker.


> Everything might change. Not everything will change.

True. Not everything will change. But you don't know which ones will and which ones won't. So be ready.

> I think the problem I have with your principle is that many of the techniques to accommodate change (the "IOC, interfaces, etc" you mention) can have a real and immediate cost when it comes to understanding a codebase, and that's the fundamental sin when it comes to maintainability.

Not true. The benefits of IOC and loose coupling are well established and I won't even bother relisting them here. I'll only say that those benefits usually outweigh the minor indirection you get as a result.

The question then becomes, do you loose couple from the beginning? or tightly couple everywhere and loosen as you go. I tend towards doing it from the start, for consistency sake (help those poor junior engineers out), and for all the other benefits (which again, i don't want to list, but i'll add the one biggie - unit and integration testing).

I'll also note that IOC/loose coupling is rarely the cause of the over abstraction you fear. I've seen it way more in domain models or APIs.

There are many fads, new fangled doo-dads, thingamajigs in our software world. New frameworks, seismic shifts in architecture, herd mentality etc. IOC/loose coupling isn't one of them. It's good, old engineering practice.


You underline what is essentially my biggest gripe with IOC. There seemed to have been an explosion of everyone wanting you to use a new fancy framework to make it happen.

For the most part, constructor logic that simply sets dependent fields (and, importantly, does not do anything) along with not requiring fancy initialization shakeups goes a long way to easily testable things.

That said, people that go out of their way to make a fixture for each individual method can be just as annoying as the folks that only do end to end testing.

Basically, life is easy to argue at the strawman level. Breaks down quickly when there are "boots on the ground." (Or whatever other saying works.)


I'm with you there man (or lady)...the pace of "new and shiny" is exhausting.

One of the things I like about our environment is that we're always disrupting ourselves (maybe a little too much). So while it's true that there are many heavy weight DI tools (Castle.Windsor for ex), there are also lighter-weight ones (Ninject and co). At the end of the day, we each pick our heavy we want our tools to be (we can always implement IOC without a DI tool if you want to keep things bare boned).

> Basically, life is easy to argue at the strawman level. Breaks down quickly when there are "boots on the ground." (Or whatever other saying works.)

It's an interesting discussion for sure. And some of it is philosophical. When the "rubber hits the road" (or whatever saying works) and i'm stuck in the office at 2am trying refactor a ball of mud, I wish those who came before me had thought of this stuff.

Engineering. Not Programming.


I find it extra funny when "new and shiny" is actually a 30 years old idea in a new spin. (Reactive programming haha.)


Adding a proper interface is much more expensive than removing it if it proves worthless. Designing a good interface and especially components or other means of compartmentalization removes a huge source of mistakes.

Especially the last part is critical - if there is no abstraction or some other kind of tight binding between components you've already lost.


I think we may be miscommunicating; a lot of these words mean different things to different people. Designing a good and minimal "interface" for a class or module, in the sense of how it exposes its functionality to the rest of the world, is absolutely important, yes.

Deciding that mentioning classes anywhere in a signature is Evil, and that everything must implement a separately-defined interface in the Java/C# sense, is IMHO not at all useful. Not because it's "expensive" - it just takes an already-designed class API and copypastes it into an interface - but it's pure busywork. More code (which some people treat as a plus), harder to understand (because now there's always extra ambiguity at a call site as to which implementation is being called), adds nothing. This type of thinking - taking a useful tool or rule of thumb and turning it into dogma - seems far more prevalent in Java-land than in any other language I've worked in; I've never really understood why.


My motto is "Design for deletion."

Your first priority is to design your code for the inevitable day when your successor (or perhaps you yourself) dislikes it. The less effort/risk to uncouple and remove it, the better.

This helps avoid the well-meaning pitfall where you design things to be "customizable in the future", but the ways it needs to be customized aren't well known, and it turns out you've only complexity that gets in the way of the changes you eventually want.

In contrast, any code that's easy to remove implies that there's a good boundary, interface, or contract.


Disagree. You don't want to solve just the specific problem at hand in a way that will be hard/expensive/buggy to extend. And you don't want to implement the most general conceivable solution library, either.

The best solutions are in the middle ground, viz. those that solve the immediate problem in a way that anticipates and facilitates the future as well.


If something puts itself in the way of the work I am doing already, then I will attempt to solve it.

But, you have to exercise caution and prioritize. Otherwise, you'll just find yourself in the "Hal fixing a lightbulb"[1] cycle.

[1] https://www.youtube.com/watch?v=RHpJFROEOmg


There's a line to tread here: solving future problems isn't a good idea, as you say, but neither is closing off future options by being too specific about your current needs.

You don't want to create extra problems for future you to solve.


Agreed. This is what I was referring to with my code not being a problem on its own. Ideally, you don't write things that wall you off from future options. But, neither should you try to anticipate all things.


Nice post. Short, sweet, and presented to avoid endless debates over particulars. (I know, I have plenty of my own.) I especially like:

I’ve found it’s often easier to keep a few broader ideas in mind.

Funny, OP doesn't mention the broadest idea of all: ego.

I've worked with hundreds of programmers and have sensed an inverse correlation between experience and openness to suggestions about maintainable code. I believe that the main reason is not that the more experienced developers are convinced that their methods are that much better (after all, there are often many acceptable ways), but that their ego won't let them "lose a debate".

When I return peer reviewed code to a young developer with suggestions (and reasoning!) about how to make it more maintainable, I often get a big thank you. The same feedback often gets a debate from a more experienced developer.

On the other hand, when someone gives me feedback and suggestions, my first reaction is to engage them and explain why they're wrong and I'm right. But as soon as I set my ego aside and think of the code instead of myself, I open myself up to learning. This is vital because what I don't know is always much, much more than what I do.


> The same feedback often gets a debate from a more experienced developer

Maybe that's because they have the experience they are able to debate it!

I think "maintainable" will always be debatable, because we are not using scientific methods to determine what is more maintainable. So we are all subjects to subtle biases like confirmation bias.

It would be great if we could use scientific method to determine how to write more maintainable code, but you would need a good definition of "maintainable" first, and that's hard.

Personally, I don't see much, if any, hard evidence that today's code (in 30 years to the future) will be more maintainable than the code written 30 years ago. I work on 30+ years old legacy codebase, which is often crazy complicated and effectively not-refactorable. But you can still fix bugs, you can add functionality. It's expensive to do, but not impossible. Would be the accumulated cost lower if people spent more time refactoring it, like today sometimes happens? Not clear at all.


While I agree ego and human nature can be hard to suppress, I disagree that this is the driving force here. It seems to me that having more experience necessarily means you are going to have a clearer sense of the 'right way' to do a certain task, and more concrete convictions as to why that is the 'right way'.


I'd argue that sometimes you do want to hack things. Sometimes you go way out of your way to avoid that one special case for that one odd requirement, abstracting things out several layers to present a unified framework that encompasses that one oddball. Then the next day you get another oddball thrown at you that completely dorks it up. After a while you don't even remember why you had all the layers. At the end of the day, coding explicitly to the special cases and calling them out as special cases may make for more intuitive code.

(Also see "The Wrong Abstraction": https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstracti...)


For what it's worth, I agree with you. I think premature abstraction can cost a business a great deal. This article comes across as vapid - there is no silver bullet and it varies from org to org.

I went from a big tech co where we had the resources to abstract and deliver ideal solutions, to an existential startup that is hacking features together to win contracts. My biggest frustration is new engineers joining and overcomplicating simple tasks by trying to create generic solutions to every single feature request we get. The worst is that this comes across as 'investment' to management but it hardly ever bears fruit and we're just slowing down delivery to massage the developer's anxieties about technical debt. The reality is that the product may not really need that feature, or the customer may never use it, or it's good-enough, etc. I think in this environment we should ship simple features, that are hacky, until we gather enough data about it's value and use.


There is even a "rule" for what you espouse: YAGNI - You Ain't Gonna Need It

If all you ever going to deal with is humans in your class/API, there is no need to make it generic enough to work with all animals. The Clever Boy in all of us probably wants to implement this bit of "future proofing investment" but we need to resist the temptation.


YAGNI is great one.

I wish STTCPW (Simplest Thing That Could Possibly Work) made for a better acronym. Also related: KISS (which I prefer to interpret as "Keep It Stupid-Simple").


STTCPW has problems aside from its acronym; back when I was working in an XP-ish team we had any number of arguments which I eventually realized came from a fundamental ambiguity in this principle: does it mean

1. Make the simplest incremental change to the existing system to satisfy requirements, or

2. Make the simplest overall system which satisfies requirements?

The first is fine for prototyping when you're moving fast and breaking things, but guarantees technical debt out the wazoo over the longer term. The second makes for much better code but you may waste a lot of time on refactoring churn if you do too much of it before requirements have really stabilized.


The counterpart is YWNI - You Will Need It.

Usually considering the parts of abstraction that will reduce coupling and make understanding easier, or allow making an implementation high performance or secure. You know, things nobody really states in requirements but everyone wants in the end. Most of the time internationalization and localization are also needed but not stated outright.

Also consider things that are essentially impossible to bolt on later or inordinately expensive.


For every rule, there is an exception which helps to prove the rule. Yes, hacking is sometimes necessary and is frequently a more efficient use of your time in the short term, but it should always be asked: "will I understand this hack next year?"

Tomorrow's oddball might not happen, after all.


That's where I find a VCS really helpful. If I'm wondering about a particular oddball section of code, I can go back and look at the context in which it was introduced. This is very handy especially for code that was written by multiple people over time.


Sure, I'm just in the process of untangling a huge unnecessary abstraction right now, so when I read the article, I couldn't help but comment.


"Attitudes That Lead to Maintainable Code"

Well that assumes the code is the problem and not the programmer. The code might be perfectly maintainable but the programmer lack skills to do just that.

I've only seen very rarely really-hard-to-maintain code, and there were of two sorts:

1) over-engineered pseudo-senior object-oriented spaghetti plate -- generally resulting from methods that supposedly make the code more maintainable.

2) logically-wrong over logically-wrong spaghetti plate -- the kind of code that programmers hate to admit they do: simply logically-wrong code.

On the other hand, I've seen plenty of beginners making critics about a code that is supposely hard-to-maintain whereas the problem was them because:

1) they don't know how to read and learn a code base. They typically start working on a projets and they don't read the codebase. They jump on a task. They actually never read the codebase, but only piece of it.

2) they think the code is "obfuscated" whereas it is, well, just not. The programmer is just a beginner that needs to learn how to read a boolen expression for example (typically: var ok = cond1 && !cond2 || cond3 -- omg the code is obfuscated!).


When I first started working after college I had a lot to learn. For some of the first bug fixes or small feature enhancements I had, I remember fixing it with a one line change, and consequently being so frustrated with the senior code reviewer who would recommend I do some major refactoring instead.

I now see that it's not about doing things minimally, but doing things as correctly and clearly as possible. And one should never be afraid to refactor!

(Obviously there are cases where single line changes are the correct solution; but back then my mindset was "how can I do this with as little change as possible" and it should have been "as clearly as possible")


I prefer to split functional changes and refactorings in separate pull requests. First do a round of cleanup and refactoring, which is almost always necessary for any code older than what you wrote five minutes ago. Then in a separate PR, do the actual work.

Mind you, this is also in part due to traceability; I want to be able to point to a PR and go 'this is where this functionality was changed', with all the details. That makes it easier for the reviewer too, easier to find and review the actual change if it's the only change in a PR than when it's hidden in the diff alongside a major refactoring.

I've also seen another open source project that said "please, no refactoring PRs". I can see the value in that one too - code is never perfect, and you can continuously polish but it won't actually achieve much, besides causing a lot of churn in your codebase.


I've seen this expressed as "make the change easy; then make the easy change"


It makes me wonder why programming languages (and/or their development environments) are not designed for refactoring.

Refactoring should be as simple as possible.


In what way are they not designed as such? The biggest issue I can think of regarding major refactoring is coordinating across the team and ensuring you're collectively at a good point to start changing things. That's not really a language/tooling issue.


For everyday operations and refactoring, I find the tools to be the limiting factor.

I've just finished a contract where we had to deal with legacy code on iOS and Android (yes, it exists. Let two junior programmers not very keen to learn from the outside during 5 years working on the app and see with what you end up).

Refactoring the Android code to a healthy state was greatly facilitated by Android Studio and the whole IntelliJ suite. We were not afraid to move things around and could always see the light at the end of the tunnel.

On the other side, XCode has been a pain in the ass for the whole project. We had ObjC and Swift coode. Good luck with that. Even the pure Swift was hard to refactor. SourceKit is great and should help to get a top notch IDE that can resonate about source code but somehow Apple does not seem to care about the tools.

Sorry, I get passionate when talking about refactoring and tools.

Another example is how the Pharo Project (http://pharo.org/) is refactoring the Squeak source (http://squeak.org/) code/architecture. They developed custom tools for this purpose and their code cleaning is quite impressive when you know where they are coming from.


IntelliJ is designed for refactoring. I'm not sure how a programming language could be designed for refactoring. What would that look like?


Said language would make contracts easy to write and first class citizens.

It would help you formalize parts of the typically non-formal programming, allowing automated or semi-automated formal verification.

The language would allow you to bypass accidental hard restrictions imposed by type system - essentially duck typed but only if formal requirements are met on the types.

Hey, I've described Isabelle/HOL; another language that is pretty close but more typical looking is Pyret.


Actually, I believe refactoring should be done by computers. But people will hate it when we will have that capability.


Cleaning up code requires way too much human judgment for that to be possible any time soon.


Well, I have this idea that, pretty much, smaller programs (in the sense of Kolmogorov complexity) are well-written and thus maintainable. More precise argument is as follows:

You take a very simple functional language, like say, Haskell Core. Let's consider a large and representative enough (finite) set of programs in this language. We find the smallest possible (or very small) representation of this set. This will involve definition of some commonly used functions. Let's call all the functions that are in this representation "the basic library".

Then, for a new program, we can try to find shortest possible (among those you're able to consider) representation, such that, using the functions from the basic library comes for free (so you don't count the size of functions in the basic library towards the total size of the new program).

So if we can write a program that would be able to find smaller representation using definitions (functions) that we already generated in that representative set, I think it would essentially automatically refactor code to be more readable and maintainable.


This might get you the shortest code but it wouldn't necessarily get you the cleanest code.

There's often a trade off between coupling and terseness which requires (human) judgement in order to balance the competing concerns correctly. There's also the matter of using appropriate metaphors.

I have a feeling that a "cleaner" optimized like this would get you a program full of the equivalent of perl one liners, assuming it's even possible.


I don't know how the generated code would look like, but I am sure humans wouldn't like it. But that's not a big deal, because for every piece of code out there, there is somebody who doesn't like it.

Proper metaphors (better call them abstractions) would be generated at that "basic library" creation step. Basically those are functions that are commonly useful. Some of them would correspond to abstractions known by many programmers, some of them would be different, but altogether would let you write more compact code (because that's what they have been selected for). Ultimately, knowing abstractions amortize and ease understanding of the code that uses those.

We humans do the same thing - the code that is easy to understand is the code that uses commonly known abstractions, such as control structures, standard library functions, etc. What makes this possible is that we hold these shared abstractions in our heads, and create them as a part of programming culture by working with different code bases.

There is no trade off. You cannot write a terse function with high coupling, because then you cannot use the shared abstractions very efficiently inside it, nor you can build other functions easily by reusing this function. So terseness IMHO behaves very differently when you account for the fact that you can create new definitions, and you put those definitions to use. (And especially if those definitions come from some "basic library" for free - they are not included in the cost that you have to pay to understand the program - because they have already been understood.)


Abstractions are different to metaphors. You can have an abstraction with a name 'x' that you can use to send messages over an SMTP server. The metaphor could be "mailer", "mailbox", "email sender", "postman" or many other different things with subtly different implications.

Moreover, the "basic library" creation step that you allude to the point where you decide where the borders between different modules of code lie. Creating those borders at the appropriate point - deciding the coupling - is arguably the most important part of refactoring and the part (I) have found hardest to clean up in legacy code. I am currently rewriting a library which in retrospect I now realize should have been two libraries.

"You cannot write a terse function with high coupling"

Oh, you absolutely can. In fact, after a certain point terseness tends to correlate with higher coupling. There is often a trade off to be made between coupling one block of code to another (e.g. introducing a dependency) and sacrificing terseness.


> Abstractions are different to metaphors. You can have an abstraction with a name 'x' that you can use to send messages over an SMTP server.

How is that different from a function (or set of functions) that abstract the technicalities of sending messages over SMTP?

I am not sure why specific metaphor is so helpful here. I don't think it helps in understanding the function, on the contrary.

> Moreover, the "basic library" creation step that you allude to the point where you decide where the borders between different modules of code lie.

Not quite. The important thing to understand is that I am not optimizing for size of a single program, but large set of different programs. The same thing that humans do.

> There is often a trade off to be made between coupling one block of code to another (e.g. introducing a dependency) and sacrificing terseness.

Well, I understand "high coupling" differently than you, then. For me, it means things like doing different unrelated things in a single function (so the result is that it's harder to reuse it). It doesn't mean that you cannot call other functions.

I don't consider a function that avoids a library call, instead inlining the functionality, to be well-written. (If that's what you mean by the trade-off - maybe some example would help.)


>How is that different from a function (or set of functions) that abstract the technicalities of sending messages over SMTP? > >I am not sure why specific metaphor is so helpful here. I don't think it helps in understanding the function, on the contrary.

It isn't a different function, but "send_email" is better than "send_message" which is in turn better than a function just called "x" which is shorter. In each case a different metaphor is used. x is a shorter method but code that uses names like that everywhere is NOT cleaner. It's horrible.

>Not quite. The important thing to understand is that I am not optimizing for size of a single program, but large set of different programs.

Optimizing purely for size will end up increasing coupling to an insane degree. I've seen this done by people who get religious about DRY.

>Well, I understand "high coupling" differently than you, then. For me, it means things like doing different unrelated things in a single function (so the result is that it's harder to reuse it). It doesn't mean that you cannot call other functions.

If you're using one function to call another you have coupled them.

Coupling is not about what you can or cannot do it's about what is.

Coherence (or rather, a lack thereof) is about doing different things in the same function (or class, file, project, etc.).


Well, I agree that naming is a problem, but let's put that aside for this discussion. However, I should have explained that I consider all identifiers to have the same length for the purpose of trying to make the representation of program "compact". So what matters is number of functions being called inside a function (in FP, even constants are considered a function).

OK, I get your point about coupling. But I am not sure why you consider high coupling to be a bad thing, especially in case where you have an automated refactoring system.

It seems to me that to "decrease coupling" you have to add nodes to dependency graph, and this increases overall complexity.

Perhaps you could provide some example where high coupling (calling different functions from a single function) is bad, and how would you like to have it resolved.

Update: Actually, with automated refactoring, you wouldn't have to modify any existing functions (except maybe to call your new functions). You would just write new functions that would incorporate the new functionality using perhaps the existing building blocks. Then the refactorer would sort out things in DRY manner, as needed. So you don't need to care if the code is easy to refactor, it just needs to be easy to read.


>But I am not sure why you consider high coupling to be a bad thing

Isolation of code enables you to more easily understand it, test it, replace it and re-use it.

Linear increases in coupling leads to a combinatorial explosion in the difficulty in all of the above.

Coupling is actually more important than DRY.


I can see why you intuitively think that's the case, but is it really?

I don't understand how you imagine you can "isolate code" while making it less DRY. If I need to do certain functionality from some function, I need either to call other function to do it (if you already have a function that can do it) or copy that functionality into the function itself. The first approach is DRY, the second isn't. But I don't think second approach is a good idea, like, at all. So if I assume you don't mean that, what do you mean?

I can understand you can make code easier to test if you make it less DRY. Let's put that aside, because it's not really a big deal in this discussion. But the other three - I would love to see some specific example where making code less DRY will increase ease to understand it, and also possibly replace it and re-use it (provided we don't care about elegance of the result, since it would be taken care of by subsequent refactoring, as I already explained, so we can for instance copy-paste the existing code for the purpose of change).


I know easy refactoring is an explicit goal of the Elm programming language.

http://elm-lang.org/


Semantic versioning is nice but not fine grained enough, and does not help the programmer who is deriving the new version at all.


There is one attitude that's missing from the list: Change jobs frequently, and avoid old legacy code. Then you will always write the new code and it will be, by definition, maintainable (of course, who in their right mind would intentionally write an unmaintainable code?).


So that you will never see wether the code is actually maintainable or not?

You have described a strategy to avoid having to deal with bad maintainability, not a strategy to avoid creating bad maintainability.


Quite clearly, I was being sarcastic. In fact, on three different points:

1. I see "unmaintainable" as mostly a factor of time (or age of the codebase), of changing requirements and people working on the code. So giving advice on how to write the code to be maintainable isn't gonna help much. If people turnover quickly, they won't bother, if they turnover slower, they will do their best to keep it maintainable and yet fail.

2. People who follow (intentionally or not) the advice I gave only ever see maintainable code, so from their perspective, it leads to (having to deal with) maintainable code.

3. (= 1. + 2.) Sadly, the people who are the most displeased with the legacy code not being maintainable are the ones who tend to leave quickly (or write something on their own), paradoxically, compounding to the problem of the unmaintainable code in the first place!

I don't think there is a good solution to the problem. Maintaining old code is hard work.


Ok, I see it now, but the liberal conflation of "maintainable" and "code that is easy to work on" that is repeated in 2. still hurts my eyes: they don't see maintainable code, they see code that is easy to work on - not because it is maintainable, but because of its position in the code lifecycle. The confusion between these two is exactly your point, sure, but I don't know how to say it better, I think your wording is just too close to subscribing to it yourself. They think they see maintainable code. I know I do, while writing what I will later call a hopeless mess.

(Written by someone who has been doing brownfield projects, often bordering on code archeology, at the same employer for far too many years)


Not really sure what your objection is; what exactly difference you see between "maintainable" and "easy to work on"?

Could it be that a good definition of maintainable is simply "easy to understand by others"? Then, of course, code earlier in the life cycle tends to be easier to understand.

Then there could be a difference between "easy to work with" (by the original author(s)) and "easy to understand by others". But assuming original authors were good programmers, I am not sure how they could cause this difference to occur.


Having seen more than one code base is a good way to learn good and maintainable ways to write code though, so that part is still decent advice. Avoiding maintenance task of legacy apps is pretty silly though..


That's the joke!


Some positive sounding concepts that are used to wreck code and software engineering at a large:

- "Get it done attitude": Declare something as done while it's still a prototype.

- "Fail early": start with prototypes, end with prototypes. ship a prototype to production.

- "Knowing the software business": know how to trick non-technical people into buying overpriced prototypes made of spaghetti.

- "Product driven", "Feature driven", "Customer driven": implement only what can be sold (features), disregard everything else. e.g: security, scalability, etc.

It is becoming a business model to sell prototypes as finished products.


I'm experiencing this in my current job, and it's a bit demoralizing. It seems "agile" has been internalized to some extent to mean "start writing code on day 1 or else we're basically doing waterfall which sucks," too.


Agile is often cherry-picked and reduced to only the parts that people like. Burn-down charts are ubiquitous but sprint retrospectives are not.

Then some other parts are conveniently misinterpreted. People misunderstand the role of the scrum master and interpret it as the development team leader... confuse points with hours, or try to measure team performance using velocity... which is the same as measuring a truck driver performance by how much gas they're using rather than concrete results.

Everyone claims to be doing scrum correctly, just like everyone claims to be doing git-flow correctly. It's not only cool to be agile and use scrum, it's more like a dogmatic cult where scrum is the ultimate truth that solves every problem in every case and cannot be criticized.


What I'm reading is mostly management directives for getting a bunch of code monkeys to produce code in the safest, most generic and repeatable way possible; by enforcing the lowest common denominator. Given a sound team culture, not using the full power of tools and limiting output to dumbed down templates are anti-patterns; it's not like the job isn't difficult enough the way it is.


Another tautology filled article about how to write good code you just need to write good code.


The article breaks down some things that you need to do to write good code. Proper naming, write self-documenting code, no fancy stuff, have the correct indentation and other formatting, avoid special cases, etc.

Which is more than we can say for this dismissal.


Somehow, a lot of people seem to write maintainable code, but everybody always ends up having to maintain unmaintainable code.

A strange thing, this.


> Comments should be reserved for the rare bits of code that are influenced by factors beyond the code itself (for example, a customer requirement or a bit of historical context)

This statement may be true in language domains the author is used to, but is certainly not true across the full spectrum. Also - there are two sorts of comments: 'what' comments, and 'why' comments.

'What' comments tell you what the code is doing. A lot of the time, in a lot of languages, if you code clearly these are needed only rarely, if at all. They are telling you what the code is doing, mostly as a convenience - they can always be replaced by spending time reading the code to see what it's doing. There are a number of situations where they are useful though, mostly obviously in languages like C or even Assembly where code often cannot be made easily readable. The 'what' comments allow you to quickly parse code to build up understanding of what's going on. Good assembly has a very high comment to code ratio - you are mostly reading the comments rather than the code to grok it. Occasional 'what' comments on a bit of C pointer arithmetic allow you to understand the authors intention and check against that 'specification'. I agree that in more expressive languages, these sort of comments become rarer and rarer.

'Why' comments are irreplacable in every language, and are what the author seems to be referring to in the above quote. "I have chosen to do this like this here because...". Because otherwise X unforseen bug, because otherwise Y performance issue, because Z client requirement. However expressive your code, that extra context is never going to be communicated by the code itself.


I agree with the comment about assembly of course, but ironically I find myself spending more time writing comments when programming in high level languages like Python, rather than in C. The reason most of the time seems to be that many high level scripting languages (like Python) are dynamically typed languages. So things like function definitions contain no information (other than the variable name) that explains what the inputs/arguments might actually be. I often find myself having to write comments to make it clear what the parameters are supposed to be so someone reading the code instantly understands what needs to be passed to the function, whereas in a language like C or C++ that has static typing, the type would make it obvious.


> I often find myself having to write comments to make it clear what the parameters are supposed to be so someone reading the code instantly understands what needs to be passed to the function, whereas in a language like C or C++ that has static typing, the type would make it obvious.

I'd encourage turning these sorts of 'comments' into Javadoc-style 'annotations'; they're still comments, so language implementation will ignore them, but that small amount of structure makes it possible for separate tools to perform sanity checks (i.e. obvious combinations of incompatibly-annotated types), or even to show annotations at call sites in an IDE (e.g. when hovering over a name).


I'd rather call that 'how' instead of 'what', because there are two things you might take "what does this code do?" to mean: its purpose or the way it works. A square-root function, for example, might have

How: newton-raphson iteration, 2 rounds

What: return a number within 1% of the positive square root of the input, which must be in a certain range

Why: it's for a rendering, it doesn't need to be precise

In this breakdown the 'what' is what I tend to want the most often, as a reader. It gives an anchor to check the code against, and enables reading that code without having to understand all the other code it calls on -- just the 'what' comments of the other code.


Why is the more important one - it is the requirement.

I'd rather see something like this:

Why: We need to calculate distances between widgets to pack them so that it looks nice to the end user, but is also fast - start must not take longer than about 1 second. We are running on embedded hardware.

What: Use square root approximation for distance defined over the range of pixels available on screen.

How: Newton-raphson iteration, 2 rounds, fixed point.


Fair enough -- I mainly wanted to point out an important distinction blurred by "why vs. what". It's up to the context whether you'd care more about this kind of why or what. However, it's this what -- what the code is meant to do -- that's both the most basic info for maintenance and in poorer supply, as I've seen it. It's often easier to get an off-the-cuff answer to "why do that?" (or to not need it) than "exactly what was this code meant to do? Does the rest of the system depend on the fact that it freems the bleetches, or can I change that?" -- in that case you're more likely to get back "Hm, I'd better read all this related code to swap it into my head".

Your experience might be different, and that'd be kind of interesting.


The biggest factors to me: use appropriate, future proof (insofar as possible) reliable tools and frameworks. Favour popular established libraries over custom code. This can dramatically reduce surface area you need to maintain, and simplify the code that remains. Simple but often neglected.


If we are talking attitudes, then what's really needed is to care about code you write. I don't think you want terribly many hard and fast rules, but a considered approach to everything. Cleverness is great when appropriate. Things shouldn't be used just because you want to use them, things get used because they enhance the engineering. I think the greatest danger is probably having too many rigid ideas about coding.


Another big one. For Object oriented code. Composition over inheritance. It's hard to overstate how important this is. Many people know this but still don't do it enough.

Don't make large classes that implement many features. Split those features/responsibilities into smaller classes and then have a class that calls to the smaller ones.

A good way to think about this is your classes should have as few member variables as possible.


I really agree with the "Avoid special cases", or the Zen of Python version: "Special cases aren't special enough to break the rules."

For large systems it's absolutely critical to be careful around special case, at best avoid them. The hard part of dealing with special cases is to explain to business people that their "It's just ONE special case, how bad can it be" will snowball if allowed.



I've always had the attitude that its easy to not put bugs in in the first place than to have to find and fix them later. Whatever you do that makes the code simpler and clearer up front, which might take longer, is always saved in the end when there are fewer things to fix. I started thinking this way 30 years ago and it's always done me well since.


I think this applies to more than just coding.

Being a sysadmin, code that I write tends to be in the form of puppet manifests, and these things do apply. There's one more rule of thumb I use to determine how I should approach building systems. Basically, I think that shortcuts are not (usually) worth the trouble.

What I mean by this is that usually the least-effort, fastest (or "obvious") solution to a particular problem is not worth the time saved, when compared to just spending the time figuring out how to do things "properly".

There is a continuous cost-benefit analysis where you keep asking yourself whether you will save you enough time taking the shortcut that potentially having to re-do it later (or fix potential issues caused by it) will still end up less "expensive".

When you do choose to take things slow, though, you usually end up having a better understanding of the problem as well as the solution, which naturally leads to a better result.


The documentation for the application I maintain is scattered with similar buzzwords.

Unfortunately the database design is crap leading to way more code than is necessary. It also includes important information that has been pickled and stored in a field, so its a nightmare to debug. It's done in Django but he has used a "fat controllers thin models philosophy" - the opposite of Django best practices, so it doesn't take advantage of many of Django's features such as lazy loading and doesn't run quickly as a result.


YAGNI. Not being an abstraction freak. Not implementing hundreds of features in the hope that they may be useful one day.

I have been going through the code of a well-known document database... I am horrified to discover over 400 classes in their abstractions project, including some very clever stuff, which I then try to find examples of in the rest of the code, only to come up with nothing.


The HN comments on this are a lot more interesting than the article itself. Just a couple of platitudes we've all heard and repeated and lived by. "You can write more maintainable code if you think about making it easier to maintain!" OK, duh.


Be consistent. Pick one style and use it everywhere. It's nice if you can predict how something will be implemented from previous experience. And don't be fancy.


Style is overrated. Changing attitudes and requirements force different styles. It is not an accident of nature that generally the nicest languages to use are multi-paradigm, therefore allow multiple styles.

Previous experience can be very misleading. It is safest to come at the code and design as a blank slate. Prediction (also known as educated guesses) is worse than actual understanding, especially deep understanding.

Sometimes being fancy saves a lot of effort down the road. It is being fancy for its own sake or for purely imagined reasons that is dangerous.


Avoid special cases. Thats a good idea, unfortunately it's not quite possible if you have to adjust code to changing requirements over a long time...


It should be rephrased as: design so that many cases can be handled, thus reducing number of special cases.

They cannot be avoided, but their impact can be reduced. If you end up drowning in them, it means the design is broken and needs to be rethought.


We write code to be read by humans, and not by computers.


I like the alternate quote from the article:

There are only two hard things in computer science: cache invalidation, naming things, and off by one errors.


How about attitudes that lead to performance code? That page doesn't load properly.


It loaded very fast for me, and I have crappy internet.


The most maintainable line of code is the one which you don't write. Remember the cardinal virtues:

Laziness Impatience Hubris

Back when I was a Java dev, judiciously following the SOLID principles was a vital part of keeping code maintainable. They're still as true today as they ever were.

* a class should have only a single responsibility

* software entities should be open for extension, but closed for modification

* objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

* many client-specific interfaces are better than one general-purpose interface.

* depend upon abstractions, not concretions.


Can you expand a little about how each of these parts of SOLID have influenced how you have written code? I feel like I've heard people say conflicting things to some degree on at least some of these (e.g.: single responsibility) and my own experience feels somewhat like following some of them to their natural conclusion can end up with code that is more complex and harder to maintain. Again, for example, the S seems to imply needing a larger number of classes that would have to be searched through to find a specific implementation or bug, and D feels like it would shove the code that actually does something down a layer and be harder to find. That could be my inexperience talking or just not knowing how to employ the ideas properly, though.

I guess a decent amount of what I'm feeling comes from my current job, which breaks a decent number of those rules. (S, O, I at least, possibly D but I don't think so) But despite that, code generally seems to be pretty easy to read through, so I'm a bit conflicted.


I find SOLID to be less reliable than applying ages old principles of contract-oriented programming. S is matter of taste, you can define it to be "show the whole GUI" or "show a text label in given position".

Depending upon abstractions directly is much less reliable than depending upon contract (which is usually not explicit in the abstraction) and preferably also verifying it as both preconditions and postconditions.

The other letters are important, though software should be designed for easy modification instead of being closed to it. Otherwise you end up with abstraction forests and deprecated code.

Many interfaces is typically a smell, as the developer now has to juggle multiple balls in his head. There is good medium ground out there - making the interface maximize usefulness while not sacrificing too much of Liskov substitution capability is key.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: