By now, I assume you’ve abandoned Mint.com and embraced the checkbook balancer that we’ve built over the last two chapters. But there’s one feature that you might miss: the ability to track additional accounts beyond checking, savings, and your mattress. For the final version of the checkbook balancer, we’re going to add the ability to add and remove accounts. We’re also going refactor our code into classes (and into more than one file!), making it easier to think of the program in terms of separate, reasonably decoupled components.
Let’s start by installing our dependencies, which will be the same as in checkbooks2:
| $ npm init |
| $ npm install --save inquirer |
| $ npm install --save numeral |
| $ npm install --save jsonfile |
We’re going to have a reasonably concise checkbooks3.coffee, containing only our application’s core logic, by require-ing three other project modules that we’re going to define separately.
| inquirer = require('inquirer') |
| |
| Account = require('./account') |
| PromptFactory = require('./promptFactory') |
| utils = require('./utils') |
| |
| # Define our logic for each prompt the user can reach |
| promptFactory = new PromptFactory({allAccounts: Account.allAccounts}) |
| |
| mainStep = -> |
| inquirer.prompt promptFactory.accountPrompt(), ({account}) -> |
| if account is null |
| createAccountStep() |
| else |
| actionStep({account}) |
| |
| createAccountStep = -> |
| inquirer.prompt promptFactory.newAccountPrompt(), ({name}) -> |
| new Account({balance: 0, name}) |
| Account.saveState() |
| mainStep() |
| |
| actionStep = ({account}) -> |
| inquirer.prompt promptFactory.actionPrompt({account}), ({action}) -> |
| postActionStep({account, action}) |
| |
| postActionStep = ({account, action}) -> |
| prompts = [promptFactory.amountPrompt({action})] |
| if action is 'transfer' |
| prompts.unshift promptFactory.toAccountPrompt({fromAccount: account}) |
| |
| inquirer.prompt prompts, ({amount, toAccount}) -> |
| amount = utils.inputToNumber(amount) |
| account[action]({amount, toAccount}) |
| mainStep() |
| |
| # Load data |
| Account.loadState() |
| |
| # Show the first prompt |
| mainStep() |
I’ve capitalized Account and PromptFactory because these are class names. The new Account class is an amalgam of the old createAccount function and our persistence logic:
| jsonfile = require('jsonfile') |
| |
| utils = require('./utils') |
| |
| # Account implements all actions that affect data |
| |
| class Account |
| constructor: ({@name, @balance}) -> |
| Account.allAccounts.push @ |
| return |
| |
| description: -> |
| "#{@name}: #{utils.dollarsToString(@balance)}" |
| |
| deposit: ({amount}) -> |
| @balance += amount |
| Account.saveState() |
| @ |
| |
| withdraw: ({amount}) -> |
| @balance -= amount |
| Account.saveState() |
| @ |
| |
| transfer: ({toAccount, amount}) -> |
| @balance -= amount |
| toAccount.balance += amount |
| Account.saveState() |
| @ |
| |
| @allAccounts = [] |
| |
| @saveState: -> |
| jsonfile.writeFileSync('./data.json', Account.allAccounts) |
| |
| @loadState: -> |
| try |
| Account.allAccounts = for accountData in jsonfile.readFileSync('./data.json') |
| new Account(accountData) |
| catch e |
| Account.allAccounts = [ |
| new Account({balance: 0, name: 'checking'}) |
| new Account({balance: 0, name: 'savings'}) |
| new Account({balance: 0, name: 'mattress'}) |
| ] |
| return |
| |
| module.exports = Account |
Some of our prompts require the data now stored as Account.allAccounts. We don’t want to create a global, but we also don’t want the repetition of passing that data every time we call a prompt function. So, I’ve attached those prompt functions as methods on a class called PromptFactory, which takes an array of accounts in its constructor and stores the reference:
| utils = require('./utils') |
| |
| # PromptFactory defines the presentation of each prompt in the app |
| |
| class PromptFactory |
| constructor: ({@allAccounts}) -> |
| |
| accountPrompt: -> |
| { |
| name: 'account' |
| message: 'Pick an account:' |
| type: 'list' |
| choices: (for account in @allAccounts |
| {name: account.description(), value: account} |
| ).concat({name: 'new account', value: null}) |
| } |
| |
| newAccountPrompt: -> |
| { |
| name: 'name' |
| message: 'Enter a name for this account:' |
| type: 'input' |
| validate: (input) => |
| for account in @allAccounts |
| if account.name is input |
| return 'That account name is already taken!' |
| true |
| } |
| |
| actionPrompt: ({account}) -> |
| { |
| name: 'action' |
| message: 'Pick an action:' |
| type: 'list' |
| choices: [ |
| {name: 'Deposit $ into this account', value: 'deposit'} |
| {name: 'Withdraw $ from this account', value: 'withdraw'} |
| {name: 'Transfer $ to another account', value: 'transfer'} |
| ] |
| } |
| |
| toAccountPrompt: ({fromAccount}) -> |
| { |
| name: 'toAccount' |
| message: 'Pick an account to transfer $ to:' |
| type: 'list' |
| choices: for account in @allAccounts when account isnt fromAccount |
| {name: account.description(), value: account} |
| } |
| |
| amountPrompt: ({action}) -> |
| { |
| name: 'amount' |
| message: "Enter the amount to #{action}:" |
| type: 'input' |
| validate: (input) => |
| if isNaN(utils.inputToNumber(input)) |
| return 'Please enter a numerical amount.' |
| if utils.inputToNumber(input) < 0 |
| return 'Please enter a non-negative amount.' |
| true |
| } |
| |
| module.exports = PromptFactory |
All that’s left is our utility functions, which now reside in their own handy file of miscellany:
| numeral = require ('numeral') |
| |
| # Utility functions |
| |
| module.exports = |
| dollarsToString: (dollars) -> |
| numeral(dollars).format('$0,0.00') |
| |
| inputToNumber: (input) -> |
| parseFloat input.replace(/[$,]/g, ''), 10 |
And here’s what our final, feature-packed iteration of the checkbook balancer looks like:
| $ coffee checkbooks3.coffee |
| [?] Pick an account: (Use arrow keys) |
| > checking: $22.00 |
| savings: -$5.00 |
| mattress: $0.00 |
| money market: $5.00 |
| hedge fund: $9,999.00 |
| new account |
3.144.255.87