많은 매개 변수를 가진 단일 메소드 대 순서대로 호출해야하는 많은 메소드


16

나는 많은 일을해야하며 (이동, 회전, 특정 축을 따라 크기 조정, 최종 위치로 회전) 코드 가독성을 유지하기 위해 이것을 수행하는 가장 좋은 방법을 잘 모르겠습니다. 한편으로, 필요한 것을 수행하기 위해 많은 매개 변수 (10+)를 사용하여 단일 방법을 만들 수 있지만 이것은 악몽을 읽는 코드입니다. 반면에 각각 1-3 개의 매개 변수로 여러 메서드를 만들 수 있지만 올바른 결과를 얻으려면 이러한 메서드를 매우 구체적인 순서로 호출해야합니다. 나는 메소드가 한 가지 일을하고 잘하는 것이 가장 좋다는 것을 읽었지만, 찾기 어려운 버그에 대한 코드를 열기 위해 호출 해야하는 많은 메소드가있는 것처럼 보입니다.

버그를 최소화하고 코드를 쉽게 읽을 수있는 프로그래밍 패러다임이 있습니까?


3
가장 큰 문제는 '순서대로 호출하지 않는 것'이 아니라, (또는 미래의 프로그래머가) 순서대로 호출해야한다는 것을 알지 못하는 것입니다. 유지 보수 프로그래머가 세부 사항을 알고 있는지 확인하십시오 (요구 사항, 설계 및 스펙을 문서화하는 방법에 크게 좌우 됨). 단위 테스트, 주석 사용 및 모든 매개 변수를 사용하고 다른 매개 변수를 호출하는 도우미 기능 제공
— mattnz

그냥 오프 손 발언으로, 유창 인터페이스 및 명령 패턴이 유용 할 수 있습니다. 그러나 어떤 디자인이 가장 적합한 지 결정하는 것은 사용자 (소유자)와 라이브러리 사용자 (고객)의 책임입니다. 다른 사람들이 지적했듯이, 사용자는 조작이 비순환 적 이며 (실행 순서에 민감 함) 사용자에게 올바르게 사용하는 방법을 찾지 못하면 의사 소통 해야합니다.
— rwong

계산 불가능한 연산의 예 : 이미지 변환 (회전, 크기 조절 및 자르기), 행렬 곱셈 등
— rwong

어쩌면 카레를 사용할 수 있습니다. 이렇게하면 메소드 / 함수를 잘못된 순서로 적용하는 것이 불가능합니다.
— Giorgio

여기서 어떤 방법을 사용하고 있습니까? 내 말은, 표준은 그것을 적용하는 메소드 에 전달하는 변형 객체 (예 : Java의 Affine Transform for 2D 물건)를 전달하는 것입니다. 변환의 내용은 설계에 따라 초기 작업을 호출하는 순서에 따라 다릅니다 (따라서 "원하는 순서로"가 아니라 "필요한 순서로 호출합니다").
— Clockwork-Muse

답변:


24

시간적 커플 링에 주의하십시오 . 그러나 이것이 항상 문제가되는 것은 아닙니다.

순서대로 단계를 수행해야하는 경우 1 단계는 2 단계에 필요한 일부 오브젝트 (예 : 파일 스트림 또는 기타 데이터 구조)를 생성합니다. 이 혼자 두 번째 기능이 있어야 한다 이 실수로 잘못된 순서로 호출도 가능하지, 처음으로 불리.

기능을 한 입 크기로 분할하면 각 부분을 이해하기 쉽고 테스트하기가 훨씬 쉽습니다. 100 줄 의 거대한 함수 와 중간 부분에 무언가 가 있다면 실패한 테스트에서 무엇이 잘못되었는지 어떻게 알 수 있습니까? 5 가지 라인 방법 중 하나가 중단되면 실패한 단위 테스트에서주의가 필요한 하나의 코드로 즉시 지시합니다.

복잡한 코드 는 다음과 같습니다.

