클래스의 복잡성 감소


10

몇 가지 답변 을보고 Google에서 검색했지만 유용한 정보를 찾을 수 없었습니다 (예 : 불편한 부작용이 없음).

내 문제는 요약하자면, 나는 객체가 있고 그것에 대해 일련의 작업 을 수행해야한다는 것입니다. 차를 만드는 것과 같은 일종의 조립 라인이라고 생각합니다.

이러한 객체는 Method Objects 라고 합니다 .

따라서이 예제에서는 어느 시점에서 installWithSeat, installFrontSeat, installWoodenInserts를 실행 해야하는 CarWithoutUpholstery가 있습니다 (작업은 서로 간섭하지 않으며 병렬로 수행 될 수도 있음). 이 작업은 CarWithoutUpholstery.worker ()에 의해 수행되고 CarWithUpholstery가 될 새로운 객체를 생성합니다.이 객체는 cleanInsides (), verifyNoUpholsteryDefects () 등을 실행합니다.

단일 단계에서의 작업은 이미 독립적입니다. 즉, 이미 임의의 순서로 실행될 수있는 하위 세트를 사용하고 있습니다 (전면 및 후면 좌석은 임의의 순서로 설치 가능).

내 논리는 현재 구현 단순화를 위해 Reflection을 사용합니다.

즉, 일단 CarWithoutUpholstery가 있으면 객체는 performSomething ()이라는 메소드를 스스로 검사합니다. 이때 모든 메소드를 실행합니다.

myObject.perform001SomeOperation();
myObject.perform002SomeOtherOperation();
...

오류와 물건을 확인하는 동안. 작업 순서 중요하지 않지만 , 결국 어떤 순서가 중요하다는 것을 알게되면 사전 편찬 순서를 할당했습니다. 이것은 YAGNI 와 모순 되지만 비용이 거의 들지 않습니다-간단한 sort ()-막대한 메소드 이름 바꾸기 (또는 테스트를 수행하는 다른 메소드, 예를 들어 메소드 배열)를 줄이면 절약 할 수 있습니다.

다른 예

차를 만드는 대신 누군가에 대한 비밀 경찰 보고서를 작성하여 내 사악한 대 군주 에게 제출해야한다고하자 . 내 마지막 개체는 ReadyReport입니다. 그것을 구성하기 위해 기본 정보 (이름, 성, 배우자 ...)를 수집하여 시작합니다. 이것은 나의 단계 A입니다. 배우자가 있는지 여부에 따라 B1 또는 B2 단계로 진행하여 한두 사람에 대한 성적 데이터를 수집해야 할 수도 있습니다. 이것은 나이트 라이프, 길거리 카메라, 섹스 샵 판매 영수증 등을 제어하는 ​​다른 이블 미니언에 대한 여러 가지 쿼리로 구성됩니다. 그리고 등등.

피해자가 가족이 없다면, GetInformationAboutFamily 단계에 들어 가지 않을 것입니다. 그러나 그렇게한다면, 처음 아버지 나 어머니 또는 형제 자매를 대상으로하는지 여부는 관계가 없습니다. 그러나 FamilyStatusCheck를 수행하지 않으면 이전 단계에 속하면 그렇게 할 수 없습니다.

그것은 모두 훌륭하게 작동합니다 ...

  • 추가 작업이 필요한 경우 전용 메소드 만 추가하면됩니다.
  • 작업이 여러 단계에 공통적이라면 수퍼 클래스에서 상속받을 수 있습니다.
  • 작업은 간단하고 독립적입니다. 한 작업의 가치가 다른 작업에 필요 하지 않습니다 ( 다른 단계에서 수행되는 작업 ).
  • 줄 아래에있는 개체는 만든 개체가 해당 조건을 처음 확인하지 않은 경우 에도 존재 하지 않았기 때문에 많은 테스트를 수행 할 필요 가 없습니다. 대시 보드를 청소하고 대시 보드를 확인, 대시 보드에 삽입을 배치 할 때 즉, 내가 대시 보드가 실제로 있는지 확인 할 필요가 있다 .
  • 쉽게 테스트 할 수 있습니다. 부분 개체를 쉽게 조롱하고 그에 대한 방법을 실행할 수 있으며 모든 작업은 결정적인 블랙 박스입니다.

