Angular Architecture: 명령 모드를 사용하여 여러 컨텍스트 메뉴 작업을 관리하는 방법

디자인 모델은 대부분의 프로그래머들이 영원히 사용할 수 없다고 생각하는 주제이다. 왜냐하면 이것은 사람을 이렇게 추상적이고 복잡하게 느끼게 하기 때문이다.이 문서에서는 Angular로 작성된 초대형 웹 응용 프로그램의 명령 모드의 용례를 보여 드리겠습니다.만약 네가 모른다면 걱정하지 마라. 중요한 것은 이 생각이다.

문제.


저는 현재 SDI Media에서 일하고 있습니다. 저희는 소형 작업실과 넷플릭스와 디즈니 등 거두들을 위해 번역, 더빙, 자막 영화와 시리즈를 번역하고 있습니다.
이 작업 흐름을 지원하는 웹 응용 프로그램에서 우리는 약 100개의 보기를 가지고 있다. 그것은 작고 복잡하며, 작업, 작업, 사용자, 시설 등 역 실체에서 실행된다. 이러한 보기는 하나의 실체를 둘러싸고 실행되는 것이 아니라 서로 연결된 실체의 혼합이라고 생각하기 쉽다.예를 들어 사용자 프로필은 사용자 자료뿐만 아니라 그가 일하는 시설, 그에게 분배된 업무 목록 등을 보여준다.
모든 실체에는 약간의 동작 집합이 있다.예를 들어 우리의 작업 실체는 약 20개의 작업(예를 들어 작업 시작, 작업 분배, 우선순위 변경 등)이 있는데 전체 응용 프로그램에서 기본적으로 비슷하지만 일부 보기는 특수 처리를 필요로 한다. 예를 들어 하나의 보기에서 작업이 성공할 때 우리는 하나의 표를 새로 고쳐야 하지만 다른 보기에서는 대화상자를 닫고 3개의 표를 새로 고쳐야 한다.
이전에 우리는 모든 작업 조작을 하나의 전용 서비스JobActionsService에 저장했다. 우리가 특정한 용례를 해결할 때 점점 더 많은 논리를 추가하면서 이 서비스는 계속 증가했다.500줄이 1000줄이 됐어.1000줄이 1500줄이 됐어.안에 스파게티가 너무 많아서 나는 일주일 동안 밥을 할 필요가 없다.하나의 조작은 다른 방법을 사용할 수 있는 방법으로 모든 방법에 여러 개의 설정 파라미터가 있기 때문에 서로 다른 보기에 대한 다양한 흐름의if문장을 만들 수 있다.
우리는 분식 요리사가 필요하다. 그는 이 일성급 밥을 깨끗이 쓸어버릴 수 있고, 정성들여 준비한 밥도 만들 수 있으며, 심지어는 우리를 자랑스럽게 할 수도 있다.( ಠ◡ಠ )

예제 응용 프로그램


본고에 대해 저는 두 가지 보기를 포함하는 응용 프로그램을 준비했습니다. Jobs Master ListUser Jobs입니다.이 두 보기에서 우리는 모두 작업 상태를 바꾸고 사용자에게 작업을 분배할 수 있다.다음은 모양새입니다.

순진한 방법 #1 - 반복


두 뷰의 컨텍스트 메뉴 작업을 단순하게 정의하는 방법에 대해 살펴보겠습니다.
// jobs.component.ts
const actionsForJobMasterList = [
  {
    name: 'Assign to User',
    icon: 'how_to_reg',
    isHidden: actor => !!actor.assignedUser,
    action: () => {/* Action */},
  },
  {
    name: 'Unassign from User',
    icon: 'voice_over_off',
    isHidden: actor => !actor.assignedUser,
    action: () => {/* Action */}
  },
  {
    name: 'Start',
    icon: 'play_arrow',
    isHidden: actor => actor.status !== JobStatusEnum.NEW,
    action: () => {/* Action */}
  },
  {
    name: 'Complete',
    icon: 'done',
    isHidden: actor => actor.status !== JobStatusEnum.IN_PROGRESS,
    action: () => {/* Action */}
  },
  {
    name: 'Restart',
    icon: 'repeat',
    isHidden: actor => actor.status !== JobStatusEnum.DONE,
    action: () => {/* Action */}
  },
];

