한 가지만하는 클래스의 패턴


24

하자 내가 절차가 있다고 물건을하지를 :

void doStuff(initalParams) {
    ...
}

이제 나는 "일을하는 것"이 ​​상당히 복잡한 작업이라는 것을 알게되었습니다. 절차가 커지고 여러 개의 작은 절차로 나누고 곧 어떤 종류의 상태 를 갖는 것이 작업을 수행하는 동안 유용 할 것이므로 작은 절차 사이에 더 적은 매개 변수를 전달해야합니다. 그래서 나는 그것을 자신의 클래스로 분해합니다.

class StuffDoer {
    private someInternalState;

    public Start(initalParams) {
        ...
    }

    // some private helper procedures here
    ...
}

그리고 나는 이것을 다음과 같이 부릅니다.

new StuffDoer().Start(initialParams);

또는 이와 같이 :

new StuffDoer(initialParams).Start();

그리고 이것이 잘못된 느낌입니다. .NET 또는 Java API를 사용할 때 항상 호출하지 않으므로 new SomeApiClass().Start(...);잘못하고 있다고 의심됩니다. 물론 StuffDoer의 생성자를 비공개로 만들고 정적 도우미 메소드를 추가 할 수 있습니다.

public static DoStuff(initalParams) {
    new StuffDoer().Start(initialParams);
}

그러나 외부 인터페이스가 하나의 정적 메소드로만 구성된 클래스가 있는데 이상하게 느껴집니다.

따라서 내 질문 :이 유형의 클래스에 대해 잘 설정된 패턴이 있습니까?

  • 하나의 진입 점 만 있고
  • "외부 적으로 인식 할 수있는"상태가없는 경우, 즉 하나의 진입 점을 실행 하는 동안에 만 인스턴스 상태가 필요 합니까?

내가 bool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");돌보는 모든 것이 특정 정보와 LINQ 확장 방법을 사용할 수 없다는 것과 같은 일을하는 것으로 알려져 있습니다. 잘 작동하고 if()농구대를 뛰어 넘을 필요없이 조건 안에 맞습니다 . 물론 그런 코드를 작성하는 경우 가비지 수집 언어를 원합니다.
CVn

언어에 구애받지 않는다면 왜 javac # 을 추가해야 합니까? :)
Steven Jeuris

궁금합니다.이 '한 가지 방법'의 기능은 무엇입니까? 특정 시나리오에서 최상의 접근 방식이 무엇인지 결정하는 데 많은 도움이되지만 그럼에도 불구하고 흥미로운 질문입니다!
Steven Jeuris

@StevenJeuris : 다양한 상황 에서이 문제를 우연히 발견했지만 현재의 경우 로컬 데이터베이스로 데이터를 가져 오는 방법입니다 (웹 서버에서로드하고 일부 조작을 수행하여 데이터베이스에 저장).
Heinzi

1
인스턴스를 생성 할 때 항상 메서드를 호출한다면 생성자 내에서 초기화 로직으로 만들지 않겠습니까?
NoChance

답변:


29

Method Object 라는 패턴이 있는데, 여기에는 많은 임시 변수 / 인수가 포함 된 하나의 큰 메서드를 별도의 클래스로 분해합니다. 메소드의 일부를 로컬 상태 (매개 변수 및 임시 변수)에 액세스해야하기 때문에 메소드의 일부를 별도의 메소드로 추출하는 것이 아니라 메소드 변수에 로컬이기 때문에 인스턴스 변수를 사용하여 로컬 상태를 공유 할 수 없습니다. (및 그로부터 추출 된 방법들) 만 나머지 객체에 의해 사용되지 않을 것이다.

대신 메서드는 자체 클래스가되고 매개 변수와 임시 변수는이 새 클래스의 인스턴스 변수가되고 메서드는 새 클래스의 작은 메서드로 나뉩니다. 결과 클래스에는 일반적으로 클래스가 캡슐화하는 작업을 수행하는 단일 공용 인스턴스 메소드가 있습니다.


