General Programming Rubric
CSC 161 - Imperative Problem Solving and Data Structures - Weinman
- Summary:
- Information on programming and testing considerations
used for grading.
Overview
This course features regular programming activities, as listed on
the course schedule. While each exercise will have its own rubric
based on the elements of the assignment, all programming-based activities
will feature several rubric elements in common. Each of these items
are measured on the following scale:
- Excellent (Always)
- Four points; A. Demonstrates the criterion
either without fail or a minor indiscretion; a complete or superlative
exemplar.
- Good (Usually)
- Three points; B. Misses one notable or two
lesser instances of the criterion yet still demonstrates sound grasp
of the practice.
- Satisfactory (Sometimes)
- Two points; C. Features at least
two positive demonstrations of the criterion or misses several important
opportunities.
- Passing (Rarely)
- One point; D. May have token, incomplete,
or incorrect demonstrations of the criterion.
- Failing (Never)
- Zero points; F. Completely overlooks the
criterion.
Some of the criteria below may seem artificial and superfluous in
a context where you are writing relatively small programs that an
instructor has largely specified for you. You might even sensibly
argue that, in following the composition principle of "writing for
the reader," it is is petty or excessive to deduct for omissions
that can easily be understood by the instructor. However, that argument
overlooks the larger purpose, which is to develop good habits that
translate to practice in systems of much larger scale. The importance
of these elements increases exponentially with program complexity,
and we hope to enhance your ability to "write for the reader"
in that context as well.
In summary, these metrics represent important skills in the broader
world of software development and maintenance. We measure them to
help ensure you learn their primary characteristics.
Contents
1 Structure
1.1 Program factored into appropriate functions
1.2 Functions have appropriate length and complexity
1.3 Clear function, parameter, and variable names used
1.4 Program is easy to read and understand
2 Comments
2.1 Functions begin with appropriate comments
2.2 Comments describe main computations
3 Design
3.1 Types are appropriate to the problem
3.2 Algorithms are appropriately efficient
4 I/O
4.1 Reminds user what input is required
4.2 Formats and labels output
5 Verification
5.1 Programs verify user input
5.2 Programs handle errors appropriately
6 Testing
6.1 Test plan enumerates the range of problem circumstances
6.2 Test cases include specific inputs and expected outcome(s)
6.3 Transcripts demonstrate functionality
1 Structure
1.1 Program factored into appropriate functions
Strunk and White emphasize that the paragraph is the unit of composition
in writing, "one paragraph to each topic" [SW]. Functions
are a basic unit of programs, so: one (abstracted) task to
each function.
Maintainable code is well designed. Good design means using appropriate
levels of abstraction that allow a decomposition into functions that
accomplish singular tasks comprehensibly. Moreover, functionality
factored into separate functions makes it easier to reuse, avoiding
redundant code. Because they are not repeated, bugs are easier to
fix. Because functions perform a singular task, they are easier to
unit test.
1.2 Functions have appropriate length and complexity
A corollary of factoring means your functions are not too long or
overly complex. One guideline is to make an entire function
to fit on one "screen" or less (about 50-65 lines). This scale
makes interpreting a function's action more readable by forcing you
to break it down into named steps that can be seen at once. This criterion
also promotes easier debugging and unit testing.
1.3 Clear function, parameter, and variable names used
Speaking of named steps, those names should be sensible to your reader.
We also remind you that you need to think beyond the scale of the
program due this week. Your reader might be a programmer from another
division, but it might also be yourself at a later time. Do not expect
to remember what SMRHSHSCRTCH [KP, p. 5] means more
than an hour after you write that down.
Like good writing, function names should indicate an action
"verb" to promote understanding. An accompanying noun "object"
in the name will also help.
In another analogy to writing, functions, parameters, or variables
operating in analogous roles should be named analogously, the programmer's
version of, "express co-ordinate ideas in similar form" [SW].
Clear parameter names are critical because they function as a bridge
between two different scopes of execution. Avoid single letters; parameter
names should suggest the role and semantics of each value entering
the function. As the system scale grows larger, other programmers
may only be familiar with a different corner of the system. Along
with documentation, the published interface (parameter names) of your
code will be key to understanding how to use it.
Generally variables are values local to a small function (cf 1.1
and 1.2). Yet these names too should clearly
indicate their corresponding values. While no code is "self documenting,"
good names go hand-in-hand with understanding. They are the programmer's
version of the writer's injunction to "avoid pronouns with unclear
antecedents."
Global names of functions and variables should be particularly
descriptive. Because they might occur anywhere in a program, their
names need to completely evoke their meaning, regardless of context.
Loop variables representing an array index are often named i;
that's not intrinsically bad. In fact, such conventional uses often
benefit from short names, where long names would adversely impact
brevity. However, nested loop variable names should indicate
more about the meaning of each index. For example, r and
c for rows and columns in a two-dimensional structure, t
for a thread index, etc. When the corresponding array has a clear
name, using mnemonic index variables (rather than i) can
help you avoid errors.
1.4 Program is easy to read and understand
This criterion may encompass many abstract properties, but we can
list a few concretely. Indentation, white space, and style consistency.
First, your program indentation should match the program flow.
While most modern programming languages (Python excluded) care not
a whit where your code appears because whitespace is irrelevant, the
compiler is not the primary audience for your program. Human
readers rely on indentation to help understand program flow with respect
to functions, conditionals, and loops. Fortunately, your text editor
will mostly do this for you automatically.
White space should help the human reader understand your programs
at the expression, block, function, and source code level. After all,
"white space is free", at least on screen. For grading, your programs
will be printed (on paper, where white space is decidedly not free),
so we take the comment with a grain of salt, admitting a measure of
truth. Help your reader understand your code, but do not add separation
beyond what is needed to aid clarity.
Practice a consistent style. When you write code from scratch,
you may wish to practice one of the many existing "standard" coding
styles. This course will not dictate a specific style, though your
employers or open source projects may demand adherence to a particular
style. When you are adding to or modifying an existing piece
of software, match its style.
Finally, Kernighan and Pike [KP, pp. 6-8] give us a few additional
style injunctions
- Use the natural form for boolean expressions. ("Conditional
expressions that include negations are always hard to understand"
[KP, p. 6])
- Parenthesize to resolve ambiguity
- Break up complex expressions
- Be clear ("the goal is to write clear code, not clever
code" [KP, p. 6])
- Be careful with side effects
We add one further concrete style guideline: Line must be
no wider than 80 characters. This makes them easier to read both
on screen and on paper. (Note: If running enscript on your
program code produces the output "X lines
were wrapped," where X is some number, then your
lines are wider than 80 characters and you should re-format your code
with additional linebreaks.
2 Comments
2.1 Functions begin with appropriate comments
Documenting before you write a procedure will help you plan and clarify
the requirements of your implementation; it may even help you assess
whether you need to refactor 1.1. For each
function, documentation must include a sentence describing
the purpose and contextualizing the purpose of the program unit in
English. Furthermore, documentation must give preconditions
and postconditions of each function.
2.2 Comments describe main computations
Kernighan and Pike [KP] remind us:
Comments are meant to help the reader of a program. They do not help
by saying things the code already plainly says, or by contradicting
the code, or by distracting the reader with elaborate typographical
displays. The best comments aid the understanding of a program
by briefly pointing out salient details or by providng a larger-scale
view of the proceedings. [emphasis added][KP, p. 23]
They also give us the following injunctions:
- Don't belabor the obvious ("Comments should add something
that is not immediately evident from the code, or collect into one
place information that is spread though the source" [KP, p. 23])
- Don't comment bad code, rewrite it ("When the comment outweighs
the code, the code probably needs fixing" [KP, p. 25])
- Don't contradict the code ("When you change code, make
sure the comments are still accurate" [KP, p. 25])
For details, see the following exemplars for a program to compute
mortgage payments:
3 Design
3.1 Types are appropriate to the problem
Data types chosen should model the characteristics of the problem.
For instance, a float variable should not store the count
of a number of students in a class, because that count should always
be a whole number, better represented by an int, or even
an unsigned int. However, representing the average
number of students in a class might indeed use a float appropriately,
as the fractional precision is meaningful and useful.
Appropriate type choices extend to richer data structures as well.
For instance, using a list type in Scheme to do random access
lookup in a fixed-length collection wastes significant time in cdring
to satisfy list-ref. Instead, a vector should be
used because it supports fast lookup with vector-ref. However,
if the entire collection needs to be traversed and a resulting collection
has unknown size, a list would be preferred, since traversal
costs are the same and the result can be easily built dynamically.
3.2 Algorithms are appropriately efficient
Program and algorithm design should reflect a balance between the
computational complexity intrinsically required by the problem and
code simplicity promoting easy maintainability.
- When two designs have different computational complexities (say linear
versus quadratic), use the more efficient approach.
- When two designs have similar efficiency, use the simpler and easier
to understand approach.
4 I/O
4.1 Reminds user what input is required
It may be obvious while you are writing and developing a program what
you are to type when running the executable, most users do not share
this advantage. When multiple inputs are required, even the developer
may forget the order in which inputs are expected. Thus it is imperative
to provide clear prompts for any user input.
4.2 Formats and labels output
To the extend required by the program, any output should be clearly
labeled and formatted for easy readability. Under varying circumstances,
programs should:
- Use a reasonable level of precision for numerical values
(that is, print only the decimal places warranted by the problem)
- Organize related numbers into columns
- Indicate what each numerical or textual output represents
5 Verification
5.1 Programs verify user input
When functions verify their preconditions, they can immediately isolate
the detected violation, making debugging much easier. While not all
functions need to do so, programs that receive input from users are
less likely to have their input carefully crafted or guarded. Thus,
you must enable helpful feedback to a user in the form of
input verification. The most common forms of input verification include
type (i.e., for numbers given as command line parameter character
arrays) and ranges.
5.2 Programs handle errors appropriately
Good design accommodates failures. When things go wrong, it is important
to deliver the unfortunate news to the right audience. When
you write library code (code linked to other programs), the correct
approach is usually an appropriate return code. After all, your client
program likely wants to decide how to deal with your failure, rather
than have your library print output to the user that is likely to
befuddle them.
When writing programs directly for the end user, the correct
approach to an unrecoverable error is usually to print an informative
error message to the unbuffered stderr
file stream. When verifying user input (cf. 5.1),
the error message should state the valid form of input [KP, p. 114].
6 Testing
6.1 Test plan enumerates the range of problem circumstances
Enumerating the full range of circumstances that can arise in a problem
helps the programmer ensure they have throught through important contingencies,
which should result in a more reliable program. Thus your
test plan should list the range of states or inputs pertinent to the
problem, including invalid though possible values (e.g., a negative
number for a double-valued area).
6.2 Test cases include specific inputs and expected outcome(s)
Test cases spanning the input space help convince us of program correctness.
Thus, in covering the range of circumstances (cf. 6.1)
your test plan must explicitly list test cases with specific
input values and the expected result(s) or outcome(s). In particular,
borderline cases and extreme values should be tested.
In addition, test cases should span a complete range of possible
(valid) inputs, and enough invalid inputs to confirm error handling (cf. 5.2).
Occasionally, additional commands (e.g., cat, ls,
diff, etc.) may be needed to verify correct behavior.
6.3 Transcripts demonstrate functionality
Every program you submit must be accompanied by a transcript
documenting the program's compilation and execution. Many
large scale projects feature automated builds and tests as built-in
progress checks. In the absence of these automated tools, we still
want to promote the idea that the code you submit has demonstrated
behaviors. Moreover, these demonstrations help facilitate expedited
grading. (Though you are asked to submit your source code in compilable
form to enable manual verification.)
For non-robot problems, the transcript must record the execution
of the test cases (cf. 6.2).
Acknowledgments
In broad strokes, the contents of this rubric have been influenced
by many sources. Many of the criteria are adapted from an earlier
version of Henry Walker's Program
Style Summary and Checklist and used under a CC-BY-NC-SA 4.0 license.
Marge Coahran influenced some of the style considerations and John
Stone suggested expressing the learning objective as forming habits.
Although cited when possible, many uncited ideas no doubt have been
influenced by Kernighan and Pike [KP]. The mortgage programs
and descriptions are from Henry Walker's Detailed
Coursework Instructions and used under a CC-BY-NC-SA 4.0 license.
References
- [KP]
- Brian W. Kernighan and Rob Pike. The Practice
of Programming. Addison-Wesley, 1999.
- [SW]
- William Strunk, Jr. and E.B. White. The Elements
of Style. 1959.
Copyright © 2014, 2015 Jerod
Weinman. This work is licensed under a Creative
Commons Attribution-Noncommercial-Share Alike 4.0 International License.