많은 정적 메서드를 사용하는 것이 나쁜가요?


97

클래스가 내부 상태를 추적 할 필요가 없을 때 클래스의 모든 메서드를 정적으로 선언하는 경향이 있습니다. 예를 들어 A를 B로 변환해야하고 변할 수있는 내부 상태 C에 의존하지 않는 경우 정적 변환을 만듭니다. 조정할 수있는 내부 상태 C가있는 경우 생성자를 추가하여 C를 설정하고 정적 변환을 사용하지 않습니다.

정적 메서드를 과도하게 사용하지 말라는 다양한 권장 사항 (StackOverflow 포함)을 읽었지만 위의 경험 법칙에서 무엇이 잘못되었는지 이해하지 못했습니다.

그게 합리적인 접근인가?

답변:


153

일반적인 정적 메서드에는 두 가지 종류가 있습니다.

  • "안전한"정적 메서드는 항상 동일한 입력에 대해 동일한 출력을 제공합니다. 전역을 수정하지 않으며 클래스의 "안전하지 않은"정적 메서드를 호출하지 않습니다. 기본적으로 제한된 종류의 함수형 프로그래밍을 사용하고 있습니다. 두려워하지 마세요. 괜찮습니다.
  • "안전하지 않은"정적 메서드는 전역 상태 또는 프록시를 전역 개체 또는 기타 테스트 할 수없는 동작으로 변경합니다. 이는 절차 적 프로그래밍에 대한 후퇴이며 가능하면 리팩토링해야합니다.

예를 들어 Singleton 패턴에서 "안전하지 않은"정적의 몇 가지 일반적인 용도가 있지만 이름이 예쁜 이름에도 불구하고 전역 변수를 변경하는 것입니다. 안전하지 않은 정적을 사용하기 전에 신중하게 생각하십시오.


이것이 바로 제가 해결해야 할 문제였습니다. Singleton 객체의 사용 또는 오용이었습니다.
overslacked apr

가장 훌륭한 답변에 감사드립니다. 내 질문은 싱글 톤이 정적 메서드에 매개 변수로 전달되면 정적 메서드가 안전하지 않게 만드는 것입니까?
Tony D

1
"순수한 함수"와 "불순한 함수"라는 용어는 함수형 프로그래밍에서 "안전한"및 "안전하지 않은"정적이라고 부르는 이름입니다.
Omnimike 2015-09-22

19

내부 상태가없는 개체는 의심스러운 것입니다.

일반적으로 객체는 상태와 동작을 캡슐화합니다. 행동만을 캡슐화하는 객체는 이상합니다. 때로는 Lightweight 또는 Flyweight 의 예입니다. .

다른 경우에는 객체 언어로 수행되는 절차 설계입니다.


6
나는 당신이 말하는 것을 들었지만 Math 객체와 같은 것이 어떻게 행동 이외의 것을 캡슐화 할 수 있습니까?
JonoW 2009

10
그는 단지 의심스럽고 틀린 것이 아니라고 말했고 그는 절대적으로 옳습니다.
Bill K

2
@JonoW : 수학은 상태 비 저장 함수가 많은 매우 특별한 경우입니다. 물론 Java로 함수형 프로그래밍을 수행하는 경우 상태 비 저장 함수가 많이있을 것입니다.
S.Lott

14

이것은 John Millikin의 훌륭한 답변에 대한 후속 조치 일뿐입니다.


상태 비 저장 메서드 (대부분의 함수)를 정적으로 만드는 것이 안전 할 수 있지만 때때로 수정하기 어려운 결합으로 이어질 수 있습니다. 다음과 같은 정적 메서드가 있다고 가정합니다.

public class StaticClassVersionOne {
    public static void doSomeFunkyThing(int arg);
}

당신이 부르는 :

StaticClassVersionOne.doSomeFunkyThing(42);

정적 메서드의 동작을 수정해야하는 경우를 발견 할 때까지 모두 훌륭하고 매우 편리합니다. StaticClassVersionOne . 아마도 코드를 수정할 수 있고 괜찮을 것입니다. 그러나 이전 동작에 의존하는 다른 호출자가 있다면 메서드 본문에 설명해야합니다. 어떤 경우에는 메서드 본문이 이러한 모든 동작의 균형을 맞추려고하면 매우 추악하거나 유지 관리 할 수 ​​없게 될 수 있습니다. 메서드를 분할하는 경우이를 고려하기 위해 여러 위치에서 코드를 수정하거나 새 클래스를 호출해야 할 수 있습니다.

