예전에 다뤘던 Obj2D
클래스의 이름을 Shape
으로 구성된 2차원 객체라는 의미에서
ShapeObj2D
로 클래스의 이름을 바꾸어 정의하자.
그리고 이번 강의 노트에서는 여러 개의 ShapeObj2D
를 포함하는 GroupObj2D
이라는 클래스를 정의하려고 한다.
GroupObj2D
는 ShapeObj2D
배열을 인스턴스 변수로 갖도록 설계하자.
ShapeObj2D
와 GroupObj2D
는 상속 관계(is-a 관계)가 아닌 포함 관계(has-a 관계)이므로
상위(부모)클래스/하위(자녀)클래스 관계로는 나타낼 수 없지만, 공통적으로
SVG 형식으로 화면에 내용을 나타낼 수 있는 동작을 하도록 설계하고자 한다.
이럴 때 활용할 수 있는 것이 바로 지난 시간에 배웠던 인터페이스다.
ShapeObj2D
와 GroupObj2D
모두ToSVG
라는 인터페이스를 구현하도록 하면 될 것이다.
또한 Displayable
이라는 display
메소드를 구현하도록 요구하는 인터페이스를 정의하였다.
ShapeObj2D
와 GruopObj2D
뿐만 아니라 Shape
클래스도 Displayable
을 구현한다.
import org.apache.commons.lang3.tuple.*;
interface Displayable {
public void display();
}
interface ToSVG {
public String toSVG();
// points of the bounding rectangle
public Integer minX();
public Integer minY();
public Integer maxX();
public Integer maxY();
// default methods using above four abstract methods
default public Pair<Integer,Integer> minPoint() {
return Pair.of( this.minX(), this.minY() );
}
default public Pair<Integer,Integer> maxPoint() {
return Pair.of( this.maxX(), this.maxY() );
}
}
import io.github.spencerpark.ijava.runtime.*;
abstract class Shape implements Displayable {
// 인스턴스 변수
int width; // 양과 음의 정수값 모두 가능
int height; // 양과 음의 정수값 모두 가능
String fill; // 도형의 안쪽을 채우는 색깔
double opacity; // 도형을 그렸을 때 투명도
Shape(int width, int height, String fill, double opacity) {
this.width = width;
this.height = height;
this.fill = fill;
this.opacity = opacity;
}
// 추상 메소드
abstract double area(); // 넓이 계산
abstract String toSVGshape(Pair<Integer,Integer> point); // SVG 기본 도형 태그 생성
@Override
public String toString() {
return super.toString()
+ String.format("(width=%d, height=%d, fill=%s, opacity=%f)",
width, height, fill, opacity );
}
@Override
public void display() { // 이미지 형태로 보여주기 위한 메소드
Pair<Integer,Integer> point = Pair.of( (width<0)? Math.abs(width) :0,
(height<0)? Math.abs(height):0 );
String svgStr = String.format(
"<svg width='%d' height='%d'>%s</svg>",
Math.abs(width), Math.abs(height), this.toSVGshape(point) );
Display.display(svgStr,"text/html");
}
}
class RightTri extends Shape {
RightTri(int width, int height, String fill, double opacity) {
super(width, height, fill, opacity);
}
@Override
double area() { return Math.abs(width * height) / 2; } // 삼각형 넓이공식에 맞게
@Override
String toSVGshape(Pair<Integer,Integer> point) {
int x0 = point.getLeft();
int y0 = point.getRight();
return
String.format("<circle cx='%d' cy='%d' r='3' fill='%s' opacity='%f' />",
x0,y0, fill, opacity)
+
String.format("<polygon points='%d,%d %d,%d %d,%d' fill='%s' opacity='%f' />",
x0,y0, x0+width,y0, x0,y0+height, fill, opacity );
}
}
class Rectangle extends Shape {
Rectangle(int width, int height, String fill, double opacity) {
super(width, height, fill, opacity);
}
@Override
double area() { return Math.abs(width * height); } // 직사각형 넓이공식에 맞게
@Override
String toSVGshape(Pair<Integer,Integer> point) {
int x0 = point.getLeft();
int y0 = point.getRight();
return
String.format("<circle cx='%d' cy='%d' r='3' fill='%s' opacity='%f' />",
x0,y0, fill, opacity)
+
String.format("<rect width='%d' height='%d' fill='%s' opacity='%f' />",
Math.abs(width), Math.abs(height), fill, opacity );
}
}
new RightTri(20,40,"red",0.3).display()
new RightTri(-20,40,"green",0.3).display()
new RightTri(20,-40,"purple",0.3).display()
new RightTri(-20,-40,"brown",0.3).display()
new Rectangle(20,40,"red",0.3).display()
new Rectangle(-20,40,"green",0.3).display()
new Rectangle(20,-40,"purple",0.3).display()
new Rectangle(-20,-40,"brown",0.3).display()
class ShapeObj2D implements Displayable, ToSVG {
Pair<Integer, Integer> point;
Shape shape;
ShapeObj2D(Pair<Integer, Integer> point, Shape shape) {
this.point = point;
this.shape = shape;
}
@Override
public String toString() {
return
super.toString()
+
String.format("( point=%s, shape=%s )",
point.toString(), shape.toString() );
}
@Override // 지금은 무조건 원점부터 1사분면만 그리고 있다고 생각하고 작성하고 있음
public void display() { // 이미지 형태로 보여주기 위한 메소드
String svgStr = String.format(
"<svg width='%d' height='%d'>%s</svg>",
maxX(), maxY(), this.toSVG() );
Display.display(svgStr,"text/html");
}
@Override
public String toSVG() { return shape.toSVGshape(point); }
// points of the bounding rectangle
@Override
public Integer minX() { return Math.min(point.getLeft(), point.getLeft()+shape.width); }
@Override
public Integer minY() { return Math.min(point.getRight(), point.getRight()+shape.height); }
@Override
public Integer maxX() { return Math.max(point.getLeft(), point.getLeft()+shape.width); }
@Override
public Integer maxY() { return Math.max(point.getRight(), point.getRight()+shape.height); }
}
Pair.of(150,100);
(150,100)
Pair.of(150,100).getClass()
class org.apache.commons.lang3.tuple.ImmutablePair
ShapeObj2D otri1 = new ShapeObj2D( Pair.of(150,100), new RightTri(30,40,"red",0.3) );
ShapeObj2D otri2 = new ShapeObj2D( Pair.of(150,100), new RightTri(-30,-40,"red",0.3) );
otri1
REPL.$JShell$27$ShapeObj2D@fdb02d5( point=(150,100), shape=REPL.$JShell$17$RightTri@3e75d50f(width=30, height=40, fill=red, opacity=0.300000) )
otri2
REPL.$JShell$27$ShapeObj2D@55722bd0( point=(150,100), shape=REPL.$JShell$17$RightTri@2554f7e6(width=-30, height=-40, fill=red, opacity=0.300000) )
ShapeObj2D orect1 = new ShapeObj2D( Pair.of(150,100), new Rectangle(30,40,"blue",0.3) );
orect1
REPL.$JShell$27$ShapeObj2D@4384f7d( point=(150,100), shape=REPL.$JShell$18$Rectangle@16b7f0ee(width=30, height=40, fill=blue, opacity=0.300000) )
orect1.toSVG()
<circle cx='150' cy='100' r='3' fill='blue' opacity='0.300000' /><rect width='30' height='40' fill='blue' opacity='0.300000' />
otri1.toSVG()
<circle cx='150' cy='100' r='3' fill='red' opacity='0.300000' /><polygon points='150,100 180,100 150,140' fill='red' opacity='0.300000' />
otri1.display()
otri2.toSVG()
<circle cx='150' cy='100' r='3' fill='red' opacity='0.300000' /><polygon points='150,100 120,100 150,60' fill='red' opacity='0.300000' />
otri2.display()
ShapeObj2D otri3 = new ShapeObj2D( Pair.of(20,30), new RightTri(-30,-40,"red",0.3) );
otri3.toSVG()
<circle cx='20' cy='30' r='3' fill='red' opacity='0.300000' /><polygon points='20,30 -10,30 20,-10' fill='red' opacity='0.300000' />
otri3.display()
하나의 Shape
오브젝트로부터 여러 개의 ShapeObj2D
오브젝트를 만들 수 있다.
Shape s = new RightTri(30,40,"blue",0.3);
ShapeObj2D o1 = new ShapeObj2D( Pair.of(10,10), s );
ShapeObj2D o2 = new ShapeObj2D( Pair.of(60,20), s );
ShapeObj2D o3 = new ShapeObj2D( Pair.of(20,60), s );
o1.display()
o2.display()
o3.display()
Shape
오브젝트와 ShapeObj2D
오브젝트도 Displayable
이라는 공통된 인터페이스가 있으므로
이들을 함께 모아 놓고 각각을 display
하도록 일괄 처리가 가능하다.
s instanceof Shape
true
s instanceof Displayable
true
o2 instanceof ShapeObj2D
true
o2 instanceof Displayable
true
otri1 instanceof ShapeObj2D
true
otri1 instanceof Displayable
true
Displayable[] ds = { s, otri1, o2 };
ds[0].display()
ds[1].display()
ds[2].display()
for (int i = 0; i < ds.length; ++i)
ds[i].display()
for (var d : ds)
d.display()
위에서는 배열 ds
여러 개의 Displayable에 대한 각각의 SVG이미지를 따로따로 반복해서 display했다.
이제는 여러 개의 2차원 오브젝트(Obj2D
)를 한데 그룹으로 묶어 같은 하나의 SVG 이미지안에 한꺼번에 나타내는
GroupObj2D
클래스를 정의해 보라.
(실습으로 진행하고 관련 내용으로 HW4 과제 진행 예정)
class GroupObj2D implements Displayable, ToSVG {
ShapeObj2D[] objects;
GroupObj2D(ShapeObj2D[] objects) {
this.objects = objects;
}
/*
@Override
public String toString() { ... }
*/
/*
// Displayable에서 오버라이드하라고 요구하는 메소드를 구현
*/
/*
// ToSVG에서 오버라이드하라고 요구하는 메소드들을 구현
*/
}
GroupObj2D의 클래스를 완성하고 간한한 예제를 몇 개 시험해 보라.
ShapeObj2D의 인스턴스 여러 개를 모아 하나의 GroupObj2D의 인스턴스를 만들어 SVG 이미지로 나타낼 수 있다.
예를 들면,
위와 같은 GruopObj2D를 코드를 완성하더라도 여전히 뭔가 아쉬운 점이 있다.
왜냐하면 GroupObj2D 오브젝트를 여러 개 모아서 하나의 그룹으로 생각하는 것도 유용할 수 있기 때문이다.
예를 들면,
그리고 일반적으로 GroupObj2D와 ShapeObj2D를 한꺼번에 모을 수도 있으면 더 편리할 것 같다.
예를 들면, 집 형태의 GroupObj2D 오브젝트 여러 개와 큰 사각형 모양의 ShapeObj2D 오브젝트 하나로 여러 개의 집들이 큰 사각형 모양의 마을 경계 안에 위치한 그림을 나타낼 수도 있다.
이런 것까지 가능하도록 하려면 GroupObj2D 클래스의 정의를 어떻게 바꾸면 될지 생각해 보라.