10

In short: what are your software development and design rules of thumb, which you strive to apply in every project.


I wouldn't start such a question, but recently I started working on a project based on a eCommerce platform, and I found that many of my rules of thumb are just not observed there (in the platform code).

The idea of this would be to generate a good list of things not to do and things to definitely do when designing and developing a project.

Some notes:

  • concrete code-style rules should be avoided
  • when possible, give rationale for your 'rule'
  • the topic is language-agnostic, but some context specifics are OK (for example PHP is interpreted, so hot-swap is there automatically)

To begin, I'll list some of mine, which were not followed in the aforementioned project:

  • do not use and rely on build-time generated sources - I just feel rather fragile when extending from a class, that is generated on each build. One-time generation is OK.

  • have all essential configurations in files, not a database, so that they can be committed to repository

  • do not have layers where code just "passes through" to the next layer - It is a sign of overdesign and generates boilerplate code.

  • always have a hot-swap local development environment, so that you can change a piece of code, switch to the browser/application and the change is reflected - it vastly increases productivity compared to having to go through the whole build process

  • don't make interfaces that define getters and setters - just feels bad

13 accepted
  • Automate as much as possible - get the build process, test cases, database setup etc. out of the powerpoints/developer heads/documentation and into the repository.
12
  • Don't live with broken windows - when you spot a problem, make sure you try to fix it as soon as possible. It won't be easier to justify the fix when you're even closer to the deadline.
8

Design and write the code as if it is going to be maintained by an axe wielding maniac who knows where you live.

Rationale: someday it may be true.

6

Have proper version management

  • always have source control revision embedded
  • configuration must specify requested binaries version
  • if version does not match, or binaries compiled from "modified" revision, it must issue a warning (but not refuse to run)
  • version of all separate files must also be checked

Use UTF-8 encoding everywhere

Needless to say

  • continuous integration (build + full system tests on every commit)
  • use stable branches
  • manage risks, requirements, development schedule
  • have consistent code and security reviews
5

Do what you say, say what you mean

What your code does should be obvious from the name and arguments.

Avoid primitive types

Prefer types that contain semantic information about what's being passed, and can possibly constrain the input. A Name is better than a String, even if the Name simply wraps the String. This helps get the most out of static languages (if you're using a static language).

Write your code as if your ideal environment existed

Write to the interfaces that you wish existed, and then write a class to bridge between your ideal interface, and the real world. This helps keep your code doing the right thing, and makes it easier to change dependencies later.

Prefer void methods when possible

Except on immutable types, obviously. Void methods make it far less likely to develop God-classes, they promote loose coupling, make state-based code less likely, and make refactoring easier.

Abstract rather than generalize

Don't try to solve all of the problems in the world. Solve the problems you have. Use proper abstractions to separate specific solutions from the code that wants them. Abstractions should define what problems will be solved without specifying their implementation. Don't abstract general problems.

Use immutable types when possible

Immutability solves entire classes of problems.

Don't be afraid of data

Data is your friend! You don't care where it came from, or where it's going. You can mutate it, put it in different forms, and send it on its way without worrying about side effects.

3

If I had to pick one single rule and enforce it, I would pick this one:

Put your code in the right layer

Yes that sounds like something trivial, but it actually needs some discipline to be enforced. There is just so many way to screw the layering in subtle (or not subtle) ways, but it always result in a big mess over the time. If you make a hack according to the responsibility of each layer (which sometimes can happen), be sure to document it.

2

If you see the same snippet of code in 3 places, refactor to remove the duplication.

2

Keep the design as simple as possible.

2
  • Optimize for the human's needs, not the computer's
  • Communicate
  • Automate
  • Know the reason behind every rule, so that you will not become dogmatic
  • Test everything.
  • Don't Repeat Yourself
  • Refactor mercilessly
  • Code today's requirements, not what you think they will be tomorrow (YAGNI)
  • Make the code communicate your intent ("why" as well as "how")
  • Consider a comment a necessary evil, used only when you cannot make the code speak for itself
  • Value simplicity over cleverness
  • The code in a method should be at the same level of abstraction
  • A name (for a method, class, or variable) should be truthful.
  • If you can't come up with a truthful name for a class/method/whatever, the design is wrong

Optimize for the human's needs, not the computers

Always use the simpler code, even if it's slower. Use the less simple, faster code if (and only if) it needs to be faster. Someone will have to pay the cost of maintaining the code. Make that cost as low as you can.

Automate

We didn't invent computers so that we could act like computers, mindlessly following algorithms and doing the same thing over and over again. Any time you find a task repetitive, automate it.

Know the reason behind every rule

If you know, for example, that doing an assignment in a conditional is bad because it can be difficult to tell when it is intentional and when it is accidental, then you will understand when you can do an assignment in a conditional. If you do not know the reason behind the rule, you are adhering to dogma and will miss out on opportunities to make better code.

Communicate

Programming done well is a noisy process involving lots of communication. You can't write code that does the right thing if you don't know what the right thing is. If you want other people to benefit from your code, you must talk to them. A lot.

