Values versus entities.
Pure functions.
Error handling.
Testing.
TypeScript Compiler
Node Package Manager npm.
Entity objects have identity and changing state, value objects do not.
Typically value objects are immutable.
Examples:
An employee is an entity. Typically having multiple simultaneous instances of the same employee would be a problem.
Money would be a value.
A specific dollar bill would be an entity having a Money value.
A pure function only computes a result which depends only on its parameters.
It does not change any of its parameters.
It does not have any side-effects: no changes to non-local state; no I/O.
It cannot access any non-local variables.
Code which uses pure functions are referentially transparent: i.e. the same expression always results in the same result.
It will always be the case that assertEqual(f(x), f(x))
if f()
is pure.
Easy to understand as each function can be understood in isolation.
Possible to cache function results.
Easy to test as it is not necessary to mock I/O like databases.
A program without side-effects is pretty useless.
Does not fit in with OO, which is built around objects having mutable state.
Necessary to pass arguments via intermediate functions which have no interest in them.
Cannot update data structures: only build new versions. No writable arrays; need to use persistent data-structures.
z = z + 1
is destructive assignment; previous value of z
is
lost.
arr.push(22)
mutates arr
but arr.concat(22)
does not
mutate arr
.
arr.sort()
mutates arr
Reasoning about programs with mutable state is difficult.
Try to minimize use of mutability. Specifically, avoid mutation visible outside a function so that function remains pure even though it uses mutation internally.
Try to keep core of an application pure, keep impure code restricted to the edges of the application.
I restrict mutation to local variables within a function, unless I
am programming in an OO-style when I may mutate this
.
Sometimes it is necessary to access global information like configuration information all over the program. Often dealt with by passing some kind of read-only context parameter between functions. However, can also use a non-local access as long as the access is read-only; think of it as an implicit parameter.
Pure functional languages like Haskell support patterns to make this easier.
When code encounters an error, it outputs error message directly. Totally unacceptable as all code becomes I/O dependent.
Report errors by throwing an exception. Violates principal of reserving exceptions for exceptional situations. Makes an otherwise pure function impure.
Return errors by side-effecting arguments; resulting function impure.
Report errors with some kind of special error return value.
Combine error and success return values and provide a way of easily continuing the happy path, with short-circuit on errors. Again, languages like Haskell provide this.
Distinguish between panics which terminate the program or request and recoverable errors.
A panic error corresponds to an unexpected condition usually indicative of a program bug.
For example, if a value is null
when it is not supposed
to be so, then that is a program bug. It is usually best
to terminate the program (or possibly a request).
Other errors may be recoverable. So for example, running out of memory may be a recoverable error if the programmer can control the code sufficiently to cause memory deallocation.
A database or network error may disappear when retried.
User errors are definitely not panics, they should simply be reported back to the user.
In errors.ts: Working TS Playground code.
Have processing functions return result
having type Result<T>
having one of two properties:
If result.isOk
is true, then result.val: T
provides
success result.
If result.isOk
is false, then result.errors
provides an array of errors.
Create a success Result<T>
using okResult(val: T)
and an error
Result<T>
using
errResult(message: string, code?: string, widget?: string); errResult(message: string, options: ErrOptions); errResult(error: Err); errResult(error: Error, options?: ErrOptions); errResult(error: object, options?: ErrOptions);
Result
provides a chain(fn)
method to allow chaining
processing functions. fn
should be set up to take the previous
success value (plus additional parameters if necessary) and return
a Result
.
if result
is a Result
and result.errors
is present,
then result.chain(fn) === result
; otherwise result.chain(fn) ===
fn(result.val)
which should be another Result
allowing further
chaining.
Allows chaining processing functions, with short-circuit on error:
return ( okResult(val) //success Result wrapping val .chain(fn1, arg1, arg2) //returns Result .chain(fn2) //returns Result .chain(fn3, arg3) );
IDs are used to identify entities.
IDs should be unique for a particular namespace of entities.
If the entities need to be secure, then the IDs should be hard to guess.
Usually, IDs should be opaque strings; opaque means that users of the IDs should not care about the structure of the strings.
If efficiency is particularly important, they may be integers (this may be the case for applications like games or when the implementation language is a language like C with poor support for strings). See Twitter's discussion of IDs. The downside of using integers are embarrassing episodes if the integers IDs overflow.
It is usually a bad idea to expose database IDs as external IDs; doing so makes it difficult to migrate to a different database instance or use a different database system.
A typical scheme to generate unique and secure IDs:
An ID is generated as a string of the form
`${counter}_${rand}`
.
counter
is a counter which is incremented whenever creating a
new ID. It follows that counter
must be stored across the
lifetime of the system. The counter
portion of the ID
guarantees that IDs are unique.
rand
is a random integer generated using a secure random number
generator. The rand
portion of the ID makes IDs hard to guess
and secure.
So IDs may look like "0_31324204"
or "42_04394134"
.
During development, it may be useful to restrict the random part
of the ID to say 2 digits, resulting in IDs like "0_31"
or
"42_04"
.
The previous scheme requires persisting the counter
. An alternate
scheme for generating ID's is to use a Universally Unique Identifier (UUID).
UUIDs are 128-bit numbers: 16 octets written in groups 8-4-4-4-12;
Example 12345678-1abc-def0-a234-56789abcdef0
.
Most languages will come with a library function for generating UUIDs.
Uniqueness guaranteed by using UUID v1. However, it may be guessable, hence not good for security.
If generated IDs must be secure, then use UUID v4; tiny chance \(2^{122}\) of non-uniqueness.
Automated tests are an essential aspect of agile development as it provides confidence to change code in order to respond rapidly to changing requirements.
Distinguish faults (bugs, defects) from failures which are manifestations of faults.
Program testing can be used to show the presence of bugs, but never to show their absence!: Quote from Edsger W. Dijkstra.
Automated testing has been around for ever as regression testing, current resurgence emerged with junit.
There may be more testing code than code in the system under test!!
Test isolated units of code.
Test interfaces between multiple units of code.
Test complete usage scenaries for overall system.
Tests for non-functional aspects like performance tests, stress tests, security tests.
Develop code first, then write tests: Can work, but often gets difficult to make code observable enough to test thoroughly. Since tests are written later, they are often an afterthought and may get omitted under time pressure.
Test Driven Development TDD: Write tests first, then write code. Tests will initially fail since there is no implementing code. Often referred to as Red-Green testing.
Behavior Driven Development BDD: Describe tests in terms of desired system behavior often in business-oriented terms. Often higher-level than unit tests.
Tests must be isolated; i.e. each test should be independent of other tests.
A test collaborator is an external object required for testing the object under test.
A test fixture is used to set up environment for each test case.
A typical automated test case consists of the following three steps:
Test setup: set up the preconditions for the test case. This may require setting up an environment containing the test collaborators.
Perform test: execute the code being tested and validate the result.
Test teardown: cleanup after test, destroy all test collaborators.
Multiple test cases may often be grouped into test suites.
Setting up test collaborators can often be problematic, especially when a collaborator represents side-effects like I/O or database access.
Test doubles allow using synthetic collaborators instead of real collaborators. Types of test doubles include (terms often used inconsistently):
Provide fixed inputs to the test case. For example, a collaborator which reads from the user can be replaced by a stub which provides a fixed input.
Provide different outputs based on inputs. May be a restricted version of the actual collaborator.
Retain history of use. Can also be a fake.
Full replacement for collaborator.
There are many mocking frameworks available like mockito, jsmockito.
mocha is one of many testing frameworks. Another popular framework is jest.
A test suite consists of:
//suite describe("suite description", () => { beforeEach(() => { //set up fixture }); afterEach(() => { //tear down fixture }); it("test case description", () => { //set up and call code to be tested //correctness assertions for result }); ... it(..., () => { ... }); });
mocha
does not contain any assertion library.
chai provides a rich set of assertions.
Behavioral style assertions using should()
or expect()
allow chaining of assertions.
More traditional style (without chaining) using
assert()
.
Simple error handling example.
TypeScript compiler tsc compiles TypeScript to JavaScript. Governed by
tsconfig.json
or command-line options:
target
specified version of JS to be generated.
module
specifies module system to be used.
Other options allow selecting source files, destination folder/file.
Unlike strict JSON, tsconfig.json
allows comments and trailing
commas.
Example tsconfig.json for
cs544-js-utils
library.
Track dependencies of project transitively.
Handles multiple versions of same package.
Assume project requires v1 of packages A and B.
Package A requires v1 of package C.
Package B requires v2 of package C.
npm sets things up so as to have both v1 and v2 coexistent at runtime. This can occasionally cause trouble, but usually works seamlessly.
Distinguish different types of dependencies:
Runtime dependencies.
Development dependencies (use option -D
or --save-dev
).
Build-time dependencies like webpack
.
Testing dependencies like mocha
.
Config file package.json
is strict JSON: hence no comments
allowed!!
Sample package.json for
cs544-js-utils
library.
npm install PACKAGE
will install PACKAGE
and all its
dependencies as runtime dependencies in the node_modules
directory.
npm install -D PACKAGE
will install PACKAGE
and all its
dependencies as development dependencies in the node_modules
directory.
Above commands will create a package-lock.json
which specifies
the exact versions of the dependencies found.
npm install
will install all dependencies from package.json
.
Unfortunately, it does not
respect package-lock.json.
I use npm ci
to get exactly the same versions as
package-lock.json
.
return
with return-expression on the next line will
result in undefined
being returned. If inconvenient
to begin return-expression on same line as return
keyword, use:
return ( longExpr );
Problems in TS when using as
type assertion at the start of a line.
Something like
someLongExpression as number
results in a syntax error. Use something like:
someLongExpression as number
Never use ==
and !=
for checking equality. Surprising
type converions. Always use ===
and !==
; no conversions.
> null == undefined true > null === undefined false > '' == 0 true > '' === 0 false >
Need break
after case
to avoid fall-thru.
switch (type) { case 'number': x = 1; //need a break statement here case 'string'; x = 42; break; }
Integer literals starting with leading 0 are treated as octal:
> 010 8