예상치 못한 복잡성에 대한 계획을 세우지 않으면 이에 압도당하게 됩니다.

복잡성
세상은 믿을 수 없을 만큼 복잡하며, 소프트웨어 작성 기술만큼 이를 더욱 분명하게 보여주는 곳은 없습니다. 소프트웨어는 사회 구조, 비즈니스 욕구, 제한된 지식 간의 복잡한 상호 작용의 결과입니다. 이러한 상호 작용의 복잡성은 필연적으로 코드 구조에 나타납니다. 엔지니어로서 우리의 임무는 이를 관리하고 시간이 지남에 따라 증가하는 복잡성을 처리할 수 있도록 추상화를 준비하는 것입니다. 이와 관련하여 가장 중요한 기술은 논리 조각이 익사하지 않고 더 많은 복잡성을 수용할 수 있는 공간을 만드는 것입니다. 복잡성이 있으면 복용량이 독이 됩니다.
그렇기 때문에 여러분이 듣게 될 가장 일반적인 소프트웨어 작성 조언 중 하나는 로직이 여러 개의 작고 집중된 메서드로 분할된 작고 집중된 클래스를 많이 작성하라는 것입니다. 이 스타일로 작성된 소프트웨어는 개별 부분이 복잡해질 수 있는 공간을 제공합니다. Rescale 개발 팀에서는 성장하는 메소드나 클래스에서 로직을 분리하는 시간이 개발자가 일반적으로 생각하는 것보다 훨씬 빠르다는 것을 발견했습니다. 우리는 클래스와 메소드의 첫 번째 및 두 번째 버전을 작성할 때 추상화를 끌어내는 것을 선호합니다.
예를 들어 설명하기 위해 최근에는 전송할 파일, 실행할 분석, 변수 정의 등 워크플로를 설명하는 타사 xml 파일을 구문 분석하는 코드를 작성했습니다. 처음에 우리의 구문 분석 코드는 상대적으로 간단했는데, 그 이유는 주로 우리가 구문 분석해야 하는 모든 것을 알지 못했기 때문입니다. 우리는 입력 파일과 변수를 구문 분석하는 것부터 시작했습니다. 이들 각각은 xml 노드로 표시되며, 각 xml 노드는 type이라는 속성으로 유형을 나타내며, 유형에 따라 달라지는 속성으로 중요한 값을 나타냅니다.
일부 초기 구문 분석 코드는 다음과 같습니다.

컬렉션 inputFileNames = getNodeStream() .filter(n -> n.getAttributes() .getNamedItem("type") .getNodeValue().equals("inputFileType")) .map(n -> n.getAttributes() .getNamedItem( "파일 이름") .getNodeValue()) .collect(Collectors.toList()); 컬렉션 inputVariableNames = getNodeStream() .filter(n -> n.getAttributes() .getNamedItem("type") .getNodeValue().equals("inputVariableType")) .map(n -> n.getAttributes() .getNamedItem( “variableName”) .getNodeValue()) .collect(Collectors.toList());

도우미 메서드 getNodeStream 위의 변환은 노드 목록 문서에서 흐름 조작의 용이성을 위해.
이 코드에 대해 주목해야 할 두 가지 사항이 있습니다. 상수 대신 마법 문자열, 코드를 복제하여 속성 값을 추출합니다. 이러한 간단한 리팩토링을 적용한 후에는 구문 분석 코드가 구현 세부 사항으로 덜 복잡해지고 의도에 더 가깝게 읽혀집니다.

컬렉션 inputFileNames = getNodeStream() .filter(n -> INPUT_FILE_TYPE.equals(getAttribute(n, TYPE))) .map(n -> getAttribute(n, FILE_NAME)) .collect(Collectors.toList()); 컬렉션 inputVariableNames = getNodeStream() .filter(n -> INPUT_VARIABLE_TYPE.equals(getAttribute(n, TYPE))) .map(n -> getAttribute(n, VARIABLE_NAME)) .collect(Collectors.toList());

이는 복잡성이 증가하는 코드를 준비하는 데 모범 사례가 어떻게 도움이 될 수 있는지 보여주는 첫 번째 예입니다. 표면적으로는 우리가 많은 일을 한 것처럼 보이지 않지만, 우리가 하는 일에 더 가까운 코드를 작성함으로써 평균, 컴퓨터보다는 하지, 새로운 추가 사항을 작성할 때 머릿속에 기억해야 할 컨텍스트가 줄어들기 때문에 이 코드에 더 많은 복잡성을 추가하기가 더 쉬워졌습니다.
이러한 문자열 컬렉션을 구문 분석하는 것이 이 XML로 수행한 전부라면 이 코드를 그대로 두는 것이 좋습니다. 그러나 이것이 소프트웨어이기 때문에 상황은 더욱 복잡해졌습니다. 세 번째 또는 네 번째 유형/속성 구문 분석 쌍을 작성한 후 일부 열거형을 추출하기로 결정했습니다.