...그러나...

I 첨가하면 문제가 발생 마지막 동작을 전체 모듈 ( "이하 N 전용 방법보다 ') 필수 복잡성 인덱스를 초과 발생 제에있어서의 물체 중 하나.

나는이 문제를 이미 위층에서 가져 왔고,이 경우에 많은 사적인 방법은 재난에 대한 이야기가 아니라고 제안했다. 복잡성 이다 가 있지만, 작업이 있기 때문에 거기에 있다 복잡하고, 실제로는 모든 것을 복잡 아니다 - 그것은 단지 오래 .

사악한 대 군주 예를 사용하여, 내 문제는 모든 대식 정보 를 요청한 사악한 대 군주 (일명 거부하는 사람 ) ,식이 하수인은 레스토랑, 간이 주방, 노점상, 라이센스가없는 노점상, 온실을 쿼리해야한다고 말합니다. 친밀로 알려진 - 등의 소유자 및 이블 (하위) 오버로드 또한 거부 할 수 없다 그는 - 나는 GetDietaryInformation 단계에서 너무 많은 쿼리를 수행하고있어 불평.

참고 : 여러 관점에서 이것은 전혀 문제가 아니라는 것을 알고 있습니다 (가능한 성능 문제 등 무시). 일어나고있는 것은 특정 측정 항목이 불만족 스럽다는 것입니다.

어떤 생각 내가 할 수있는

첫 번째 옵션 외에도 이러한 모든 옵션이 가능하며 방어 적이라고 생각합니다.

  • 나는 비열한 방법을 반 내 선언 할 수 있음을 확인했습니다 protected. 그러나 나는 테스트 과정에서 약점을 악용하고 있었고, 잡힐 때 자신을 정당화하는 것 외에는 이것을 좋아하지 않습니다. 또한, 그것은 스톱 갭 측정입니다. 필요한 작업 수가 두 배가되면 어떻게됩니까? 그렇지 않더라도, 그렇다면 어떻게해야합니까?
  • 이 단계를 임의로 AnnealedObjectAlpha, AnnealedObjectBravo 및 AnnealedObjectCharlie로 나눌 수 있으며 각 단계에서 수행되는 작업의 1/3을 가질 수 있습니다. 나는 이것이 실제로 시험을 통과하는 것 외에는 아무런 이점없이 복잡성 (N-1 더 많은 클래스)을 추가 한다는 인상 아래 있습니다. 물론 CarWithFrontSeatsInstalled와 CarWithAllSeatsInstalled는 논리적으로 연속적인 단계라는 것을 알 수 있습니다. 나중에 Alpha가 요구하는 Bravo 방법의 위험은 적으며, 잘 연주하면 더 작습니다. 그러나 여전히.
  • 원격으로 비슷한 다른 작업을 단일 작업으로 묶을 수 있습니다. performAllSeatsInstallation(). 이것은 스톱 갭 측정 일 뿐이며 단일 작업의 복잡성을 증가시킵니다. 작업 A와 B를 다른 순서로 수행해야하고 E = (A + C)와 F (B + D) 안에 포장하면 E와 F를 묶어서 코드를 섞어 야합니다. .
  • 람다 함수 배열을 사용하고 검사를 깔끔하게 회피 할 수는 있지만 그 혼란을 발견합니다. 그러나 이것은 지금까지 가장 좋은 대안입니다. 반사를 제거합니다. 내가 가진 두 가지 문제는 아마도 가설뿐만 아니라 모든 메소드 객체 를 다시 작성하라는 요청을 받았을 것입니다 CarWithEngineInstalled. 그리고 그것은 매우 좋은 직업 보안이 될 것이지만, 실제로 그렇게 많이 호소하지는 않습니다. 코드 커버리지 검사기는 람다 (해결 가능하지만 여전히 )에 문제 가 있음을 나타냅니다.

그래서...

  • 내 최선의 선택은 어느 것입니까?
  • 내가 고려하지 않은 더 좋은 방법이 있습니까? ( 아마도 깨끗하게 와서 직접 물어 보는 것이 더 좋을까요? )
  • 이 디자인은 절망적으로 결함이 있습니까? 그리고이 아키텍처는 완전히 패배와 도랑을 인정하는 것이 좋습니까? 내 경력에는 좋지 않지만 잘못 설계된 코드를 작성하는 것이 장기적으로 더 나을까요?
  • 현재 선택이 실제로 하나의 진정한 방법이며 더 나은 품질의 메트릭 (및 / 또는 계측)을 설치하기 위해 싸워야합니까? 이 마지막 옵션을 위해 참조가 필요합니다 ... 불평하는 동안 @PHB에서 손을 can 수 는 없습니다. 이들은 당신이 찾고있는 통계가 아닙니다 . 아무리 할 수 ​​있고 싶어도

3
또는 "최대 복잡도 초과"경고를 무시할 수 있습니다.
Robert Harvey

가능하다면 할 것입니다. 어쨌든이 지표는 그 자체로 신성한 질을 얻었습니다. 나는 PTB를 유용한 도구로 받아 들일 수 있도록 PTB를 계속 시도하고 설득하고 있습니다. 나는 아직 성공할지도 모른다 ...
LSerni

작업이 실제로 개체를 구성하고 있습니까? 아니면 언급 한 특정 속성 (부분 종속성 순서가있는 여러 작업)을 공유하기 때문에 어셈블리 라인 은유를 사용하고 있습니까? 중요한 것은 전자가 빌더 패턴을 제안하는 반면, 후자는 더 자세한 내용이 채워진 다른 패턴을 제안 할 수 있다는 것입니다.
outis

2
통계의 폭정. 지표는 유용한 지표이지만 지표가 사용되는 이유를 무시하면서 시행하는 것은 도움이되지 않습니다.
Jaydee

2
위층 사람들에게 본질적 복잡성과 우발적 복잡성의 차이를 설명하십시오. en.wikipedia.org/wiki/No_Silver_Bullet 규칙을 어 기고있는 복잡성이 필수적이라고 확신하는 경우 프로그래머 별로 리팩터링 하면 규칙 주위를 스케이팅하고 확산시킬 수 있습니다. 복잡성. 사물을 단계로 캡슐화하는 것이 임의적이지 않고 (단계는 문제와 관련이 있음) 재 설계가 코드를 유지 관리하는 개발자의인지 부하를 줄이면이 ​​작업을 수행하는 것이 좋습니다.
Fuhrmanator

답변:


15

긴 작업 순서는 자체 클래스 여야하는 것처럼 보이며 작업을 수행하려면 추가 개체가 필요합니다. 이 솔루션에 가까이있는 것 같습니다. 이 긴 절차는 여러 단계 또는 단계로 나눌 수 있습니다. 각 단계 또는 단계는 공용 메소드를 노출하는 자체 클래스 일 수 있습니다. 각 단계에서 동일한 인터페이스를 구현하려고합니다. 그런 다음 조립 라인은 단계 목록을 추적합니다.

자동차 어셈블리 예제를 작성하려면 Car클래스를 상상해보십시오 .

public class Car
{
    public IChassis Chassis { get; private set; }
    public Dashboard { get; private set; }
    public IList<Seat> Seats { get; private set; }

    public Car()
    {
        Seats = new List<Seat>();
    }

    public void AddChassis(IChassis chassis)
    {
        Chassis = chassis;
    }

    public void AddDashboard(Dashboard dashboard)
    {
        Dashboard = dashboard;
    }
}

나는 구성 요소와 같은 일부의 구현을 떠날거야 IChassis, Seat그리고 Dashboard간결성을 위해.

Car자체 구축을위한 책임을지지 않습니다. 조립 라인은 다음과 같이 클래스로 캡슐화합시다.

public class CarAssembler
{
    protected List<IAssemblyPhase> Phases { get; private set; }

    public CarAssembler()
    {
        Phases = new List<IAssemblyPhase>()
        {
            new ChassisAssemblyPhase(),
            new DashboardAssemblyPhase(),
            new SeatAssemblyPhase()
        };
    }

    public void Assemble(Car car)
    {
        foreach (IAssemblyPhase phase in Phases)
        {
            phase.Assemble(car);
        }
    }
}

여기서 중요한 차이점은 CarAssembler에 구현하는 어셈블리 단계 객체 목록이 있다는 것 IAssemblyPhase입니다. 이제는 공개 메소드를 명시 적으로 다루고 있습니다. 즉, 각 단계를 자체적으로 테스트 할 수 있으며 하나의 단일 클래스에 너무 많이 넣지 않기 때문에 코드 품질 도구가 더 행복합니다. 조립 과정도 간단합니다. 단계를 반복 Assemble하고 차에 통과를 호출 하십시오. 끝난. 이 IAssemblyPhase인터페이스는 Car 객체를 취하는 단일 공용 메소드만으로도 간단합니다.

public interface IAssemblyPhase
{
    void Assemble(Car car);
}

이제 각 특정 단계에 대해이 인터페이스를 구현하는 구체적인 클래스가 필요합니다.

public class ChassisAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.AddChassis(new UnibodyChassis());
    }
}

