[エレガントな技術路線#6]抽象画がいいかどうかを判断する


なぜ抽象を適用するのか

VideoListは何種類もあります!
  • ビデオを格納するSaved Video List領域
  • 検索されたビデオを表示するSearch Video List領域
  • 最初に,この2つのUI領域を1つのクラスに開発した.抽象前の方式
    しかし、次のような問題が発生する可能性があると思います!!

  • 新しいビデオリストUI領域があれば、拡張が困難になる可能性があります.

  • 他の方法を使う必要がなくても
  • したがって、VideoListComponentクラスは、以下に示すように、ビデオリストのプロパティに従って展開されるコードとして抽象化される.

    抽象的なビデオリストUI


    コードをよく見る必要はありません!!コードの終了時に、コードについて簡単に説明しました.
    VideoListComponent:このクラスには、複数のビデオリストが共有されるロジックがあります.
    import SkeletonListComponent from './SkeletonListComponent';
    
    class VideoListComponent {
      $videoList;
    
      parentElement = null;
    
      componentType = null;
    
      videoComponents = [];
    
      lazyLoadThrottleTimeout = null;
    
      skeletonListComponent = null;
    
      constructor(parentElement, type) {
        this.parentElement = parentElement;
        this.componentType = type;
    
        this.#mount();
        this.#initDOM();
        this.#bindEventHandler();
      }
    
      renderSkeletonVideoList(isWaitingResponse) {
        if (isWaitingResponse) {
          this.skeletonListComponent = new SkeletonListComponent(this.$videoList);
          return;
        }
        this.skeletonListComponent?.unmount();
      }
    
      unmount() {
        // 화면에서 사라질 때의 메소드
        this.$videoList.innerHTML = '';
      }
    
      #mount() {
        // 화면에서 처음 나타날 때의 메소드
        const template = this.#generateTemplate();
        this.parentElement.insertAdjacentHTML('beforeend', template);
      }
    
      #initDOM() {
        // UI 내부에서 사용되는 DOM Element를 멤버화하는 메소드
        this.$videoList = this.parentElement.querySelector('.video-list');
      }
    
      #generateTemplate() {
        // 제일 처음 렌더링 되는 템플릿을 만들어내는 메소드
        return `<ul class="video-list"></ul>`;
      }
    
      #bindEventHandler() {
        // 사용되는 이벤트 핸들러를 바인딩하는 메소드
        this.$videoList.addEventListener('scroll', this.#onScrollInVideoContainer);
      }
    
      #onScrollInVideoContainer = () => {
        // 이미지의 레이지 로드를 수행하는 스크롤 이벤트 핸들러 메소드
        if (this.lazyLoadThrottleTimeout) {
          clearTimeout(this.lazyLoadThrottleTimeout);
        }
        this.lazyLoadThrottleTimeout = setTimeout(() => {
          this.videoComponents.forEach((videoComponent) => {
            const { bottom } = this.$videoList.getBoundingClientRect();
            videoComponent.loadImg(bottom);
          });
        }, 100);
      };
    }
    export default VideoListComponent;
    
    SearchVideoListコンポーネント、SavedVideoList:VideoListComponentに拡張されたコンポーネントクラス.インストール後の再レンダリングロジックと、それぞれの領域でのみ使用されるUIロジックが大きく異なります.(レンダリングロジックが異なります.したがって、クラスとして管理されると、ifに分割されたメソッドを分離し、呼び出しを分離する不要な点が発生します)
    // SearchVideoListComponent.js 
    import VideoListComponent from '.';
    import { VIDEO_COMPONENT_TYPE } from '../../constants/components';
    import { CUSTOM_EVENT_KEY } from '../../constants/events';
    import { dispatch } from '../../modules/eventFactory';
    import { isFirstSearchByKeyword } from '../../utils/validation';
    import SearchVideoComponent from '../VideoComponent/SearchVideoComponent';
    
    class SearchVideoListComponent extends VideoListComponent {
      #searchVideoObserver = null;
    
      constructor(parentElement, type = VIDEO_COMPONENT_TYPE.SEARCH) {
        super(parentElement, type);
    
        this.$videoList.addEventListener('click', this.#onClickVideoList);
        this.#searchVideoObserver = new IntersectionObserver(this.#observeEntries, {
          root: null,
          rootMargin: '0px',
          threshold: 0.3,
        });
      }
    
      render({ videoList, prevVideoListLength }) {
        if (isFirstSearchByKeyword(prevVideoListLength)) {
          this.videoComponents = [];
          this.$videoList.innerHTML = '';
        }
    
        this.$videoList.classList.remove('hide');
        this.videoComponents = this.#generateVideoComponents(videoList, prevVideoListLength);
      }
    
      #observeEntries(entries, observer) {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            observer.unobserve(entry.target);
            dispatch(CUSTOM_EVENT_KEY.LOAD_NEW_VIDEO_LIST);
          }
        });
      }
    
      #onClickVideoList = (e) => {
        const {
          target: { className },
        } = e;
        const { dataset } = e.target.closest('.video-item');
    
        if (className.includes('video-item__save-button')) {
          dispatch(CUSTOM_EVENT_KEY.CLICK_SAVE_BUTTON, {
            detail: {
              saveVideoId: dataset.videoId,
            },
          });
        }
      };
    
      #generateVideoComponents(videoList, prevVideoListLength) {
        return [
          ...this.videoComponents,
          ...videoList.slice(prevVideoListLength).map(
            (video, idx, arr) =>
              new SearchVideoComponent(this.$videoList, {
                video,
                observer: idx === arr.length - 1 ? this.#searchVideoObserver : null,
                notLazyLoad: prevVideoListLength === 0,
              })
          ),
        ];
      }
    }
    export default SearchVideoListComponent;
    
    // SavedVideoListComponent.js
    import VideoListComponent from '.';
    import { VIDEO_COMPONENT_TYPE } from '../../constants/components';
    import { CUSTOM_EVENT_KEY } from '../../constants/events';
    import { dispatch } from '../../modules/eventFactory';
    import SavedVideoComponent from '../VideoComponent/SavedVideoComponent';
    
    class SavedVideoListComponent extends VideoListComponent {
      constructor(parentElement, type) {
        super(parentElement, type);
        this.$videoList.addEventListener('click', this.#onClickVideoList);
      }
    
      render({ videoList: savedVideoList }, watchedVideoIdList) {
        this.$videoList.innerHTML = '';
    
        const { watchVideoList, watchedVideoList } = this.#parseWatchAndWahtchedVideo(
          savedVideoList,
          watchedVideoIdList
        );
        this.videoComponents = this.#generateVideoComponents(
          this.componentType === VIDEO_COMPONENT_TYPE.WATCH ? watchVideoList : watchedVideoList
        );
      }
    
      #onClickVideoList = (e) => {
        const {
          target: { className },
        } = e;
        const { dataset } = e.target.closest('.video-item');
    
        if (className.includes('video-item__delete-button')) {
          dispatch(CUSTOM_EVENT_KEY.CLICK_SAVED_DELETE_BUTTON, {
            detail: {
              savedVideoId: dataset.videoId,
            },
          });
        }
        if (className.includes('video-item__check-button')) {
          dispatch(CUSTOM_EVENT_KEY.CLICK_SAVED_CHECK_BUTTON, {
            detail: {
              savedVideoId: dataset.videoId,
              element: e.target,
            },
          });
        }
      };
    
      #generateVideoComponents(savedVideoList) {
        return savedVideoList.map(
          (savedVideo, idx) =>
            new SavedVideoComponent(this.$videoList, {
              video: savedVideo,
              notLazyLoad: idx < 10,
              type: this.componentType,
            })
        );
      }
    
      #parseWatchAndWahtchedVideo(savedVideoList, watchedVideoIdList) {
        return savedVideoList.reduce(
          (prev, savedVideo) => {
            const { videoId } = savedVideo.getVideoInfo();
            const isWatched = watchedVideoIdList.includes(videoId);
    
            return {
              ...prev,
              watchVideoList: isWatched ? prev.watchVideoList : [...prev.watchVideoList, savedVideo],
              watchedVideoList: isWatched
                ? [...prev.watchedVideoList, savedVideo]
                : prev.watchedVideoList,
            };
          },
          {
            watchVideoList: [],
            watchedVideoList: [],
          }
        );
      }
    }
    export default SavedVideoListComponent;
    
    整理するVideoListComponent:複数のビデオリストを共有する論理~~~VideoListComponent:各ビデオリスト->にはそれぞれの追加論理があり、VideoListComponentを拡張します.

    だから抽象化したらどうですか?


    長所

  • 拡張は簡単だと思います.
    만약 새로운 Video List UI가 생긴다면 `VideoListComponent`를 확장 후 추가적인 로직만을 추가해주면 되니깐요

  • 他の方法で使用される論理(メソッド、プロシージャ)を持つ必要はありません.
    각자 클래스에서만 가지고 있으면 되니깐요

  • クラスファイルが大きくなるのを防ぐことができます.
    하나의 클래스 파일이 무거워지는 것은 가독성을 떨어뜨리는 일인것 같아요. 또 유지보수에도 많은 `side effect`가 따라오지 않을까 싶네요.

  • それぞれの論理を修正するだけならもっと便利でしょうか?
    공통 로직은 추상화된 클래스에 있을테니 이 부분을 수정하면되고, 확장된 UI에서 변경사항이 발생해야한다면 그 부분만 수정하면 되니 유지보수에는 유리할 것 같아요 ! 
  • 短所

  • 構造が複雑すぎる
    내가 만약 이 코드를 처음보는 입장이라면 이해하기 힘들었을지도 모르겠다. 
    
    (이를 쉽게 납득하기 위한 구조도가 있지 않은 이상...
    
    또 확장되는 UI가 더 많다면, 이 모든 UI들의 공통 로직을 뽑아내기에 이해하기 더 힘들었겠지..)
  • あるコメンテーターのコメント(正解ではありません):すべての論理を拡張可能に記述するには、時間がかかり、構造も複雑です.拡張性を実現するために、変更する必要のないロジックを作成する必要はありません.できれば、単純なのが一番です.抽象化した瞬間に複雑度が上がった私たちは常に移籍を考えなければならない.

  • 軽率な抽象画かもしれませんが、整理されていない感じが多すぎます.
    처음부터 추상화를 해두고 개발한 것이 아닌, 하나의 파일을 공통로직과 각자의 로직을 분리하여
    
    공통 로직만을 추상화한 경우여서 추상화의 기준이 코드에서 안보이는 느낌?
  • では、いつ抽象化を考えると思いますか?


    抽象画は必ずしも正解とは限らない今回の任務を通じて悟ったのだ.しかし、私にも長所があります.いつごろ使えるか整理してみます.

  • 常に拡張されているUIです
    拡張できないUIであれば、抽象的なことは考えないと思います.しかし,UIがしばしば再利用され,論理拡張がしばしば再利用される場合,開発を加速させ,それぞれの論理を分離するために抽象化を検討する可能性がある.

  • もしあなたが同じ속성, 역할を持っていたら、抽象的になりますか?概念的に取って代わられなければ抽象化のための抽象化!!
    2つのオブジェクトが同じ「ロール」を持っているかどうかを十分に考慮し、抽象化します.
    同じキャラクタ表現を置き換えることができます.抽象概念で置き換えられない2つのオブジェクトが抽象的なオブジェクトになる可能性があります.
  • 抽象的な欠点をつける



    mount関数と追加されたdom elementの後にそれぞれのUI templateのサブ関数mountがあり、明確な抽象化基準がないため軽率に行われ、構造が複雑になり、コードを読む人が理解できないという問題があった.