1
이것은 내가하고있는 것과 정확히 같습니다. 고마워, 적어도 나는 지금 그것에 대한 이름이있다. :-)
Heinzi

2
매우 관련있는 링크! 그것은 나에게 반 패턴처럼 냄새가 난다. 방법이 길면 재사용 가능한 클래스 에서 추출 하거나 일반적으로 더 나은 방식으로 리팩터링 할 수 있습니까? 또한이 패턴에 대해 들어 본 적이 없으며 일반적으로 많이 사용되지 않는다고 생각하는 다른 많은 리소스를 찾을 수 없습니다.
Steven Jeuris


1
@StevenJeuris 나는 그것이 안티 패턴이라고 생각하지 않습니다. 안티 패턴은 인스턴스 변수에서 이러한 메소드간에 공통적 인이 상태를 저장하는 대신 로트 매개 변수로 리팩토링 된 메소드를 복잡하게 만드는 것입니다.
Alfredo Osorio

@ AlfredoOsorio : 글쎄, 그것은 많은 매개 변수로 리팩토링 된 메소드를 혼란스럽게 만듭니다. 그것들은 객체 안에 존재하기 때문에, 그것들을 모두 전달하는 것보다 낫지 만, 각각의 제한된 서브 세트만을 필요로하는 재사용 가능한 컴포넌트로 리팩토링하는 것보다 나쁩니다.
Jan Hudec

13

문제의 클래스 (단일 진입 점 사용)가 잘 설계되었다고 말할 수 있습니다. 사용 및 확장이 쉽습니다. 상당히 견고합니다.

에 대해서도 마찬가지입니다 no "externally recognizable" state. 캡슐화가 좋습니다.

다음과 같은 코드에는 아무런 문제가 없습니다.

var doer = new StuffDoer(initialParams);
var result = doer.Calculate(extraParams);

1
물론 doer다시 사용할 필요가 없으면로 줄어 듭니다 var result = new StuffDoer(initialParams).Calculate(extraParams);.
CVn

계산은 doStuff로 이름을 바꿔야합니다!
shabunc

1
클래스를 사용하는 곳에서 클래스를 초기화하지 않으려면 (아마도 팩토리를 사용하고 싶을 경우) 비즈니스 로직의 다른 곳에 객체를 생성하고 매개 변수를 직접 전달하는 옵션이 있습니다. 당신이 가진 하나의 공개 방법에. 다른 옵션은 팩토리를 사용하고 매개 변수를 가져 와서 생성자를 통해 모든 매개 변수가있는 객체를 만드는 make 메소드를 사용하는 것입니다. 당신이 원하는 곳에서 매개 변수없이 공개 메소드를 호출하는 것보다.
Patkos Csaba

2
이 답변은 지나치게 단순합니다. 잘 StringComparer사용 하는 수업이 new StringComparer().Compare( "bleh", "blah" )잘 설계되어 있습니까? 전혀 필요하지 않은 상태를 만드는 것은 어떻습니까? 좋아, 행동은 잘 캡슐화되어 있지만 (적어도 적어도 클래스 사용자 에게는 내 대답에 더 자세히 설명되어 있지만) 기본적으로 '좋은 디자인'으로 만들지는 않습니다. 당신의 '결과 결과'에 관해서. 는와 ( doer과) 별도로 사용하는 경우에만 의미가 있습니다 result.
Steven Jeuris

1
존재하는 한 가지 이유는 아닙니다 one reason to change. 차이가 미묘 해 보일 수 있습니다. 그 의미를 이해해야합니다. 하나의 메소드 클래스를 만들지 않습니다
jgauffin

8

A로부터 SOLID 원칙의 관점 jgauffin의 대답 의미가 있습니다. 그러나 일반적인 디자인 원칙 (예 : 정보 숨기기)을 잊어서는 안됩니다 .