public class DashboardAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        Dashboard dashboard = new Dashboard();

        dashboard.AddComponent(new Speedometer());
        dashboard.AddComponent(new FuelGuage());
        dashboard.Trim = DashboardTrimType.Aluminum;

        car.AddDashboard(dashboard);
    }
}

public class SeatAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
    }
}

각 단계는 개별적으로 매우 간단 할 수 있습니다. 일부는 하나의 라이너입니다. 일부는 4 개의 시트를 추가하는 것을 막는 것일 수도 있고, 다른 하나는 자동차에 대시 보드를 추가하기 전에 대시 보드를 구성해야합니다. 이렇게 오랫동안 도출 된 프로세스의 복잡성은 쉽게 테스트 할 수있는 재사용 가능한 여러 클래스로 나뉩니다.

지금 주문이 중요하지 않다고 말했지만 앞으로는 그럴 수 있습니다.

이 작업을 마치려면 자동차 조립 과정을 살펴 보겠습니다.

Car car = new Car();
CarAssembler assemblyLine = new CarAssembler();

assemblyLine.Assemble(car);

CarAssembler를 하위 클래스로 지정하여 특정 종류의 자동차 또는 트럭을 조립할 수 있습니다. 각 단계는 전체적으로 하나의 단위이므로 코드를 쉽게 재사용 할 수 있습니다.


