[번역] Defining classes and inheritance fr. prototype.org linguistics

http://prototypejs.org/learn/class-inheritance


Defining classes and inheritance


이전 버젼의 Prototoype.js 에서는, 클래스 생성을 위해서 Class.create() 메소드를 제공했다.
지금까지 이렇게 생성된 클래스에는, constructor 인 initialize() 메소드가 호출되는 것이 유일한 기능이었다.
Prototype.js 1.6 에서는 - 물론 직전 버전부터 수차례의 단계가 더 있긴 하다 - inheritance 지원이 추가되어,
사용자는 보다 풍부한(?) 클래스를 보다 쉽게 생성할 수 있게 되었다.

Prototype.js 에서의 클래스 생성은 여전히 Class.create() 인 것은 변하지 않는다(변할 수 없다).
또한 이전과 동일하게 class-based 코드를 작성하고 사용할 수 있다.
변한 것은,
property 들을 가져오기 위해서 object 의 prototype 을 건드리거나 Object.extend()를 사용할 필요가 없어졌다는 것이다.

예를 들면,
클래스를 생성하고 상속을 구현하기 위해 이전 버젼과 새로운 버젼으로 작성된 두 코드를 비교해보자.

/** obsolete syntax **/

var Person = Class.create();
Person.prototype = {
  initialize : function(name) {
    this.name = name;
  },
  say : function(message){
    return this.name + ' : ' + message;
  },
};

var guy = new Person('Miro');
guy.say('hi');
// -> "Miro : hi"
var Pirate = Class.create();
// inherite from Person class
Pirate.prototype = Object.extend(new Person(), {
  // redefine the speak method
say : function(message) {
  return this.name + ' : ' + message + ', yarr!';
},
});

var john = new Pirate('Long John');
john.say('ahoy matey');
// -> "Long John : ahoy matey, yarr!"

protytpe 에 직접 접근해야 하고, Object.extend() 를 사용해서 거추장스럽게 상속을 구현하고 있다.
또한, say() 메소드를 재정의한 Pirate 에서는 overriden 메소드를 호출할 방법이 없다.

다음 새로운 코드를 비교해보자.

/** new, preferred syntax **/

// properties are directly passed to 'create' method
var Person = Class.create({
  initialize : function(name) {
    this.name = name;
  },
  say : function(message) {
    return this.name + ' : ' + message;
  },
});

// when subclassing, specify the class  you want to inherit  from
var Pirate = Class.create(Person, {
  // redefine the speak method
say : function($super, message){
  return $super(message) + ', yarr!';
},
});

var john = new Pirate('Long John');
john.say('ahoy matey');
// -> "Long John : ahoy matey, yarr!"

prototype 에 접근하지 않고 class 정의와 동시에 subclassing 하는 것이 얼마나 간편해 졌는지 모른다.
또한 "supercall" -루비에서는 super 키워드로 이미 구현된- 또는 overridden 메소드 호출이 가능해졌다.


이를 이용해서 모듈 mix-in 하는 법

다음은 전형적인 Class.create() 호출 방법이다.

var Pirate = Class.create(Person, { /* instance methods */ } );

사실은, Class.create() 메소드가 한정되지 않은 매개변수를 갖는 것이다(역자 주; 메소드 정의에서는 매개변수가 2개이지만 내부적으로 볼때 그 수가 한정되지 않았다는 뜻). 첫번째 매개변수는 -그 클래스가 또다른 클래스라면- 상속받으려는 클래스이고, 그 외의 매개변수는 instance method 의 형태로 추가된다 -내부적으로는 addMethods()라는 메소드의 호출을 수반한다- . 이것을 이용하면 모듈의 mix-in 을 용이하게 할 수 있다.

// define a module
var Vulnerable = {
  wound: function(hp) {
    this.health -= hp;
    if (this.health < 0) tihs.kill();
  },
  kill : function() {
    this.dead = true;
  },
};

// the first argument isn't a class object, so there is no heritance ...
// simply mix in all the arguments as methods:
var Person = Class.create(Vulnerable, {
  initialize : function() {
    this.health = 100;
    this.dead = false;
  },
});

var bruce = new Person;
bruce.wound(55);
bruce.health; // -> 45


메소드 정의에서 제공되는 $super 키워드

subclass 에서 메소드를 override 하고자 할 때, 그러나 여전히 기존의 메소드의 호출도 가능하게 하고 싶을 때라면, reference 가 필요하다. 이 reference 는, 해당 메소드 앞에 $super 키워드를 사용하면 얻을 수 있다. 그러나 외부에는 Pirate#say 메소드는 단하나의 매개변수만 필요한 메소드로 표출된다. 이러한 세부적인 구현 사항이 개발자의 소스코드의 작동에 영향을 미치지는 않는다.


각종 프로그래밍 언어에서의 상속 구현의 유형들

