ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Programming Ruby (3) 클래스, 객체, 변수
    Ruby 2016. 10. 19. 23:10
    [출처] Programming Ruby 
    (본 게시물은 저작권의 문제 발생시 출판사의 요청에 의해 삭제될 수 있습니다.)





    3.. 클래스, 객체 변수 


    클래스 선언

    루비에선 클래스 이름은 대문자로 시작해야 하며, 메소드 이름은 소문자로 시작해야 한다.

    class BookInStock
    end

    클래스의 인스턴스를 생성할 때는 new 메서드를 사용해야한다.

    a_book = BookInStock.new
    another_book = BookInStock.new 

    클래스의 생성자는 객체가 생성되는 시점에 생성하고자 하는 객체의 특정한 상태를 저장할 수 있다. 이러한 상태는 객체의 인스턴스 변수로 저장된다. (인스턴스 변수는 @로 시작된다.)

    class BookInStock
         def initialize(isbn, price)
              @isbn = isbn
              @price = Float(price)
         end 
    end


    BookInStock.new를 호출하면 루비는 초기화 되지 않은 객체를 메모리에 할당하고 new의 매개 변수를 이용해 그 객체의 initialize 메서드를 호출한다.

    주의할 점은 매개 변수는 지역 변수와 같은 스코프를 가지므로 initialize 메서드가 끝나면 함께 사라져 버린다.

    @isbn과 isbn은 연관이 있어 보이지만 아무런 연관이 없다. 전자는 인스턴스 변수로 @의 이름의 일부이다.


    puts 메서드는 프로그램의 표준 입출력에 문자열을 출력한다. puts에 어떤 클래스에서 생성된 객체를 인자로 넘겨주면, puts는 이를 어떻게 다뤄야 할지 모르므로 가장 간단한 방법으로 처리한다.

    puts 객체 인스턴스 // #<객체이름:16진수고유번호>

    객체를 문자열로 나타내는 표준 메시지은 to_s가 있다. puts에 객체가 넘겨지면 puts는 내부적으로 넘겨받아서 to_s를 호출하고 이 결과를 출력한다. to_s 메서드를 재정의한 예제이다.

    class BookInStock
         def initialize(isbn, price)
              @isbn = isbn
              @price = Float(price)
         end 
         def to_s
              “ISBN: #{@isbn}, price: #{@price}"
         end
    end

    b1 = BookInStock.new(“isbn1”, 3)
    puts b1

    실행결과
    ISBN : isbn1, price: 3.0

    매우 사소하면서도 중요한 사실은 객체를 초기화하면서 정의한 @isbn과  @price 인스턴스 변수들은 to_s 메서드에서 참조가 가능하다. 이러한 인스턴스 변수들은 각 객체에 저장되며 객체에 정의되는 모든 인스턴스 메서드에서 참조가 가능하다.


     
    3.1.  객체와 속성 

    객체의 내부가 외부에 노출되는 부분을 객체의 속성(attribute)라고 부른다. 객체 외부에서 객체 상태에 접근하거나 조작하는 메서드를 별도로 정의해서 외부에서도 객체 상태에 접근 가능하도록 만들어 줄 수 있다.

    class BookInStock
         def initialize(isbn, price)
              @isbn = isbn
              @price = Float(price)
         end 
         def isbn
              @isbn
         end
         def price
              @price 
         end
         # ...
    end


    book = BookInStock.new(“isbn1”, 12.34)
    puts “ISBN = #{book.isbn}"
    puts “Price = #{book.price}"

    실행결과
    ISBN = isbn1
    Price = 12.34


    isbn 메서드는 인스턴스 변수 @isbn에 저장된 정보를 반환한다. (루비에서 기본적으로 메서드 마지막에 평가된 표현식의 평가 결과를 반환한다.)

    attr_reader 메서드는 앞에서 작성한 것과 같이 접근자 메서드를 대신 생성해준다. 

    class BookInStock
         
         attr_reader :isbn, :price

         def initialize(isbn, price)
              @isbn = isbn
              @price = Float(price)
         end 
         # ...
    end

    book = BookInStock.new(“isbn1”, 12.34)
    puts “ISBN = #{book.isbn}"
    puts “Price = #{book.price}"

    실행결과
    ISBN = isbn1
    Price = 12.34


    처음으로 사용된 심벌(“:변수명") 표현을 볼 수 있다. 심벌은 단순히 이름을 참조할 때 사용할 수 있는 편리한 방법이다. :isbn은 isbn이라는 이름을 지칭하며, 콜론 없이 isbn을 사용하면 이는 이 변수의 값 자체를 의미한다. 이 예제에서 접근자 메서드 이름으로 isbn, price를 사용하고 있다. 여기에 대응하는 인스턴스 변수는 각각 @isbn과 @price다. 

    attr_reader 를 통해 생성된 접근자 메서드는 바로 앞 예제에서 직접 작성했던 인스턴스 변수를 반환하는 메서드와 완전히 같다. 


    쓰기 가능한 속성

    객체 밖에서 속성을 설정해야 하는경우 메서드 이름 뒤에 =기호를 사용해 대입 기능을 구현할 수 있다. 

    class BookInStock
         
         attr_reader :isbn, :price

         def initialize(isbn, price)
              @isbn = isbn
              @price = Float(price)
         end 
         
         def price=(new_price)
              @price = new_price
         end 
         #...
    end

    book = BookInStock.new(“isbn1”, 12.34)
    puts “ISBN = #{book.isbn}"
    puts “Price = #{book.price}"
    book.price = book.price * 10
    puts “New price = #{book.price}"

    실행결과
    ISBN = isbn1
    Price = 12.34
    New price = 123.4


    이름이 =로 끝나는 메서드를 정의하면 = 앞의 메서드 이름을 대입문의 좌변에 사용할 수 있게 된다.


    인스턴스 변소의 값을 속성으로 읽는 것과 대입하는 것을 한 번에 정의해주는 attr_accessor 메서드를 제공한다.



    class BookInStock
         
         attr_reader :isbn
         attr_accessor :price

         def initialize(isbn, price)
              @isbn = isbn
              @price = Float(price)
         end 
         #...
    end

    book = BookInStock.new(“isbn1”, 12.34)
    puts “ISBN = #{book.isbn}"
    puts “Price = #{book.price}"
    book.price = book.price * 10
    puts “New price = #{book.price}"

    실행결과
    ISBN = isbn1
    Price = 12.34
    New price = 123.4



    가상속성

    가상 속성에 대한 대입이 가능한 예제를 살펴보자. 이는 가상 속성값을 인스턴스 변수에 매핑하는 방법으로 이루어진다.



    class BookInStock
         
         attr_reader :isbn
         attr_accessor :price

         def initialize(isbn, price)
              @isbn = isbn
              @price = Float(price)
         end 
         
         def price_in_cents
              Integer(price*100 + 0.5)
         end

         def price_in_cents=(cents)
              @price = cents / 100.0
         end
         #...
    end


    book = BookInStock.new(“isbn1”, 33.80)
    puts “Price = #{book.price}"
    puts “Price in cents = #{book.price_in_cents}"
    book.price_incents = 1234
    puts “Price = #{book.price}"
    puts “Price in cents = #{book.price_in_cents}"

    실행결과
    Price = 33.8
    Price in cents = 3380
    Price = 12.34
    Price in cents = 1234 


    위의 예제에서는 속성 메서드를 사용해서 가상 인스턴스 변수를 생성한다. price_in_cents는 다른 속성들과 마찬가지로 객체의 속성으로 보이지만 내부적으로 이 속성에 대응하는 인스턴스 변수는 존재하지 않는다. 

    단일 접근 원칙(Uniform Access Principle)이라는 말로 표현된 바 있는데 인스턴스 변수와 계산된 값의 차이점을 숨겨서, 클래스 구현에서 나머지 세상을 보호할 수 있는 방법 을 제공한다. 다시말해 만든 클래스를 사용할 수백만 줄의 코드에 영향을 주지 않고 내부 구현을 바꿀 수 있게 된 것이다. 


    속성, 인스턴스 변수, 메서드

    속성은 단지 메서드일 뿐이다. 속성은 단순히 인스턴스 변수의 값을 반환한다. 때때로 속성은 계산의 결과를 반환하기도 한다. 그리고 이름 끝에 등호를 달고 메서드를 만들어 객체의 상태를 바꾸는 용도로 사용하기도 한다. 여기서 질문은.. 어디까지가 속성이고 어디까지가 일반 메서드인가 하는 것이다. 사실 속성을 일반메서드와 구분짓는 것은 쓸데없는 논쟁이다.

    클래스를 설계할 때 내부적으로 어떤 상태를 가지고 이 상태를 외부에 어떤 모습으로 노출할지 결정해야 한다. 내부상태는 인스턴스 변수에 저장한다. 외부에 보이는 상태는 속성이라고 부르는 메서드를 통해야만 한다. 그 밖에 클래스가 할 수 있는 모든 행동은 일반 메서드를 통해야 한다. 



    3.2. 다른 클래스와 함께 사용하기 

    CSV 데이터를 읽어 들여 통계를 내고 요약하는 클래스를 정의한다.

    class CsvReader
         def initialize 
              # ...
         end 

         def read_in_csv_data(csv_file_name)
              #...     
         end

         def total_value_in_stock
              # ...
         end

         def number_of_each_isbn
              # ...
         end
    end

    이 클래스는 다음과 같이 호출할 수 있다.

    reader = CsvReader.new
    reader.read_in_csv_data(“file1.csv”)
    reader.read_in_csv_data(“file2.csv”)
    ...
    pus “Total value in stock = #{reader.total_value_in_stock}"

    읽어들인 reader객체에 넘겨들인 값을 인스턴스 변수의 배열에 저장한다. 

    csv를 읽어들이는 CSV 라이브러리가 헤더라인에 있다고 가정하고 반복적으로 읽어들여 이름으로 값을 뽑아낸다.

    class CsvReader
         def initialize
              @books_in_stock = []
         end

         def read_in_csv_data(csv_file_name)
              CSV.foreach(csv_file_name, headers: true) do |row|
                   @books_in_stock << BookInStock.new(row[“ISBN”], row[“Price"])
              end
         end
    end

    CSV 라이브러리를 이용해 파일을 연다 headers:true 옵션은 파일의 첫 번째 행을 각 열의 이름으로 분석할지 여부를 나타낸다.
    CSV 라이브러리는 파일의 내용을 읽어오며 각각의 줄을 블록(do와 end 사이의 코드)에 넘겨준다. 블록에서는 ISBN과 Price 컬럼의 데이터를 읽어들여 BookInStock 객체를 생성한다. 이렇게 생성한 객체를 @books_in_stock 인스턴스 변수에 더한다. 


    코드 조각들을 통제하는 프로그램을 만들어보자 첫번째 파일은 book_in_stock.rb 파일이고 BookInStock 클래스의 정의를 포함한다. 두 번째는 csv_reader.rb 이다 이 파일에는 CsvReader 클래스가 정의되어 있다. 메인프로그램은 stock_start.rb이다.

    book_in_stock.rb 부터 살펴보자

    class BookInStock
         attr_reader :isbn, :price

         def initialize(isbn, price)
              @isbn = isbn
              @price = price 
         end
    end


    다음은 csv_reader.rb이다 두개의 외부 라이브러리에 의존한다. CSV 라이브러리와 book_in_stock.rb 파일이다. 
    루비는 외부 파일을 읽어들이는 헬퍼 메서드를 제공한다. require는 루비의 CSV 라이브러리를 읽어들이고 require_relative를 사용해 상대경로의 파일을 읽어들인다.

    csv_reader.rb파일을 살펴보자

    require ‘csv'
    require_relative ‘book_in_stock'

    class CsvReader
         def initialize
              @books_in_stock = []
         end
         
         def read_in_csv_data(csv_file_name)
              CSV.foreach(csv_file_name, headers: true) do |row|
                    @books_in_stock << BookInStock.new(row[“ISBN”], row[“Price"])
               end   
         end

         def total_value_in_stock # 뒤에서 inject를 통해 어떻게 합계를 구하는지 살펴본다.
              sum = 0.0
              @books_in_stock.each {|book| sum += book.price}
              sum
         end

         def number_of_each_isbn
              #...
         end 
    end

    마지막으로 메인 프로그램 stock_stats.rb는 다음과같다.

    require_relative ‘csv_reader'

    reader = CsvReader.new

    ARGV.each do |csv_file_name| 
         STDERR.puts “Processing #{csv_file_name}"
         reader.read_in_csv_data(csv_file_name)
    end

    puts “Total value = #{reader.total_value_in_stock}"



    이 프로그램은 정말 세 개의 파일이나 필요할까?? 꼭 그렇진 않다. 
    하지만 프로그램이 커지기 시작하면서 하나의 파일에서 모든 걸 다루기가 어려워진다. 하나의 덩어리로 되어 있는 코드는 자동화된 테스트를 작성하기가 어렵다. 모든 클래스가 하나의 파일에 포함되어 있다면 클래스 재사용이 불가능해진다. 





    3.3. 접근제어

    클래스 인터페이스를 설계할 때 클래스 외부에 어느 정도까지 노출할지 결정하는 것은 중요한 일이다. 너무 깊이 접근하도록 허용하면 애플리케이션에서 각 요소 간의 결합도가 높아질 우려가 있다. 다시 말해 클래스의 사용자 코드는 클래스 내부 구현의 세세한 부분에까지 종속이 되기 쉽다는 것이다. 다행스럽게도 루비에서 객체 상태를 변경하는 방법이 메서드 호출하는 것 뿐이라는 점이다. 그말은 곧 메서드 접근을 적절히 설정하면 객체에 대한 접근 제어가 가능하다.
    루비에는 세가지 보호 단계가 있다. 

    public 메서드는 누구나 호출할 수 있으며 접근 제어를 하지않는다. 루비에서 메서드는 기본적으로 public이다.

    protected 메서드는 그 객체를 정의한 클래스와 하위 클래스에서만 호출할 수 있다.

    private 메서드는 수신자를 지정해서 호출할 수 없다. 이 메서드의 수신자는 항상 self 이기 때문이다. private는 오직 현재 객체의 문맥 하에서만 호출 할 수 있다. 다른 객체의 private 메서드에서는 접근할 수 없다.


    protected이면 정의한 클래스나 하위클래스의 어떤 인스턴스에서도 호출할 수있다. private는 객체 그 자신에서만 호출할 수 있다. 

    루비와 다른 객체지향 언어의 차이점은 접근 제어가 동적이라는 점이다. 정적이 아니라 프로그램이 실행 될 때 결정된다. 


    접근 제어 기술하기


    class MyClass
         def method1      # 기본값은 ‘public’이다 
              #...
         end

    protected               # 이제부터 선언하는 메서드는 모두 ‘protected’가 된다.
         def method2     # ‘protected’ 메소드 
              # ...
         end

    private                    # 이제부터 선언하는 메서드는 모두 ‘private’가 된다.
         def method3     # ‘private’ 메서드
              # ...
         end

    public                     # 이제부터 선언하는 메서드는 모두 ‘public’이 된다.
         def method4     # ‘public’ 메서드 
              # … 
         end
    end

    다른 방법으로는 접근 제어 함수 뒤인자로 메서드 이름을 써준다. 이미 정의된 메서드의 접근 단계도 다시 정의할 수 있다.

    class MyClass
         def method1
         end
         def method2
         end

         #...

         public :method1, :method4
         public :method2
         public :method3
    end




    3.4. 변수 

    루비에서 변수는 객체가 아니다. 변수는 단순히 객체의 참조를 가지고 있다.

    person1 = “Tim"
    person2 = person1
    person1[0] = ‘J'

    puts “#{person1}"
    puts “#{person2}"

    실행결과
    Jim
    Jim


    위와같이 변수는 단순히 객체에 대한 참조를 가지고 있을뿐 객체 자체를 가지고 있지 않는다. person1을 person2에 대입해도 새로운 객체는 생성되지 않는다. 단지 person1 객체에 대한 참조를 person2로 복사해서 person1과 person2가 같은 객체를 참조하도록 만들 뿐이다. 

    String의 dup메서드는 같은 내용을 담은 문자열 객체를 새로 생성할 수 있다.
    freeze 메서드는 는 객체 수정을 막는다. freeze 메서드 호출후 객체를 수정하려고 하면 RuntimeError 예외를 발생시킨다.

    3장 끝


    'Ruby ' 카테고리의 다른 글

    Programming Ruby (6) 열거자와 외부 반복자  (0) 2016.10.24
    Programming Ruby (5) 반복자  (0) 2016.10.24
    Programming Ruby (4) 배열, 해시, 블록  (0) 2016.10.22
    Ruby Naming Guide - 루비 네이밍 가이드  (0) 2016.10.17
    What is Ruby  (0) 2016.10.06
Designed by Tistory.