Skip to content
Newer
Older
100644 359 lines (302 sloc) 17.1 KB
a790e5e @steveklabnik Adding 'Single Text, many masters.'
authored May 21, 2011
1 ---
2 published: true
3 title: Single text, many masters
4 layout: post
5 ---
6
7 Hey everyone. Here's a draft of an essay I've been working on. I'd love
8 to hear your feedback.
9
10 The word 'engineering' has a deep connection to the word 'trade-offs' in
11 my mind. Most engineering decisions come down to evaluating a few
12 differing alternatives, and often multiple factors end up being
13 negatively correlated. You can make something stronger, but then it will
14 be heavier. It can be made faster, but then it's significantly more
15 expensive. A good engineer is able to take all of these factors into
16 account, and design a system such that it maximizes its effectiveness
17 across the sum of all of the relevant constraints. No matter if you consider
18 the act of writing software an art, science, or engineering, its indisputable
19 that designing complex software systems is identical in this respect. There are
20 dozens of different metrics that system architects take into consideration while
21 crafting a plan of attack, but but there's a deeper question of balance
22 here that's significantly different than these more traditional
23 engineering issues.
24
25 Text, in the form of source code, presents unique challenges of
26 composition. These difficulties all stem from the same root: source code is a
27 singular text, but must be intelligible to multiple, simultaneous
28 audiences. More traditional forms of authorship still take audience into
29 consideration, of course, but the decision to engage a diverse group of
30 people is the choice of the writer. While mass appeal may be
31 something that certain authors strive to attain, it's not an inherent
32 property of their chosen form of expression. Source code, while text,
33 inhabits a multiplicity of forms, and software developers are confronted
34 with this inherent multi-faceted complexity when composing any
35 particular software work. Some of these forms suit certain audiences
36 better than others, and so it falls to the programmer to manage which
37 form they are currently working in, consider which audience they are
38 attempting to write for, and arrange all of these forms amongst one
39 another in such a way that any particular audience is able to navigate
40 and extract the proper information from the source without confusion.
41
42 In this post, I'll expand on the concept of audiences for code, and in a
43 future post, I'll explore the simultaneous forms that code takes.
44
45 ## The multitude of audiences
46
47 ### The default audience: the computer
48
49 > Science is what we understand well enough to explain to a computer. Art
50 is everything else we do.
51 >
52 > - Don Knuth
53
54 This may seem obvious, but the when considering the question of "Who are
55 programs written for?", many would say "The computer. Duh." In many
56 ways, a computer is the primary reader of a particular piece of source
57 code. The code that's given to a computer will be executed billions of
58 times per second, repeated, recalculated, and re-interpreted over and
59 over and over again.
60
61 The computer's native understanding of software comes from machine
62 code. Machine code are the binary numbers that the CPU loads into memory
63 and processes directly. For example, here's a line of machine code for
64 an x86 machine that puts the value '97' into the AL register, graciously
65 stolen [from
66 Wikipedia](http://en.wikipedia.org/wiki/Assembly_language#Assembly_language):
67
68 10110000 01100001
69
70 While most would consider this unintelligible, computers were actually
71 programmed this way at one time. My uncle actually did this by flipping
72 switches to set the binary and pushed a button to store it in memory.
73 Unfortunately, what's good for the computer isn't good for the human
74 programmer. This is why assembly language was created. Assembly language
75 has a 1 to 1 mapping to machine code, but is much easier for humans to
76 understand. Here's that same line in assembly:
77
78 MOV AL, 61h ; Load AL with 97 decimal (61 hex)
79
80 The `MOV` corresponds with `10110`, `AL` maps to `000`, and 61 hex is
81 `01100001`. `MOV` is short for 'move,' though, and this mnemonic is just
82 a bit easier to understand than `10110`. This is the most basic example
83 of a compositional trade-off. It's not a true trade-off, because they
84 map perfectly to one another, but it illustrates the difference between
85 composing in a language that the computer understands and one that's
86 more natural for the programmer. Another important concept comes into
87 play, that of _compilation_. Virtually every work of composition created
88 in software is automatically translated to another form before it is
89 executed. We'll address this concept more fully when we discuss form
90 later.
91
92 If that's where assembly stopped, it would remain a 1 to 1 mapping.
93 However, virtually every assembly language also offers macros, and this
94 moves the code further away from the machine and destroys the
95 synchrony of the two forms. Here's an example:
96
97 MOV EAX, [EBX]
98
99 The `[]` characters change the meaning of `EBX`, rather than be the
100 value stored in that particular register, they imply that the value is a
101 memory address, and we want to move the contents of that address to
102 `EAX`. The generated machine code could now be processed into multiple
103 valid assembly forms, and so the transformation is only perfect in one
104 direction, even if it's possible to 'decompile' it into one of those
105 possible encodings. This is considered to be an acceptable trade-off for
106 human readability; we very rarely want to turn machine code back into
107 assembly.
108
109 There's also a jump between higher level languages, as well. Here's the
110 assembly statements that adds 2 and 3 together:
111
112 MOV EAX, 2
113 ADD EAX, 3
114
115 `ADD`, of course, is the statement that adds a number to the register
116 that's given. Now `EAX` has the value 5. Here's the same code, but in C:
117
118 int x = 2;
119 x = x + 3;
120
121 Pretty simple. You can see how the C is much easier to understand; we
122 say what type `x` is (an integer), and it's a bit more explicit. `x` is
123 equal to `x` + three. However, since the C is divorced from the machine,
124 and is written for the person, we can change our compiler to make
125 assembly code that works on a different kind of processor architecture.
126 If we were to compile the above C for x86\_64, a 64 bit version of x86,
127 we might get some assembly that'd look like this:
128
129 MOVL RAX, 2
130 ADDL RAX, 3
131
132 While this code looks similar to the above, it is quite different. This
133 uses the native 64 bit types, rather than the 32 bit types above. The
134 other important thing is that by writing code that's divorced from the
135 machine, and written for people, we're able to translate it into the
136 code for multiple machines. If we had written the assembly above, when
137 moving to another architecture, it would have required a total re-write.
138 And while this particular sample looks very similar, a more complex
139 piece of code will be significantly divergent, but I don't want to go
140 into the details of two kinds of assembly code. Because we can define
141 the languages for humans, and the language of computers is somewhat
142 beholden to the physical machine itself, it's significantly easier to do
143 the translation from C to the two kinds of machines, rather than trying
144 to translate from one machine to another. What we lose in this kind of
145 translation, though, is efficiency. Code that was hand-crafted for each
146 machine would be more performant, and better represent each individual
147 platform.
148
149 Even though we may choose to use a language that's more understandable
150 to people, it still has to be understood by the computer in some form.
151 This translation will introduce some amount of penalty, and so it's
152 important that this gets taken into consideration. Sometimes, code must
153 be written in a way that's not easy for a person to read, because it's
154 easier for the computer to be efficient with a more opaque
155 implementation.
156
157 ### The reflexive audience: the programmer himself
158
159 > Debugging is twice as hard as writing the code in the first place.
160 Therefore, if you write the code as cleverly as possible, you are, by
161 definition, not smart enough to debug it.
162 >
163 > - Brian Kernighan
164
165 Everyone who writes code has experienced this at some time or another.
166 You write a whole bunch of code, and then a few months goes by, and you
167 take a look at it, and it's absolutely unintelligible. This happens
168 because at the time of inception, the author of a particular piece of
169 code has an incredibly close relationship to it. As it was just written,
170 the code is obvious to the author. They're in the proper mindset to
171 understand the intention that was drawn upon to necessitate bringing
172 those lines into the world, and so no extra explanation is necessary. As
173 time goes on, however, the author becomes more close to the third
174 audience, other programmers. It's important for coders to recognize this
175 fact, and take preventative steps to ameliorate this confusion.
176
177 Even though the author will approach the position of the other audience
178 eventually, this audience is distinct because there is a certain level
179 of explanation that sits between undocumented, inexpressive code and
180 code that's well explained, and this is the position most code is in. An
181 explanation that's helpful to those who understand the code, but not to
182 those who don't is better than nothing. This sort of code may be overly
183 contextual, and could use some added information to improve its clarity.
184
185 ### The other audience: colleagues and coworkers
186
187 > Always code as if the guy who ends up maintaining your code is a violent
188 psychopath who knows where you live.
189 >
190 > - Martin Golding
191
192 As I touched on earlier, there's a similarity between the 'other'
193 audience and the reflexive. The primary distinction is drawn around the
194 proximity to the source. The other does not have the advantage of having
195 authored the code, and therefore doesn't have the native's understanding
196 of the underlying logical organization of the source. This disadvantage
197 can be overcome via composing in such a manner that the meaning is
198 emergent from the design. Even if it's too complex to be obvious, good
199 documentation can address this particular deficiency.
200
201 Ultimately, much of software design is about modeling. Software that
202 solves a particular problem should emulate the nature of the challenge
203 it's attempting to address. If this can be achieved, it's significantly
204 easier for those who understand the problem to figure out how the
205 software works. Therefore, good design can help improve the
206 effectiveness of a given piece of source to communicate its intent.
207 Along a similar vector, if the design is similar to code that solves a
208 particular issue, it's easier to understand. As an example, a friend
209 recently asked for feedback about an interface that he'd designed. It
210 loaded a save game file for StarCraft 2. This is what he came up with:
211
212 replay_file = File.new("spec/fixtures/1v1-game1.sc2replay")
213 @replay = SC2Refinery::Parser.parse(replay_file)
214
215 However, Ruby already has several kinds of code in its standard library
216 that loads some information from disk and parses it into some kind of
217 data structure that you can use in your program. The JSON, YAML, and
218 Marshall classes already use a set of methods to import and export data,
219 and they're named `load` and `dump`, and they're part of the class
220 directly. Also, in this case, the user of the code shouldn't need to
221 deal with the creation of a file, since it's unreasonable to assume that
222 a game replay would come from any other source. Therefore, after some
223 discussion, he adopted the following interface instead:
224
225 @replay = SC2Refinery.load("spec/fixtures/1v1-game1.sc2replay")
226
227 This is much nicer to use, and is much simpler. While it may not seem
228 like a whole lot, when rules like this are applied across an entire
229 codebase, they can significantly increase understanding. Multiple
230 reductions of mental overhead add up quickly.
231
232 My new favorite trick for adding a little bit of modeling that
233 significantly reduces overhead for the user is the Presenter Pattern.
234 Jeff Casimir demonstrated this very clearly in his presentation at
235 RailsConf 2011, "[Fat Models Aren't
236 Enough](http://dl.dropbox.com/u/69001/Fat%20Models%20Aren%27t%20Enough%20-%20RailsConf.pdf)".
237 Here's a slightly modified example. Imagine that we have a system that
238 manages students, and we'd like to display a report card for them. We
239 might start with some code that looks like this:
240
241 student = Student.find(options[:student_id])
242 term = Term.find(options[:term_id])
243 report_type = ReportType.find(options[:report_type])
244
245 puts "#{student.last_name}, #{student.first_name}"
246 puts "#{report_type.report_title} for #{term.start_date} to #{term.end_date}"
247 student.courses.each do |course|
248 course_report = student.report_data_for_course(course)
249 puts course_report.to_s
250 end
251
252 Honestly, until this past week, this is the exact code that I would have
253 written. But it turns out that we can do better. Basically, we're
254 displaying some information that comes from a combination of three
255 different objects. If we think about it some more, we're really trying
256 to display a report card. So let's make an object that represents the
257 card, and delegates to its sub-objects. It will then know how to display
258 itself.
259
260 class ReportCard
261 delegate :start_date, :end_date, :to => :term
262 delegate :first_name, :last_name, :courses, :report_data_for_course, :to => :student
263 delegate :report_title, :to => :report_type
264
265 def initialize(params)
266 @student = Student.find params[:student_id]
267 @term = Term.find params[:term_id]
268 @report_type = ReportType.find params[:report_type_id]
269 end
270
271 def student_name
272 [last_name, first_name].join(", ")
273 end
274
275 def title
276 "#{report_title} for #{start_date} to #{end_date}"
277 end
278
279 def course_reports
280 out = ""
281 courses.each do |course|
282 out += report_data_for_course(course)
283 end
284 out
285 end
286 end
287
288 Now, this is a lot of code. However, as you can see, it's all focused on
289 composing the information and exposing an interface that makes sense for
290 a report card. Using it is super easy:
291
292 report = ReportCard.new(options)
293 puts report.student_name
294 puts report.title
295 puts report.course_reports
296
297 Bam! It's incredibly obvious. This code is much more clear than before.
298 We'll see if I'm still as hot on this pattern as I am now in a few
299 months, but I feel the extra object adds a significant amount of
300 clarity.
301
302 If the model is too hard to create, or if additional clarity is needed,
303 documentation in the form of comments can also help to improve the
304 understanding of the 'other' audience. Comments can be a difficult form
305 of prose to write, because they need to be written at the correct level
306 of abstraction. If they simply repeat what the code does, they're
307 useless, and if they're too high-level, certain details and semantics
308 may not be made clear.
309
310 Individual bits of code can also be made more clear by developing a
311 narrative within any particular method that's being written. Telling a
312 story with code may not be something you've considered before, but it's
313 really about maintaining a proper flow in the actions your code is
314 taking. For example, if there's a bunch of error handling strewn about
315 inside of a method, it's less clear than bunching all of the error
316 handling near the end. Most code should be an act in three parts: input,
317 processing, and output. If these three parts are mixed together, it can
318 appear much more complicated.
319
320 ### The forgotten audience: end-users
321
322 > If I asked my customers what they wanted, they'd have told me, "A
323 > faster horse."
324 >
325 > - Henry Ford
326
327 In the end, all software is used by someone. Use-value is the driving
328 force of virtually all code. Code that doesn't do anything may be making
329 some kind of important philosophical statement, but isn't really the
330 sort that I'm talking about.
331
332 The introduction of a user imposes significant restrictions upon the way
333 that code is composed. End-users do not need to understand the code
334 itself, but they do need to be able to understand its external
335 interfaces. These needs place an imposition on the way that the code
336 needs to be written, because it _must_ address this issue of interface.
337 Sometimes, interface requirements can create a burden on the internal
338 implementation. Needing to support certain behaviors and forms can
339 create complexity for an implementor.
340
341 Documentation created for end users must be completely different than
342 that which is created for those inspecting the code itself. Most
343 end-users will not be literate in the arts of software development, and
344 so approach the software object in an entirely different way than those
345 who write code do. Yet, the same semantics must be passed on to them,
346 but at a higher level. There's a strong movement within the community to
347 start designing software with this kind of end-user documentation in
348 mind, called [README driven development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html).
349 There are advantages to thinking on this level when beginning, but a
350 nice effect of doing it first is that you can ensure it gets done. A
351 surprising amount of software has poor documentation for its users,
352 because it's created after the software is finished, and at that time
353 there's intense pressure to ship it out the door. Writing down
354 information for the end user first ensures that it's done properly, that
355 all development works in accordance with the documentation, and that all
356 of the use-cases for an end-user have been thought of and are being
357 addressed.
358
Something went wrong with that request. Please try again.