이 디자인은 절망적으로 결함이 있습니까? 그리고이 아키텍처는 완전히 패배와 도랑을 인정하는 것이 좋습니까? 내 경력에는 좋지 않지만 잘못 설계된 코드를 작성하는 것이 장기적으로 더 나을까요?

내가 쓴 내용을 다시 써야한다고 결정하는 것이 나의 경력에 ​​검은 표식 이었다면, 나는 지금 도랑 파는 사람으로 일하고있을 것이며, 당신은 나의 조언을 취해서는 안됩니다. :)

소프트웨어 엔지니어링은 학습, 적용 및 재 학습의 영원한 과정입니다. 코드를 다시 작성하기로했다고해서 해고 당한다는 의미는 아닙니다. 이 경우 소프트웨어 엔지니어가 없을 것입니다.

현재 선택이 실제로 하나의 진정한 방법이며 더 나은 품질의 메트릭 (및 / 또는 계측)을 설치하기 위해 싸워야합니까?

지금까지 설명한 것은 One True Way가 아니라 코드 메트릭 One True Way가 아니라는 점을 명심 하십시오. 코드 메트릭은 경고로 간주해야합니다. 향후 유지 관리 문제를 지적 할 수 있습니다. 법률이 아니라 지침입니다. 코드 메트릭 도구가 무언가를 지적하면 먼저 코드를 조사하여 SOLID 프린시 이 올바르게 구현되는지 확인합니다 . 더 많은 SOLID를 만들기 위해 코드를 리팩토링하는 경우가 종종 코드 메트릭스 도구를 충족시킵니다. 때로는 그렇지 않습니다. 이러한 메트릭을 사례별로 가져와야합니다.