그러나 메서드를 제공하기 위해 인터페이스를 만들고 호출자에게 제공했다면 이제 동작이 변경되어야 할 때 인터페이스를 구현하기 위해 새 클래스를 만들 수 있습니다.이 인터페이스는 더 깔끔하고 테스트가 쉽고 유지 관리가 더 쉽습니다. 대신 호출자에게 제공됩니다. 이 시나리오에서는 호출 클래스를 변경하거나 다시 컴파일 할 필요가 없으며 변경 사항이 지역화됩니다.

가능성이있는 상황 일 수도 있고 아닐 수도 있지만 고려할 가치가 있다고 생각합니다.


5
나는 이것이 가능한 시나리오 일뿐만 아니라 정적을 최후의 수단으로 만든다고 주장합니다. 정적 때문에 TDD도 악몽이됩니다. 정적을 사용하는 곳에서는 모의 작업을 할 수 없으며 관련없는 클래스를 테스트하기 위해 입력 및 출력이 무엇인지 알아야합니다. 이제 정적의 동작을 변경하면 해당 정적을 사용하는 관련없는 클래스에 대한 테스트가 손상됩니다. 또한 잠재적으로 중요한 종속성을 개발자에게 알리기 위해 생성자에 전달할 수없는 숨겨진 종속성이됩니다.
DanCaveman

6

다른 옵션은 원본 객체에 비 정적 메서드로 추가하는 것입니다.

즉, 변경 :

public class BarUtil {
    public static Foo transform(Bar toFoo) { ... }
}

으로

public class Bar {
    ...
    public Foo transform() { ...}
}

그러나 많은 상황에서 이것이 가능하지 않거나 (예 : XSD / WSDL / etc에서 정규 클래스 코드 생성) 클래스가 매우 길어지고 변환 메서드는 종종 복잡한 개체에 대한 진정한 고통이 될 수 있으며 원하는 경우 별도의 수업에서. 예, 유틸리티 클래스에 정적 메서드가 있습니다.


5

정적 클래스는 올바른 위치에서 사용되는 한 괜찮습니다.

즉, '리프'메서드 인 메서드 (상태를 수정하지 않고 단순히 입력을 어떻게 든 변형). 이에 대한 좋은 예는 Path.Combine과 같은 것입니다. 이러한 종류의 것들은 유용하고 간결한 구문을 만듭니다.

문제 내가 정적에있는 수많은 있습니다 :

첫째, 정적 클래스가있는 경우 종속성이 숨겨집니다. 다음을 고려하세요:

public static class ResourceLoader
{
    public static void Init(string _rootPath) { ... etc. }
    public static void GetResource(string _resourceName)  { ... etc. }
    public static void Quit() { ... etc. }
}

public static class TextureManager
{
    private static Dictionary<string, Texture> m_textures;

    public static Init(IEnumerable<GraphicsFormat> _formats) 
    {
        m_textures = new Dictionary<string, Texture>();

        foreach(var graphicsFormat in _formats)
        {
              // do something to create loading classes for all 
              // supported formats or some other contrived example!
        }
    }

    public static Texture GetTexture(string _path) 
    {
        if(m_textures.ContainsKey(_path))
            return m_textures[_path];

        // How do we know that ResourceLoader is valid at this point?
        var texture = ResourceLoader.LoadResource(_path);
        m_textures.Add(_path, texture);
        return texture; 
    }

    public static Quit() { ... cleanup code }       
}

TextureManager를 보면 생성자를 보면 어떤 초기화 단계를 수행해야하는지 알 수 없습니다. 클래스를 조사하여 종속성을 찾고 올바른 순서로 초기화해야합니다. 이 경우 실행하기 전에 ResourceLoader를 초기화해야합니다. 이제이 의존성 악몽을 확장하면 어떤 일이 일어날 지 짐작할 수 있습니다. 명시적인 초기화 순서가없는 코드를 유지하려고한다고 상상해보십시오. 인스턴스를 사용한 종속성 주입과 대조하십시오.이 경우 종속성이 충족 되지 않으면 코드가 컴파일 되지 않습니다!

또한 상태를 수정하는 정적을 사용하는 경우 카드의 집과 같습니다. 누가 무엇에 접근 할 수 있는지 결코 알 수 없으며 디자인은 스파게티 괴물과 비슷합니다.

