본문으로 바로가기

Tcl의 오브젝트 지향 [incr Tcl]

category 카테고리 없음 2024. 6. 21. 15:04

http://incrtcl.sourceforge.net/itcl

 

[incr Tcl] is the most widely used O-O system for Tcl. The name is a play on C++, and [incr Tcl] provides a similar object model, including multiple inheritence and public and private classes and variables.

 

Tcl/Tk를 사용하면 수십에서 수백 라인으로 작은 윈도우 애플리케이션을 놀랄 만큼 고속으로 개발해 낼 수 있습니다. 그것은 Tcl이 인터프리터 언어이기 때문인 것 외에도, 무엇이든 문자열로 취급하는 커맨드를 만들어내는 유연성과 언어 규칙이 딱딱하지 않고, 예외나 제약이 적기 때문이기도 합니다. 상기의 특징과 반대되는 수백 개의 글로벌 변수, 계속 추가되는 모듈로 인한 데이터 구조등의 문제에 의하여, 대규모 Tcl코드는 마치 미로와 같이 되어버리고, 디버그도 어렵게 되는 상태가 되어 버립니다. 이와 같은 문제점은 비교적 초기부터 Tcl 커뮤니티에 인식이 되어 있었기 때문에, 이것을 극복하기 위해 Tcl의 기본 구문에 오브젝트 지향의 개념을 도입한 패치들의 시도가 있었으며, 그중에 incr Tcl은 1990년대 Tcl/Tk의 보급과 더불어 다양한 용도에 Tcl/Tk가 사용되어지면서, 점점 폭넓게 지지를 받게 되어 전 세계에서 가장 대중적인 Tcl의 확장이 되었습니다. 그 결과, 1997년에 발표된 Tcl/Tk 8.0에 incr Tcl의 namespace개념을 도입하여, 데이터나 코드의 구조화에 크게 기여를 했습니다. incr Tcl은 그 이름에서 알 수 있듯이 C++의 OO(Object Oriented)를 지원합니다. 클래스와 인스탄스, 인스탄스 변수, 메쏘드, public, protected, private, constructor, destructor, 상속, 다중 상속의 개념을 포함하고 있으며, incr Tcl을 사용한 Tcl에서의 OO 지원은 기존의 Tcl 사용자에게는 적잖은 충격이 아닐 수 없습니다. 그 후 incr Tcl의 사용자가 많아지면서, Tcl/Tk 코어 개발진도 이를 의식하여, 현재는 Tcl/Tk 인터프리터의 소스를 수정한 패치가 아닌 확장 패키지 형태로 배포되고 있습니다. 현재 ActiveTcl 에는 incr Tcl(itcl) 이 기본으로 포함되어 있습니다.

Python/Tkiner에 대하여

Tk를 표준 GUI 라이브러리로 채택 하고 있는 스크립트 언어가 있습니다. 그것은 다른 곳에서도 찾아볼 수 없는 초고급 언어인 파이썬(Python)입니다. Python 도 원래의 개발진이 posix 버전과 windows 버전을 병행하여 개발하고 있고, Python 배포판에 이미 Tk를 기본 모듈(Tkiner)로 포함하고 있는 등, Tcl과 닮은 것이 현재의 상태입니다. Python은 비교적 incr Tcl보다 완성도가 높은 OO를 실현하고 있고, Tcl과 마찬가지로 인터프리터 언어이기 때문에 부담이 적으며, GUI 애플리케이션 개발의 재미도 구비하고 있습니다.

incr Tcl/incr Widgets

incr Tcl 외에도 incr Tk, incr Widgets 이 있습니다. incr Tk는 incr Tcl의 Tk 버전이라 생각할수 있으나, 그리 단순한 것이 아닙니다. incr Tcl의 OO 문법으로 표준 Tk의 위젯을 사용하여 윈도우 애플리케이션을 작성하는 것은 어려움이 있을 수 있습니다. incr Tk는 incr Tcl의 OO 문법을 사용하여, 표준 Tk 위젯을 조합시킨 다양하고 복잡한 GUI위젯을 incr Tcl의 OO 개념하에 정의하기 위한 프레임워크입니다. 예를 들면 스크롤바를 붙인 텍스트 위젯을 보통 Tk로는 아래와 같이 작성합니다.

frame .f
scrollbar .f.scrv -orient vertical \
-command ".f.txaa yview"
text .f.txaa -font fixedsys -width 60 \
-yscrollcommand ".f.scrv set"
pack .f.txaa .f.scrv -side left -fill y

이것을 예로 들면 ScrolledText를 incr Tcl 클래스로써 정의해 두고

ScrolledText .st -font fixedsys -width 60
pack .st