주어진 접근 방식에 몇 가지 문제가 있습니다.

  • 자신이 지적한 것처럼 사람들은 생성 된 객체 가 상태를 관리하지 않을 때 'new'키워드를 사용하지 않을 것으로 기대합니다 . 당신의 디자인은 의도를 반영합니다. 클래스를 사용하는 사람들은 관리하는 상태 및 메소드에 대한 후속 호출로 인해 다른 동작이 발생할 수 있는지 혼동 될 수 있습니다.
  • 클래스를 사용하는 사람의 관점에서 내부 상태는 잘 숨겨져 있지만 클래스를 수정하거나 이해하려고 할 때 사물을 더 복잡하게 만듭니다. 나는 이미 상태를 클래스 범위로 옮길 때 메소드를 작게 만들기 위해 분할 메소드로 볼 때 발생하는 문제에 대해 이미 많이 작성했습니다 . 더 작은 기능을 갖기 위해 API 사용 방법을 수정하고 있습니다! 내 의견으로는 분명히 너무 멀리 가고 있습니다.

일부 관련 참조

아마도 주된 논점은 단일 책임 원칙 을 어느 정도까지 확장 할 수 있는가에있다 . "만약 당신이 그것을 극단적으로 받아들이고 존재하는 한 가지 이유가있는 클래스를 만들면, 클래스 당 하나의 메소드로 끝날 수 있습니다. 이로 인해 가장 간단한 프로세스조차도 클래스가 크게 늘어나 시스템이 이해하기가 어렵고 변화하기가 어렵다. "

이 주제와 관련된 또 다른 관련 참조 : "프로그램을 식별 가능한 하나의 태스크를 수행하는 메소드로 나누십시오. 메소드의 모든 조작을 동일한 추상화 레벨로 유지하십시오 ." - 켄트 벡 키는 "동일한 추상화 수준"입니다. 그것은 종종 해석되기 때문에 "한 가지"를 의미하지 않습니다. 이 추상화 수준은 전적으로 설계 하려는 컨텍스트 에 달려 있습니다.

그렇다면 올바른 접근법은 무엇입니까?

구체적인 유스 케이스를 알지 못하면 말하기가 어렵습니다. 때때로 (종종 그렇지는 않지만) 비슷한 접근법을 사용하는 시나리오가 있습니다. 이 기능을 전체 클래스 범위에서 사용 가능하게하지 않고 데이터 세트를 처리하려고 할 때. 나는 람다가 캡슐화를 더욱 향상시킬 수있는 방법 에 대한 블로그 게시물을 썼습니다 . 나는 또한 프로그래머에 관한 주제에 대한 질문을 시작했다 . 다음은이 기술을 사용한 최신 예입니다.

new TupleList<Key, int>
{
    { Key.NumPad1, 1 },
            ...
    { Key.NumPad3, 16 },
    { Key.NumPad4, 17 },
}
    .ForEach( t =>
    {
        var trigger = new IC.Trigger.EventTrigger(
                        new KeyInputCondition( t.Item1, KeyInputCondition.KeyState.Down ) );
        trigger.ConditionsMet += () => AddMarker( t.Item2 );
        _inputController.AddTrigger( trigger );
    } );

내부의 '로컬'코드 ForEach는 다른 곳에서 재사용되지 않기 때문에 관련성이있는 정확한 위치에 코드 를 유지할 수 있습니다. 서로 의존하는 코드가 강력하게 그룹화되는 방식으로 코드를 요약하면 내 의견으로는 더 읽기 쉽습니다.

가능한 대안

  • C #에서는 대신 확장 메서드를 사용할 수 있습니다. 따라서이 '한 가지'방법으로 전달하는 인수를 직접 조작하십시오.
  • 이 함수가 실제로 다른 클래스에 속하지 않는지 확인하십시오.
  • 정적 클래스에서 정적 함수로 만듭니다 . 이것은 당신이 언급 한 일반 API에도 반영되어있는 가장 적절한 방법 일 것입니다.

