Ch6: Defend the data
6.1 Encapsulating without getters
6.1.1 Rule: DO NOT USE GETTERS OR SETTERS
Do not use setters or getters for non-Boolean fields.
原因是:
- setter和getter會使類的不變量變得全局,破壞了封裝性。 任何獲得該對象的人都可以呼叫getter和setter,構成未知的修改。
- 使用getter和setter會導致pull-based的架構; 這通常會產生不必要的數據類和肥大的管理類,且數據類和管理類會緊密耦合。
Pull-based vs Pushed-based
class Website {
constructor(private url: string) { }
getUrl() {
return this.url;
}
}
class User {
constructor(private username: string) { }
getUsername() { return this.username; }
}
class BlogPost {
constructor(private author: User,
private id: string) { }
getId() { return this.id; }
getAuthor() { return this.author; }
}
function generatePostLink(website: Website,
post: BlogPost) {
let url = website.getUrl();
let user = post.getAuthor();
let name = user.getUsername();
let postId = post.getId();
return url + name + postId;
}
class Website {
constructor(private url: string) { }
generateLink(name: string, id: string) {
return this.url + name + id;
}
}
class User {
constructor(private username: string) { }
generateLink(website: Website, id: string) {
return website.generateLink(
this.username,
id);
}
}
class BlogPost {
constructor(private author: User,
private id: string) { }
generateLink(website: Website) {
return this.author.generateLink(
website,
this.id);
}
}
function generatePostLink(website: Website,
post: BlogPost) {
return post.generateLink(website);
}
Law of Demeter.
It means "Don't talk to strangers." A stranger in this context is an object that we do not have direct access to but can obtain a reference to.
6.1.3 Refactoring pattern: ELIMINATE GETTER OR SETTER
- Make the getter or setter private to get errors everywhere it is used.
- Fix the errors with PUSH CODE INTO CLASSES.
- The getter or setter is inlined as part of PUSH CODE INTO CLASSES. It is therefore unused, so delete it to avoid other people trying to use it.
- Initial
- Make the getter private to get errors everywhere it is used.
- Fix the errors with PUSH CODE INTO CLASSES.
- delete it to avoid other people trying to use it.
- Final.
class Website {
constructor(private url: string) { }
getUrl() { return this.url; }
}
class User {
constructor(private username: string) { }
getUsername() { return this.username; }
}
class BlogPost {
constructor(private author: User, private id: string) { }
getId() { return this.id; }
getAuthor() { return this.author; }// from User, should make it private
}
function generatePostLink(website: Website, post: BlogPost) {
let url = website.getUrl();
let user = post.getAuthor();
let name = user.getUsername();
let postId = post.getId();
return url + name + postId;
}
class Website {
constructor(private url: string) { }
getUrl() { return this.url; }
}
class User {
constructor(private username: string) { }
getUsername() { return this.username; }
}
class BlogPost {
constructor(private author: User, private id: string) { }
getId() { return this.id; }
private getAuthor() { return this.author; }
}
function generatePostLink(website: Website, post: BlogPost) {
let url = website.getUrl();
let user = post.getAuthor();// error
let name = user.getUsername();
let postId = post.getId();
return url + name + postId;
}
function generatePostLink(website: Website,
post: BlogPost) {
let url = website.getUrl();
let name = post.getAuthorName(); // push to code
let postId = post.getId();
return url + name + postId;
}
class BlogPost {
// ...
getAuthorName() {
return this.author.getUsername();
}
}
class BlogPost {
constructor(private author: User, private id: string) { }
getId() { return this.id; }
getAuthorName() {
return this.author.getUsername();
}
}
class Website {
constructor(private url: string) { }
generateLink(name: string, id: string) {
return this.url + name + id;
}
}
class User {
constructor(private username: string) { }
generateLink(website: Website, id: string) {
return website.generateLink(
this.username,
id);
}
}
class BlogPost {
constructor(private author: User,
private id: string) { }
generateLink(website: Website) {
return this.author.generateLink(
website,
this.id);
}
}
function generatePostLink(website: Website,
post: BlogPost) {
return post.generateLink(website);
}
6.1.4 Eliminating the final getter
Remove the final getter FallStrategy.getFalling.
- Initial
- Make the getter private.
- PUSH CODE INTO CLASSES
- Getter removed.
- NEVER USE if WITH else.
- Push drop to FallingState and remove FallStrategy.drop.
class FallStrategy {
// ...
getFalling() {
return this.falling;
}
}
class Stone implements Tile {
// ...
moveHorizontal(dx: number) {
this.fallStrategy.getFalling()// error
.moveHorizontal(this, dx);
}
}
class Box implements Tile {
// ...
moveHorizontal(dx: number) {
this.fallStrategy.getFalling()// error
.moveHorizontal(this, dx);
}
}
class FallStrategy {
// ...
private getFalling() {//set as private
return this.falling;
}
}
class Stone implements Tile {
// ...
moveHorizontal(dx: number) {// push to code
this.fallStrategy
.moveHorizontal(this, dx);
}
}
class Box implements Tile {
// ...
moveHorizontal(dx: number) {// push to code
this.fallStrategy
.moveHorizontal(this, dx);
}
}
class FallStrategy {
// ...
moveHorizontal(tile: Tile, dx: number) {//實作moveHorizontal
this.falling
.moveHorizontal(tile, dx);
}
private getFalling() {//不需要了
return this.falling;
}
}
class Stone implements Tile {
// ...
moveHorizontal(dx: number) {
this.fallStrategy
.moveHorizontal(this, dx);
}
}
class Box implements Tile {
// ...
moveHorizontal(dx: number) {
this.fallStrategy
.moveHorizontal(this, dx);
}
}
class FallStrategy {
constructor(private falling: FallingState) { }
update(tile: Tile, x: number, y: number) {
this.falling = map[y + 1][x].isAir()// !出現if-else
? new Falling()
: new Resting();
this.drop(tile, x, y);
}
private drop(tile: Tile,
x: number, y: number) {
if (this.falling.isFalling()) {
map[y + 1][x] = tile;
map[y][x] = new Air();
}
}
moveHorizontal(tile: Tile, dx: number) {
this.falling.moveHorizontal(tile, dx);
}
//remove getFalling()
}
interface Tile {
// ...
getBlockOnTopState(): FallingState;// return state
}
class Air implements Tile {
// ...
getBlockOnTopState() {
return new Falling();// return state
}
}
class Stone implements Tile {
// ...
getBlockOnTopState() {
return new Resting();// return state
}
}
class FallStrategy {
// ...
update(tile: Tile, x: number, y: number) {
this.falling =
map[y + 1][x].getBlockOnTopState();// push to code
this.drop(tile, x, y);
}
private drop(tile: Tile,
x: number, y: number) {
if (this.falling.isFalling()) {
map[y + 1][x] = tile;
map[y][x] = new Air();
}
}
}
interface FallingState {
// ...
drop(
tile: Tile, x: number, y: number): void;// push to code
}
class Falling {
// ...
drop(tile: Tile, x: number, y: number) {// push to code
map[y + 1][x] = tile;
map[y][x] = new Air();
}
}
class Resting {
// ...
drop(tile: Tile, x: number, y: number) { }// push to code
}
class FallStrategy {
// ...
update(tile: Tile, x: number, y: number) {
this.falling =
map[y + 1][x].getBlockOnTopState();
this.falling.drop(tile, x, y)
}
// delete drop()
}
6.2 Encapsulating simple data
Our code should not have methods or variables with common prefixes or suffixes.
當多個元素具有相同的前後綴時,它表示元素之間的一致性,所以應該把它們封裝成一個類,好處:
- 完全控制外部接口
- 隱藏不變量,確保只有在類中會修改這些不變量
符合單一職責原則。
single responsibility principle
Methods/Classes should do one thing.
function accountDeposit(
to: string, amount: number) {
let accountId = database.find(to);
database.updateOne(
accountId,
{ $inc: { balance: amount } });
}
function accountTransfer(amount: number,
from: string, to: string) {
accountDeposit(from, -amount);
accountDeposit(to, amount);
}
class Account {
private deposit(
to: string, amount: number) {
let accountId = database.find(to);
database.updateOne(
accountId,
{ $inc: { balance: amount } });
}
transfer(amount: number,
from: string, to: string) {
this.deposit(from, -amount);
this.deposit(to, amount);
}
}
6.2.2 應用NEVER HAVE COMMON AFFIXES
例如,我們的程式碼中有以下方法和變量:
- playerx
- playery
- drawPlayer
應該要把上述内容封裝起來。
- Initial
- 一個全新的類
- 把之前用到playerx/y的地方用Player的getter/setter替換
- 把player作爲參數傳進方法中
- 把draw player也放Player中
- Final
interface Tile {
// ...
moveHorizontal(
dx: number): void;//會用到playerx的地方
moveVertical(
dy: number): void;//會用到playery的地方
}
/// ...
function moveToTile(
newx: number, newy: number) {
map[playery][playerx] =
new Air();
map[newy][newx] = new PlayerTile();
playerx = newx;//會用到playerx的地方
playery = newy;//會用到playery的地方
}
/// ...
let playerx = 1;//全局變數
let playery = 1;
class Player {
private x = 1;
private y = 1;
getX() { return this.x; }
getY() { return this.y; }
setX(x: number) { this.x = x; }
setY(y: number) { this.y = y; }
}
function moveToTile(
newx: number, newy: number) {
map[player.getY()][player.getX()] = // replace with getter
new Air();
map[newy][newx] = new PlayerTile();
player.setX(newx);// replace with setter
player.setY(newy);// replace with setter
}
/// ...
interface Tile {
// ...
moveHorizontal(
player: Player, dx: number): void;
moveVertical(
player: Player, dy: number): void;
}
function drawPlayer(player: Player, // drawPlayer應該要在Player中
g: CanvasRenderingContext2D) {
g.fillStyle = "#ff0000";
g.fillRect(
player.getX() * TILE_SIZE,// replace with getter
player.getY() * TILE_SIZE,// replace with getter
TILE_SIZE,
TILE_SIZE);
}
function drawPlayer(player: Player,
g: CanvasRenderingContext2D) {
player.draw(g);
}
class Player {
// ...
draw(g: CanvasRenderingContext2D) {
g.fillStyle = "#ff0000";
g.fillRect(
this.x * TILE_SIZE,
this.y * TILE_SIZE,
TILE_SIZE,
TILE_SIZE);
}
}
interface Tile {
// ...
moveHorizontal(
player: Player, dx: number): void;
moveVertical(
player: Player, dy: number): void;
}
/// ...
function drawPlayer(player: Player,
g: CanvasRenderingContext2D) {
player.draw(g);
}
/// ...
function moveToTile(
newx: number, newy: number) {
map[player.getY()][player.getX()] =
new Air();
map[newy][newx] = new PlayerTile();
player.setX(newx);
player.setY(newy);
}
/// ...
class Player {
private x = 1;
private y = 1;
getX() { return this.x; }
getY() { return this.y; }
setX(x: number) { this.x = x; }
setY(y: number) { this.y = y; }
draw(g: CanvasRenderingContext2D) {
g.fillStyle = "#ff0000";
g.fillRect(
this.x * TILE_SIZE,
this.y * TILE_SIZE,
TILE_SIZE,
TILE_SIZE);
}
}
let player = new Player();
接下來把getter/setter拿掉。
- Before
- make private
- PUSH CODE INTO CLASSES
class Player {
// ...
getX() { return this.x; }// make private
getY() { return this.y; }
setX(x: number) { this.x = x; }
setY(y: number) { this.y = y; }
}
class Player {
// ...
private getX() { return this.x; }
}
class Right implements Input {
handle(player: Player) {
map[player.getY()][player.getX() + 1]//error
.moveHorizontal(player, 1);
}
}
class Resting {
// ...
moveHorizontal(
player: Player, tile: Tile, dx: number) {
if (map[player.getY()]
[player.getX() + dx + dx].isAir()//error
&& !map[player.getY() + 1]
[player.getX() + dx].isAir()) {//error
map[player.getY()]
[player.getX() + dx + dx] = tile;//error
moveToTile(player,
player.getX() + dx,//error
player.getY());
}
}
}
/// ...
moveToTile(player,
player.getX(), player.getY() + dy);//error
/// ...
function moveToTile(player: Player,
newx: number, newy: number) {
map[player.getY()][player.getX()] =//error
new Air();
map[newy][newx] = new PlayerTile();
player.setX(newx);
player.setY(newy);
}
/// ...
class Player {
// ...
}
class Right implements Input {
handle(player: Player) {
player.moveHorizontal(1);
}
}
class Resting {
// ...
moveHorizontal(
player: Player, tile: Tile, dx: number) {
player.pushHorizontal(tile, dx);
}
}
/// ...
player.move(0, dy);
/// ...
function moveToTile(player: Player,
newx: number, newy: number) {
player.moveToTile(newx, newy);
}
/// ...
class Player {
// getter removed
// ...
moveHorizontal(dx: number) {
map[this.y][this.x + dx]
.moveHorizontal(this, dx);
}
move(dx: number, dy: number) {
this.moveToTile(this.x + dx, this.y + dy);
}
pushHorizontal(tile: Tile, dx: number) {
if (map[this.y]
[this.x + dx + dx].isAir()
&& !map[this.y + 1]
[this.x + dx].isAir()) {
map[this.y][this.x + dx + dx] = tile;
this.moveToTile(this.x + dx, this.y);
}
}
moveToTile(newx: number, newy: number) {
map[this.y][this.x] = new Air();
map[newy][newx] = new PlayerTile();
this.x = newx;
this.y = newy;
}
}
6.2.3 Refactoring pattern: ENCAPSULATE DATA
- Create a class.
- Move the variables into the new class, replacing let with private. Simplify the variables’ names; also make getters and setters for the variables.
- Because the variables are no longer in the global scope, the compiler helps us find all the references by giving errors. Fix these errors in the following five steps:
- Pick a good variable name for an instance of the new class.
- Replace access with getters or setters on the pretend variable.
- If we have errors in two or more different methods, add a parameter with the variable name from earlier as the first parameter, and put the same variable as the first argument at call sites.
- Repeat until only one method errors.
- If we encapsulated variables, instantiate the new class at the point where the variables were declared. Otherwise, put the instantiation in the method that errors.
- Initial
- new class並設不變量為private
- 用getter/setter取代賦值
- 把新的class作爲參數傳入
- 在原有的地方初始化新的class
let counter = 0;
function incrementCounter() {
counter++;
}
function main() {
for (let i = 0; i < 20; i++) {
incrementCounter();
console.log(counter);
}
}
class Counter {
private counter = 0;
getCounter() { return this.counter; }
setCounter(c: number) {
this.counter = c;
}
}
function incrementCounter() {
counter.setCounter(
counter.getCounter() + 1);
}
function main() {
for (let i = 0; i < 20; i++) {
incrementCounter();
console.log(counter.getCounter());
}
}
function incrementCounter(counter: Counter) {
counter.setCounter(
counter.getCounter() + 1);
}
function main() {
for (let i = 0; i < 20; i++) {
incrementCounter(counter);
console.log(counter.getCounter());
}
}
class Counter { ... }
let counter = new Counter();
6.3 封裝複雜的資料 Encapsulate Complex Data
使用 Encapsulate Data 方法
將對 Map
的操作透過一個 Map class 完成
- Before
- After
interface Tile{
// ...
moveHorizontal(player: Player, dx: number):void;
moveVertical(player: Player, dy: number):void;
update(x: number, y:number): void;
}
// ...
function remove(shouldRemove: RemoveStrategy){
for(let y = 0; y < map.length; y++){
for(let x = 0; x < map[y]; x++){
if(shouldRemove.check(map[y][x])){
map[y][x] = new Air();
}
}
}
}
// ...
let map: Tile[][];
interface Tile{
// ...
moveHorizontal(map: Map, player: Player, dx: number):void;
moveVertical(map: Map, player: Player, dy: number):void;
update(map: Map, x: number, y:number): void;
}
function remove(map: Map, shouldRemove: RemoveStrategy){
for(let y = 0; y < map.getMap().length; y++){
for(let x = 0; x < map.getMap()[y].length; x++){
if(shouldRemove.check(map.getMap()[y][x])){
map.getMap()[y][x] = new Air();
}
}
}
}
class Map{
private map: Tile[][];
getMap(){ return this.map;}
setMap(map:Tile[][]){this.map = map;}
}
接著,使用 Push code into classes 與 Inline method 簡化了 function 名稱
- Before
- After
function transformMap(map: Map) {
map.setMap(new Array(rawMap.length));
for (let y = 0; y < rawMap.length; y++){
map.getMap()[y] = new Array(rawMap[y].length);
for (let x = 0; x < rawMap[y].length; x++){
map.getMap()[y][x] = transformTile(rawMap[y][x])
}
}
}
function updateMap(map: Map){
for (let y = map.getMap().length - 1; y >= 0; y--){
for (let x = 0; x < map.getMap()[y].length; x++) {
map.getMap()[y][x].update(map, x, y);
}
}
}
function drawMap(map: Map, g: CanvasRenderingContext2D){
for (let y = 0; y < map.getMap().length; y++){
for (let x = 0; x < map.getMap()[y].length; x++) {
map.getMap()[y][x].draw(g, x, y);
}
}
}
class Map{
private map: Tile[][];
getMap(){
return this.map;
}
setMap( map:Tile[][] ){
this.map = map;
}
transform(){
this.map = new Array(rawMap.length);
for (let y = 0; y < rawMap.length; y++){
this.map[y] = new Array(rawMap[y].length);
for (let x = 0; x < rawMap[y].length; x++){
this.map[y][x] = transformTile(rawMap[y][x]);
}
}
}
update() {
for (let y = this.map.length - 1; y >= 0; y--){
for (let x = 0; x < this.map[y].length; x++) {
this.map[y][x].update(this, x, y);
}
}
}
draw( g: CanvasRenderingContext2D ) {
for (let y = 0; y < this.map.length; y++){
for (let x = 0; x < this.map[y].length; x++) {
this.map[y][x].draw(g, x, y);
}
}
}
}
如同前面對於 Player
的做法,移除 getter 與 setter(Eliminate getter or setter)
- Before
- After
class Falling {
// ...
drop(map: Map, tile: Tile, x: number, y: number){
map.getMap()[y + 1][x] = tile;
map.getMap()[y][x] = new Air();
}
}
class FallStrategy {
// ...
update(map: Map, tile: Tile, x: number, y: number){
this.falling = map.getMap()[y+1][x].isAir() ? new Falling() : new Resting();
this.falling.drop(map, tile, x, y);
}
}
class Player {
// ...
moveHorizontal(map: Map, dx: number) {
map.getMap()[this.y][this.x + dx].moveHorizontal(map, this, dx);
}
moveVertical(map: Map, dy: number) {
map.getMap()[this.y + dy][this.x].moveVertical(map, this, dy);
}
pushHorizontal(map: Map, tile: Tile, dx: number) {
if(map.getMap()[this.y][this.x + dx + dx].isAir()
&& !map.getMap()[this.y + 1][this.x + dx].isAir()){
map.getMap()[this.y][this.x + dx + dx] = tile;
this.moveToTile(map, this.x + dx, this.y);
}
}
private moveToTile(map: Map, newx: number, newy: number){
map.getMap()[this.y][this.x] = new Air();
map.getMap()[newy][newx] = new PlayerTile();
this.x = newx;
this.y = newy;
}
}
class Map {
// ...
}
class Falling {
// ...
drop(map: Map, tile: Tile, x: number, y: number){
map.drop(tile, x, y);
}
}
class FallStrategy {
// ...
update(map: Map, tile: Tile, x: number, y: number){
this.falling = map.getBlockOnTopState(x, y + 1);
this.falling.drop(map, tile, x, y);
}
}
class Player {
// ...
moveHorizontal(map: Map, dx: number) {
map.moveHorizontal(this, this.x, this.y, dx);
}
moveVertical(map: Map, dy: number) {
map.moveVertical(this, this.x, this.y, dy);
}
pushHorizontal(map: Map, tile: Tile, dx: number) {
if(map.isAir(this.x + dx + dx, this.y)
&& !map.isAir(this.x + dx, this.y + 1)){
map.setTile(this.x + dx + dx, this.y, tile);
this.moveToTile(map, this.x + dx, this.y);
}
}
private moveToTile(map: Map, newx: number, newy: number){
map.movePlayer(this.x, this.y, newx, newy);
this.x = newx;
this.y = newy;
}
}
class Map {
// ...
drop(tile: Tile, x: number, y: number){
this.map[y + 1][x] = tile;
this.map[y][x] = new Air();
}
getBlockOnTopState(x: number, y: number){
return this.map[y][x].getBlockOnTopState();
}
isAir(x: number, y:number){
return this.map[y][x].isAir();
}
setTile(x: number, y: number, tile: Tile){
this.map[y][x] = tile;
}
moveHorizontal(player: Player, x: number, y: number, dx: number) {
this.map.[y][x + dx].moveHorizontal(this, player, dx);
}
moveVertical(player: Player, x: number, y: number, dy: number) {
this.map.[y + dy][x].moveVertical(this, player, dy);
}
}
Remove 這個方法也直接移進 Map (Inline Method)
- Before
- After
function remove(map: Map, shouldRemove: RemoveStrategy){
for(let y = 0; y < map.getMap().length; y++){
for(let x = 0; x < map.getMap()[y].length; x++){
if(shouldRemove.check(map.getMap()[y][x])){
map.getMap()[y][x] = new Air();
}
}
}
}
class Map {
// ...
getMap() {
return this.map;
}
}
class Map {
// ...
remove(shouldRemove: RemoveStrategy){
for(let y = 0; y < map.getMap().length; y++){
for(let x = 0; x < map.getMap()[y].length; x++){
if(shouldRemove.check(map.getMap()[y][x])){
this.map[y][x] = new Air();
}
}
}
}
}
一般而言不會在 public 介面,像是 Player 中使用像是 setTile 這樣的存在 因此我們將 PushHorizontal 也放進 Map 裡,由 map 去呼叫 player
- Before
- After
class Player {
// ...
pushHorizontal(map: Map, tile: Tile, dx: number) {
if(map.isAir(this.x + dx + dx, this.y)
&& !map.isAir(this.x + dx, this.y + 1)){
map.setTile(this.x + dx + dx, this.y, tile);
this.moveToTile(map, this.x + dx, this.y);
}
}
private moveToTile(map: Map, newx: number, newy: number){
map.movePlayer(this.x, this.y, newx, newy);
this.x = newx;
this.y = newy;
}
}
class Player {
// ...
pushHorizontal(map: Map, tile: Tile, dx: number) {
map.pushHorizontal(this, tile, this.x, this.y, dx);
}
// 改成 public method
moveToTile(map: Map, newx: number, newy: number){
map.movePlayer(this.x, this.y, newx, newy);
this.x = newx;
this.y = newy;
}
}
class Map {
// ...
pushHorizontal(player: Player, x: number, y: number, dx: number) {
if(this.map[y][x + dx + dx].isAir() && !this.map[y + 1][x + dx].isAir() ){
this.map[y][x + dx + dx] = tile;
player.moveToTile(this, x + dx, y);
}
}
}
上述的改動讓 setTile 變成只有在 Map 中使用,可以考慮直接拿掉或是改成 private function
6.4 移除循序不變的條件
循序不變性(sequence invariant):當某些程式需要在其他程式前被呼叫
在前一章節中所建立的 Map 是透過 transform 來初始化就符合這個情境,透過 constructor 強制這個 class 在被實例化的同時就需要先初始化 強制其照特定的順序執行
- Before
- After
class Map {
// ...
transform() {
// ...
}
}
// ...
window.onload = () =>{
map.transform();
gameLoop(map);
}
class Map {
// ...
constructor() { // transform 改成 constructor
// ...
}
}
window.onload = () => {
gameLoop(map);
}
6.4.1 重構模式:強制循序(Enforce Sequence)
核心:『教』編譯器如何執行我們的程式
- Before
- After
function print(str: string){
// string should be capitalized
console.log(str)
}
class CapitalizedString {
private value: string;
constructor(str: string) {
this.value = capitalized(str);
}
print() {
console.log(value);
}
}
Enforce Sequence 有兩種變體
- 內部:將目標函式移動到新的 class 內,如同前面的 CapitalizedString
- 外部:只將目標函式的參數改傳入一個特定型別
內部 & 外部
class CapitalizedString {
private value: string;
constructor(str: string) {
this.value = capitalize(str);
}
print() {
console.log(value);
}
}
class CapitalizedString {
private readonly value: string;
constructor(str: string) {
this.value = capitalize(str);
}
}
function print(str: CapitalizedString){
console.log(str.value);
}
Example
銀行出入帳 我們想要確保在把錢轉給對方前,錢會先從轉出方扣除
步驟如下:
- 使用 Encapsulate Data 重構最後要執行的方法
- 讓 constructor 呼叫第一個方法
- 如果這兩個方法的引數有關聯,把這些引數變成欄位,並從方法中移除
- Step.0
- Step.1
- Step.2
- Step.3
function deposit(to: string, amount: number){
let accountId = database.find(to);
database.updateOne(accountId, { $inc: { balance: amount } })
}
class Transfer {
deposit(to: string, amount: number){
let accountId = database.find(to);
database.updateOne(accountId, { $inc: { balance: amount } })
}
}
class Transfer {
constructor(from: string, amount: number) {
this.deposit(from, -amount);
}
deposit(to: string, amount: number){
let accountId = database.find(to);
database.updateOne(accountId, { $inc: { balance: amount } })
}
}
class Transfer {
constructor(from: string, private amount: number) {
this.deposit(from, -this.amount);
}
private depositHelper(to: string, amount: number) {
let accountId = database.find(to);
database.updateOne(accountId, { $inc: { balance: amount } })
}
deposit(to: string){
this.depositHelper(to, this.amount);
}
}
此為負向轉移的寫法.確保 amount 是在 Transfer 建立時就定義好的,但同時也會需要確保 deposit 被執行,否則錢就消失了
6.5 另一種消除 enum 的方法
enum 不支援方法
6.5.1 利用 private constructor
每個物件需要透過 constructor 建立,如果把 constructor 設為 private ,物件就只能在 class 內部被建立 可以更有效控制實例的數量 如果把這些實例放在 public const 中,就視同 enum 的用法
- Enum
- Private Constructor
- Replace Type Code With Classes
enum TShirtSize {
SMALL,
MEDIUM,
LARGE
}
function sizeToString(s: TShirtSize) {
if (s === TShirtSize.SMALL) {
return "S"
}else if (s === TShirtSize.MEDIUM) {
return "M"
}else if (s === TShirtSize.LARGE) {
return "L"
}
}
class TShirtSize {
static readonly SMALL = new TShirtSize();
static readonly MEDIUM = new TShirtSize();
static readonly LARGE = new TShirtSize();
private constructor() { };
}
function sizeToString(s: TShirtSize) {
if (s === TShirtSize.SMALL) {
return "S"
}else if (s === TShirtSize.MEDIUM) {
return "M"
}else if (s === TShirtSize.LARGE) {
return "L"
}
}
interface SizeValue {}
class SmallValue implements SizeValue {}
class MediumValue implements SizeValue {}
class LargeValue implements SizeValue {}
class TShirtSize {
static readonly SMALL = new TShirtSize(new SmallValue());
static readonly MEDIUM = new TShirtSize(new MediumValue());
static readonly LARGE = new TShirtSize(new LargeValue());
private constructor(private value: SizeValue) { };
}
用 Replace Type Code With Classes 雖然可以解決 sizeToString 中 大量的 if else 但作者大多數的情況下不會選擇這樣的做法,因為在有些程式語言並不適用,例如:Java 如果要消除 enum ,選用先前的方法優先
回到遊戲範例,建立一個新類別用來消除 enum RawTile
interface RawTileValue { }
class AirValue implements RawTileValue { }
// ... implement 多個 RawTileValue
class RawTile2 {
static readonly AIR = new RawTile2(new AirValue());
// ... 放入多個 static readonly class
}
6.5.2 重新對映數值到類別
將數字轉換為新的 RawTile2,建立一個與 enum 相同的順序的陣列
- Before
- After
enum RawTile {
AIR,
FLUX,
UNBREAKABLE,
PLAYER,
STONE,
FALLING_STONE,
BOX,
FALLING_BOX,
KEY1, LOCK1,
KEY2, LOCK2
}
const RAW_TITLES = [
RawTile2.AIR,
RawTile2.FLUX,
RawTile2.UNBREAKABLE,
RawTile2.PLAYER,
// ...
]
有了這個陣列就可以將原本的 enum 的數字對映到相應的實例
- Before
- After
let rawMap: RawTile[][] = [
//...
];
class Map {
private map: Tile[][];
constructor() {
this.map = new Array(rawMap.length);
for(let y = 0; y < rawMap.length; y++){
this.map[y] = new Array(rawMap[y].length);
for(let x = 0; x < rawMap[y].length; x++ ){
this.map[y][x] = transformTile(rawMap[y][x]);
}
}
}
// ...
}
function transformTile(tile: RawTile){
// ...
}
let rawMap: number[][] = [
//...
];
class Map {
private map: Tile[][];
constructor() {
this.map = new Array(rawMap.length);
for(let y = 0; y < rawMap.length; y++){
this.map[y] = new Array(rawMap[y].length);
for(let x = 0; x < rawMap[y].length; x++ ){
this.map[y][x] = transformTile(RAW_TITLES[rawMap[y][x]]);
}
}
}
// ...
}
function transformTile(tile: RawTile2){
// ...
}
在先前重構的過程中,目前的 transformTile 會出錯,主要是因為原本是用 switch case 為消除這些 switch 以及 enum ,對 RawTile2 進行一次 Push Code Into Classes
- Before
- After
interface RawTileValue { }
class AirValue implements RawTileValue { }
class StoneValue implements RawTileValue { }
class Key1Value implements RawTileValue { }
class RawTile2 {
// ...
}
function assertExhausted(x: never) : never {
throw new Error("Unexpected object: " + x);
}
function transformTile(tile: RawTile2){
switch(tile){
case RawTile.AIR:
return new Air();
case RawTile.STONE:
return new Stone(new Resting());
// ...
default:
assertExhausted(tile);
}
}
interface RawTileValue {
transform(): Tile;
}
class AirValue implements RawTileValue {
transform() {
return new Air();
}
}
class StoneValue implements RawTileValue {
transform() {
return new Stone(new Resting());
}
}
class Key1Value implements RawTileValue {
transform() {
return new Key1(YELLOW_KEY);
}
}
class RawTile2 {
// ...
transform() {
return this.value.transform();
}
}
function transformTile(tile: RawTile2){
return tile.transform();
}
如此一來,switch case 與 enum 就可以被消除了,我們將 RawTile2 改為 RawTile 作為正式的類別
總結
- DO NOT USE GETTERS OR SETTERS:以免暴露 private value ;
=> 可以透過 ELIMINATE GETTER OR SETTER 這個方法來消除
- NEVER HAVE COMMON AFFIXES:有共同前後綴的方法或變數應該放在同一個類別做管理;
=> 可以透過 ENCAPSULATE DATA 來解決
- 可以透過 ENFORCE SEQUENCE 強制讓 function 間的執行間有一定的順序
- 處理 enum 的另一個方法是使用帶有 private constructor 的 class ,同時也可以消除 switch case