1
네, 하나의 신 수업을 갖는 것이 훨씬 좋습니다.
BЈовић

이 접근법은 효과가 있지만 대부분 매개 변수없이 자동차 요소를 얻을 수 있기 때문에 가능합니다. 통과해야한다면 어떻게해야합니까? 그런 다음이 150 가지 매개 변수가있는 자동차 공장이 없기 때문에이 모든 것을 창 밖으로 던져서 절차를 완전히 다시 생각할 수 있습니다.
Andy

@DavidPacker : 많은 것들을 매개 변수화해야하더라도 "어셈블러"객체의 생성자에 전달하는 일종의 Config 클래스로 캡슐화 할 수 있습니다. 이 자동차 예에서 페인트 색상, 내부 색상 및 재료, 트림 패키지, 엔진 ​​등은 사용자 정의 할 수 있지만 150 개의 사용자 정의가 반드시 필요한 것은 아닙니다. 아마도 12 개 정도가있을 것입니다. 옵션 개체에 적합합니다.
Greg Burghardt

그러나 구성을 추가하는 것만으로도 아름다운 for 루프의 목적을 무시할 수 있습니다. 이는 구성을 구문 분석하고이를 적절한 객체에 제공하는 적절한 메서드에 할당해야하기 때문에 엄청난 수치입니다. 따라서 원래 디자인과 비슷한 결과가 나올 것입니다.
Andy

내가 참조. 하나의 클래스와 50 개의 메소드를 사용하는 대신 실제로 50 개의 어셈블러 린 클래스와 하나의 오케스트레이션 클래스가 있습니다. 결국 결과는 동일하게 보이고 성능도 현명하지만 코드 레이아웃은 더 깨끗합니다. 나는 그것을 좋아한다. 작업을 추가하는 것이 조금 더 어색합니다 (클래스에서 작업을 설정하고 오케스트레이션 클래스에 알려 주어야합니다.이 필요성에서 쉽게 벗어날 수있는 방법은 없습니다). 이 접근법을 탐구하는 데 며칠이 걸릴 것입니다.
LSerni

1

이것이 가능한지 모르겠지만 더 데이터 지향적 인 접근 방식을 사용하려고 생각하고 있습니다. 아이디어는 규칙 및 제약 조건, 작업, 종속성, 상태 등의 (선언적) 표현을 캡처하는 것입니다. 그런 다음 클래스와 코드가 더 일반적이며 규칙을 따라 방향을 지정하고 인스턴스의 현재 상태를 초기화하고 작업을 수행합니다. 코드를 직접 사용하지 않고 규칙과 상태 변경을 선언적으로 캡처하여 상태를 변경합니다. 프로그래밍 언어 또는 일부 DSL 툴링 또는 규칙 또는 논리 엔진에서 데이터 선언을 사용할 수 있습니다.


일부 작업은 실제로 이러한 방식으로 규칙 목록으로 구현됩니다. 자동차의 예를 유지하기 위해 퓨즈, 조명 및 플러그 상자를 설치할 때 실제로 세 가지 다른 방법을 사용하지 않고 일반적으로 2 핀 전기 물건을 제자리에 맞추고 퓨즈 목록 3 개를 취하는 방법 만 사용합니다 , 조명 및 플러그. 불행히도 다른 방법의 대부분은이 방법으로 쉽게 적응할 수 없습니다.
LSerni

1

어떻게 든 문제는 의존성을 상기시킵니다. 특정 구성의 자동차를 원합니다. Greg Burghardt와 마찬가지로 각 단계 / 항목 /…에 대한 객체 / 클래스와 함께갑니다.

각 단계는 믹스에 추가하는 것 (무엇이든 provides) (여러 가지 일 수 있음)뿐만 아니라 필요한 것을 선언 합니다.