일반적으로 우리는, 클래스 기반 상속과 프로토타입 상속 -자바스크립트의 유형- 을 구분짓는다. Javascript 2.0 에서 클래스가 제대로 지원되기 전까지, Javascript 는 프로토타입 상속에 제한될 수 밖에 없는 실정이다.
프로토타입 상속, 물론, 굉장히 유용한 특성이긴 하지만, 객체를 다루기에는 좀 번거로운 것이 사실이다. 이것이 우리가 내부적으로 프로토타입 상속을 사용해서 클래스 기반 상속을 흉내내려는 이유이다-루비에서 처럼-. 이것은 확실히 의미가 있는데, instance 에 대해서 보자면, PHP에서는 instance variable 에 초기값을 지정할 수 있다. (역자 주; 이 부분을 제대로 이해하기 위해서는 class-based programming 에 있어서의 instance variable과 class method 에 대한 개념이 필요하다. Javascript : The Definitive Guide 9장 참고.)

class Logger {
  public $log = array();

  function write($message) {
    $this->log[] = $message;
  }
}

$logger = new Logger;
$logger->write('foo');
$logger->write('bar');
$logger->log; // -> ['foo', 'bar']

Prototype 에서도 동일하게 이것이 가능하다.

var Logger = Class.create({
  initialize : function() {  },
  log : [],
  write : function(message) {
    this.log.push(message);
  },
});

var logger = new Logger;
logger.log; // -> []
logger.write('foo');
logger.write('bar');
logger.log; // -> ['foo', 'bar']

이것은 제대로 작동한다. 그러나 만약 새로운 Logger instance 를 만들면?

var logger2 = new Logger;
logger2.log; // -> ['foo', 'bar']

//... hey, the log should have been empty!

목격한 바대로, 새로운 instance 의 빈 배열에 대한 우리의 기대와 상관없이, 이전 logger의 배열과 동일한 배열을 갖게 된다. 실제적으로, value 에 의한 것이 아닌 reference 에 의한 복제가 일어나기 때문에, 모든 logger object 는 동일한 배열 object 를 공유하게 된다.instance 에 초기값을 지정하는 올바른 방법은 다음과 같다.

var Logger = Class.create({
  initialize : function() {
    // this is the right way to do it:
    this.log = [];
  },
  write : function(message) {
     this.log.push(message);
  },
});


class method 정의

Prototype 1.6.0 에서 class method 에 대한 특별한 지원은 없다. 기존 클래스에 대해 간단한 이의 구현은 다음과 같다.

Pirate.allHandsOnDeck = function(n) {
  var voices = [];
  n.times(function(i) {
    voices.push(new Pirate('Sea Dog').say(i+1));
  });
  return voices;
}

Pirate.allHandsOnDeck(3);
// -> ["Sea Dog: 1, yarr!", "Sea Dog: 2, yarr!", "Sea Dog: 3, yarr!"]

만약 한꺼번에 여러개를 정의할 필요가 있다면, 예전과 같이 Object.extend 를 사용하면 된다.

Object.extent(Pirate, {
  song : function(pirates) {...},
  sail : function(crew) {...},
  booze : ['grog', 'rum'],
});

현재로서는, Pirate 로부터 상속을 받아도 class method 는 상속되지 않는다. 이는 다음 버전에서 추가될 예정이다. 그때까지는 수동으로 복제하는 수밖에 없는데, 다만, 다음과 같이 하지는 말자.

var Captain = Class.create(Pirate, {});
// this is wrong!
Object.extend(Captain, Pirate);

클래스 생성자는 Function object 이다. 그리고 만약, 클래스 내의 모든 것을 또다른 클래스로 몽땅 복제한다면, 혼란만 가중될 뿐이다. 썩 좋지는 않지만, 최선의 경우는, subclass들과 그 속성들을 overriding 하는 것이다.


특별한 클래스 속성들

Prototype 1.6.0 에서는 2개의 특별한 클래스 속성 -superclass, subclasses- 이 정의된다. 말그대로, 현재 클래스의 superclass 와 subclass 를 가리킨다.

Person.superclass
// -> null
Person.subclasses.length
// -> 1
Person.subclasses.first() == Pirate
// -> true
Pirate.superclass == Person
// -> true
Captain.superclass == Pirate
// -> true
Captain.superclass == Person
// -> false

보다 쉬운 class introspection 을 위한 속성들이다.


Class#addMethods()

Class#addMethods 는 Prototype 1.6 RC0 에서 Class.extend 였다. 이에 따라 코드를 수정할 것을 공지한다.

추가적으로 메소드를 더하려고 하는 기정의된 클래스가 있다고 가정하자. 자연적으로, 이 추가적인 메소드들이 subclass 및 이미 메모리상에 존재하는 instance 에도 사용가능하길 원한다. 이것은 prototype chain 에 속성을 끼워넣는 것으로 가능한데, Prototype을 사용하는 경우 가장 안전한 방법이 바로 Class#addMethods 를 사용하는 것이다.

var john = new Pirate('Long John');
john.sleep();
// -> ERROR: sleep is not a method

// every person should be able to sleep, not just pirates!
Person.addMethods({
  sleep: function() {
    return this.say('ZzZ');
  },
});

john.sleep();
// -> "Long John: ZzZ, yarr!"

이 sleep 메소드는 새로운 Person instance 들 뿐 아니라, subclass 와 이미 메모리상에 존재하는 instance 들에도 호출가능하다.

끝.