-
[스칼라] 상속과 구성스칼라 2022. 2. 3. 00:03
Programming in scala 4th edition을 읽고 정리한 글입니다.
1. 2차원 레이아웃 라이브러리
상속, 구성, 추상클래스, 오버로드 등을 설명하기 위해서 2차원 레이아웃 라이브러리를 예시로 설명한다. 이 라이브러리는 elem이라는 팩토리 메서드를 통해 Element 객체를 생성한다. Element 객체는 각 요소를 가지고 있고 above나 beside같은 메서드를 호출해서 요소들을 연산할 수 있다. 예를 들어서 아래와 같은 표현식은 높이 2인 열 2개를 만든다.
val column1 = elem("hello") above elem("***") val column2 = elem("***") above elem("world") column1 beside column2 // 출력 결과 hello *** *** world
2. 추상클래스
가장 먼저 Element를 정의한다. Element 클래스는 String의 배열로 이루어진 contents를 갖는다.
abstract class Element { def contents: Array[String] }
contents는 구현체가 없는 추상메서드이므로 class 앞에 abstract 키워드를 붙여 추상클래스로 만든다. 추상 멤버가 있는 클래스는 추상클래스여야만 한다. 추상 클래스는 객체로 만들 수 없다. 메서드의 경우에는 따로 키워드를 붙여줄 필요 없이 구현이 없으면 추상 메서드이다.
3. 파라미터 없는 메서드 정의
다음은 Element 에 height와 width를 추가한다. height는 contents가 몇 줄인지, width는 첫번째 줄의 길이를 반환한다. height 가 0이면 width 도 0이다.
abstract class Element { def contents: Array[String] def height = contents.length def width = if(height == 0) 0 else contents(0).length }
width처럼 괄호 없이 정의된 메서드를 파라미터 없는 메서드 (parameterless method)라고 한다. width()처럼 괄호를 붙여 빈 괄호 메서드(empty-paren method)로 선언해도 아무 문제가 없긴 하다. 관례적으로 메서드가 아무 파라미터도 받지 않으면서 객체의 상태 변화도 일으키지 않을 경우 파라미터 없는 메서드로 사용한다. height, width를 함수로 선언한 부분은 메서드 대신 필드로 정의해도 문제없다.
abstract class Element { def contents: Array[String] val height = contents.length val width = if(height == 0) 0 else contents(0).length }
클라이언트 입장에서는 메서드로 정의되어있으나 필드로 정의되어있으나 똑같은 코드로 접근할 수 있다. 이런 특징은 단일 접근 원칙에 부합한다. (단일 접근 원칙: 메서드나 필드 중 어떤 방식으로 멤버가 정의되어있더라도 클라이언트의 코드에는 영향을 끼치면 안된다.)
위의 두 경우에 장단점은 다음과 같다. val으로 선언할 경우 객체가 메모리를 좀 더 사용하긴 하지만 계산결과값을 저장해두는 장점이 있고, def로 선언할 경우 메모리는 아끼지만 메번 결과를 계산한다는 단점이 있다.
원칙적으로 부수효과가 있는 함수의 경우는 ()를 붙여서 콜해주고 단순 프로퍼티에 접근하는 메서드의 경우에는 괄호를 생략한다.
4. 클래스의 확장
이제 Element는 준비되었으니 이를 구체화한 클래스를 작성해보도록 한다.
class ArrayElement(conts: Array[String]) extends Element { def contents: Array[String] = conts }
Element 를 상속받아 ArrayElement를 구현하면 ArrayElement는 Element의 서브타입이 되고 private이 아닌 멤버들을 모두 물려받는다.
5. 메서드와 필드 오버라이드
스칼라에서는 필드와 같은 메서드를 선언할 수 없다. 아래와 같은 클래스 선언은 불가능하다.
class ArrayElement(conts: Array[String]) extends Element { def contents: Array[String] = conts val contents = "String" }
6. 파라미터 필드 정의
ArrayElement에는 conts 클래스 파라미터가 있다. 이 클래스파라미터는 클래스 멤버에 넘겨주기위한 역할을 한다. 필요하다면 파라미터 필드로 정의해서 이런 불필요한 코드 중복을 피할 수도 있다.
class ArrayElement(val contents: Array[String]) extends Element { }
파라미터 필드에도 private, protected, override 같은 키워드를 활용할 수 있다.
7. 슈퍼클래스 생성자 호출
한 줄짜리 문자열로 이뤄진 LineElement를 구현해보자
class LineElement(val s: String) extends ArrayElement(Array(s)) { override def width = s.length override def height = 1 }
한 줄 짜리 문자열로 이루어진 레이아웃이므로 width, height를 override해서 지정해주었다. 그리고 클래스 파라미터로 받은 스트링은 슈퍼클래스의 주 생성자로 바로 넘겨주었다. 슈퍼클래스의 생성자를 활용하고싶으면 위의 예시처럼 extends 키워드 뒤에 슈퍼클래스 주 생성자를 호출해주면 된다.
8. Override 수식자 사용
스칼라에서는 부모 클래스의 구체적 멤버를 오버라이드 하는 모든 멤버에는 override 수식어를 붙어야한다. (추상 멤버일 경우 생략 가능) 만약 슈퍼클래스의 구체 멤버와 이름이 같은데 서브클래스에서 override로 수식자를 넣어주지 않았을 경우 에러가 발생한다. 이런 규칙은 이미 배포된 슈퍼클래스에 어떤 메서드가 추가되었고, 어떤 서브클래스에서 같은 이름으로 이미 메서드를 사용중일경우 의도대로 코드가 업데이트 되지 않는 버그(우연한 오버라이드)를 막아준다.
8. 다형성과 동적 바인딩
다형성: 슈퍼클래스 타입의 변수가 그 하위 클래스의 객체를 참조할 수 있음. Element 타입의 변수가 ArrayElement나 LineElement의 객체를 참조할 수 있다.
동적바인딩: 실제로 불리는 메서드는 변수의 타입을 따르지 않고 실제로 실행시점에 참조되어있는 객체의 타입을 따름. 예를들어 Element 변수 타입에 ArrayElement 객체가 할당되어 있을 경우 어떤 메서드가 호출되면 ArrayElement 객체의 메서드가 호출됨
abstract class Element { def printType(): Unit = { println("Element type") } } class ArrayElement extends Element{ override def printType() = { println("ArrayElement type") } } class LineElement extends ArrayElement { override def printType() = { println("LineElement type") } } class DefaultElement extends Element { } def printer(e: Element): Unit = { e.printType() } printer(new DefaultElement) printer(new ArrayElement) printer(new LineElement) // 출력 결과 Element type ArrayElement type LineElement type
9. final 멤버 선언
메서드나 클래스가 더이상 상속되는것을 막으려면 final 키워드를 추가하면 된다. 아래와 같이 printType을 final로 수식해주면 ArrayElement클래스의 서브클래스는 printType을 상속 받을 수 없다. (에러 발생함)
class ArrayElement extends Element{ final override def printType() = { println("ArrayElement type") } } class LineElement extends ArrayElement { override def printType() = { println("LineElement type") } }
class 자체의 상속을 막으려면 final class [클래스명] 으로 사용할 수 있다.
10. 상속과 구성
상속관계는 is-a 모델링에 적합하다. 예를들어 ArrayElement는 Element 인가? 그렇다. 그럼 이 상속관계는 적절하게 모델링된 것이다. LineElement는 ArrayElement 인가? 이부분은 좀 애매하다. 하지만 LineElement는 ArrayElement의 contents구현을 사용하려고 상속한 것이기때문에 굳이 상속관계를 복잡하게 만들지 않는 것이 낫다.
// Element.scala abstract class Element { def contents: Array[String] def height = contents.length def width = if(height == 0) 0 else contents(0).length } // ArrayElement.scala class ArrayElement(val contents: Array[String]) extends Element { } // LineElement.scala class LineElement(val s: String) extends Element { override def contents: Array[String] = Array(s) override def width = s.length override def height = 1 }
11. above, beside, toString 구현
above 메서드는 어떤 요소를 다른 요소 위에 올리는 기능이다. 두 contents를 합치는 방식으로 구현하면 된다. beside 메소드는 같은 row에 오는 문자열끼리 합쳐주면 된다.
abstract class Element { def contents: Array[String] def height = contents.length def width = if(height == 0) 0 else contents(0).length def above(other: Element): Element = { new ArrayElement(this.contents ++ other.contents) } def beside(other: Element): Element = { new Element( for((line1, line2) <- this.contents zip other.contents) yield line1 + line2 ) } override def toString: String = contents.mkString("\n") }
12. 팩토리 객체 정의
팩토리 객체는 객체의 생성을 도맡아 대신해주는 객체를 말한다. 직접 클라이언트가 객체를 만드는것보다 이해하기 쉽고 추후에 코드를 좀 더 쉽게 변경할수 있도록 도와준다. 팩토리 객체는 다양한 방법으로 구현할 수 있다. 여기에서는 Element의 동반객체를 생성해서 팩토리 객체를 구현한다. 팩토리 객체를 생성한 뒤에는 above, beside에서 객체를 생성할때 활용했던 함수 또한 변경한다.
import Element.elem abstract class Element { def contents: Array[String] def height = contents.length def width = if(height == 0) 0 else contents(0).length def above(other: Element): Element = { elem(this.contents ++ other.contents) } def beside(other: Element): Element = { elem( for((line1, line2) <- this.contents zip other.contents) yield line1 + line2 ) } override def toString: String = contents.mkString("\n") } object Element { def elem(contents: Array[String]): Element = new ArrayElement(contents) def elem(contents: String): Element = new LineElement(contents) }
팩토리 메서드가 있다면 ArrayElement, LineElement 와 같은 구체클래스가 굳이 보일 필요가 없다. 스칼라에서는 클래스나 싱글톤 객체를 다른 클래스나 싱글톤 객체 내부에 정의할 수 있다. 이것을 이용하여 Element 내부의 private class로 옮겨주도록 한다.
object Element { def elem(contents: Array[String]): Element = new ArrayElement(contents) def elem(contents: String): Element = new LineElement(contents) private class ArrayElement(val contents: Array[String]) extends Element { } private class LineElement(val s: String) extends Element { override def contents: Array[String] = Array(s) override def width = s.length override def height = 1 } }
'스칼라' 카테고리의 다른 글
[스칼라] 트레이트 (0) 2022.02.20 [스칼라] 스칼라 계층구조 (0) 2022.02.13 [스칼라] 흐름 제어 추상화 (0) 2022.02.01 [스칼라] 함수와 클로저 (0) 2022.01.31 [스칼라] 내장 제어 구문 (0) 2022.01.31