Unleash Your Potential - Namagunga Girls Coding Club
Stop that!
1. Stop That! Questioning Dogmatic Programming
or Things I Wish My Coworkers Would Stop Doing
Doug Hiebert
Thank you to our Sponsors
2. About Me
Doug Sparling / Doug Hiebert
Software Dev - Protegra
Twitter: @doughiebert
Email: doug.hiebert@protegra.com
3. Dogmatism
noun
1. The tendency to lay down principles as undeniably true,
without consideration of evidence or the opinions of
others.
4. Programming Dogma
•
•
•
•
“A, B and C are best practices”
“X considered harmful”
“Y is evil”
“language/technology/tool Z is the best”
5. Programming Dogma
“Break any of these rules sooner than [code] anything
outright barbarous.”
-- George Orwell, "Politics and the English Language,"
1946
9. Comment Your Code
• Communicates programmer intent
- The “why” to the code’s “what”
• Explains tricky/clever code
• Explains how to use APIs/interfaces
11. Communicating Intent
for (Person person : people) {
// send cheque to persons old enough
// to qualify for pension payout
if (person.getAge() >= 65) {
sendCheque(person.getAddress());
}
}
12.
13. Communicating Intent
for (Person person : people) {
// send cheque to persons old enough
// to qualify for pension payout
if (person.getAge() >= 65 ||
person.getYearsOfService() > 30) {
sendCheque(person.getAddress());
}
}
14. Communicating Intent
•
•
•
•
Skipped by refactoring tools
Not essential for program function
Poorly written or misleading
Some people ignore them (like me!)
16. Comment as Method
for (Person person : people) {
if (oldEnoughForPensionPayout(person)) {
sendCheque(person.getAddress());
}
}
...
boolean oldEnoughForPensionPayout(Person p) {
return p.getAge() >= 65;
}
17. Comment as Local Variable
for (Person person : people) {
boolean oldEnoughForPensionPayout =
person.getAge() >= 65;
if (oldEnoughForPensionPayout) {
sendCheque(person.getAddress());
}
}
18. Comment as Constant
private static final int
MIN_AGE_FOR_PENSION_PAYOUT = 65;
for (Person person : people) {
if (person.getAge() >=
MIN_AGE_FOR_PENSION_PAYOUT) {
sendCheque(person.getAddress());
}
}
26. Explain APIs/Interfaces
@Test
public void testFindPeopleByFirstAndLastName() {
Person bobJones = testPerson("Bob Jones");
Person joelStevens = testPerson("Joel Stevens");
Person shirleySmith = testPerson("Shirley Smith");
PersonRepository exampleRepo = makeRepository(
bobJones, joelStevens, shirleySmith);
List<Person> persons = exampleRepo.findPeople("jo");
assertEquals(bobJones, persons.get(0));
assertEquals(joelStevens, persons.get(1);
}
27. Documentation
• Comments are a type of documentation
• Good examples of documentation:
- User’s guide/manual
- Architecture diagrams
- Decision log
28. A Final Note About Comments
/**
* The person's name.
*/
private String name;
...
/**
* Returns the person's name.
*
* @return the person’s name
*/
public String getName() {
return name;
}
29.
30.
31. Don’t Waste Cycles
• Improve UI response
• Speed up long-running batch jobs
• Satisfaction of “eliminating waste”
38. Decide with Conditionals
• if, unless, else, ?:, etc.
- Implement conditional logic
- Handle errors
- Null checking
• switch, case, etc.
- exhaustively handle different cases
39. Decide with Conditionals
public List<String> parseNames(String names) {
if (names == null) {
return null;
}
return Arrays.asList(names.split(","));
}
void readNames() {
String line = System.console().readLine();
List<String> names = parseNames(line);
if (names != null) {
for (String name : names) {
// ...
}
}
}
40.
41. Null Object Pattern
public List<String> parseNames2(String names) {
if (names == null) {
return Collections.emptyList();
}
return Arrays.asList(names.split(","));
}
42. Null Object Pattern
public class CommandLookup {
public Command findCommand(String command) {
if (validCommand(command)) {
// ...
} else {
return new NullCommand();
}
}
public class NullCommand implements Command {
public void execute() {
// do nothing
}
}
}
47. Decide with Conditionals
public int readSensor(SensorType sensorType) {
switch (sensorType) {
case TEMPERATURE:
return weatherSensor.getTemperature();
case HUMIDITY:
return weatherSensor.getHumidity();
case PRESSURE:
return weatherSensor.getPressure();
default:
throw new IllegalArgumentException("...");
}
}
48.
49. Replace Conditional with
Polymorphism
enum SensorType {
abstract int getReading(SensorPackage sensors);
TEMPERATURE {
public int
return
}
}, HUMIDITY {
public int
return
}
}, PRESSURE {
...
};
}
getReading(SensorPackage sensors) {
sensors.getTemperature();
getReading(SensorPackage sensors) {
sensors.getHumidity();
50. Replace Conditional with
Polymorphism
abstract class SensorWrapper {
abstract int getReading();
static class TemperatureSensorWrapper extends SensorWrapper {
private SensorPackage sensorPackage;
public TemperatureSensorWrapper(SensorPackage sensorPackage) { /*...*/ }
int getReading() { return sensorPackage.getTemperature(); }
}
static SensorWrapper create(
SensorType sensorType, SensorPackage sensorPackage) {
switch (sensorType) {
case TEMPERATURE: return new TemperatureSensorWrapper(sensorPackage);
case ...
default: throw new IllegalArgumentException("...");
}
}
}
51. Decide with Conditionals
if (province == Province.MB
&& productType == ProductType.WIDGET) {
tax = 0.05; // no PST on widgets in MB
} else if (province == Province.MB) {
tax = 0.13;
} else if (province == Province.AB) {
tax = 0.05;
} else if (...) {
// and so on
} else if (...) {
52. Replace Conditional with Map
static {
taxByProvince.put(Province.MB, 0.13);
taxByProvince.put(Province.AB, 0.05);
// ... etc.
}
if (province == Province.MB
&& productType == ProductType.WIDGET) {
tax = 0.05; // no PST on widgets in MB
} else {
tax = taxByProvince.get(province);
}
Welcome to my talk: Questioning Dogmatic Programming, or, “things I wish my coworkers would stop doing”. This is a a semi-serious talk about lots of programming practices that are taken for granted, but that I think should be avoided at all costs.
What form does dogmatism take in the programming community?<read list>Considering programming is a logical, rational activity people sure have a lot of rigid beliefs. You’ll never find justification provided in similar strength to these claims.
Here’s a butchered quote that has guided my personal philosophy regarding so-called rules, idioms, best practices, whatever you want to call them.<quote>So what rules should we be following and what rules should we be breaking?
Not saying all dogma is bad: there are lots of good examples.Formatting code: no reason not to, tools do it automatically, improves readabilityThings like using camelcase for methods, don’t allow multiple returns, whatever. Improves readability among the team and we don’t waste time thinking about trivial things.All things being equal: less code is better than more code, and simpler designs are better than complex design.Everybody should be refactoring their code as they go.Modular fashion, parts of the system are essentially black boxes that talk to each other. Can't think of a reason not to do this.
Here are some more statements that are broadly accepted as true but are actually almost always wrong.<read the list to prime people>In this talk we’ll go over some situations where dogmatic adherence to these principles will get you into lots of trouble. Or at least get the future maintainers of your code into trouble.
Okay, let’s start with comments. Every programming language has this neat feature: you can put whatever you want after or in between certain characters. Then, using the syntax highlighting, readers are trained to ignore blue and green text. It’s really handy.
Seriously, though: first get introduced to dogmatic thinking in education: always comment your code. Lose marks for uncommented code, or profs give you standard headers describing inputs, outputs etc. and you have to fill them in.Intent: what the programmer was trying to accomplishTricky: if code is non-obvious, does something unusual, or is otherwise unclear, comments can elaborateAPI/interface: for code intended for re-use such as APIs or interfaces, comments provide examples of use, what parameters to pass, what the structure of results are, etc.One argument says: code is literal; can only explain what is going on: not why it needs to be done.
Very clear what this code is doing, but why is it doing it? What business rule can explain it?
Might be tempted to throw in a comment to explain why we need to send the cheque…
Stop doing this! Already have something that communicates what the programmer is trying to do… the code!Repetitious (violates DRY) to simply re-state what the code is doing, even if you add extra information or summarize.
Oops, the code is working correctly but the comment is no longer accurate. At best, people will ignore the comment, or at worst somebody reading it may be misled. Might try to track down a bug and assume that the problem lies with the person’s age. <explanation of wrongness ahead>
What else can go wrong?Skipped: refactoring tools won’t update comments, and some people use them blindly without checking updated code (e.g. rename method at 100 call sites… are you going to check them all?)Not-essential: Poorly written: we’re hired as programmers, not writers. Lots of comments are just badly written, full of typos. Don’t give you much confidence.Ignored: some people like myself have been burned so many times by bad comments that they are just trained to ignore them entirely. <anecdote about not seeing Dave’s comment block in rails partial)>. So they end up as a waste of your time to write and waste of space.
So how to we eliminate these sorts of comments? Each of these refactorings takes a comment and embeds it into the program as a symbol, which you are forced to read as part of the program. They are refactorable, can be as descriptive, and leave the code cleaner than comments.
In this refactoring, we give method a name to include additional meaning, or why it's being done.This embeds the “why” directly in the code and improves the readability of the original code.Maintainers are practically forced to read the method name, and it occurs in multiple places, so the code isless likely to be modified such that the method name is invalid. I’ve seen it happen, but not nearly as often.
more compact than the "comment as method" refactoring, but equally descriptive. Good for one-offs.
Constant helps us to understand the conditional check - it's a minimum so we use greater than or equal.With all of these refactorings, there’s no reason to use short names. We have fancy autocompletion from text editors like vi all the way to IDEs like Eclipse.
Using a language (Ruby) that people aren’t likely to be familiar with to illustrate the point.If you write clever or tricky code, sometimes you get this feeling, like an itch, telling you “oh, I should add a comment”.
Stop! This is a product of dogma.Instead, interpret that itch or feeling as a sign that you need to refactor, simplify, rename, whatever it takes to make the code’s purpose and function obvious.Comments are admissions of failure to express your intent in code.When you add an explanatory comment, it's because you weren't clear enough using the programming language, so you fall back to the comfort of a natural language.
<explain code>Maintains the original cleverness, but the reader can tell at a glance what the purpose is, and has a better chance of being able to understand how the two steps work. It’s a trivial example, but you can blow it up to larger sizes and the idea is the same. You should always attempt to simplify/refactor/rename/etc. before falling back to comments.
The last reason you might want to add comments to your code: it’s intended for re-use. Generally a collection of classes, methods, utils. If you want to get fancy, can publish documentation using a tool like rdoc or javadoc.Might be compelled to add comments to achieve a higher level of qualitythan regular code. Feels more complete, done due diligence. You just have this feeling that high quality code should have lots of comments. No, you can’t explain it. That’s kind of a bad sign…
<explain code>Obviously this isn’t very descriptive on its own. You can’t push much meaning into constants, method names or locals here… you decide: give in, have to add a comment.What form might that comment take? Here’s a hint: after learning about what a library generally does, what is the next step you take? …. <code example upcoming>
Look for code examples! So write some code examples for your API. Good examples should demonstrate what valid parameters are and what the return values look like.So here’s one approach: a code example embedded into a comment. Here, the example has been marked up with HTML so it displays nicely as Javadoc. Usage of the interface is nicely illustrated for the reader, at the source.Looks like we’ve got this one solved, right?
No, not solved at all. This is even worse than the comments we looked at before… Because the interface definition is in a different file, it’s likely somebody will miss the comment entirely when changing the impl.Another issue, you’re mixing three languages together: HTML, English and Java, which is a maintenance headache. I think we’ve all been in the situation where we’re manually formatting a code snippet in a comment by spacing every line just so…Finally, the compiler isn’t checking your comment so make sure it compiles, and even if it does, there’s no guarantee it will work. Code examples in comments might become obsolete and omit key steps.So how do we keep our example code and resolve these issues? Well, we have to get the compiler to check it. It also has to be in the source repository so other developers can find it. And lastly, it also has to be periodically executed to make sure it works.To some of you, that probably sounds like a real headache. Hopefully the rest of you are thinking: that sounds a lot like a test.
You’re right, I am talking about tests! So let’s document the usage of our API with a test. We have everything that useful documentation should have:1) input: a string2) output: a list of people3) what the method does: appears to be matching on the people’s first and last namesWe know it’s valid code as it is checked by the compiler. As an example, if the test passes, it very likely to be a valid use case. There are also likely to be multiple examples testing different cases, as good tests typically exercise all parts of the code.If all API documentation came in the form of runnable tests, I would be pretty happy.----- Meeting Notes (2013-10-17 23:54) -----make them "live" -> make sure they compilewant to execute them -> make sure they actually worktest -> thing that you're already writing anyway, so no extra work. fix code exampleknow code example works -> running tests all teh timetests make better API documentation than inline comments----- Meeting Notes (2013-10-18 13:12) -----fix last bracket
I’d like to clarify: not saying “don’t write documentation”, I’m just talking about inline comments.Documentation in general is much broader, and includes lots of good stuff <examples above>. Write code when communicating with programmers, and save English and diagrams for non-programmers.
As a final note about comments, I'd like to bring out this fabulous snippet of code, which I've seen countless variations of over the yearsI'd like to think stuff like this is written by people silently rebelling against poor coding standards, or planted there by people like me who secretly hate comments, because I'd be depressed to find out that somebody did this and actually thought they were being helpful.
So please, stop writing comments! Unless you are being paid by the line. Then right on <thumbs up>
Next, let’s talk about optimization: making code go faster at the expense of other concerns.
There are good reasons to optimize. But, most of the time our efforts are wasted, because we've convinced ourselves that we know where the important bottlenecks are, but in reality we don't.
Going to go through one example,drawn from a recent code review.<explain code>Somebody might look at this and think: hey, you’re iterating over the list twice! These two loops could be combined and that would make this go a lot faster.----- Meeting Notes (2013-10-17 23:54) -----"some recent code review feedback"
The only obvious way to me to combine the loops is to inline the two methods and mix up the sums and calculations.
Slow down!Now the calculate method has two new problems:1) It’s doing more than one thing.2) The method is much longer and harder to decompose.2) It’s operating at multiple levels of abstraction: not only does it have to know about all the averages we want to calculate, it has to know how to calculate them!Okay, so we’ve mangled the code, but at least it's faster right?
An extra loop over a million objects is 3.6 milliseconds, nothing to get worked up about, and certainly not worth sacrificing the readability and maintainability of the code. 3.5 ms -> UI, imperceptiblebatch job -> irrelevantIntegration with other systems, e.g. database -> those calls will greatly outweigh thisThe point isn’t that looping over a collection multiple times is okay. The point is that there’s nothing wrong with wasting some cycles and putting readability first. Programmer time is more expensive than computer time. Also, small code optimizations like this are almost never worthwhile.Another way to reiterate this similar to what I said about comments: write code for people first, computers second.
I feel like this quote is in the collective consciousness, but people choose to ignore it anyway.Maybe folks don’t realize how broad the advice is intended to be, so I’ll expand on it a bit:1) What is ‘premature’? - before the feature has been determined to be too slow. What’s too slow? Need to answer this question. Set performance goals for max response time. Maybe you never come near them and so no optimization required. - before the code has been profiled - before you have determined whether the code will be kept or not (i.e. don’t waste time polishing temporary or volatile code)2) What is ‘optimization’? -thinking of bit twiddling? Other low level stuff? No, it’s designing or modifying code with performance as the foremost consideration. Using these definitions, you pretty much never need to optimize.
Let’s talk about program flow, specifically conditional statements.
What’s wrong with these things? We’re almostnever told to avoid them. Everybody uses them. What’s the problem? We need to use them, right?
One of those things that people do without thinking about it. Have to check for nulls, because somebody might pass them into our method, or because another method might return one. Stuff like this ispretty common
You might be thinking: wait, what’s wrong with that code? We have to check for nulls, and they make a really convenient value to indicate “no result”.The problem:1) Null checks violate DRY: every time you check the same return value for null, you’re essentially saying over and over again that some value X could be null. And you’re likely to miss one.2) Null checks are sometimes unnecessary. Such checks at best add clutter, and at worst mislead readers or cause them to question their understanding of code.
Here’s a neat idea: the null object pattern. replace nulls with objects containing “empty” behaviour.In this case, an empty list makes a natural null object. <explain code>Now callers can just iterate over the list without checking for null.
Another application of the Null Object Pattern with a custom type that has empty behaviour defined. <explain code> It might be be natural to simply null here, but if what you really want to do is nothing, use a null object to represent a missing command. Then callers don't need to worry about null checks. You can also log calls to execute or print a stack trace similar to what you would get if a NPE was thrown.
Another option for eliminating null checks: the option type.Lets you wrap nullable values like objects, strings, etc. Provides multiple benefits:1) Encodes the fact that the value might be null using the type system, which forces you to deal with that possibility by going through Option rather than remembering to sprinkle null checks everywhere.2) Provides mechanism for transforming or using the value wrapped in a new Option (via map, passing in a transformation function).3) Can elegantly return the value or if missing, a default (via orSome) without a conditional.4) Can query it to see if the value is null, so at worst you’re just in the same boat as you were before<explain code>
<explain how it works>TODOadd parseNames4 using map??
As an example of another case where you might want to use conditional statements, suppose you want to create a method for sampling weather data from a 3rd party library.You think all of these different methods are kinda clunky, so you want to replace them with a single, generic call.
might be thinking: what the heck is wrong with that?Having just one switch statement isn’t awful, but when you add enums, checks with switch or if tend to pop up everywhere. This is just repeating the knowledge of what the complete list is in multiple places. This means adding new sensor types becomes difficult as all of these will need to be located and updated. Adding new behaviour based on type (e.g. print with units) isn’t much better : copy & paste switch and modify
<title>: Fancy way of saying: push the differing behaviour into method of a class hierarchy.A fairly well-known refactoring:This works out quite nicely in Java with a rarely used feature and just a bit of boilerplate, but all of the switch statements’ behaviour can be consolidated into a single location, and no conditionals are required at all.<explain how it works as a replacement>TODO: add revised readsensor (one-liner)
In situations where we can’t imbue our enumerated types with methods, we can still achieve the same effect by creating a wrapper hierarchy around the sensor package that has polymorphic behaviour. <explain code> This solution adds more boilerplate still, but we get most of the benefits:1) elimination of all but one switch2) consolidation of the different behaviours into one place3) compiler errors if new behaviours are requiredMight not look great right now, but when you need to add new types or behaviours, it'll pay off
I’m sure we’ve all written code like this at some point, and it's kind of embarassing. Very clumsy, just go through every condition to get an answer. This sort of code makes it hard to test, hard to maintain, hard to reason about; especially if later checks depend on earlier ones being false.There’s no magic bullet to refactor stuff like this. Could use a map from province to tax to replace some of it. Either way you should try to avoid writing if statements that invite this kind of behaviour.
A good start would be “replace conditional with map”. This refactoring replaces the conditions with a map key, and the assignment with a value from a map. Most of the if statement is eliminated.You could take this further by moving some logic into the enums if appropriate, or splitting up the tax into pst and gst, or extending the map to work on pairs of province and product.
If you think I’m being extreme, there’s an entire campaign organized around the elimination of conditional statements altogether.
Some further arguments against conditionals:1) Adding conditional statements can exponentially increase the number of paths through code. (e.g. 8 independent if statements results in 256 unique paths!) One measure of code paths is called cyclomatic complexity. This is a well-known issue and there are tools you can use to enforce per-method limits on cyclomatic complexity, and you should set the limit as low as is bearable.2) If-else chains invite people to simply add additional cases to meet needs.3) If you are switching on enum values or various subtypes of something, it’s a missed opportunity to use polymorphism, and we’ve already seen how that works4) Writing tests against code with lots of conditionals is tedious if you want 100% coverage, and the tests often end up full of duplication.So try some of the previous techniques and refactorings before adding if statements to your programs. I’ll talk more about this in the next section…
Next I’d like to talk about error handling.
Here’s some beginneradvice: make sure your programs can handle all of the errors thrown at it. This code might be a bit exaggerated, but it’s common.
Stop doing this! This code is a sprawling, confusing mess at least compared to what it could be. Null checks everywhere and all possible exceptions are caught. Code like this is very common, and people seem to write it without thinking. Yet you might even be proud because you "caught all the errors", good job. Unfortunately, there are some problems with coding this way:Most of the code is fluff, not related to the work we are trying to do. We justify this by saying it is necessary, but I'll show you why not in a bit.2) If not finding the image was actually an error case, best case the code doesn’t do anything, and worst case, the error causes an exception later on, but now the original cause has been obfuscated by our misguided attempt to handle all errors.3) Our methods tend up being responsible for multiple things: their primary task, and handling errors. This mixing of responsibilitiesmakes it hard to figure out what code is doing.4) Sometimes duplicate action may be taken. Countless times I’ve seen exceptions double-logged, which can be confusing.
Error checking should exist at two levels in the system:First, errors and exceptions should be initially detected at the lowest possible level, only if necessary, and then thrown and unwind through the entire app. Most of the app shouldn’t even care about errors at all. If third party code throws an exception, just let it go. Don’t bother catching it. Most of the time we can’t or shouldn’t attempt to recover.Second, at the top-most level where we can centralize error handling", all exceptions should be caught and logged. Top-level depends on type of app, e.g.1) webapp, this might be in the initial request handler, often frameworks provide a hook for dealing with exceptions2) command-line apps, this will likely be in or close to the main method3) GUI app, this will be in the event dispatcher. The common theme is: decide what to do with exceptions at a single point, and avoid duplication and clutter.So how do we implement this architecture?
Checked exceptions are dumb. Why? The language is forcing you to either handle errors immediately even if it doesn’t make any sense, or to clutter up your method declarations with lists of checked exceptions that may be thrown.If your language has checked exceptions, start by writing wrappers against APIs that throw them to pass the results through and wrap the exception in an unchecked variety.For example, let’s wrap the ImageIO.read and make the exception unchecked. You could even create some new exception types if you want to (e.g. UncheckedIOException)
Here, we’ve made use of our wrapper to simplify our original image reading code. The behaviour did change, as now we’re passing the exception off to be handled elsewhere. We don’t care anymore. The decision about how to handle the error is no longer at this level.We’ve still got that pesky map lookup to deal with, so let’s fix that next.
We add a new exception type to represent missing image keys, then create a method to try and read the key from the map, and fail with an exception.This gets back to what I was saying earlier about null checks: don’t even let this method return null and it will simplify all of the calling code. When you can’t recover, exceptions are superior to returning error values.
Our original code has been reduced to just the important stuff and all of the details of error handling have been pushed down to lower levels of abstraction. The exceptions can now be caught and handled at a higher level.
So what’s the point?Maybe you’ll leave this talk thinking “wow I’m never going to use conditionals or nulls ever again!” Well, no, that’s actually the opposite of what I want. I may have used some strong language, but you shouldn’t always do what I say, for the exact same reasons you shouldn’t always follow existing dogma. Instead, if you just pause and think before writing a switch statement or adding a long-winded comment to some code, that’s great.Maybe you disagree with me, and that’s good too! If you disagree, at the very least you understood what I was saying, spent an hour thinking about your craft and came to your own conclusions. Hopefully the techniques and refactorings I’ve demonstrated will help you to improve your code, but if not, hopefully I’ve got you started down the path of finding ones that will.
Or maybe the best I can hope for is that you heed the advice given by this user of StackExchange: <quote>Best practices, dogma, design patterns or what have you are not a substitute for careful thought. Instead of getting hung up on particular techniques or idioms, just be do whatever makes sense, and then be consistent.
The goal with most of this talk is to encourage you to write code that is readable by people first. This isn’t easy, so here are some handy books.- clean code – PDF available free online, paper copy available.- pragmatic programmer – ebook or paper, a classic full of great principles and practices- code complete – exhaustive, covers everything. E.g. entire chapter devoted to variable names. Would be hard pressed to read it front to back but it's a good addition to the bookshelf.