Make the code communicate your intent

Code should communicate why it is doing something, not just what it is doing. For example, instead of this:

def read_input
  # Read the file and remove printer codes
  read_file(input_path).gsub(/\e.*?;/, '')
end

something like this:

def read_input
  remove_printer_codes(read_file(input_path))
end

def remove_printer_codes(s)
  s.gsub(/\e.*?;/, '')
end

Don't repeat yourself

Every concept, algorithm, magic number, etc. should exist in one and only one place. This makes refactoring surer ("I just changed the sort; how come it's not sorting differently in report 2?") and the addition of names for the factored-out elements make the code more expressive, For example, from this:

if a.size > 100
  a = a[0...100]
end

to this:

MAX_QUEUE_SIZE = 100
if a.size > MAX_QUEUE_SIZE
  a = a[0...MAX_QUEUE_SIZE]
end

refactor mercilessly

The code isn't done when it works and passes tests. It's done when it works, passes tests and is refactored. That means it uses expressive names, doesn't repeats itself. We spend more of our careers extending and fixing existing code, so taking the time to keep the code easily maintainable pays large dividends.

Consider today's requirements, not what you guess the future will bring

Adding "because we might need it" features is great, iff: We actually do end up needing it, the requirement doesn't arrive differently than you guessed it would, and if you can suffer the carrying costs of the unused code between the time you create and when it becomes needed.

It's pretty rare that this happens. Most often, we guess wrong about the future requirement and end up scrapping or rewriting that code. So don't try to predict the future unless you're psychic.

Consider a comment a necessary evil

A comment means, "I couldn't figure out how to make the code say this." See "Make the code communicate your intent" (above) for an example of how to replace a comment with code that declares its intent.

The reason you don't want unnecessary comments is because comments come with a cost as well as a benefit. Comments have to be modified to match the code every time you change the code. And how often have you seen code where the comments don't match the code? The benefit of extensive comments may not be as great as the cost.

The code in a method should be at the same level of abstraction

Consider this function:

  def lines
    lines = read_input
    lines = remove_private_methods(lines)
    lines.each do |line|
      puts line if line =~ /^ *def /
    end
  end

It starts out at a relatively high level of abstraction. read input and remove_private_methods say what's being done, but not how. But then there's a jarring disconnect as we switch to iterating over the lines, doing a regular expression compare, and printing lines--all low level compared to the previous methods. The low-level code also does not declare its intent. It only says what it's doing, not why. Better would be:

  def lines
    lines = read_input
    lines = remove_private_methods(lines)
    print_method_declarations(lines)
  end

  def print_method_declarations(lines)
    lines.each do |line|
      puts line if line =~ /^ *def /
    end
  end

Now the code in each method operates at the same level of abstraction.

A name should be truthful and If you can't come up with a truthful name for something, the design may be wrong

Consider:

  def print_method_names
    @method_names = method.names.reject do |name|
      name =~ /foo/
    end
    puts @method_names
  end

This method first modifies the instance variable @method_names, removing any names with "foo" in them. Then it prints the method names. The name of this function doesn't tell the truth. Someone reading the code would reasonably assume that it does what it says, but instead it includes a surprising side-effect, modifying data. A better name might be "reject_foo_names_and_then_print_names," but that's awful. When you can't come up with a good name for something, it's a clue that the design may be wrong. Better would be to split the modification into its own method, each method doing exactly what its name says (and no more).

  def reject_foo_names
    @method_names = method.names.reject do |name|
      name =~ /foo/
    end
  end

  def print_method_names
    puts @method_names
  end
2

think twice before writing code

I've seen too many quick hacks turn later in a big mess

*this includes my hacks too :)

1

My rules of thumb:

  • Engage brain before engaging keyboard.

  • Write the code + comments such that if you come back to this code 3 months later you can understand what it does without spending a week reverse engineering the source. It's what I'd like to call "The Bus Rule", i.e if you get hit by a bus who inherits your source code.

  • Don't be too smart and use the least number of lines instead leave the ego at the door and write the code with the view that someone else will be reading it sometime, somewhere.

  • Clear documentation (within the code) and sensible artefacts (class models, photos of whiteboard designs etc).

  • Try to evaluate the problem and determine at least two plausible solutions just in case you are over designing the objects in the model.

  • Remove the infrastructure overheads. In other words have a development environment which is easily setup, deployment system, SCM, code build and possibly continuos integration.This way you are not wrestling with the extraneous stuff and getting on with the code/design/implementation. This should follow the rule of quick, easy and convenient ; quick to setup, easy to use and convenient so that it is adopted within the team.

  • Don't over design. The age of OO meant that everyone viewed everything as a generic object and every design had to cater for the just in case scenario in the design. An over generic design is just that - too general, complicated etc. Instead design for the domain and follow the Unix philosophy of build it to do one job well

0

Every design more complicated than "Hello World" should be supported by an outline (probably in some nice outlining software, for efficiency), and the outline should be iterated a few times over.