라고 한다면 간단하게 GUI를 작성할 수 있습니다. 이 'incr Tcl의 클래스로 정의한다'는 것을 바로 incr Tk라 합니다. 또한 incr Tk의 개발자 자신이, incr Tk가 제공하는 여러 개의 위젯을 사용하여 복잡한 GUI 위젯을 실현하고, 이것을 라이브러리화 한 것이 incr Widgets입니다. incr Widgets의 위젯을 사용 시, incr Tcl의 OO 문법도 incr Tk의 프레임 워크도 알 필요가 없습니다. 하지만 자신만의 GUI 위젯을 제작하고 싶은 경우는 incr Tk의 규칙에 따라 클래스를 정의할 필요가 있으므로, 한편으론 incr Tcl의 OO 문법의 지식도 필요합니다.

incr Tcl의 설치

incr Tcl은 Tcl/Tk의 확장 패키지로써 가장 유명하고, 가장 많이 보급되었습니다. incr Tcl의 posix 버전은 incr Tcl, incr Tk, incr Widgets이 각각 독립된 소스로 제공되고 있으며, windows 용은 컴파일된 Tcl/Tk, incr Tk, incr Tcl, incr Widgets 이 바이너리의 인스톨 형태로 제공되고 있습니다. 또한 Active Tcl에는 incr Tcl의 최신 버전과 incr Widgets의 최신 버전이 기본으로 포함되어 있습니다.

incr Tcl 샘플

incr Tcl의 OO 문법을 간단히 짚어 보겠습니다.

package require Itcl
package require Itk
 
itcl::class Hello {
   variable w
   variable Colors   {red green blue #600060 #006060 #606000}
   variable CurIndex 0
 
   constructor {toplevel title message} {
      set w $toplevel
      toplevel $w
      wm title $w $title
      label $w.laba -text $message -fg [lindex $Colors $CurIndex]
      frame $w.fa
      button $w.fa.cmda -text Push  -command "$this changeColor"
      button $w.fa.cmde -text Close -command "itcl::delete object $this"
      pack $w.fa.cmde $w.fa.cmda -side right
      pack $w.laba $w.fa
   }
 
   destructor {
      destroy $w
   }
 
   method changeColor {} {
      incr CurIndex
      if {$CurIndex == 6} {set CurIndex 0}
      $w.laba configure -fg [lindex $Colors $CurIndex]
   }
}
 
Hello he .hello "Demo Demo" "Hello, World"

incr Tcl 역시 클래스 정의는 class 커맨드를 사용합니다. 첫 번째 인자는 클래스 이름이며, 클래스의 내용은 Tcl의 기본 커맨드인 proc와 마찬가지로 { }으로 감쌉니다. variable 커맨드는 클래스 변수(멤버 변수)를 선언하고 초기화한 것입니다. 변수명의 뒤에 인자를 붙이면 지정된 값으로 초기화됩니다. incr Tcl의 variable 커맨드는 incr Tcl로부터 역 도입된 Tcl의 variable 커맨드의 사용방법과 약간 차이가 있으므로 주의하셔야 합니다. destructor의 인자가 없는 것도 C++ 혹은 java와 같습니다. 인스턴스화는 아래와 같이 합니다.

Hello he .hello "Demo Demo" "Hello, World"

이것을 자바코드로 표현하면 아래와 같습니다.

Hello he = new Hello(.hello, "Demo Demo", "Hello, World");

 

he는 인스턴스 이름입니다. incr Tcl의 인스턴스 이름은 Tcl 변수와는 다른 영역에서 관리되긴 하지만, Tcl변수와 incr Tcl의 인스턴스 이름을 겹쳐 사용하면 혼란스러움을 초래할 수 있습니다. 이를 미연에 방지하지 하기 위한 방법은 아래에서 설명합니다.

클래스와 변수, 메쏘드

오브젝트 지향의 짧은 예제를 소개하기에 알고리즘만큼 좋은 것은 없을 것입니다. 아래의 예제에서 사용한 클래스는 Moroqueue이며, 간단히 소개를 하자면 다음과 같습니다.

  • 임의의 수치나 문자열의 데이터 구조를 관리합니다.
  • 데이터를 추가한 경우, 가장 뒤에 추가합니다.
  • 데이타를 추출한 경우, 가장 앞에서 추출합니다.

Moroqueue는 단순한 큐(queue) 데이타 구조와 다르게 새로운 데이터의 입력 시, 이미 같은 값이 있다면, 그것을 꺼내어 가장 뒤에 추가합니다. 간단한 예제를 보겠습니다.

package require Itcl
 
itcl::class Moroqueue {
   variable Q
   constructor {args} {
      set Q $args
   }
   method enqueue e {
      if {[set p [lsearch -exact $Q $e]] != -1} {
         set Q [lreplace $Q $p $p]
      }
      lappend Q $e
   }
   method dequeue {} {
      set r [lindex $Q 0]
      set Q [lreplace $Q 0 0]
      return $r
   }
   method get {} { return $Q }
}
 
Moroqueue mo 5 6 7
puts [mo get]
mo enqueue 4; puts [mo get]
mo enqueue 8; puts [mo get]
mo dequeue;   puts [mo get]
mo enqueue 7; puts [mo get]
mo dequeue;   puts [mo get]
mo enqueue 8; puts [mo get]
mo dequeue;   puts [mo get]

클래스의 블록 내에서 공통으로 사용할 수 있는 변수는, C++에서는 멤버변수, Java에서는 인스턴스 변수라고 부르며, incr Tcl 은 variable 커맨드로 선언한 후 이를 인스턴스 변수라 부릅니다.

variable profiles
variable filename "untitled"

두 번째와 같이, 선언과 동시에 값의 대입도 가능합니다. 클래스를 인스턴스화할 때 내부적으로 반드시 걸치는 절차를 생성자라 합니다. 생성자는 proc 대용으로 constructor 커맨드를 사용하며, Tcl언어의 프로시져(procedure)와 같은 모양입니다. 생성자는 보통 클래스의 초기화 및 인스턴스 변수의 초기화를 행하도록 정의합니다. 클래스 안에는, 클래스 안에 속한 procedure라는 의미에서, Tcl의 proc를 사용하지 않고, 대신 method 커맨드를 사용합니다. 클래스에 속한 procedure를 메쏘드(method)라 부르며, method의 정의는 Tcl의 문법과 같으나 proc 대신 method 라 사용하는 것만 다를 뿐입니다.

method enqueue e {
   if {[set p [lsearch -exact $Q $e]] != -1} {
      set Q [lreplace $Q $p $p]
   }
   lappend Q $e
}

클래스에서는 단지 몇 개의 변수와, 메쏘드로 데이터 구조나 범위를 지정했을 뿐 이것을 실제로 사용하기 위해서 인스턴스화(오브젝트를 만드는 것) 과정이 필요합니다. 인스턴스화는 incr Tcl에서는 다음과 같이 합니다.

클래스이름 인스턴스이름 인자1 인자2...
예) Moroqueue mo 5 6 7

