eolas/neuron/322b6bbc-173e-49e6-8583-b6932f45b165/The_Pragmatic_Programmer_1999.md

360 lines
16 KiB
Markdown
Raw Normal View History

2024-10-19 11:00:03 +01:00
---
tags: []
---
# The Pragmatic Programmer (Hunt/Thomas, 1999)
## General
### Meyer's Uniform Access Principle
> All services offered by a module should be available through a uniform
> notation, which does not betray whether they are implemented through storage
> or through computation
This is a clear recommendation for using getters and setters with classes. You
should not see method calls outside of the class, they should appear as
properties of the object.
## Don't Repeat Yourself
> Every piece of knowledge must have a single, unambiguous, authoritative
> representation within a system
## The Principle of Orthogonality
This notion comes from geometry. Two lines are orthogonal to each other if they
form a right-angle when they meet.
Their meeting isn't the important part. Think of a simple x, y graph:
> If you move along one of the lines, **your position projected onto the other
> doesn't change**
In computing this is expressed in terms of **decoupling** and is implemented
through modular, component-based architectures. As much as possible code should
be scoped narrowly so that a change in one area does not cause changes in
others. By keeping components discrete it is easier to make changes, refactor,
improve and extend the codebase.
> We want to design components that are self-contained: independent and with a
> single, well-defined purpose. When components are isolated from one another,
> you know that you can change one without having to worry about the rest. As
> long as you don't change that component's external interfaces, you can be
> comfortable that you won't cause problems that ripple through the entire
> system.
### Benefits of orthogonality: productivity
- Changes are localised so development time and testing time are reduced
- Orthogonality promotes reuse: if components have specific, well-defined
responsibilities, they can be combined with new components in ways that were
not envisioned by their original implementors. The more loosely coupled your
systems, the easier they are to reconfigure and reengineer.
- Assume that one component does _M_ distinct things and another does _N_
things. If they are orthogonal and you combine them, the result does _M x N_
things. However if the two components are not orthogonal, there will be
overlap, and the result will do less. You get more functionality per unit
effort by combining orthogonal components.
### Benefits of orthogonality: reduced risk
- Diseased sections of code are isolated. If a module is sick, it is less likely
to spread the symptoms around the rest of the system.
- Overall the system is less fragile: make small changes to a particular area
and any problems you generate will be restricted to that area.
- Orthogonal systems are better tested because it is easier to run and design
discrete tests on modularised components.
> Building a unit test us itself a an interesting test of orthogonality: what
> does it take to build and link a unit test? Do you have to drag in a large
> percentage of the rest of the system just to get a test to compile or link?
> **If so, you've found a module that is not well decoupled from the rest of the
> system**
### Relationship between DRY and orthogonality
With DRY you're looking to minimize duplication within a system, whereas with
orthogonality, you reduce the interdependency among the system's components. If
you use the principle of orthogonality combined closely with the DRY principle,
you'll find that the systems you develop are more flexible, more understandable
and easier to debug, test, and maintain.
### Reversibility
The principles of orthogonality and DRY result in code that is reversible. This
means it is able to change in an agile way when the circumstances of its use and
deployment change. This is important because when developing software in a
business setting, the best decisions are not always made the first time around.
By following the principles it should be relatively easy to change your
program's interfaces, platform and scale. In other words, with the principle of
orthogonality and DRY, refactoring becomes less of a chore.
## Prototyping and Tracer Bullets
'Tracer bullets' are used by the military for night warfare. They are
phosphorous bullets that are included in the magazines of guns alongside normal
bullets. They are not intended to kill but instead light-up the surrounding area
making it easier to see the terrain and target more efficiently.
The authors use the notion of tracer bullets as a metaphor for developing
software at the early stages of a project. This is **not** the same thing as
prototyping. A tracer bullet model is useful for building things that haven't
been built before. They exist to 'shed light' on the project's needs and to help
the client understand what they want.
They differ from prototypes in that they include integrated overall
functionality but in a rough state. Whereas prototypes are more for singular,
specific subcomponents of the project. Because tracer bullet models are
joined-up in this way, even if they turn out to be inappropriate in some regard,
they can be adapted and developed into a better form, without losing the core
functionality.
> Tracer bullets work because they operate in the same environment and under the
> same constraints as the real bullets. They get to the target fast, so the
> gunner gets immediate feedback. And from a practical standpoint they are a
> relatively cheap solution. To get the same effect in code, we're looking for
> something that gets us from a requirement to some aspect of the final system
> quickly, visibly and repeatably.
> Tracer code is not disposable: you write it for keeps. It contains all the
> error-checking, structuring, documentation and self-checking that a piece of
> production code has. It simply is not fully functional. However, once you have
> made an end-to-end connection among the components of your system, you can
> check how close to the target you are, adjusting as necessary.
### Distinguishing from prototyping
> Prototyping generates disposable code. Tracer code is lean but complete, and
> forms part of the skeleton of the final system. Think of prototyping as the
> reconnaissance and intelligence gathering that takes place before a single
> tracer bullet is fired.
## Design by contract
To understand DBC we have to think of a computational process as involving two
stages: the call and the execution of the routine that happens in response to
the call (henceforth **caller** and **routine**).
- the caller could be a function expression that invokes a function and passes
arguments to it expecting a given output. The function that executes is the
routine
- the caller could be an object instantiation that calls a method belonging to
its parent class
- the caller could be a parent React component that passes props to a child
component
Design by contract means specifying clear and inviolable rules detailing what
must obtain at both the call stage and the routine stage if the process is to
execute.
Every function and method in a software system does something. Before it starts
that something, the routine may have some expectation of the state of the world
and it may be able to make a statement about the state of the world when it
concludes. These expectations are defined in terms of preconditions,
postconditions, and invariants. They form that basis of a **contract** between
the caller and the routine. Hence _design by contract\*\*._\*\*
### Preconditions
Preconditions specify what must be true in order for the routine to be called.
In other words, the requirements of the routine. What it needs and what should
be the case before it even considers executing the task. A **routine should
never get called when its preconditions would be violated**.
### Postconditions
Providing the preconditions are met, this is what the routine is guaranteed to
do. In other words: the state of affairs that must obtain after the routine has
ran.
### Invariants
Once established, the preconditions and postconditions should not change. If
they need to change, that is a separate process and contract. In the processing
of a routine, the data may be variant relative to the contract, but by the end
the overall conditions establish the equilibrium of the contract.
There is an analogue here with functional programming philosophy: the function
should always return the same sort of output, without ancillary processes
happening, i.e side-effects.
One way to achieve this is to be miserly when setting up the contract, which
overlaps with orthogonality. Only specify the minimum return on a contract
rather than multiple postconditions. This only increases the likelihood that the
contract will be breached at some point. If you need multiple postconditions,
spread them out an achieve them in a compositional way, with multiple separate
and modular processes.
> Be strict in what you will accept before you begin, and promise as little as
> possible in return. If your contract indicates that you'll accept anything and
> promise the world in return, then you've got a lot of code to write!
### Division of responsibilities
> If all the routine's preconditions are met by the caller, the routine shall
> guarantee that all postconditions and invariants will be true when it
> completes.
Note that the emphasis of responsibilities is on the caller.
Imagine that we have a function that returns the count of an array of integers.
It is not the job of the count routine to verify that it has been passed
integers and then to execute the count. Or, in the event that it is not passed
integers, to mutate the data to integers and then execute.
This should be resolved by the caller: it is the responsibility of the caller to
pass integers. If it doesn't, the routine simply crashes or raises an exception.
It doesn't try to accommodate the input because that does not come down on its
side of the contract. The caller has failed to meet the preconditions. If, due
to some bug, the routine receives integers and fails to output the sum, then it
has failed on its side
### Example: type checking
An obvious example of this philosophy is when you perform checks or validation
within your code (although validation is more of an issue when you are dealing
with user data, not your own internal code). For instance using type checking
with dynamically-typed languages.
When we use the `prop-types` library with React we are specifying preconditions:
so long at the prop (effectively the caller) passed to the component
(effectively the routine) is of type X, the component will render invariantly as
R. If the prop is of type Y, an exception will be raised highlighting a breach
in the contract.
Another example would be more advanced type checking with Javascript written
using Typescript.
## The Law of Demeter
Demeter's Law has applicability chiefly when programming with classes.
It's a fancy name for a simple principle summarised by 'don't talk to
strangers'. Demeter's law is violated when code has more than one step between
classes. You should avoid invoking methods of an object returned by another
method. You should only use your own methods when dealing with it.
### Formal
A method _m_ of object _O_ may only invoke the methods of the following kinds of
objects:
- _O_ itself
- _m_'s parameters
- any objects created or instantiated within _m_
- _O_'s direct component objects (in other words nested objects)
- a global variable (over and above _O_) accessible by _O_, within the scope of
_m_
## Model, View, Controller design pattern
The key concept behind the MVC idiom is separating the model from both the GUI
that represents it and the controls that manage the view.
- **Model**
- The abstract data model representing the target object
- The model has no direct knowledge of any views or controllers
- **View**
- A way to interpret the model. It subscribes to changes in the model and
logical events from the controller
- **Controller**
- A way to control the view and provide the model with new data. It publishes
events to both the model and the view
For comparison, distinguish React from MVC. In React data is unidirectional: the
JSX component as controller cannot change the state. The state is passed down to
the controller. Also MVC lends itself to separation of technologies: code used
to create the View is different from Code that manages Controller and data
Model. In React it's all one integrated system.
## Refactoring
> Rewriting, reworking, and re-architecting code is collectively known as
> refactoring
### When to refactor
- **Duplication**: you've discovered a violation of the DRY principle
- **Non-orthogonal design**: you've discovered some code or design that could be
made more orthogonal
- **Outdated knowledge**: your knowledge about the problem and you skills at
implementing a solution have changed since the code was first written. Update
and improve the code to reflect these changes
- **Performance: y**ou need to move functionality from one area of the system to
another to improve performance
### Tips when refactoring
- Don't try to refactor and add new functionality at the same time!
- Make sure you have good tests before you begin refactoring. Run the tests as
you refactor. That way you will know quickly if your changes have broken
anything
- Take short, deliberative steps. Refactoring often involves making many
localised changes that result in a larger-scale change.
## Testing
> Most developers hate testing. They tend to test-gently, subconsciously knowing
> where the code will break and avoiding the weak spots. Pragmatic Programmers
> are different. We are _driven_ to find our bugs _now_, so we don't have to
> endure the shame of others finding our bugs later.
### Unit testing
A unit test is code that exercises a module. It consists in testing each module
in isolation to verify its behaviour. Unit testing is the foundation of all
other forms of testing. If the parts don't work by themselves, they probably
won't work well together. All the modules you are using must pass their own unit
tests before you can proceed.
We can think of unit testing as **testing against contract** (detailed above).
We want to test that the module delivers the functionality it promises over a
wide range of test cases and boundary conditions.
Scope for unit testing should cover:
- Obviously, returning the expected value/outcome
- Ensuring that faulty arguments/ types are rejected and initiate error handling
(deliberately breaking your code to ensure it is handled appropriately)
- Pass in the boundary and maximum value
- Pass in values between the zero and the maximum expressible argument to cover
a range of cases
Benefits of unit testing include:
- It creates an example to other developers how to use all of the functionality
of a given module
- It is a means to build **regression tests** which can be used to validate any
future changes to the code. In other words, the future changes should pass the
older tests to prove they are consistent with the code base
### Integration testing
Integration testing shows that the major subsystems that make up the project
work and play well with each other.
Integration testing is really just an extension of the unit testing described,
only know you're testing how entire subsystems honour their contracts.
## Commenting your code
In general, comments should detail **why** something is done, its purpose and
its goal. The code already shows _how_ it's done, so commenting on this is
redundant, and violates the DRY principle.
> We like to see a simple module-level comment, comments for significant data
> and type declarations, and a brief class and per-method header describing how
> the function is used and anything it does that is not obvious
```js
/*
Find the highest value within a specified data range of samples
Parameter: aRange = range of dates to search for data
Parameter: aThreshold = minimum value to consider
Return: the value, or null if no value found that is greater than or equal to the threshold
*/
```