이번 장에서 다룰 내용
클래스는 동일한 데이터에 대한 기능을 결합해서 불변속성을 더 가깝게 모아 지역화한다.
이번 장에서는 데이터와 기능에 대한 접근을 제한하는 캡슐화에 초첨을 맞춰 불변속성이 지역에만 영향을 주게 만드는 데 중점을 둔다.
부울(Boolean)이 아닌 필드에 setter나 getter를 사용 하지 말라
여기서 setter 또는 getter를 언급할 때는 각각 부울이 아닌 필드를 직접할당하거나 반환하는 메서드
getter와 setter 는 흔히 private 필드를 다루기 위한 메서드로 캡슐화와 함께 배운다. 그러나 필드에 대한 getter가 존재하는 순간 캡슐화를 해제하고 불변속성을 전역적으로 만들게 된다.
객체를 반환한 후 이를 반환받은 곳에서는 이 객체를 더 많은 곳에 전달할 수 있으며 이는 우리가 제어할 수 없다. 객체를 얻은 어느 곳에서나 public 메서드를 호출할 수 있으며 아마도 예상하지 못한 방식으로 객체를 수정할 수 있다.
setter도 비슷한 문제를 제기한다. 이론적으로 setter는 내부 데이터 구조를 변경하고 해당 setter를 수정해도 시그니처(signature)를 유지 할 수 있는 또 다른 간접적인 레이어를 도입할 수 있다. (이것은 setter가 아니므로 문제 되지 않음)
그러나 실제로 일어나는 일은, setter를 통한 새로운 데이터 구조를 반환하도록 getter를 수정하는 것이다. 그런다음 수신자 측에서 이 새로운 데이터 구조를 받을 수 있도록 수정해야한다. 이것은 정확히 우리가 피하고 싶어하는 밀 결합(tight coupling)의 형태이다.
필드를 비공개로 하는 것에 장점은 그렇게 하는 것이 푸시기반의 아키텍처를 장려하기 때문 푸시기반 아키텍처에서는 가능한 한 데이터에 가깝게 연산을 이관한다.
풀 기반의 아키텍처에서는 데이터를 가져와 중앙에서 연산을 수행한다.
푸시 기반 아키텍처에서는 데이터를 가져오는 대신 인자로 데이터를 전달한다. 결과적으로 모든 클래스가 자신의 기능을 가지고 있으며 코드는 그 효용에 따라 분산된다.
예제는 블로그 게시물에 대한 링크를 생성한다.
// 풀 기반 아키텍처
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.uername,id);
}
}
class BlogPost {
constructor (private author: User, private id:string){ }
getnergateList(website:Website){
return this.author.generateLink(website, this.id)
}
}
function generatePostLink(website, WebSite, post: BlogPost){
return post.generateLink(website);
}
푸시 기반 예제에는 덧붙여진 정보 없이 한 줄에 불과하기 때문에 generatePostLink를 인라인화할 가능성이 높다.
// 변경 전
class KeyConfiguration {
// ...
getRemoveStrategy() {
return this.removeStrategy;
}
}
// 변경 후
class KeyConfiguration {
// ...
private getRemoveStrategy() {
return this.removeStrategy;
}
}
// 변경 전
class Key implements Tile {
// ...
moveHorizontal(dx:number){
remove(this.keyConf.getRemoveStrategy());
moveToTile(playerx + dx, player);
}
moveVertical(dy: number){
remove(this.keyConf.getRemoveStrategy());
moveToTile(playerx, playery + dy);
}
}
class KeyConfiguration {
// ...
getRemoveStrategy() {
return this.removeStrategy;
}
}
// 변경 후
class Key implements Tile {
// ...
moveHorizontal(dx:number){
this.keyConf.removeLock();
moveToTile(playerx + dx, player);
}
moveVertical(dy: number){
this.keyConf.removeLock();
moveToTile(playerx, playery + dy);
}
}
class KeyConfiguration {
// ...
removeLock(){
remove(this.removeStrategy);
}
}
//변경 전
class KeyConfiguration{
// ...
private getRemoveStrategy() {
return this.removeStrategy;
}
}
//변경 후
class KeyConfiguration {
}
getColor에 대해 이 절차를 반복하면 다음과 같다.
//변경 전
class KeyConfiguration {
constructor(
private color: string,
private _1:boolean,
privage removeStrategy:RemoveStrategy
){ }
getColor() {return this.color;}
_1() {return this._1};
getRemoveStrategy(){
return this.removeStrategy;
}
}
class Key implements Tile {
constructor(
private keyConf: KeyConfiguration
)
}{ }
//...
draw(g: CanvasRenderingContext2D,x:number,y:number){
g.fillStyle =this.keyConf.getColor();
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
moveHorizontal(dx:number){
remove(this.keyConf.getRemoveStrategy());
moveToTile(playerx + dx, playery);
}
moveVertical(dy: number){
remove(this.keyConf.getRemoveStrategy());
moveToTile(playerx, playery + dy);
}
}
class Lock implements Tile {
constructor(
private keyConf: KeyConfiguration
){ }
//...
isLock1(){return this.keyConf.is1();}
isLock2(){return !this.keyConf.is1();}
draw(g: CanvasRenderingContext2D,x:number,y:number){
g.fillStyle =this.keyConf.getColor();
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
}
// 변경 후
class KeyConfiguration {
constructor(
private color: string,
private _1:boolean,
privage removeStrategy:RemoveStrategy
){ }
setColor(g: CanvasRenderingContxt2D){
g.fillStyle = this.color;
}
removeLock() {
remove(this.removeStrategy);
}
_1() {return this._1};
}
class Key implements Tile {
constructor(
private keyConf: KeyConfiguration
)
}{ }
//...
draw(g: CanvasRenderingContext2D,x:number,y:number){
this.keyConf.setColor(g);
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
moveHorizontal(dx:number){
this.keyConf.removeLock();
moveToTile(playerx + dx, playery);
}
moveVertical(dy: number){
this.keyConf.removeLock();
moveToTile(playerx, playery + dy);
}
}
class Lock implements Tile {
constructor(
private keyConf: KeyConfiguration
){ }
//...
isLock1(){return this.keyConf.is1();}
isLock2(){return !this.keyConf.is1();}
draw(g: CanvasRenderingContext2D,x:number,y:number){
this.keyConf.setColor(g);
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
}
// 변경 전
class BlogPost {
// ...
getAuthor() {
rteurn this.author;
}
}
// 변경 후
class BlogPost {
private getAuthor(){ //private 키워드 추가
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 BlogPost {
// ...
}
// 변경 후
function generatePostLink(website:Website, post:BlogPost){
let url = website.getUrl();
let name = post.getAuthorname();
let postId = post.getId();
return url + name + postId;
}
class BlogPost {
// ...
getAuthorName() {return this.author.getUsername();
}
}
//변경 전
class BlogPost {
//...
private getAuthor(){
return this.author;
}
}
//변경 후
class BlogPost {
//...
// getAuthor가 삭제됨
}
마지막 getter는 FallStrategy.getFalling 이다. 이것을 제거하는 절차도 동일하다.
//변경 전
class FallStrategy {
getFalling() {
return this.falling;
}
}
//변경 후
class FallStrategy {
// ...
private getFalling() { // 추가된 private 키워드
return this.falling;
}
}
// 변경 전
class Stone implements Tile {
// ...
moveHorizontal(dx: number){
this.fallStrategy.getFalling()
.moveHorizontal(this, dx);
}
}
class Box implements Tile {
// ...
moveHorizontal(dx: number){
this.fallStrategy.getFalling()
.moveHorizontal(this, dx);
}
}
class FallStrategy {
// ...
}
// 변경 후
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 {
// ...
moveHorizontal(tile:Tile, dx:number){
this.falling.moveHorizontal(tile, dx);
}
}
FallStrategy를 살펴보면 몇 가지 다른 개선 사항이 있음을 알게된다. 먼저 삼항연산자 ?: 가 if문에서 else를 사용하지 말 것 규칙을 위반한다.
둘째 drop에 사용된 if 문은 falling과 연관돼 보인다. 삼항연산자로 시작해 줄을 Tile로 이관해서 제거할 수 있다.
//변경 전
interface Tile {
//...
}
class Air implements Tile {
//...
}
class Stone implements Tile {
//...
}
class FallStrategy {
//...
update(tile: Tile, x:number, y:number){
this.falling = map[y + 1][x].isAir()
? new Falling()
: new Resting();
this.drop(tile, x, y);
}
}
// 변경 후
interface Tile {
//...
getBlockOnTopState(): FallingState;
}
class Air implements Tile {
// ...
getBlockOnTopState(){
return new Falling();
}
}
class Stone implements Tile {
// ...
getBlockOnTopState() {
return new Resting();
}
}
class FallStrategy {
//...
update(tile: Tile, x:number, y:number){
this.falling = map[y + 1][x].getBlockOnTopState(); //이관된 코드
this.drop(tile, x, y);
}
}
FallStrategy.drop에서 메서드를 FallingState로 이전하고 FallStrategy.drop을 인라인화해서 if를 완전히 제거할 수 있다.
// 변경 전
interface FallingState {
// ...
}
class Falling {
// ...
}
class Resting {
// ...
}
class FallStrategy {
// ...
update(tile: Tile, x:number, y:number){
this.falling =
map[y + 1][x].getBlockOnTopState();
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;
}
class Falling {
// ...
drop(tile: Tile, x:number, y:number){
map[y + 1][x] = tile;
map[y][x] = new Air();
}
}
class Resting {
// ...
drop(tile: Tile, x: number, y:number) { }
}
class FallStrategy {
// ...
update(tile: Tile, x:number, y:number){
this.falling =
map[y + 1][x].getBlockOnTopState();
this.falling.drop(tile, x, y);
}
}
코드에는 공통 접두사나 접미사가 있는 메서드나 변수가 없어야한다.
클래스를 사용해서 이런 메서드와 변수를 그룹화하는 장점은 외부 인터페이스를 완전하게 제어할 수 있다는 것이다.
가장 중요한 것은 데이터를 숨김으로써 해당 불변속성이 클래스 내에서 관리되게 하는 것이다. 그러면 지역 불변속성이 되어 유지보수하기 쉬워진다.
deposit을 직접 호출하면 인출 없이 돈을 예금할 수 있었던 예를 생각해보자. deposit을 직접 호출하지 않아야 하기 때문에 이 기능을 구현하는 좋은 방법은 두 메서드를 클래스에 넣고 deposit을 비공개로 만드는 것이다.
// 나쁜 코드
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);
}
}
클래스에는 단 하나의 책임만 있어야한다!
단일 책임(특정 주제의 기능 집합)으로 클래스를 설계하려면 원칙과 개요가 필요하다. 이 규칙은 하위 책임을 식별하는 데 도움이 된다.
공통 접사가 암시하는 구조는 해당 메서드와 변수가 공통 접사의 책임을 공유한다는 것을 의미한다. 따라서 이런 메서드는 이 공통 책임을 전담하는 별도의 클래스에 있어야한다.
이 규칙은 또한 애플리케이션이 발전하고 시간이 지남에 따라 책임이 발생할 때 어떤 클래스가 책임을 가지는지 파악하는 데 도움이 된다. 클래스는 시간이 지나면서 큐모가 커질 떄가 많다.
동일한 접사, 메서드 및 변수를 가진 확실한 그룹이 있다.
이것은 이것들을 Player라는 클래스로 옮겨야 함을 시사한다. 이미 Player이라는 클래스가 있지만 완전히 다른 목적을 갖고 있다.
기존 Player클래스를 PlayerTile로 변경하자!
이제 새로운 Player 클래스를 만들자!
class Player { }
// 변경 전
let playerx = 1;
let playery = 1;
// 변경 후
class Player { // 새로운 클래스
private x = 1; // 이름에서 player를 삭제
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[playery][playerx] = new Air();
map[newy][newx] = new PlayerTile();
playerx = newx;
playery = newy;
}
// 변경 후
function moveToTile(newx: number, newy: number){
map[player.getY()][player.geyX()] = new Air(); //접근이 getter로 변경 됨
map[newy][newx] = new PlayerTile();
player.setX(newX); //할당이 setter로 변경됨
player.setY(newY);
}
c 두 개 이상의 다른 메서드에서 오류가 있을 경우 첫 번째 매개변수로 player:Player를 추가하고 인자로 player를 추가해서 새로운 오류를 발생시킨다.
// 변경 전
interface Tile {
//...
moveHorizontal(dx: number):void;
moveVertical(dy:number):void;
}
// 변경 후
interface Tile {
//...
moveHorizontal(player:Player, dx: number):void;
moveVertical(player:Player, dy:number):void;
}
d 한 메서드에서만 오류가 발생할 때까지 반복한다.
e 변수를 캡슐화했기 때문에 변수가 있던 지점에 let player = new Player() 를 넣는다.
let player = new Player();
// 변경 전
interface Tile {
//...
moveHorizontal(dx: number):void;
moveVertical(dy:number):void;
}
// ...
function moveToTile(newx:number, newy:number){
map[playery][playerx] = new Air();
map[newy][newx] = new PlayerTile();
playerx = newx;
playery = newy;
}
// ...
let playerx = 1;
let playery = 1;
// 변경 후
interface Tile {
//...
moveHorizontal(player:Player, dx: number):void;
moveVertical(player:Player, dy:number):void;
}
function moveToTile(newx: number, newy: number){
map[player.getY()][player.geyX()] = new Air(); //접근이 getter로 변경 됨
map[newy][newx] = new PlayerTile();
player.setX(newX); //할당이 setter로 변경됨
player.setY(newY);
}
class Player { // 새로운 클래스
private x = 1; // 이름에서 player를 삭제
private y = 1;
getX() {return this.x;}
getY() {return this.y;}
setX(x:number){this.x = x;}
setY(y:number){this.y = y;}
}
let player = new Player();
클래스를 도입했기 때문에 이제 Player 접사가 있는 모든 메서드를 이 클래스로 이전 할 수 있다. draPlayer를 클래스로 이전하자.
// 변경 전
function drawPlay(palyer: Player, g: CanvasRenderingContext2D)
{
g.fillStyle = "#ff0000";
g.fillRect(
player.getX() * TILE_SIZE,
player.getY() * TILE_SIZE,
TILE_SIZE,
TILE_SIZE);
}
class Player {
// ...
}
// 변경 후
function drawPlay(palyer: 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);
}
}
drawPlayer에서 메서드의 인라인화를 수행한다.
이 새로운 클래스는 getter와 setter를 사용하지 말 것 이라는 새로운 규칙을 위반한다. 따라서 관련 리팩털이 패턴인 getter와 setter 제거하기를 적용한다.
// 변경 전
class Player{
getX() {return this.x};
}
// 변경 후
class Player {
private getX() {return this.x};
}
//변경 전
class Right implements Input {
handle(player: Player){
map[player.getY()][player.getX() + 1]
.moveHorizontal(player, 1);
}
}
class Resting {
//...
moveHorizontal(player:Player, tile:Tile, dx:number){
if(map[player.getY()][plater.getX()+dx + dx].isAir()
&& !map[player.getY() + 1][player.getX()+dx].isAir()) {
map[player.getY()][player.getX()+dx +dx] = tile;
moveToTile(player,player.getX()+dx, player.getY());
}
}
}
/// ...
moveToTile(player, player.getX(), player.getY() + dy);
///...
function moveToTile(player:Player, newx:number, newy:number){
map[player.getY()][player.getX()] = 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);
}
}
/// ...
moveToTile(player, player.getX(), player.getY() + dy);
///...
player.move(0, dy);
function moveToTile(player:Player, newx:number, newy:number){
player.moveToTile(newx, newy);
}
//...
class Player {
//...
moveHorizontal(dx: nmber){
map[this.y][this.x + dx]
.moveHorizontal(this, dx);
}
move(dx:number, dy:numer){
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;
moveToTile(this,this.x+dx, this.y);
}
}
private movetoTile(newX: number, newY:nubmer){
map[this.y][this.x] = new Air();
map[newy][newx] = new PlayerTile();
this.x = newx;
this.y = newy;
}
}
//변경 전
class Player { // 새로운 클래스
//...
getX() {return this.x;}
getY() {return this.y;}
setX(x:number){this.x = x;}
setY(y:number){this.y = y;}
}
//변경 후
class Player { // 새로운 클래스
//...
// 제거
}
변수와 메서드를 캡슐화해서 접근할 수 있는 지점을 제한하고 구조를 명확하게 만들 수 있다.
매서드를 캡슐화하면 이름을 단순하게 만들고 응집력을 더 명확하게 하는 데 도움이 된다.
사람들은 클래스를 만드는 데 너무 소극적이다..
데이터는 속성에 대한 어떤 전제(가정) 가 존재할 수 있다. 이러한 속성은 더 많은 위치에서 데이터에 접근할 수록 유지보수가 어려워진다.
범위를 제한하면 클래스 내의 메서드만 데이터를 수정할 수 있으므로 이런 메서드들만 그 속성에 영향을 줄 수 있게 된다. 불변속성을 검증해야 할 경우 클래스 내부의 코드만 확인하면 된다.
class Counter {}
//변경 전
let counter = 0;
class Counter {}
// 변경 후
class Counter {
private counter = 0;
getCounter() {return this.counter;}
setCounter(c:number){
this.counter = c;
}
}
a. 새 클래스의 인스턴스에 적합한 변수 이름을 선택한다.
b. 접근을 가상의 변수에 대한 getter 또는 setter로 바꾼다.
//변경전
function incrementCounter(){
counter++;
}
function main(){
for(let i = 0; i < 20; i++){
incrementCounter();
console.log(counter);
}
}
//변경 후
function incrementCounter(){
counter.setCounter(counter.cetCounter() + 1); // 할당이 setter로 대체됨
}
function main() {
for(let i = 0; i < 20; i++){
incrementCounter()'
console.log(counter.getCounter());
}
}
c. 2개 이상의 다른 메서드에서 오류가 발생한 경우 이전의 변수명을 가진 매개변수를 첫 번째 매개변수로 추가하고 동일한 변수를 첫 번째 인자로 호출하는 쪽에 넣는다.
//변경 전
function incrementCounter(){
counter.setCounter(counter.getCounter() + 1); // 할당이 setter로 대체됨
}
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());
}
}
d. 한 메서드에서만 오류가 발생할 때 까지 반복한다.
e. 실수로 루프 내에서 클래스를 초기화 할 수 있다. 코드가 루프 내에서 실행되는지 아닌지를 아는 것이 항상 쉽지는 않다. 다음 코드는 컴파일은 되지만 제대로 동작하지는 않는다.
function main() {
for(let i =0; i < 20; i++){
let counter = new Counter(); //잘못된 인스턴스화 위치
incrementCounter(counter);
console.log(counter.getCounter());
}
}
이런 실수를 하지 않도록 변수를 캡슐화했는지 여부를 확인한다. 이 경우 캡슐화했으므로 변수가 있던 지점에서 새로운 클래스를 인스턴스화 한다.
//변경 전
class Counter {...}
//변경 후
class Counter {...}
let counter = new Counter(); // 이전 변수가 있던 위치에서 변수를 인스턴스화
이 책의 게임 코드베이스에는 메서드와 변수에 또 다른 명확한 그룹이 있다.
이것들은 Map 클래스에 있어야 하므로 데이터 캡슐화를 적용한다.
class Map { }
// 변경 전
let map: Title[][];
// 변경 후
class Map {
private map: Tile[][]; // let을 private으로 변경해서 변수를 이동.
getMap() {return this.map;}
setMap(map:Tile[][]){this.map = map;}
}
a Map 클래스의 인스턴스에 적합한 변수 이름을 map으로 선택한다.
b 가상의 변수 map에 getter와 setter로 접근을 바꿉니다.
// 변경 전
function remove(shouldRemove:RemoveStrategy){
for(let y = 0; y < map.length; y++){
for (let x = 0; x < map[y].length; x++)
if(shoudRemove.check(map[y][x]))
map[y][x] = new Air();
}
}
// 변경 후
function remove(shouldRemove:RemoveStrategy){
for(let y = 0; y < map.getMap().length; y++){
for (let x = 0; x < map.getMap[y].length; x++)
if(shoudRemove.check(map.getMap()[y][x]))
map.getMap()[y][x] = new Air();
}
}
c 2개 이상의 다른 메서드에서 오류가 나는 경우 이전의 변수명을 가진 매개변수를 첫 번째 매개변수로 추가하고 같은 변수를 첫 번째 인자로 해서 호출하는 측에 넣어준다.
// 변경 전
interface Tile {
// ...
moveHorizontal(player: Player, dx:number):void;
moveVertical(player: Player, dy:number):void;
update(x:number, y:number):void;
}
// 변경 후
interface Tile {
// ...
// map이 인자로 추가됨
moveHorizontal(map:Map, player: Player, dx:number):void;
moveVertical(map: Map, player: Player, dy:number):void;
update(map:Map, x:number, y:number):void;
}
// map이 여러 곳에 추가됨
d. 한 개의 메서드에서만 오류가 발생할 때까지 반복한다.
e. 변수를 캡슐화했으므로 map이 있던 곳에서 let map = new Map(); 을 넣는다.
let map = new Map();
이제 클래스로의 코드 이관으로 메서드의 이름을 단순화하고 이전에도 여러 번 했던것 처럼 메서드의 인라인화를 사용한다.
// 변경 전
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 {
transformMap(){
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]);
}
}
updateMap(){
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);
}
drawMap(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);
}
}
getter와 setter가 있으므로 제거하기를 수행한다.
// 변경 전
class Falling {
//...
drop(map: Map, tile: Tile, x: number, y:number){
map.getMap()[y + 1][x] = tile;
map.getMap()[y][x] = new Air();
}
}
class Map {
//...
}
// 변경 후
class Falling {
//...
drop(map: Map, tile: Tile, x: number, y:number){
map.drop(tile, x, y) // Map으로 이전된 코드
}
}
class Map {
// ...
drop(tile: Tile, x: number, y:number){
this.map[y + 1][x] = tile;
this.map[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 Map {
//...
}
// 변경 후
class FallStrategy {
// ...
update(map: Map, tile: Tile, x:number, y:number){
this.falling = map.getBlockOnTopState(x, y + 1); // map으로 이전된 코드.
this.falling.drop(map, tile, x,y);
}
}
class Map {
//...
getBlockOnTopState(x:number, y:number){
return this.map[y][x].getBlockOnTopState();
}
}
//변경 전
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()[yhis.y][yhis.x + dx + dx] = 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);
map.getMap()[newy][newx] = new PlayerTile();
this.x = newx;
this.y = newy;
}
}
class Map {
// ...
}
// 변경 후
class Player {
// ...
moveHorizontal(map: Map, dx:number){
map.moveHorizontal(this, this.x, this.y, dx);
}
moveVertical(map: Map, dy:number){
map.moveVertiacal(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 {
// ...
isAir(x: number, y:number){
return this.map[y][x].isAir();
}
setTile(x: number, y:number, tile:Tile){
this.map[y][x] = tile;
}
movePlayer(x:number, y:number, newx:number, newy:number){
this.map[y][x] = new Air();
this.map[newy][newx] = new PlayerTile();
}
moveHorizontal(player:Player, x:number, y:numer,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);
}
}
// 변경 전
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 {
// getMap 제거됨
remove(shouldRemove: RemoveStrategy){
for (let y = 0; y < this.map.length; y++)
for(let x= 0; x < this.map[y].length; x++)
if(shouldRemove.check(this.map[y][x]))
this.map[y][x] = new Air();
}
}
원래의 remove 함수는 이제 한 줄이므로 메서드의 인라인화를 사용한다.
일반적으로 setTile과 같이 강력한 메서드를 공개 인터페이스에 도입하는 것은 바람직하지 않다.
비공개 필드인 map에 대한 완전한 제어를 허락하는 것과 마찬가지이기 떄문이다. 그래도 일단은 코드를 계속 추가한다..
Player.pushHorizontal에 있는 한 줄을 제외한 모든 줄이 map을 사용한다는 것을 알 수 있다. 따라서 코드를 map으로 이전한다.
// 변경 전
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);//Map으로 이전된 코드.
}
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, tile:Tile, 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] = tilel
player.moveToTile(this, x + dx, y);
}
}
}
setTile은 Map 내에서만 사용된다. 그럼 이를 비공개로 설정하거나 더 선호하는 코드 제거를 할 수 있다.
map은 map.transform 호출로 초기화된다. 그러나 객체지향 프로그래밍에서는 초기화를 위한 다른 메커니즘이 있다. 바로 생성자이다.
여기서는 transform을 constructor로 변경하고 transform 호출을 제거한다.
// 변경 전
class Map {
// ...
transform(){
//...
}
}
/// ...
window.onload = () => {
map.transform();
gameLoop(map);
}
// 변경 후
class Map {
// ...
constructor() {
// transform을 생성자로 변경
}
}
window.onload = () => {
// transform 호출 제거
gameLoop(map);
}
돈을 받는 사람의 잔고에서 금액을 더하기 전에 항상 보낸 사람의 잔고에서 먼저 금액을 빼야한다고 생각해보자. 따라서 순서는 금액의 음수 값으로 deposit을 호출한 후 금액의 양수 값으로 deposit을 호출해야한다.
//변경 전
function deposit(to:string, amount:number){
let accountId = database.find(to);
database.updateOne(accountId, {$inc : {blance:amount}});
}
//변경 후
class Transfer { //새로운 클래스
deposit(to:string, amount:number){
let accountId = database.find(to);
database.updateOne(accountId, {$inc : {blance:amount}});
}
}
// 변경 전
class Transfer { //새로운 클래스
deposit(to:string, amount:number){
let accountId = database.find(to);
database.updateOne(accountId, {$inc : {blance: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 : {blance:amount}});
}
}
이제 송금자측의 계좌에서 음수의 amount로 deposit이 먼저 호출된다는 것을 보중할 수 있지만, 더 개선할 수 있다. amount 인자를 필드로 만들고 deposit 메서드에서 amount를 제거하여 두 amount를 연결할 수 있다.
한 번은 음수의 amount가 필요하기 때문에 도우미 메서드를 입한다.
class Transfer {
constructor(from:string, private amount:number){
this.depositHelper(fron, -this.amount);
}
private depositHelper(to:string, amount:number){
let accountId = database.find(to);
database.updateOne(accountId, {$inc : {blance:amount}});
}
deposit(to:string){
this.depositHelper(to, this.amount);
}
}
출금 없이 입금될 수 없다는 것을 보증할 수 있지만 수취인 (to)을 인자로 deposit을 호출하는 것을 잊어버리면 돈이 사라질 수 있다. 따라서 입금도 반드시 발생하도록 이 클래스를 다른 클래스로 감쌀 수도 있다.
열거형은 메서드를 가질 수 없다.
사용하는 언어가 열거형에 대해 메서드를 지원하지 않는 경우 private 생성자를 사용해서 이를 우회한다. 모든 객체는 생성자를 호출해서 생성해야한다.
생성자를 private으로 만들면 클래스 내부에서만 객체를 생성할 수 있다.
// 열거형
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 readoly SMALL = new TShirtSize();
static readoly MEDIUM = new TShirtSize();
static readoly 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";
}
이제 TShirtSize는 클래스이며, 이클래스에 코드를 넣을 수 있따. 불행히도 이 환경에서는 if문을 단순화 할 수 없다. 지난번과 달리 각 값에 대한 클래스가 없고 클래스는 하나만 존재하기 때문이다. 이 장점을 누리려면 클래스로의 타입 코드 대체가 필요하다.
//타입코드 값들을 대체하는 클래스
interface SizeValue { }
class SmallValue implements SizeValue { }
class MediumValue implements SizeValue { }
class LargeValue implements SizeValue { }
이 새로운 클래스를 비공개(privte) 생성자 클래스의 각 값에 대한 인자로 사용한다. 또한 인자를 필드로 저장한다.
// 변경 전
class TShirtSize {
static readoly SMALL = new TShirtSize();
static readoly MEDIUM = new TShirtSize();
static readoly LARGE = new TShirtSize();
private constructor() { }
}
// 변경 후
class TShirtSize {
static readoly SMALL = new TShirtSize(new SmallValue());
static readoly MEDIUM = new TShirtSize(new MediumValue());
static readoly LARGE = new TShirtSize(new LargeValue());
private constructor(private value: SizeValue) { }
}
TshirtSize에 무언가를 추가할 때 마다 SizeValue를 구현한 모든 클래스에 코드를 추가한다.