Moroqueue 클래스를 정의하는 것은, Moroqueue의 이름을 가진 Tcl 커맨드를 만드는 것과 같은 의미입니다. 인스턴스 이름은, incr Tcl의 매뉴얼에서는 오브젝트 핸들(object handle)이라 불리고 있습니다. 메쏘드를 호출하는 방법은 간단히 행할 수 있습니다.

mo enqueue 4

위의 코드는 incr Tcl에서 메쏘드를 호출하는 가장 간단한 예제입니다. 인스턴스 이름에 메쏘드 이름을 붙이고 그 뒤에 인자의 값을 넣어주면 됩니다.

액세스 권한

이번 예제는 종전의 예제와 기능면에서는 같지만, 데이터를 50개까지만 입력 가능 하도록 하고 있습니다.

package require Itcl
 
itcl::class Moroqueue {
   private variable Q
 
   constructor {args} { set Q $args }
 
   private method isfilled {} {
      if {[llength $Q] >= 50} { return 1 } { return 0 }
   }
 
   public method enqueue e {
      if {[isfilled]} {
         error "OOPS! Queue size exceeds maximum."
      }
      if {[set p [lsearch -exact $Q $e]] != -1} {
         set Q [lreplace $Q $p $p]
      }
      lappend Q $e
   }
 
   public method dequeue {} {
      set r [lindex $Q 0]
      set Q [lreplace $Q 0 0]
      return $r
   }
 
   public method get {} {
      return $Q
   }
}
 