// user.component.ts
const actionsForUserJobs = [
  // we cannot reassign User in this view
  {
    name: 'Start',
    icon: 'play_arrow',
    isHidden: actor => actor.status !== JobStatusEnum.NEW,
    action: () => {/* Action */}
  },
  {
    name: 'Complete',
    icon: 'done',
    isHidden: actor => actor.status !== JobStatusEnum.IN_PROGRESS,
    action: () => {/* Action */}
  },
  {
    name: 'Restart',
    icon: 'repeat',
    isHidden: actor => actor.status !== JobStatusEnum.DONE,
    action: () => {/* Action */}
  },
];
작업 목록 보기에는 5개의 작업이 있고 사용자 작업에는 3개의 작업이 있음을 알 수 있습니다.또한 모든 속성을 반복합니다.대부분의 보기는 정적이다.

더 쉬운 방법 #2 - 생성기 함수


코드를 복제하지 않으려면 다음과 같은 특정 뷰의 모든 작업을 반환하는 생성기 방법을 만들 수 있습니다.
function getActionsForView(viewType: 'jobsMasterList' | 'userJobs', usersListTable: UsersListTable) {
  const actionsForJobMasterList = [
    viewType === 'jobsMasterList' ? {
      name: 'Assign to User',
      action: () => {/* Action */},
      ...
    } : null,
    viewType === 'jobsMasterList' ? {
      name: 'Unassign from User',
      action: () => {/* Action */},
      ...
    } : null,
    {
      name: 'Start',
      action: () => {
         if (viewType === 'userJobs') {
            sendNotification();
         } else {
            usersListTable.reloadTable();
         }
      }, 
      ...
    },
    {
      name: 'Complete',
      action: () => {/* Action */},
      ...
    },
    {
      name: 'Restart',
      action: () => {/* Action */},
      ...
    }
  ].filter(Boolean);
}
이런 방법에서 우리는 어떤 것도 복제하지 않았지만, 지금 우리는 더 큰 문제가 하나 있다. 이것은 못하는 것이 없는 기능이다.우리는 특정한 보기의 특정한 조작을 되돌려주는 싫은if문장이 있습니다.'시작'행동에서 우리는 서로 다른 관점에 대한 반응이 다르다.저희가 보기가 세 개면요?아니면 다섯 개의 보기?만약 어떤 대상이 단지 상하문만 특정한 것이라면?예를 들어usersview는 전용 서비스UsersListTable를 사용하는데 이 서비스는 그 자체만 사용하고 응용 프로그램의 다른 어느 곳에서도 사용하지 않는다.현재 우리는 이 생성기를 사용하고자 하는 모든 보기에서 그것을 전달해야 한다.이것은 받아들일 수 없는 것이다.이런 논리는 개발자들의 모든 열정을 꺾어 하와이로 보내는 것을 고려하게 만든다.
더 나은 솔루션이 필요합니다.
  • 모든if문장
  • 제거
  • 상하문과 관련된 대상을 존중한다. 예를 들어UsersListTable
  • 솔루션 제안 - 간단한 초안


    우리가 해결 방안을 실시하기 전에, 나는 항상 우리가 원하는 방식으로 그것을 사용하는 것을 건의한다.Dell의 솔루션은 다음과 같습니다.
    // jobs.component.ts
    const actionsForJobMasterList = [
      ...,
      JobStartAction.build({
        isHidden: actor => actor.status !== JobStatusEnum.NEW,
        onSuccess: () => sendNotification()
      })
      JobCompleteAction.build({
        ...
      })
    ];
    
    // user.component.ts
    const actionsForUserJobs = [
      ...
      JobStartAction.build({
        isHidden: actor => actor.status !== JobStatusEnum.NEW,
        onSuccess: () => usersListTable.reloadTable()
      }),
      JobCompleteAction.build({
        ...
      })
    ];
    
    좋아, 우리는 몇 가지 문제를 해결했다.
  • ✔ 어느 곳에도'만약'이 없다.좋아.
  • usersListTable 전 세계에 전파되지 않았다.좋다.
  • ✔ 동작 정의는 JobStartActionJobCompleteAction 클래스에서 정의됩니다.우리는 단지 그들로 하여금 상하문 메뉴의 대상을 토하게 할 뿐이다.괜찮은데.
  • There is also repeated isHidden property, but I will leave it until the end.


    하지만 또 하나의 문제가 있다.우리는 가능한 한 동작류가 통용될 필요가 있다.이것은 그들이 JobModel와 같은 전체 실체 모델을 사용할 수 없다는 것을 의미한다. 왜냐하면 일부 보기는 UserJobModel, MinimalJobModel, CachedJobModel 등 다른 모델을 사용할 수 있기 때문이다. 만약JobStartAction에 이 모든 모델을 사용한다면 우리는 이전보다 더 많은ifs를 얻을 수 있다.우리는 이 방면에서 다시 한 번의 교체를 진행해야 한다.
    // jobs.component.ts
    const actionsForJobMasterList = [
      ...,
      JobStartAction.build({
        resolveParams: actor => ({ jobId: actor.id, userId: actor.assignedUser.id }),
        isHidden: actor => actor.status !== JobStatusEnum.NEW,
        onSuccess: () => sendNotification()
      })
    ];
    
    // user.component.ts
    const actionsForUserJobs = [
      ...
      JobStartAction.build({
        resolveParams: actor => ({ jobId: actor.id, userId: currentUser.id }),
        isHidden: actor => actor.status !== JobStatusEnum.NEW,
        onSuccess: () => usersListTable.reloadTable()
      }),
    ];
    
    우리는 resolveParams 방법을 추가했는데, 이 방법은 우리의 조작에 필요한 모든 매개 변수를 제공했다.작업 목록userId에서는 실체 자체를 얻지만 사용자 작업 목록에서는 현재 범위 내의 사용자를 얻는다.
    이것은 우리의 모든 어려움을 해결했기 때문에 지금 우리는 우리의 해결 방안을 실시하기 시작할 수 있다.

    구원의 지휘 모드


    우리가 사용할 수 있는 매우 유용한 모델은 명령 모드이다.기본적으로 주요 사상은 다음과 같다.

    모든 동작은 하나의 단독 클래스로 표시된다


    이 프로젝트에서 나는 action-definitions라는 단독 디렉터리를 만들었다.

    다섯 개의 동작에 대해 우리는 다섯 개의 목록이 있다.디렉토리당 2개의 파일:

  • 동작 정의 - 컨텍스트 메뉴의 모양과 기능을 지정합니다.이 동작은 전체 응용 프로그램에서 사용할 수 있기 때문에 로컬 서비스를 인용할 수 없습니다. 모든 데이터는 Params를 통해 제공되어야 합니다.이것이 바로 왜냐providedIn: 'root'.
  • @Injectable({
      providedIn: 'root',
    })
    export class JobRestartAction extends ActionDefinition<JobRestartActionParams> {
      // Thanks to Angular's dependency injection the action can use any global service.
      constructor(
        private jobsService: JobsService,
        private snackBar: MatSnackBar,
      ) {
        super();
      }
    
      // in this action we send request with status change
      // and display a notification with a success message
      invoke(params: JobRestartActionParams): any | Observable<any> {
        return this.jobsService.setStatus(params.jobId, JobStatusEnum.NEW)
          .pipe(
            tap(() => this.snackBar.open(`Job restarted successfully.`))
          );
      }
    
      // we return how the menu looks like
      protected getMenu(): ActionDefinitionContextMenu {
        return {
          name: 'Restart',
          icon: 'repeat',
        };
      }
    }
    

  • Action definition params - 어떤 데이터를 사용했는지 알려주는 인터페이스상하문 메뉴를 구축하는 동안 resolveParams 필드에서 그것들을 제공합니다.우리는 가장 구체적이지 않은 데이터를 사용해야 한다. 이렇게 하면 어느 곳에서든 다시 사용할 수 있다.
  • export interface JobRestartActionParams {
      jobId: string;
    }
    

    모든 동작이 명령 모드를 실현한다


    모든 동작이 기본 클래스로 확장됩니다.보아하니 이렇다.
    export abstract class ActionDefinition<Params> {
    
      // it simply transforms action class into context menu object
      // that is consumed by a context menu component.
      build<Actor>(config: BuildConfig<Actor, Params>): ContextMenuActionModel<Actor> {
        const menu = this.getMenu();
    
        return {
          name: menu.name,
          icon: menu.icon,
          isHidden: actor => config.isHidden?.(actor),
          action: actor => {
            // Here we get parameters provided while building 
            // context menu actions list in specific views
            const params = config.resolveParams(actor);
    
            // now we invoke action with provided parameters
            const result = this.invoke(params);
    
            // for a conveninece action can return either raw value or an Observable,
            // so that actions can make requests or do other async stuff
            if (isObservable(result)) {
              result
                .pipe(take(1))
                .subscribe(() => config.onSuccess?.());
            } else {
              config.onSuccess?.();
            }
          },
        };
      }
    
      // methods required to be implemented by every action
      abstract invoke(params: Params): void | Observable<void>;
      protected abstract getMenu(): ActionDefinitionContextMenu;
    }
    
    //build-config.ts
    export interface BuildConfig<Actor, Params> {
      resolveParams: (actor: Actor) => Params;
      isHidden?: (actor: Actor) => boolean;
      onSuccess?: () => void;
    }
    
    따라서 이제 모든 작업을 별도의 클래스로 정의하여 컨텍스트 메뉴를 구성할 수 있습니다.
    // jobs.component.ts
    const actionsForJobMasterList = [
      this.injector.get(JobAssignAction).build({
        resolveParams: actor => ({jobId: actor.id}),
        isHidden: actor => !!actor.assignedUser,
        onSuccess: () => this.jobsService.reloadData()
      }),
      this.injector.get(JobUnassignAction).build({
        resolveParams: actor => ({jobId: actor.id, currentUserName: actor.assignedUser.name}),
        isHidden: actor => !actor.assignedUser
      }),
      this.injector.get(JobStartAction).build({
        resolveParams: actor => ({jobId: actor.id}),
        isHidden: actor => actor.status !== JobStatusEnum.NEW
      }),
      this.injector.get(JobCompleteAction).build({
        resolveParams: actor => ({jobId: actor.id}),
        isHidden: actor => actor.status !== JobStatusEnum.IN_PROGRESS
      }),
      this.injector.get(JobRestartAction).build({
        resolveParams: actor => ({jobId: actor.id}),
        isHidden: actor => actor.status !== JobStatusEnum.DONE
      })
    ];
    
    // user.component.ts
    const actionsForUserJobs = [
      this.injector.get(JobStartAction).build({
        resolveParams: actor => ({jobId: actor.id}),
        isHidden: actor => actor.status !== JobStatusEnum.NEW
      }),
      this.injector.get(JobCompleteAction).build({
        resolveParams: actor => ({jobId: actor.id}),
        isHidden: actor => actor.status !== JobStatusEnum.IN_PROGRESS
      }),
      this.injector.get(JobRestartAction).build({
        resolveParams: actor => ({jobId: actor.id}),
        isHidden: actor => actor.status !== JobStatusEnum.DONE
      })
    ];
    
    주요 수확:
  • 가 아니라ActionDefinition 우리는 JobStartAction.build()를 통해 서비스를 주입해야 한다. 왜냐하면 우리의 조작 정의는 사실상 전역 서비스이기 때문이다.
  • 보기에서 우리는 상하문에 접근할 수 있지만 동작에서 상하문에 접근할 수 없습니다.
  • 우리는 심지어 독립 모드에서 조작(상하문 메뉴 없음)을 사용할 수 있다. this.injector.get(JobStartAction).
  • TypeScript의 일반적인 마력 때문에 모든 것이 정적 유형이다.
  • 모든 논리는 액션 클래스에 숨겨져 있다.그 중 일부는 매우 복잡할 수 있다.
  • // JobUnassignAction
    // Displays 2 confirmation dialogs one after another
    // and then displays confirmation notification
    invoke(params: JobUnassignActionParams): any | Observable<any> {
      return this.confirmationDialogService
        .open({
          title: `Unassign ${params.currentUserName}?`,
          content: `You are going to unassign ${params.currentUserName} from this Job, are you completely sure?`,
        })
        .pipe(
          filter(Boolean),
          switchMap(() => this.confirmationDialogService.open({
            title: 'Are you 100% sure?',
            content: 'There is no way back!',
            cancelButtonText: 'Take me back',
            confirmButtonText: 'YES!'
          })),
          filter(Boolean),
          switchMap(() => this.jobsService.setUser(params.jobId, undefined)),
          tap(() => this.snackBar.open('User unassigned successfully'))
        );
    }
    
  • this.injector.get(JobRestartAction).invoke({...params}) 속성은 전체 뷰에서 여러 번 반복되지만 가시성을 제어하는 항목은 뷰에 따라 다릅니다.나는 필요한 중복이라고 부른다.
  • 요약


    본고에서 우리는 상하문 메뉴를 정의하는 데 사용되는 간단한 추상층을 만들었다.덕분에 우리는 명령 모드를 이용하여 모든 조작의 논리를 분리하고 그들의 보기 상하문과 연결하는 것을 도왔다.모든 조작이 매개 변수 인터페이스를 정의했기 때문에 모든 내용은 정적 형식이다.행동을 바꾸는 것은 더 이상 고통스럽지 않다.새로운 클래스를 만드는 것처럼 더 많은 작업을 추가하는 것은 다른 내용과 관련이 없습니다.
    처음에, 우리는 해결 방안의 작업 방식에 대해 간단한 초안을 만들어서 잠재적인 문제를 조속히 발견할 수 있도록 했다.이런 방법을 강력히 추천합니다!
    만약 당신에게 어떤 건의가 있다면 반드시 평론에 써야 한다.
    전체 소스 코드는github에서 찾을 수 있습니다.

    헨버드 / 각도 제어의 명령 모드


    동작 정의 명령 모드가 있는 예시 프로그램


    애플리케이션 데모:
    다음 문장에서도 나는 각도에 관한 것을 쓸 것이다.
    나중에 봐요.

    좋은 웹페이지 즐겨찾기