Let's talk about testing.
- Simple recursive-descent parser
- Abstract Syntax Tree (AST) with three node types (Binary, Unary, Atom)
- #evaluate is the only public API on Calc
- parses the input
- evaluates the AST
- It really works!
- take 1: FEELS like a decent test, but we're not testing parsing in isolation, and we CAN'T test parsing in isolation via the #evaluate method, because it's parsing AND evaluating the tree.
- take 2: this feels wrong, because it is. you should never test the private interface of an object. if you find yourself needing to call a private interface in a test, then you've designed something wrong (either your test, or your object).
- Added Parser class
- ! is for required rules (fail if nothing matches), ? is for optional rules (return nil if nothing matches)
- made methods all public.
- If you say "Parser parses expressions", then anything but #expr! becomes private interface
- If instead you just say "Parser parses", then everything is fair game for the public API, because you're exposing an API for parsing stuff.
- DOCUMENT the semantics!
- Gave responsibility to the AST nodes
- Calc does almost nothing now
- We can now test the parser directly to make sure factors, terms, and expressions are being parsed correctly.
- We can now test the evaluator directly, to make sure evaluation is happening properly.
One other gotcha I want to point out, which I've seen happening:
never ever ever use instance_variable_get in tests! if you need to access internal state, either:
expose the internal state as part of the public interface
- Makes a contract -- 'this interface is supported'
- Careful not to expose too much, and only where appropriate
- if something truly is private, exposing it for testing is probably a bad idea
refactor into separate classes so the internal state becomes testable as part of another object
- tokenizer remains as private, internal state of the parser, but accepts it as initialization data
- parser can instantiate tokenizer and reference it without violating encapsulation
- Smaller objects test better
- Smaller objects are easier to reuse
- Focused responsibilities are easier to describe and think about
- is the calculator calculating, or is it parsing too?
- is the parser parsing, or tokenizing too?