public List<Widget> process(File file) throws IOException {
  try (BufferedReader in = new BufferedReader(new FileReader(file))) {
    List<Widget> widgets = new LinkedList<>();
    String line;
    while ((line = in.readLine()) != null) {
      if (isApplicable(line)) { // Filter blank lines, comments, etc.
        Ore o = preprocess(line);
        Ingot i = smelt(o);
        Alloy a = combine(i, new Nonmetal('C'));
        Widget w = smith(a);
        widgets.add(w);
      }
    }
    return widgets;
  }
}

원시 데이터를 완성 된 위젯으로 변환하는 프로세스 중 어느 시점에서나 각 함수는 프로세스의 다음 단계에서 필요한 것을 반환합니다. 슬래그에서 합금을 형성 할 수 없으며, 먼저 슬래그를 제련 (정제)해야합니다. 입력으로 적절한 허용 (예 : 강철)이 없으면 위젯을 만들 수 없습니다.

각 단계의 특정 세부 사항은 테스트 할 수있는 개별 기능에 포함되어 있습니다. 마이닝 암석의 전체 프로세스를 단위 테스트하고 위젯을 만드는 것이 아니라 각 특정 단계를 테스트하십시오. 이제 "위젯 작성"프로세스가 실패하는 경우 특정 이유를 좁힐 수있는 쉬운 방법이 있습니다.

테스트 및 정확성 증명의 이점 외에도 이러한 방식으로 코드를 작성하는 것이 훨씬 쉽습니다. 아무도 거대한 매개 변수 목록을 이해할 수 없습니다 . 작은 조각으로 분해, 및 각 작은 조각 수단을 보여입니다 grokkable .


2
감사합니다. 이것이 문제를 해결하는 좋은 방법이라고 생각합니다. 객체의 수를 늘리고 (불필요하다고 느낄 수도 있지만) 가독성을 유지하면서 순서를 강제합니다.
— tomsrobots

10

"모든 명령을 순서대로 실행해야합니다"라는 인수는 무의미합니다. 거의 모든 코드가 올바른 순서로 실행되어야하기 때문입니다. 결국, 파일에 쓸 수 없다가 파일을 연 다음 닫을 수 없습니까?

코드를 가장 유지 관리하기 쉽게 만드는 것에 집중해야합니다. 이것은 일반적으로 작고 쉽게 이해되는 함수 작성을 의미합니다. 각 기능은 단일 목적을 가져야하며 예상치 못한 부작용이 없어야합니다.


5

» ImageProcesssor «(또는 프로젝트에 적합한 이름) 및 구성 객체 ProcessConfiguration을 작성합니다. 필요한 모든 매개 변수를 보유 합니다.

 ImageProcessor p = new ImageProcessor();

 ProcessConfiguration config = new processConfiguration().setTranslateX(100)
                                                         .setTranslateY(100)
                                                         .setRotationAngle(45);
 p.process(image, config);

이미지 프로세서 내부에서 하나의 mehtod 뒤에 전체 프로세스를 캡슐화합니다. process()

public class ImageProcessor {

    public Image process(Image i, ProcessConfiguration c){
        Image processedImage=i.getCopy();
        shift(processedImage, c);
        rotate(processedImage, c);
        return processedImage;
    }

    private void rotate(Image i, ProcessConfiguration c) {
        //rotate
    }

    private void shift(Image i, ProcessConfiguration c) {
        //shift
    }
}

이 방법은 올바른 순서로 변형 메소드를 호출 shift(), rotate(). 각 메소드는 전달 된 ProcessConfiguration 에서 적절한 매개 변수를 가져옵니다. .

public class ProcessConfiguration {

    private int translateX;

    private int rotationAngle;

    public int getRotationAngle() {
        return rotationAngle;
    }

    public ProcessConfiguration setRotationAngle(int rotationAngle){
        this.rotationAngle=rotationAngle;
        return this;
    }

    public int getTranslateY() {
        return translateY;
    }

