Posts Method Chaining & Generic(메소드 체인 패턴)
Post
Cancel

Method Chaining & Generic(메소드 체인 패턴)

메소드 체이닝 패턴


특징

  • 메소드가 객체를 반환하는 형태로 작성한다.
  • 문장을 마치지 않고 메소드 호출을 이어나갈 수 있다.
  • 가독성을 향상시킬 수 있다.


주의사항

  • 한 문장에 여러번의 메소드 호출이 존재할 수 있으므로, 에러가 발생할 경우 정확한 지점을 한 번에 찾기 힘들다.
  • C# 구조체의 메소드를 체이닝으로 구현할 경우, 매 번 구조체 전체의 복제가 발생하므로 바람직하지 않다.


예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Box
{
    private float width;
    private float height;

    public Box SetWidth(float width)
    {
        this.width = width;
        return this;
    }
    public Box SetHeight(float height)
    {
        this.height = height;
        return this;
    }
}

class MethodChaining
{
    public static void Run()
    {
        Box box = new Box()
            .SetWidth(10f)
            .SetHeight(20f);
    }
}


상속 관계에서의 메소드 체이닝


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Box
{
    protected float width;
    protected float height;

    public Box SetWidth(float width)
    {
        this.width = width;
        return this;
    }
    public Box SetHeight(float height)
    {
        this.height = height;
        return this;
    }
}

class OutlinedBox : Box
{
    protected float outlineWidth;

    public OutlinedBox SetOutlineWidth(float outlineWidth)
    {
        this.outlineWidth = outlineWidth;
        return this;
    }
}

클래스 상속 관계에서는 정상적으로 메소드 체이닝이 이루어질 수 없으며,

다양한 문제들이 발생한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// [1] 에러
OutlinedBox olBox1 = new OutlinedBox()
    .SetOutlineWidth(2f)
    .SetWidth(10f)
    .SetHeight(20f);
    // 리턴 타입이 Box이므로 변수 타입이 Box여야 한다.

// [2] 에러
OutlinedBox olBox2 = new OutlinedBox()
    .SetWidth(10f)
    .SetHeight(15f)
    .SetOutlineWidth(2f); // 현재 Box 타입이므로 호출 불가능

// [3] 정상
Box olBox3 = new OutlinedBox()
    .SetOutlineWidth(2f)
    .SetWidth(10f)
    .SetHeight(20f);

// [4] 정상
OutlinedBox olBox4 = new OutlinedBox()
    .SetOutlineWidth(2f)
    .SetWidth(10f)
    .SetHeight(20f) as OutlinedBox;

// [5] 정상
OutlinedBox olBox5 = (new OutlinedBox()
    .SetWidth(10f)
    .SetHeight(15f) as OutlinedBox)
    .SetOutlineWidth(2f);

상속 관계에서의 메소드 체이닝은 두 가지 문제점이 존재한다.

  1. 자식의 메소드를 부모의 메소드보다 우선 호출해야 한다.
  2. 변수의 타입이 부모의 타입이어야 한다.

SetWidth(), SetHeight() 메소드를 호출할 경우 리턴 타입은 Box이다.

따라서 부모의 메소드를 호출한 뒤에는 부모의 타입으로 변경되므로

자식의 메소드를 호출할 수 없으며,

결국 메소드 체인이 종료되면 결과 타입음 부모의 타입이므로

변수 역시 부모의 타입이어야 한다.


에러가 발생하지 않도록 하기 위해서는

[3]처럼 위의 문제점을 회피하여 작성하거나,

[4]와 [5]처럼 자식 타입으로의 명시적 캐스팅을 해야 한다.

하지만, 이렇게 되면 메소드 체이닝 패턴의 장점인 편의성과 가독성을 모두 해치게 된다.


메소드 체이닝과 제네릭


제네릭을 이용하면 위의 문제를 모두 해결할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Box : Box<Box> { }
class Box<T> where T : Box<T>
{
    protected float width;
    protected float height;

    public T SetWidth(float width)
    {
        this.width = width;
        return this as T;
    }
    public T SetHeight(float height)
    {
        this.height = height;
        return this as T;
    }
}

class OutlinedBox : Box<OutlinedBox>
{
    protected float outlineWidth;

    public OutlinedBox SetOutlineWidth(float outlineWidth)
    {
        this.outlineWidth = outlineWidth;
        return this;
    }
}

class MethodChaining
{
    public static void Run()
    {
        Box box = new Box()
            .SetWidth(10f)
            .SetHeight(20f);

        OutlinedBox olBox = new OutlinedBox()
            .SetOutlineWidth(2f)
            .SetWidth(10f)
            .SetHeight(20f);
    }
}