마지막으로, 정적을 사용하는 것은 프로그램을 특정 구현에 연결하는 것입니다. 정적 코드는 테스트 가능성을위한 디자인의 반대입니다. 통계로 가득 찬 코드를 테스트하는 것은 악몽입니다. 정적 호출을 테스트 이중으로 바꿀 수는 없으므로 (정적 유형을 모방하도록 특별히 설계된 테스트 프레임 워크를 사용하지 않는 한) 정적 시스템은이를 사용하는 모든 것을 즉시 통합 테스트로 만듭니다.

요컨대, 정적은 어떤 것에는 괜찮으며 작은 도구 또는 일회용 코드에는 사용을 권장하지 않습니다. 그러나 그 이상으로 유지 보수성, 좋은 디자인 및 테스트 용이성에 대한 피비린내 나는 악몽입니다.

다음은 문제에 대한 좋은 기사입니다. http://gamearchitect.net/2008/09/13/an-anatomy-of-despair-managers-and-contexts/


4

정적 메서드에서 경고를받는 이유는이를 사용하면 개체의 장점 중 하나를 상실하기 때문입니다. 개체는 데이터 캡슐화를위한 것입니다. 이것은 버그를 피하는 예기치 않은 부작용이 발생하는 것을 방지합니다. 정적 메서드에는 캡슐화 된 데이터 *가 없으므로 이러한 이점을 얻지 못합니다.

즉, 내부 데이터를 사용하지 않는 경우 사용하는 것이 좋고 실행하는 데 약간 더 빠릅니다. 그래도 글로벌 데이터를 건드리지 않았는지 확인하십시오.

  • 일부 언어에는 데이터 및 정적 메서드를 캡슐화 할 수있는 클래스 수준 변수도 있습니다.

4

그것은 합리적인 접근법 인 것 같습니다. 너무 많은 정적 클래스 / 메소드를 사용하고 싶지 않은 이유는 결국 객체 지향 프로그래밍에서 벗어나 구조화 된 프로그래밍의 영역으로 이동하기 때문입니다.

단순히 A를 B로 변환하는 경우, 우리가하는 일은 텍스트를 변환하는 것입니다.

"hello" =>(transform)=> "<b>Hello!</b>"

그러면 정적 방법이 의미가 있습니다.

그러나 객체에 대해 이러한 정적 메서드를 자주 호출하고 많은 호출에 대해 고유 한 경향이 있거나 (예 : 사용 방법이 입력에 따라 다름) 객체의 고유 한 동작의 일부인 경우 객체의 일부로 만들고 상태를 유지하는 것이 현명합니다. 이를 수행하는 한 가지 방법은 인터페이스로 구현하는 것입니다.

class Interface{
    method toHtml(){
        return transformed string (e.g. "<b>Hello!</b>")
    }

    method toConsole(){
        return transformed string (e.g. "printf Hello!")
    }
}


class Object implements Interface {
    mystring = "hello"

    //the implementations of the interface would yield the necessary 
    //functionality, and it is reusable across the board since it 
    //is an interface so... you can make it specific to the object

   method toHtml()
   method toConsole()
}

편집 : 정적 메서드를 많이 사용하는 좋은 예는 Asp.Net MVC 또는 Ruby의 html 도우미 메서드입니다. 개체의 동작과 관련이없는 html 요소를 생성하므로 정적입니다.

편집 2 : 기능 프로그래밍을 구조화 프로그래밍으로 변경했습니다 (어떤 이유로 혼란 스러웠습니다), 그것을 지적하기 위해 Torsten에 소품.


2
정적 방법을 사용하는 것이 함수형 프로그래밍으로 적합하다고 생각하지 않으므로 구조화 된 프로그래밍을 의미한다고 생각합니다.
Torsten Marek

3

최근에 처음에 정적 클래스로 구현 된 일부 클래스를 제거 / 수정하기 위해 애플리케이션을 리팩토링했습니다. 시간이 지남에 따라 이러한 클래스는 많은 것을 얻었고 사람들은 인스턴스가 떠 다니지 않았기 때문에 새 함수를 정적으로 태그 지정했습니다.

그래서 제 대답은 정적 클래스가 본질적으로 나쁘지는 않지만 지금 인스턴스 생성을 시작한 다음 나중에 리팩터링하는 것이 더 쉬울 수 있다는 것입니다.


3

나는 그것을 디자인 냄새라고 생각합니다. 대부분 정적 방법을 사용하는 경우 OO 디자인이 좋지 않을 것입니다. 반드시 나쁜 것은 아니지만 모든 냄새가 그렇듯이 나를 멈추고 다시 평가하게 만들 것입니다. 더 나은 OO 디자인을 만들 수 있거나 다른 방향으로 가서이 문제에 대해 OO를 완전히 피해야 함을 암시합니다.