public enum NodeAttribute { TYPE(“유형”), FILE_NAME(“파일 이름”), VARIABLE_NAME(“변수 이름”), … private final String attrName; 개인 NodeAttribute(String attrName) { this.attrName = attrName; } 공개 문자열 getValue(노드 노드) { return node.getAttributes() .getNamedItem(this.attrName) .getNodeValue(); } public enum NodeType { INPUT_FILE(“inputFile”), INPUT_VARIABLE(“inputVariable”), OUTPUT_VARIABLE(“outputVariable”), … private 최종 문자열 값; private NodeType(String value) { this.value = value; } 공개 부울 일치(노드 노드) { return NodeAttributes.TYPE.getValue(node).equals(this.value); } }

이제 구문 분석 코드는 다음과 같습니다.

컬렉션 inputFileNames = getNodeStream() .filter(NodeType.INPUT_FILE::matches) .map(NodeAttribute.FILE_NAME::getValue) .collect(Collectors.toList()); 컬렉션 inputVariableNames = getNodeStream() .filter(NodeType.INPUT_VARIABLE::matches) .map(NodeAttribute.VARIABLE_NAME::getValue) .collect(Collectors.toList()); …

여기서 로직을 추출하는 것은 과잉인 것처럼 보이지만 열거형이 재사용을 위해 이러한 상수를 정의하는 중앙 위치를 제공하기 때문에 그렇게 하려는 동기가 있었습니다. 우리가 곧 알게 된 또 다른 이점은 다양한 조각이 더욱 복잡해질 수 있는 공간입니다.  이제 여러분은 단지 xml 노드에서 속성을 꺼내는 것일 뿐인데 어떻게 더 복잡해질 수 있겠는가라고 생각할 수도 있습니다. 우리는 그것이 그럴 수도, 그럴 수도 있을 것이라고는 확실히 생각하지 않았습니다.
그러나 이 제XNUMX자 XML에서 일부 노드는 filename이라는 속성이 있는 파일을 참조하고 일부 참조된 파일은 fileName이라는 속성이 있는 것으로 나타났습니다. 이것은 프로그래머들이 욕하게 만드는 종류의 일이지만 운 좋게도 우리는 이 문제를 쉽게 처리할 준비가 되어 있었습니다.

public enum NodeAttribute { TYPE(“유형”), FILE_NAME(“파일 이름”, “파일 이름”), VARIABLE_NAME(“변수 이름”), … private final String[] attrNames; 개인 NodeAttribute(String... attrNames) { this.attrNames = attrNames; } 공개 문자열 getValue(노드 노드) { return Arrays.stream(this.AttrNames) .map(attr -> node.getAttributes().getNamedItem(attr)) .filter(attr -> attr != null) .findFirst() .orElseThrow(() -> new NoSuchElementException( "Node: " + node.getNodeValue() + "에는 다음과 같은 속성이 없습니다: " + Arrays.toString(mAttrNames) )); }

다른 구문 분석 코드는 변경할 필요가 없었습니다. 문자열 상수를 계속 사용했다면 파일 이름이나 파일 이름을 확인하기 위해 구문 분석 코드 전반에 걸쳐 많은 업데이트를 수행해야 하거나 파일을 참조하는 노드에 대한 특수 메서드를 작성해야 했을 것입니다. if/else 논리로 인해 코드가 더 복잡해졌을 것입니다. 하지만 우리는 일찍 추상화했기 때문에 이 논리를 넣을 자리가 있었습니다. 우리는 속성 대/소문자 구분에서 이러한 차이를 예상하지 못했다는 점을 다시 강조하고 싶습니다. 그러나 이것이 바로 요점입니다. 기대 코드가 더 복잡해집니다. 기대하지마.
왜 일부 노드는 파일 이름을 사용하고 다른 노드는 파일 이름을 사용합니까? 우리는 서로 다른 두 사람이 서로 다른 노드를 직렬화하는 작업을 수행했으며 상대방이 선택한 대문자 사용 방식을 몰랐다고 추측할 수 있습니다. 아마도 그들은 구두로 의사소통을 하고 "파일 이름"을 속성으로 결정했지만 한 사람은 낙타 케이싱을 사용했습니다. 아니면 한 노드에서 다른 노드로 작업하다가 어떤 대문자 사용 체계가 사용되었는지 잊어버렸을 수도 있습니다.
어떤 경우이든, 제XNUMX자 개발팀의 사회 구조의 복잡성은 이러한 속성 이름의 차이로 나타나며 우리의 코드베이스에도 드러납니다. 그런 종류의 복잡성에 대비하고 적절한 구조로 처리할 준비를 하는 것이 우리의 임무입니다.

저자

  • 애덤 매켄지

    CTO로서 Adam은 HPC 및 고객 성공 팀을 관리하는 책임을 맡고 있습니다. Adam은 Boeing에서 경력을 시작하여 787년 동안 XNUMX을 작업하면서 구조 및 소프트웨어 엔지니어링 프로젝트를 관리하고 날개를 설계, 분석 및 최적화했습니다. Adam은 오레곤 주립대학교에서 우등으로 기계공학 학사학위를 취득했습니다.

비슷한 게시물