    public ProcessConfiguration setTranslateY(int translateY) {
        this.translateY = translateY;
        return this;
    }

    public int getTranslateX() {
        return translateX;
    }

    public ProcessConfiguration setTranslateX(int translateX) {
        this.translateX = translateX;
        return this;
    }

    private int translateY;

}

나는 사용했다 유체 인터페이스를

public ProcessConfiguration setRotationAngle(int rotationAngle){
    this.rotationAngle=rotationAngle;
    return this;
}

위와 같이 깔끔한 초기화가 가능합니다.

필요한 매개 변수를 하나의 객체에 캡슐화하는 명백한 이점. 메소드 서명을 읽을 수있게됩니다.

private void shift(Image i, ProcessConfiguration c)

그것은 관한 이동 이미지 와 상세한 매개 변수를 어떻게 든되는 구성 .

또는 ProcessingPipeline을 만들 수 있습니다 .

public class ProcessingPipeLine {

    Image i;

    public ProcessingPipeLine(Image i){
        this.i=i;
    };

    public ProcessingPipeLine shift(Coordinates c){
        shiftImage(c);
        return this;
    }

    public ProcessingPipeLine rotate(int a){
        rotateImage(a);
        return this;
    }

    public Image getResultingImage(){
        return i;
    }

    private void rotateImage(int angle) {
        //shift
    }

    private void shiftImage(Coordinates c) {
        //shift
    }

}

메소드에 대한 메소드 호출 processImage은 그러한 파이프 라인을 인스턴스화하고 shift , rotate

public Image processImage(Image i, ProcessConfiguration c){
    Image processedImage=i.getCopy();
    processedImage=new ProcessingPipeLine(processedImage)
            .shift(c.getCoordinates())
            .rotate(c.getRotationAngle())
            .getResultingImage();
    return processedImage;
}

3

어떤 종류의 카레 사용을 고려 했습니까 ? 클래스 Processee와 클래스가 있다고 상상해보십시오 Processor.

class Processor
{
    private final Processee _processee;

    public Processor(Processee p)
    {
        _processee = p;
    }

    public void process(T1 a1, T2 a2)
    {
        // Process using a1
        // then process using a2
    }
}

이제 클래스를 대체 할 수있는 Processor두 개의 클래스로 Processor1와 Processor2:

class Processor1
{
    private final Processee _processee;

    public Processor1(Processee p)
    {
        _processee = p;
    }

    public Processor2 process(T1 a1)
    {
        // Process using argument a1

        return new Processor2(_processee);
    }
}

class Processor2
{
    private final Processee _processee;

    public Processor(Processee p)
    {
        _processee = p;
    }

    public void process(T2 a2)
    {
        // Process using argument a2
    }
}

그런 다음 다음을 사용하여 올바른 순서로 오퍼레이션을 호출 할 수 있습니다.

new Processor1(processee).process(a1).process(a2);

매개 변수가 두 개 이상인 경우이 패턴을 여러 번 적용 할 수 있습니다. 원하는대로 인수를 그룹화 할 수도 있습니다. 즉, 각 process메소드가 정확히 하나의 인수 를 가질 필요는 없습니다 .


유일한 아이디어는;) 유일한 차이점은 파이프 라인 이 엄격한 처리 순서를 시행 한다는 것 입니다.
— 토마스 정크

@ThomasJunk : 내가 이해하는 한,이 요구 사항은 "올바른 결과를 얻으려면 이러한 방법을 매우 특정한 순서로 호출해야합니다." 엄격한 실행 순서를 갖는 것은 함수 구성과 매우 흡사합니다.
— Giorgio

그러나 주문 순서가 변경되면 많은 리팩토링을 수행해야합니다.)
— Thomas Junk

@ThomasJunk : 맞습니다. 실제로 응용 프로그램에 따라 다릅니다. 처리 단계를 매우 자주 교체 할 수 있다면 아마도 더 나은 방법 일 것입니다.
— Giorgio
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.