그런 다음 최종 필수 구성을 어딘가에 정의하고 필요한 것을보고 알고리즘을 실행하여 실행할 단계 / 추가 할 부분 / 수집해야 할 문서를 결정합니다.

종속성 해결 알고리즘은 그렇게 어렵지 않습니다. 가장 간단한 경우는 각 단계가 하나의 것을 제공하고 두 가지가 동일한 것을 제공하지 않으며 종속성이 간단합니다 (종속성에 '또는'이 없음). 좀 더 복잡한 경우 : debian apt 또는 이와 유사한 도구를 살펴보십시오.

마지막으로 자동차를 제작하면 가능한 단계가 줄어들고 CarBuilder가 필요한 단계를 파악할 수 있습니다.

그러나 중요한 것은 CarBuilder에 모든 단계에 대해 알리는 방법을 찾아야한다는 것입니다. 구성 파일을 사용하여이 작업을 수행하십시오. 추가 단계를 추가하면 구성 파일에 추가 만하면됩니다. 논리가 필요한 경우 구성 파일에 DSL을 추가하십시오. 다른 모든 것이 실패하면 코드에서 정의하지 않아도됩니다.


0

당신이 언급 한 몇 가지는 나에게 '나쁜'것으로 눈에

"제 논리는 현재 구현의 단순성을 위해 Reflection을 사용합니다"

이것에 대한 변명이 없습니다. 실제로 실행할 메소드를 결정하기 위해 메소드 이름의 문자열 비교를 사용하고 있습니까?! 순서를 변경하면 메소드의 이름을 변경해야합니까?!

"단일 단계의 작업은 이미 독립적입니다"

그들이 정말로 서로 독립적이라면 그것은 당신의 반이 하나 이상의 책임을지고 있다고 제안합니다. 나에게, 당신의 두 가지 예는 일종의 단일에 빌려주는 것 같습니다.

MyObject.AddComponent(IComponent component) 

각 조작에 대한 논리를 자체 클래스로 분할 할 수있는 방법.

실제로 나는 그들이 독립적이지 않다고 생각합니다. 각각은 객체의 상태를 검사하고 수정하거나 적어도 시작하기 전에 일종의 유효성 검사를 수행한다고 상상합니다.

이 경우 다음 중 하나를 수행 할 수 있습니다.

  1. 창 밖으로 OOP를 버리고 클래스의 노출 된 데이터에 대해 작동하는 서비스를 갖습니다.

    Service.AddDoorToCar (자동차)

  2. 필요한 데이터를 먼저 조립하고 완성 된 객체를 다시 전달하여 객체를 구성하는 빌더 클래스가 있어야합니다.

    Car = CarBuilder.WithDoor (). WithDoor (). WithWheels ();

(withX 로직을 다시 하위 빌더로 분할 할 수 있음)

  1. 기능적 접근 방식을 취하고 함수 / 델리게이트를 수정 방법으로 전달

    Car.Modify ((c) => c.doors ++);


이완은 말했다 : "창 밖으로 OOP를 던져, 당신의 클래스에 노출 된 데이터에 대해 작동하는 서비스를 가지고있다"--- 이것은 객체 지향이 아닌가?
Greg Burghardt

?? OO 옵션이 아닙니다
Ewan

객체를 사용하고 있는데 이는 객체 지향이라는 의미입니다. 코드에 대한 프로그래밍 패턴을 식별 할 수 없다고해서 객체 지향적이지 않다는 의미는 아닙니다. SOLID 원칙은 좋은 규칙입니다. 일부 또는 모든 SOLID 원리를 구현 한 경우 객체 지향적입니다. 실제로 객체를 사용하고 있고 다른 프로시 저나 정적 메서드로 구조체를 전달하는 것이 아니라면 객체 지향적입니다. "객체 지향 코드"와 "좋은 코드"에는 차이가 있습니다. 잘못된 객체 지향 코드를 작성할 수 있습니다.
Greg Burghardt

데이터가 구조체 인 것처럼 노출하고 절차와 같이 별도의 클래스에서 데이터를 조작하기 때문에 'OO가 아님'
Ewan

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