Keep these unique and not overlapping!
Class-Responsibility-Collaboration (CRC) Cards are a technique for discovering and organizing classes
class Line {
constructor(
private startX: number,
private startY: number,
private endX: number,
private endY: number
) {}
move() { /*...*/ }
stretch() { /*...*/ }
changeColor() { /*...*/ }
}
//Use the class to work with the encapsulated data!
line: Line = new Line(1,2, 5,8);
line.move(); //move around
line.stretch(); //get longer
class Location {
//what attributes?
private x: number; //x-coordinate
private y: number; //y-coordinate
private latitude: number;
private longitude: number;
private address: string = "1234 Main Street";
private place: string = "Alice's Restaurant";
//methods
distanceFrom(other:Location): number { /*...*/ }
areWeThereYet(): boolean { /*..*/ }
}
class Person {
//private variables are only accessible from inside the class
private password: string;
//public variables are accessible anywhere (default access)
public name: string;
//readonly vars are public, but can't be changed (like const)
readonly species: string = "Human"
//Use functions to provide (controlled) access to private fields
getPassword(): string { return this.password; }
setPassWord(newPass: string) { this.password = newPass; }
}
class Line {
private startX:number;
private startY:number;
private endX:number;
private endY:number;
private length:number;
constructor(x1:number, y1:number, x2:number, y2:number) {
this.startX = x1; //etc
}
setStart(p:Point):void {
this.startX = p.getX();
this.startY = p.getY();
this.length = this.calculateLength();
}
getEnd():Point {
return new Point(this.endX, this.endY);
}
//...
}
Example from Pragmatic Programmer
class Line {
private start:Point;
private end:Point;
private length:number;
constructor(x1:number, y1:number, x2:number, y2:number) {
this.start = new Point(x1, y1); //etc
}
setStart(p:Point):void {
this.start = p;
this.length = this.calculateLength();
}
getEnd():Point {
return this.end;
}
//...
}
Example from Pragmatic Programmer
public interface is the same--no change needed!
duplicate information
class Line {
private start:Point;
private end:Point;
constructor(x1:number, y1:number, x2:number, y2:number) {
this.start = new Point(x1, y1); //etc
}
setStart(p:Point):void {
this.start = p;
}
getEnd():Point {
return this.end;
}
getLength():number {
return this.calculateLength();
}
}
Example from Pragmatic Programmer
class Customer { //responsible for buying
private name: string;
private wallet: Wallet;
getName(): string { return this.name; }
getWallet(): Wallet { return this.wallet; }
}
class Wallet { //responsible for tracking money
private value: number;
getTotalMoney() :number { return this.value; }
subtractMoney(debit: number) { this.value -= debit; }
}
class Merchant { //responsible for selling
sellTo(myCustomer: Customer) {
let cost = 5.00;
let theWallet: Wallet = myCustomer.getWallet();
if(theWallet.getTotalMoney() >= cost){
theWallet.subtractMoney(cost);
this.revenue += cost;
}
else { /* do not sell */ }
}
}
class Customer { //responsible for buying
private wallet: Wallet;
getPayment(bill: number): number {
if(this.wallet.getTotalMoney() >= bill) {
this.wallet.subtractMoney(bill);
return bill;
}
else { return 0; } //treat as error code ?
}
}
class Wallet { //responsible for tracking money
private value: number;
getTotalMoney(): number { return this.value; }
subtractMoney(debit: number) { this.value -= debit; }
}
class Merchant { //responsible for selling
sellTo(myCustomer: Customer) {
let cost = 5.00;
let paidAmount = myCustomer.getPayment(cost);
if(paidAmount > 0) {
this.revenue += paidAmount;
}
else { /* do not sell */ }
}
}
class Dog {
private mFlea: Flea; //instance variable
bark() { } //member function
//Inside this method we can call functions on:
fetch(aBall: Ball) {
this.bark(); //1. the object itself
aBall.squeeze(); //2. method parameters
let wag = new TailWag();
wag.show(); //3. instantiated objects
mFlea.scratch(); //4. direct component objects
}
}
exercise from Pragmatic Programmer
Does this code obey the Law of Demeter?
function showBalance(account: BankAccount):void {
let amount: Money = account.getBalance();
console.log(amount.printFormat());
}
function processTransaction(account: BankAccount, value: number) {
Money amount = new Money();
amount.setValue(123.45);
account.setBalance(amount);
let who: Person = account.getOwner();
console.log("Updated account for " + who.getName());
}
class Colada {
private blender: Blender;
private stuff: IngredientList;
constructor() {
this.blender = new Blender();
this.stuff = new IngredientList();
}
mix() {
this.blender.addIngredients(this.stuff.getIngredients());
this.blender.blend();
}
}
// age IS A number
let age: number = 23
// firstName IS A string
let firstName: string = "Ada"
//an interface for a `Person` type
interface Person {
firstName: string,
age: number
}
//ada IS A Person
let ada: Person = {firstName: "Ada", age: 33};
interface Computer {
add(x: number, y: number): number; //method signature
print(text: string): void; //no body
}
let hal: Computer = { /*...*/ };
hal.add(2, 3); //no problem!
hal.openThePodBayDoors(); //Error!
class Dog {
constructor(private name: string) {}
bark() { /*...*/ }
beg() { /*...*/ }
}
//fido IS A Dog
let fido: Dog = new Dog("Fido");
fido.bark();
fido.beg();
fido.rollOver(); //Error! Not in the interface!
interface VisaAcceptor {
payWithVisa(cardNum: number): Receipt;
}
//explicit: elements of this class fulfill the interface
//a CoffeeShop IS A VisaAcceptor
class CoffeeShop implements VisaAcceptor {
payWithVisa(cardNum: number): Receipt { /* ... */ }
}
//object IS A CoffeeShop
let shop: CoffeeShop = new CoffeeShop();
//object also IS A VisaAcceptor
let payee: VisaAcceptor = shop; //assign same object
<<interface>>
Barker
Dog
Interacting with classes in terms of a shared interface lets us work at a higher level of abstraction.
interface VisaAcceptor {
//takes in the card number, returns Receipt object
payWithVisa(cardNum: number): Receipt;
}
class CoffeeShop implements VisaAcceptor { /* ... */ }
class CharityDrive implements VisaAcceptor { /* ... */ }
let shop: CoffeeShop = new CoffeeShop();
let charity: CharityDrive = new CharityDrive();
shop.payWithVisa();
charity.payWithVisa();
interface VisaAcceptor {
//takes in the card number, returns Receipt object
payWithVisa(cardNum: number): Receipt;
}
class CoffeeShop implements VisaAcceptor { /* ... */ }
class CharityDrive implements VisaAcceptor { /* ... */ }
let shop: VisaAcceptor = new CoffeeShop();
let charity: VisaAcceptor = new CharityDrive();
shop.payWithVisa();
charity.payWithVisa();
VisaAcceptor box contains
Shop object
interface VisaAcceptor {
//takes in the card number, returns Receipt object
payWithVisa(cardNum: number): Receipt;
}
class CoffeeShop implements VisaAcceptor { /* ... */ }
class CharityDrive implements VisaAcceptor { /* ... */ }
let shop: VisaAcceptor = new CoffeeShop();
let charity: VisaAcceptor = new CharityDrive();
//put them in an array together (same type!)
let placesToPay: VisaAcceptor[] = [shop, charity, ...];
//Abstract away their differences and loop through them all!
placesToPay.forEach((place: VisaAcceptor) => {
place.payWithVisa();
}
An object can be stored in a variable for any type it is, but the value still retains it's original type(s) and code.
interface Barker {
bark(): void;
}
class Dog implements Barker {
bark() {
console.log("Bark!");
}
wagTail() {
console.log("Wag tail!");
}
}
let animal: Barker = new Dog();
animal.bark(); //works!
animal.wagTail(); //TS compiler error, but will run
//Will not compile in Java
//explicitly assert `animal` is a `Dog` (typecast)
(animal as Dog).wagTail(); //compiles
Because JavaScript is dynamically typed, any class that fulfills an interface is treated as implementing that interface.
Interfaces define a set of "minimal" properties
interface Barker {
bark(): void;
}
class Dog implements Barker {
bark() {
console.log("Bark!");
}
}
//Dog fulfills the interface,
//so IS a Barker!
let animal: Barker = new Dog();
animal.bark(); //works!
interface Barker {
bark(): void;
}
class Dog {
bark() {
console.log("Bark!");
}
}
//Dog fulfills the interface,
//so IS a Barker!
let animal: Barker = new Dog();
animal.bark(); //works!
interface Barker {
bark(): void;
}
class Dog {
bark() {
console.log("Bark!");
}
}
//Dog fulfills the interface,
//so IS a Barker!
let animal: Barker = new Dog();
animal.bark(); //works!
//does not apply to object literals (excess property checking)
let pet: Barker = { bark:() => {}, yip:() => {} } //Error
When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.
Define a class
HourlyEmployee
who has a
name, an
hourly wage, and a
number of hours worked this week.
Also define a class
SalariedEmployee
who has a
name and an
annual (52-week) salary. Both classes should include methods that return their
pay for that week.
Define an
interface that both of these classes fulfill.
Use that interface to create an array that includes
two
HourlyEmployee
objects and
two
SalariedEmployee
objects. Use a loop to print out the salaries of all of the employees.
It is possible to import external libraries and make them available in your script.
const util = require('util'); //can only import a specific value
import { Random, Widget } from 'util'; //import specific values
import random from 'util'; //import "default" value
import * as util from 'util'; //import all values into single obj
//(would use `util.Random()`)
To make a file into a module,
export
variables, functions, and classes to be available for
import
.
/*** my-module.js ***/
export function foo() { return 'foo'; } //named export
function bar() { return 'bar'; }
export bar; //export previously defined variable
//will not be available (a "private" function)
function baz() { return 'baz'; }
/*** index.js ***/
import {foo, barFunction} from './my-module'; //import multiple
foo() //=> 'foo'
barFunction() //=> 'bar'
//import everything that was exported
//loads as a single object with values as properties
import * as theModule from './my-module'; //import everything
theModule.foo(); //=> 'foo'
theModule.barFunction(); //=> 'bar'
theModule.baz(); //Error [private function]
encapsulation!
Each module can have a single default export, which provides a shortcut when importing.
/*** my-module.js ***/
export default function sayHello() {
return 'Hello world!';
}
/*** index.js ***/
//default import can assign own alias without needing name!
import greet from './my-module';
greet(); //=> "Hello world!"
Make the modules (parts of the code: files, classes, etc) correspond to components (parts of the model/system)
It is also possible to indicate modules (called packages) in UML diagrams, drawn as a "folder" containing the relevant classes.