다른 답변 Poltergeists에서 설명한 것처럼 이 안티 패턴의 가능한 이름을 찾았습니다 .
Steven Jeuris

4

나는 그것이 꽤 좋다고 말하지만 API는 여전히 정적 클래스에서 정적 메소드 여야합니다. 사용자가 기대하는 것이기 때문입니다. 정적 메소드가 new헬퍼 오브젝트를 작성하고 작업을 수행하는 데 사용한다는 사실 은 호출하는 사람으로부터 숨겨져 야하는 구현 세부 사항입니다.


와우! 당신은 내가 한 것보다 훨씬 간결하게 그것을 다룰 수있었습니다. :) 감사. (물론 나는 우리가 동의 할 수 어디에 조금 더 가서,하지만 일반적인 전제에 동의)
스티븐 Jeuris

2
new StuffDoer().Start(initialParams);

공유 인스턴스를 사용하고 사용할 수있는 실마리가없는 개발자에게는 문제가 있습니다

  1. 여러 번 (이전 (일부) 실행이 나중에 실행을 망칠 수없는 경우 확인)
  2. 여러 스레드에서 (확인되지 ​​않음, 명시 적으로 내부 상태가 있다고 말함).

따라서 이것은 스레드 안전하지 않다는 명시 적 문서가 필요하며 가벼운 경우 (빠른 생성, 외부 리소스 또는 많은 메모리를 묶지 않음) 즉시 인스턴스화해도 좋습니다.

정적 메서드로 숨기면 인스턴스 재사용이 결코 일어나지 않기 때문에 도움이됩니다.

이 비싼 초기화가 있다면, 그것은 한 번 초기화 및 전용 상태를 포함하고 복제 가능한 만들 것 또 다른 클래스를 생성하여 여러 번 사용하여 준비하는 것이 도움이 될 (항상하지 않습니다). 초기화하면에 저장된 상태가 생성됩니다 doer. start()그것을 복제하고 내부 메소드에 전달합니다.

이것은 부분 실행 상태 유지와 같은 다른 것들도 허용합니다. (전기 공급 장애와 같이 오래 걸리고 외부 요인이 필요한 경우 실행을 방해 할 수 있습니다.) 그러나 이러한 추가 공상은 필요하지 않으므로 가치가 없습니다.


좋은 지적입니다. 정리가 마음에 들지 않기를 바랍니다.
Steven Jeuris

2

좀 더 정교하고 개인화 된 다른 답변 외에도 다음과 같은 관찰은 별도의 답변이 필요하다고 생각합니다.

Poltergeists anti-pattern을 따를 수 있다는 표시가 있습니다 .

Poltergeists는 매우 제한된 역할과 효과적인 수명주기를 가진 수업입니다. 그들은 종종 다른 객체에 대한 프로세스를 시작합니다. 리팩토링 된 솔루션에는 Poltergeists를 제거하는 수명이 긴 오브젝트에 대한 책임 재 할당이 포함됩니다.

증상과 결과

  • 중복 탐색 경로.
  • 일시적 연관.
  • 무국적 수업.
  • 임시 단기 객체 및 클래스.
  • 임시 연결을 통해 다른 클래스를 "시드"또는 "호출"하기 위해 존재하는 단일 작업 클래스
  • start_process_alpha와 같이“제어와 유사한”작업 이름을 가진 클래스

리팩토링 솔루션

고스트 버스 터즈는 폴 터지 스트를 계급 계층에서 완전히 제거하여 문제를 해결합니다. 그러나 제거 후에는 poltergeist가 제공 한 기능을 교체해야합니다. 아키텍처를 수정하기위한 간단한 조정으로 쉽게 수행 할 수 있습니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.