2

나는 많은 정적 메소드와 싱글 톤이있는 클래스 사이를 오 가곤했다. 둘 다 문제를 해결하지만 싱글 톤은 하나 이상으로 훨씬 쉽게 대체 될 수 있습니다. (프로그래머는 항상 무언가가 하나만있을 것이라고 확신하는 것처럼 보이며 매우 제한된 경우를 제외하고 정적 메서드를 완전히 포기할 수있을만큼 잘못된 시간을 내 자신이 발견했습니다).

어쨌든 싱글 톤은 나중에 팩토리에 무언가를 전달하여 다른 인스턴스를 가져오고 리팩토링없이 전체 프로그램의 동작을 변경하는 기능을 제공합니다. 정적 메서드의 전역 클래스를 다른 "백킹"데이터 또는 약간 다른 동작 (하위 클래스)으로 변경하는 것은 큰 고통입니다.

그리고 정적 메서드는 비슷한 이점이 없습니다.

네, 그들은 나쁩니다.


1

내부 상태가 아닌 한 괜찮습니다. 일반적으로 정적 메서드는 스레드로부터 안전한 것으로 예상되므로 도우미 데이터 구조를 사용하는 경우 스레드로부터 안전한 방식으로 사용하십시오.


1

C의 내부 상태를 사용할 필요 가 없다는 것을 알고 있다면 괜찮습니다. 하지만 나중에 변경 될 경우 메서드를 비 정적으로 만들어야합니다. 시작하는 것이 비 정적이라면 필요하지 않으면 내부 상태를 무시할 수 있습니다.


1

유틸리티 메서드라면 정적으로 만드는 것이 좋습니다. Guava와 Apache Commons는이 원칙을 기반으로합니다.

이에 대한 제 의견은 순전히 실용적입니다. 앱 코드 인 경우 정적 메서드는 일반적으로 가장 좋은 방법이 아닙니다. 정적 메서드에는 심각한 단위 테스트 제한이 있습니다. 쉽게 조롱 할 수 없습니다. 조롱 된 정적 기능을 다른 테스트에 삽입 할 수 없습니다. 또한 일반적으로 정적 메서드에 기능을 삽입 할 수 없습니다.

그래서 내 앱 로직에는 보통 작은 정적 유틸리티와 같은 메서드 호출이 있습니다. 즉

static cutNotNull(String s, int length){
  return s == null ? null : s.substring(0, length);
}

이점 중 하나는 이러한 방법을 테스트하지 않는다는 것입니다. :-)


1

물론 은색 총알은 없습니다. 정적 클래스는 작은 유틸리티 / 도우미에게 좋습니다. 그러나 비즈니스 로직 프로그래밍에 정적 방법을 사용하는 것은 확실히 악합니다. 다음 코드를 고려하십시오.

   public class BusinessService
   {

        public Guid CreateItem(Item newItem, Guid userID, Guid ownerID)
        {
            var newItemId = itemsRepository.Create(createItem, userID, ownerID);
            **var searchItem = ItemsProcessor.SplitItem(newItem);**
            searchRepository.Add(searchItem);
            return newItemId;
        }
    }

에 당신은 정적 메서드 호출을 참조 ItemsProcessor.SplitItem(newItem);그것은 원인을 냄새

  • 명시적인 종속성을 선언하지 않았으며 코드를 파헤 치지 않으면 클래스와 정적 메서드 컨테이너 간의 결합을 간과 할 수 있습니다.
  • BusinessService격리를 테스트 할 수 없으며 ItemsProcessor(대부분의 테스트 도구는 정적 클래스를 모의하지 않음) 단위 테스트를 불가능하게 만듭니다. 단위 테스트 없음 == 낮은 품질

0

정적 메서드는 일반적으로 상태 비 저장 코드의 경우에도 나쁜 선택입니다. 대신 한 번 인스턴스화되고 메서드를 사용하려는 클래스에 주입되는 이러한 메서드로 싱글 톤 클래스를 만듭니다. 이러한 클래스는 모의 및 테스트가 더 쉽습니다. 그들은 훨씬 더 객체 지향적입니다. 필요할 때 프록시로 래핑 할 수 있습니다. 정적은 OO를 훨씬 더 어렵게 만들고 거의 모든 경우에 사용할 이유가 없습니다. 100 %는 아니지만 거의 모두.

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