set m1 [Moroqueue mo#auto 5 6 7]
set m2 [Moroqueue mo#auto]
puts [$m1 get]
$m1 enqueue 4; puts [$m1 get]
$m1 enqueue 8; puts [$m1 get]
set a [$m1 dequeue]; $m2 enqueue $a
puts "[$m1 get] / [$m2 get]"
$m1 enqueue 7;
puts "[$m1 get] / [$m2 get]"
set a [$m1 dequeue]; $m2 enqueue $a
puts "[$m1 get] / [$m2 get]"
$m1 enqueue 8;
puts "[$m1 get] / [$m2 get]"
set a [$m1 dequeue]; $m2 enqueue $a
puts "[$m1 get] / [$m2 get]"

메쏘드나 인스탄스 변수의 잘못된 액세스를 막기 위해서 C++ 이나 Java는 메쏘드나 인스탄스 변수에 3종류의 액세스 권한을 제공하고 있습니다. 이것은 public, protected, private으로, incr Tcl 역시 이러한 키워드의 의미는 같습니다. 예를 들면, 위에 예에서 메쏘드 enqueue는 public 권한이며, isfilled는 private 권한입니다. 즉, 클래스의 외부에서 아래와 같이 하면 됩니다.

$m1 enqueue 8

또는

set r [$m1 isfilled]

언제 메쏘드를 private으로 하고 public으로 할지는 C++, Java, Python과 동일한 가이드라인에 따라줘야 합니다. 그런데 클래스의 인스턴스를 같은 네이밍 룰로 여러 개 만들고 싶을 때는 수동으로 직접 적어줄 수도 있지만, 아래와 같이 incr Tcl에서 자동으로 붙여주기도 합니다.

set m1 [Moroqueue mo#auto 5 6 7]
set m2 [Moroqueue mo#auto]

오브젝트 핸들 이름의 일부에 #auto를 사용하면, 그 부분이 다른 인스턴스 이름과 겹치치 않는 적당한 숫자로 교체한 후, Moroqueue 커맨드를 리턴 시켜 주기 때문에, Tcl 변수에 세팅한 다음 호출하시면 됩니다.

$m1 enqueue 8

 Tcl 변수와 오브젝트 핸들이 많아진다면 코드 작성에 혼란을 초래할 수 있기 때문에, 보통은 위와 같이 오브젝트 핸들의 이름을 #auto를 사용하여 자동으로 생성되게끔 하여 의식하지 않는 편이 좋을 것입니다. 한편 public, protected, private를 인위적으로 붙이지 않은 경우, 메쏘드는 public, 변수는 private이 디폴트가 됩니다.

Body의 분리

package require Itcl
itcl::class Moroqueue {
   private variable Q
   constructor args {}
   private method isfilled {}
   public method enqueue e
   public method dequeue {}
   public method get {}
}
itcl::body Moroqueue::constructor args { set Q $args }
itcl::body Moroqueue::isfilled {} {
   if {[llength $Q] >= 50} { return 1 } { return 0 }
}
itcl::body Moroqueue::enqueue e {
   if {[$this isfilled]} {
      error "OOPS! Queue size exceeds maximum."
   }
   if {[set p [lsearch -exact $Q $e]] != -1} {
      set Q [lreplace $Q $p $p]
   }
   lappend Q $e
}
itcl::body Moroqueue::dequeue {} {
   set r [lindex $Q 0]; set Q [lreplace $Q 0 0]
   return $r
}
itcl::body Moroqueue::get {} { return $Q }

새로운 기능이 추가될수록 메쏘드의 수가 증가하게 되며, 상당히 긴 라인의 메쏘드도 작성하게 됩니다. 그럴수록 코드의 가독성은 떨어지게 마련이므로, C++과 같이 클래스에는 어떤 변수와 어떤 메쏘드가 있는지만 선언하고, 메쏘드를 외부에서 정의하면 메쏘드를 클래스로부터 독립시킬 수 있어 한층 가독성이 좋을 것입니다. C++의 코드로는 다음과 같습니다.

class Cafe_au_leit {
     float mix(float coffee, float milk);
};
 
float Cafe_au_leit::mix(float coffee, float milk) {
   if(coffee > milk){
      this->palsweet += coffee - milk;
   }
   return coffee + milk;
}

위의 예를 incr Tcl의 문법으로 변경하면 다음과 같습니다.

itcl::class Cafe_au_leit {
   method mix {coffee milk}
}
 
itcl::body Cafe_au_leit::mix {coffee milk} {
   if {$coffee > $milk} {
      incr palsweet [expr $coffee - $milk]
   }
   return [expr $coffee + $milk]
}

body 커맨드를 사용하면 메쏘드를 클래스 내부로부터 분리시킬 수 있습니다. 클래스의 선언과 정의를 분리한 것으로, 클래스의 구조가 상당히 보기 좋아집니다. 추가로 아래와 같이 생성자도 분리할 수 있습니다. 

itcl::body Moroqueue::constructor args { set Q $args }

소멸자(destructor)

이번에는 enqueue, dequeue의 큐 상태를 로그파일에 기록하는 기능을 추가해 보겠습니다.

package require Itcl
 
itcl::class Moroqueue {
   private variable Q
   private variable LogName
 
   constructor {filename args} {
      set LogName $filename
      set fout [open $LogName w]
      puts $fout "*** constructor of object: $this ***"
      close $fout
      set Q $args
   }
 
   destructor {
      set fout [open $LogName a]
      puts $fout "[clock format [clock seconds] -format {%Y/%m/%d %H:%M}]"
      puts $fout "*** destructor of object: $this ***"
      close $fout
   }
 
   private method isfilled {} {
      if {[llength $Q] >= 50} { return 1 } { return 0 }
   }
 
   private method writeLog {} {
      set fout [open $LogName a]
      puts $fout [$this get]
      close $fout
   }
 
   public method enqueue e {
      if {[isfilled]} {
         error "OOPS! Queue size exceeds maximum."
      }
      if {[set p [lsearch -exact $Q $e]] != -1} {
         set Q [lreplace $Q $p $p]
      }
      lappend Q $e
      $this writeLog
   }
 
   public method dequeue {} {
      set r [lindex $Q 0]; set Q [lreplace $Q 0 0]
      $this writeLog
      return $r
   }
 
   public method get {} { return $Q }
}
 
set m1 [Moroqueue mo#auto log.txt 5 6 7]
$m1 enqueue 4;
$m1 dequeue
$m1 enqueue 8;
$m1 enqueue 7;
$m1 enqueue 8;
$m1 dequeue
itcl::delete object $m1

인스탄스를 삭제할시 delete object 커맨드로 삭제합니다. 만약 인스탄스 삭제 시 자동으로 처리하고자 하는 작업이 있다면, 이것을 특수한 블록인 소멸자라고 불리는, incr Tcl로는 destructor 커맨드의 인자에 그 처리를 기술합니다.

destructor {
   set fout [open $LogName a]
   puts $fout "[clock format [clock seconds] -format {%Y/%m/%d %H:%M}]"
   puts $fout "*** destructor of object: $this ***"
   close $fout
}

 

소멸자는 C++이나 Java와 마찬가지로 incr Tcl에서도 인자를 받는 것은 불가능합니다. incr Tcl에서는 delete object를 사용하지 않는다면, 각 인스탄스가 삭제되지 않은 채 프로그램이 종료하게 되어, 소멸자는 실행되지 않습니다. 참고로 간단한 팁을 소개합니다. 메쏘드 이름을 open이나 set과 같은 Tcl 표준 커맨드와 같은 이름을 사용하는 것은 가능하지만, 이 경우 클래스 내에서 open 커맨드를 실행하면 클래스의 메쏘드를 우선적으로 처리하므로, Tcl의 표준 커맨드 open을 실행할 수 없게 됩니다. 이때는 ::open이나 ::set처럼 앞에 ::를 명시적으로 붙여 Tcl의 표준 커맨드를 실행하게 할수 있습니다.

this

incr Tcl도 C++이나 Java와 마찬가지로 this 키워드를 사용할 수 있습니다. 예를 들어, 클래스의 메쏘드내에서 로그저장 기능을 하는 커맨드를 만든후, 자기 자신의 인스탄스 이름(오브젝트 핸들)으로 $this를 사용할수 있습니다. 또한, 메쏘드를 Tcl커맨드나 Tcl프로시져와 구별하기 위해서 $this 를 사용합니다.

$this writeLog

또한 다른 예로 이 this 키워드는 Tk 윈도우를 하나의 클래스로 정의할 때 버튼의 -command 옵션, bind 커맨드의 바인드 처리, after 커맨드로의 등록 처리 시 자신 클래스의 메쏘드를 호출할 때 사용합니다. 이 처리들은 모두 글로벌 레벨(어느 클래스나 프로시져에 속하지 않는)에서 실행되기 때문에, 인스탄스의 이름인 $this를 사용할 필요가 있습니다. 

package require Itcl
 
itcl::class HelloWindow {
   constructor {title} {
      toplevel .g
      wm title .g $title
      label .g.la -text Hello
      button .g.cmda -text Push  -command "$this hello"
     # tk_messageBox -message "$this hello"
      button .g.cmde -text Close -command "$this hide .g"
     #  -command는 글로벌 레벨에서 해석되기 때문에,
     #  반드시 $this가 필요합니다
      pack .g.cmda .g.cmde -side left
   }
 
   destructor { catch {destroy .g} }
   method hello {} { tk_messageBox -message Hello! }
   method hide {w} { wm withdraw $w }
}
 
HelloWindow mt1 "Title";
#end

common

이번 예제는 큐의 사이즈를 시간별로 낮에는 50, 밤에는 10으로 바꾸어줍니다.

package require Itcl
 
itcl::class Moroqueue {
   private variable Q
   private common   QUEUE_LENGTH
 
   set h [clock format [clock seconds] -format %H]
   if {$h >= 9 && $h <= 18} {
      set QUEUE_LENGTH 50
   } else {
      set QUEUE_LENGTH 10
   }
 
   constructor {args} { set Q $args }
 
   private method isfilled {} {
      if {[llength $Q] >= $QUEUE_LENGTH} { return 1 } { return 0 }
   }
 
   private proc isValidValue e {
      return [expr { ("[string trim $e]" == "") ? 0 : 1 }]
   }
 
   public method enqueue e {
      if {! [isValidValue $e]} {
         error "OOPS! Input value '$e' is invalid."
      }
      if {[isfilled]} {
         error "OOPS! Queue size exceeds maximum."
      }
      if {[set p [lsearch -exact $Q $e]] != -1} {
         set Q [lreplace $Q $p $p]
      }
      lappend Q $e
   }
 
   public method dequeue {} {
      set r [lindex $Q 0]; set Q [lreplace $Q 0 0]
      return $r
   }
 
   public method get {} { return $Q }
}
 
set m1 [Moroqueue mo#auto 5 6 7]
set m2 [Moroqueue mo#auto]
puts [$m1 get]
#$m1 enqueue {}
$m1 enqueue 4; puts [$m1 get]
$m1 enqueue 8; puts [$m1 get]
set a [$m1 dequeue]; $m2 enqueue $a
puts "[$m1 get] / [$m2 get]"
$m1 enqueue 7;
puts "[$m1 get] / [$m2 get]"
set a [$m1 dequeue]; $m2 enqueue $a
puts "[$m1 get] / [$m2 get]"
$m1 enqueue 8;
puts "[$m1 get] / [$m2 get]"
set a [$m1 dequeue]; $m2 enqueue $a
puts "[$m1 get] / [$m2 get]"
# end.

위의 코드 중에는 아래와 같이 어느 메쏘드에도 속하지 않는 실행문이 있습니다. 이 부분은 이 스크립트가 인터프리터에 의해 인터프리팅시 한 번만 실행이 됩니다.

set h [clock format [clock seconds] -format %H]
if {$h >= 9 && $h <= 18} {
   set QUEUE_LENGTH 50
} else {
   set QUEUE_LENGTH 10
}

변수를 public로 설정하는 것은 그다지 좋지 않은 방법입니다. 이렇게 되면 구조체에 비교, 클래스의 장점이 없어지기 때문입니다.

class Rectangle {
   public int x, y, width, height;
};
Rectangle rectan = new Rectangle();
rectan.x = 10; rectan.y = 20;

incr Tcl에서도 public의 변수는 클래스의 밖에서 직접 참조하거나 수정하는 것이 가능합니다.

set value [$pm cget -Translation]
$pm configure -Translation value

 

public변수를 클래스의 밖에서 참조하거나 수정할 시 cget과 configure를 사용합니다. 보통 클래스의 인스탄스는 각각 다른 데이터를 갖고 있는 인스탄스의 데이타를 수정시 같은 클래스라도 다른 인스탄스의 데이터에는 영향을 받지 않습니다. 10개의 인스탄스를 만들면, 10개의 데이터가 독립적으로 존재하게 됩니다. 한편으로는 클래스에 대한 각각의 인스탄스라도 한 번만(혹은 공통) 선언하고 싶은 변수나 메쏘드의 정의가 있을 수 있습니다. incr Tcl은 클래스에 대해 한번 정의된 변수는 common variable이라 부르며, variable커맨드 대신 common커맨드를 사용합니다. 이것은 C++, Java의 static 변수와 같은 개념입니다. common variable에도 public이나 protected, private를 사용할 수 있습니다. 보통 common variable은 아래와 같이 정수를 정의하는데도 사용할수 있습니다.

private common LENGTH_MAX 50

클래스에 대해 한번 정의된 메쏘드는 common procedure라 부르며, 이것은 Java의 Class Method(클래스 메쏘드)와 같은 개념입니다. common procedure는 method 커맨드대신, proc커맨드로 정의합니다.

private proc isValidValue e {
   return [expr { ("[string trim $e]" == "") ? 0 : 1 }]
}

common procedure 내에서는 인스탄스 변수를 참조할 수 없고, 메쏘드의 액세스도 불가능합니다. 그래서 인자로 부여받은 값으로 모든 처리를 한 후 결과를 리턴해야 합니다. 이것은 C++이나 Java에서도 클래스 메쏘드의 공통적인 특징입니다.

상속(inheritance)과 오버라이드(override)

이번 예제는 하나의 데이터를 입력하는(enqueue) 클래스 Moroqueue를 상속받아 여러개의 데이타를 한 번에 입력할 수 있는 클래스 Momoroqueue를 정의해 봅니다.

package require Itcl
 
itcl::class Moroqueue {
   protected variable Q
   constructor args { set Q $args }
   public method enqueue e {
      if {[set p [lsearch -exact $Q $e]] != -1} {
         set Q [lreplace $Q $p $p]
      }
      lappend Q $e
   }
   public method dequeue {} {
      set r [lindex $Q 0]; set Q [lreplace $Q 0 0]
      return $r
   }
   public method get {} { return $Q }
}
 
itcl::class Momoroqueue {
   inherit Moroqueue
   constructor args {
      eval Moroqueue::constructor $args
   } {
   }
 
   public method clear {} { set Q {} }
   public method enqueue args {
      foreach e $args { Moroqueue::enqueue $e }
   }
}
 
Momoroqueue mo 5 6 7
puts [mo get]
mo clear
mo enqueue 4 7 2; puts [mo get]
mo enqueue 8 5; puts [mo get]
mo dequeue;   puts [mo get]
mo enqueue 7; puts [mo get]
mo dequeue;   puts [mo get]
mo enqueue 8; puts [mo get]
mo dequeue;   puts [mo get]
# end.

기존에 있던 클래스의 구조를 상속한 후 새로운 메쏘드를 추가하거나, 기존의 메쏘드를 수정하여 새로운 클래스를 만드는 것을 상속(inheritance)이라 합니다. 이 상속은 OO 프로그래밍에서는 가장 어려운 개념 중의 하나입니다. incr Tcl 역시 C++이나 Python처럼 상속을 지원합니다. 위의 예에서는 Momoroqueue 클래스가 Moroqueue 클래스를 상속받고 있습니다. 이것은 Momoroqueue 클래스의 첫 행에 inherit라고 하는 커맨드로 Moroqueue 클래스를 상속받습니다.

inherit Moroqueue

본래의 클래스를 기본 클래스(base class), 상속받은 클래스를 파생 클래스(derived class)라고 합니다. 상속받은 파생 클래스에는 기본클래스의 public 또는 protected가 붙어있는 매쏘드가 자동으로 포함됩니다. 위의 예인 Momoroqueue 클래스에는 Moroqueue 클래스의 enqueue, dequeue, get 등의 메쏘드가 자동으로 포함이 되어지는 것입니다. 기본 클래스로부터 상속받은 완전히 같은 이름의 메써드를 파생 클래스에서 다시 정의하고 수정하는 것도 가능합니다. 이것을 오버라이드(override)라 부르며, incr Tcl은 기본 클래스의 메쏘드와 같은 이름의 메쏘드를 정의하는 것 만으로 오버라이드가 됩니다. 위의 예에서는 enqueue 메쏘드를 오버라이드 하는 것 만으로 여러 개의 데이터를 한 번에 입력 가능하게 하고 있습니다. 다른 클래스를 상속받아 만든 클래스의 생성자는 인자를 두 개이상 줄 수 있습니다. 첫 번째 인자는 기본 클래스의 생성자 인자값을 지정하고, 두 번째 인자는 이 클래스의 독자적인 초기화 처리를 기술합니다.

constructor args {
   eval Moroqueue::constructor $args
} {
}

이것을 C++로 표현하면 아래와 같습니다.

Momoroqueue::Momoroqueue(char* args):
   Moroqueue(args)
{
}

그런데 메쏘드의 오버라이드는 가능하지만, incr Tcl에서는 메쏘드의 오버로드(overload)(같은 이름으로 인자가 다른 여러 메쏘드를 정의하는 것)는 제공하고 있지 않습니다. 이유는 Tcl 언어의 특성 때문인데 다음과 같습니다.

  • 인자의 디폴트값을 지정할 수 있다.
  • 인자의 수가 부정확한 인자 args를 사용할 수 있다.

이것으로 충분한 오버로드와 같은 역할을 할수 있기 때문입니다. 오버라이드는 기본 클래스의 메쏘드를 완전히 덮어쓰는 것이지만, 클래스이름::메쏘드이름 으로 호출하면, 기본 클래스의 메쏘드를 명시적으로 호출하는 것도 가능해집니다. 또한 incr Tcl은 보통 현재 클래스의 메쏘드를 사용하는 것으로 정해집니다.

set a [A xxx#auto]
$a method1
set a [B xxx#auto]
$a method1

변수 $a의 인스탄스 메쏘드 method1 이 어느 클래스의 매쏘드인지는, 항상 인스탄스가 생성된 시점의 클래스로 정해집니다.

다중상속(multiple inheritance)

캐시 혹은 가상기억이라 하는 것은, 제한 되어진 영역의 메모리상에 최근 사용된 디스크로부터 메모리상에 내용을 저장해 둠으로 2회 이상의 액세스시 고속화 시키는 방법입니다. 제한되어 있는 영역이 가득 찬 상태에 디스크로부터 새로운 내용을 받아온 경우에는, 제한되어 있는 영역상에 들어있는 것 중 가장 오래된 데이터를 내보내고(페이지 아웃), 그곳에 새로운 내용을 쓰는 것을 LRU (Least Recently Used)라고 합니다. 이번 예제는 제한된 영역 이상에 enqueue 할시 자동적으로 페이지 아웃하여 새로운 내용을 갱신하는 시뮬레이션을 하고 있습니다.

package require Itcl
 
itcl::class CounterArray {
   private variable hash
   constructor {} { array set hash {} }
 
   public method incrCount {key {value 1}} {
      if {[catch {
         incr hash($key) $value
      } e]} { set hash($key) $value }
   }
 
   public method getCount key {
      if {[catch {
         set r $hash($key)
      }]} { set r 0 }
      return $r
   }
}
 
itcl::class Moroqueue {
   protected variable Q
   constructor args { set Q $args }
   public method enqueue e {
      if {[set p [lsearch -exact $Q $e]] != -1} {
         set Q [lreplace $Q $p $p]
      }
      lappend Q $e
   }
   public method dequeue {} {
      set r [lindex $Q 0]; set Q [lreplace $Q 0 0]
      return $r
   }
   public method get {} { return $Q }
}
 
itcl::class PageMemory {
   inherit Moroqueue CounterArray
   private common PAGE_MAX 4
 
   constructor args {
      eval Moroqueue::constructor $args
      CounterArray::constructor
   } {
   }
 
   public method enqueue e {
      incrCount "total"
      if {[set p [lsearch -exact $Q $e]] != -1} {
         set Q [lreplace $Q $p $p]
         incrCount "cacheHit"
      } elseif {[llength $Q] >= $PAGE_MAX} {
         incrCount "pageOut"; dequeue
         lappend Q $e
      } else {
         lappend Q $e
      }
   }
 
   public method getResult {} {
      return [list [getCount "cacheHit"] [getCount "pageOut"]\
      [getCount "total"]]
   }
}
 
set pm [PageMemory pm#auto 5]
$pm enqueue 4
$pm enqueue 6
$pm enqueue 2
$pm enqueue 3
$pm enqueue 1
$pm enqueue 4
$pm enqueue 1
$pm enqueue 2
$pm enqueue 5
$pm enqueue 2
set result [$pm getResult]
puts "힛트 [lindex $result 0]회 / 페이지아웃 [lindex $result 1]회"
# end.

클래스를 상속받은 파생 클래스에서 소멸자를 정의하면, 기본 클래스의 소멸자(destructor)가 실행되고 + α 클래스의 소멸자가 실행됩니다. 이것은 지금까지의 예와 마찬가지로 C++ 혹은 자바와 같이 기본 클래스에서 파생 클래스 순서로 실행됩니다. 이번의 Momoroqueue 클래스는 두 개의 클래스를 inherit로 상속하고 있습니다. 이와 같이 incr Tcl은 다중상속(multiple inheritance)을 지원하고 있습니다. 다중 상속 시에는 inherit 커맨드로 상속 본래의 클래스 이름을 왼쪽부터 시작해 중요한 순서로 나열합니다. 여러 개의 클래스를 상속한 경우의 변수나 매쏘드의 우선순위는 왼쪽부터 나열된 클래스가 우선합니다.

constructor args {
   eval Moroqueue::constructor $args
   CounterArray::constructor
} {
}

 

다중상속에 의한 파생 클래스의 생성자는 위의 코드와 같이 기본 클래스의 생성자를 순서대로 클래스명:: 을 붙여 쓰고 나열합니다. 위의 예에서는 하나씩 증가되는 각종 카운터의 해시 변수를 관리하는 ConuterArray 클래스를 상속하여, 캐시 히트수의 횟수, 페이지 아웃의 횟수, 합계 횟수를 괸리하고 마지막으로 getResult 매쏘드로 그 결과를 얻고 있습니다. 자바에는 다중상속이 없는 대신 인터페이스(interface)를 지원하고 있습니다. incr Tcl은 추상 메쏘드(C++의 순수 가상함수) 자체가 없기 때문에, 추상 메쏘드의 집합체인 인터페이스도 없다고 할 수 있습니다. 본래 변수 타입이 없는 incr Tcl은, 추상 메쏘드나 인터페이스의 의미가 없기 때문에 이들을 지원하지 않습니다.