부모 클래스인 Box의 제네릭 타입으로 T를 사용하고,

where T : Box<T>로 한정시킨다.

그리고 각각의 체인 메소드에서 리턴 타입을 T로 지정한 다음,

return this as T를 리턴하면 된다.


이제 자식 클래스들에서 OutlinedBox : Box<OutlinedBox>와 같이

T에 자신의 타입을 집어넣고 상속하게 되면

부모의 Setter가 리턴하는 T가 자신의 타입으로 추론되며

호출하는 모든 메소드가 부모의 타입이 아니라 자신의 타입으로

자기 자신을 리턴하도록 만든다.


그리고 Box 타입 역시 new Box() 형태로 객체를 생성할 수 있도록

class Box : Box<Box> { }처럼 빈 클래스를 작성해놓는다.


자식의 자식으로 상속이 이어지는 경우

  • 같은 방식으로 이어나가면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class OutlinedBox : OutlinedBox<OutlinedBox> { }
class OutlinedBox<T> : Box<T> where T : OutlinedBox<T>
{
    protected float outlineWidth;

    public T SetOutlineWidth(float outlineWidth)
    {
        this.outlineWidth = outlineWidth;
        return this as T;
    }
}

class OutlinedColorBox : OutlinedBox<OutlinedColorBox>
{
    protected Color color;

    public OutlinedColorBox SetColor(Color color)
    {
        this.color = color;
        return this;
    }
}


추가1

위와 같이 작성하면 Box<T>, OutlinedBox<T> 타입 역시 T를 직접 지정하여 객체를 생성할 수 있게 된다.

이를 막으려면 다음과 같이 클래스 선언에 abstract를 넣어 작성한다.

1
2
abstract class Box<T> where T : Box<T>
abstract class OutlinedBox<T> : Box<T> where T : OutlinedBox<T>


추가2

Box 타입과 Box<T> 타입은 이름이 같아 동일해 보일 수 있지만,

엄연히 서로 다른 타입이다.

OutlinedBoxOutlinedBox<T> 타입 역시 마찬가지.

제네릭화 과정에서 자연스럽게 동일한 이름으로 남겨두었지만,

혼동할 가능성이 있으므로 다음과 같이 바꾸면 확실히 구분할 수 있다.

1
2
abstract class BoxBase<T> where T : BoxBase<T>
abstract class OutlinedBoxBase<T> : BoxBase<T> where T : OutlinedBoxBase<T>


결론

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
abstract class BoxBase<T> where T : BoxBase<T>
{
    protected float width;
    protected float height;

    public T SetWidth(float width)
    {
        this.width = width;
        return this as T;
    }
    public T SetHeight(float height)
    {
        this.height = height;
        return this as T;
    }
}

abstract class OutlinedBoxBase<T> : BoxBase<T> where T : OutlinedBoxBase<T>
{
    protected float outlineWidth;

    public T SetOutlineWidth(float outlineWidth)
    {
        this.outlineWidth = outlineWidth;
        return this as T;
    }
}

class Box : BoxBase<Box> { }

class OutlinedBox : OutlinedBoxBase<OutlinedBox> { }

class OutlinedColorBox : OutlinedBoxBase<OutlinedColorBox>
{
    protected Color color;

    public OutlinedColorBox SetColor(Color color)
    {
        this.color = color;
        return this;
    }
}


제네릭 클래스에 대한 메소드 체이닝


메소드 체이닝을 적용하기 전에 이미 제네릭으로 만들어진 클래스를 생각해볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
abstract class SliderBase<T> where T : struct
{
    protected T value;
    protected T minValue;
    protected T maxValue;

    public SliderBase<T> SetValue(T value)
    {
        this.value = value;
        return this;
    }
}

class IntSlider : SliderBase<int>
{
    protected string id;

    public IntSlider SetID(string id)
    {
        this.id = id;
        return this;
    }
}

기존에 제네릭 타입으로 T가 사용되는 상태.

여기에 제네릭 메소드 체이닝을 적용하려면,

두 번째 제네릭 타입 R을 추가하여 이전과 같이 작성하면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
abstract class SliderBase<T, R>
    where T : struct
    where R : SliderBase<T, R>
{
    protected T value;
    protected T minValue;
    protected T maxValue;

    public R SetValue(T value)
    {
        this.value = value;
        return this as R;
    }
}

class IntSlider : SliderBase<int, IntSlider>
{
    protected string id;

    public IntSlider SetID(string id)
    {
        this.id = id;
        return this;
    }
}
This post is licensed under CC BY 4.0 by the author.