なぜコードは完全なDRYであってはならないのか? | モイストコード

開発者なら誰でも、どこかで「ドライコード」という言葉に出会うことがあるでしょう。

 

これは、「Don’t repeat yourself」の頭文字をとったDRYに由来します。

 

しかし、コードが乾燥しすぎていると、簡単にもろくなってしまいます。

 

コードを成形可能な状態に保つには、少しの繰り返しを残すことが有効です。

 

私は個人的にこの概念を「モイストコード」と呼んでいます。

 

簡単な例

 

Don’t repeat yourself “の基本的な考え方は、その名の通り、コードを繰り返さないことです。

 

それでは、TypeScript Reactでの簡単な例を見てみましょう。

 

export function App () {
  return <>
    <nav><h2>Navigation</h2></nav>
    <main>This is the homepage</main>
  </>
}

 

ここまでは、あまり繰り返しがありません。それでは、コンタクトページを追加してみましょう。

export function App () {
  const location = useLocation()
  if (location === '/contact') {
    return <>
      <nav>
        <h2>Navigation</h2>
        <ul>
          <li><Link to='/'>Home</Link></li>
          <li><Link to='/contact'>Contact</Link></li>
        </ul>
      </nav>
      <main>
        This is the homepage
      </main>
    </>
  }

  return <>
    <nav>
      <h2>Navigation</h2>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/contact'>Contact</Link></li>
      </ul>
    </nav>
    <main>
      Contact us!
    </main>
  </>
}

 

今は少し重複してるんで、それでは、パスを分けて、ドライコードとモイストコードの違いを見てみましょう。

DRYコード

できるだけ多くの繰り返しを取り除くと、

export function App () {
  const location = useLocation()
  return <>
    <Navigation />
    <Router location={location} />
  </>
 }

function Navigation () {
  return (
    <nav>
      <h2>Navigation</h2>
      <ul>
        {
          [{
            children: 'Home',
            to: '/'
          }, {
            children: 'Contact',
            to: '/contact'
          }].map(props => <NavLink ...props key={to} />)
        }
     </ul>
    </nav>
  )
}

function NavLink ({ to, children }) {
  return <li><Link to={to}>{children}</Link></li>
}

function Router ({ location }) {
  return <main><Content location={location} /></main>
}

function Content ({ location }) {
  if (location === '/profile') {
    return 'Contact us!'
  }
  return 'This is the homepage'
}

 

ありとあらゆる繰り返しを取り除きました。

 

次の機能を追加します。

 

ログインページを用意し、ログインしていないユーザーには、他のページではなく、自動的にログインフォームが表示されるようにします。

 

export function App () {
  const location = useLocation()
  return <>
    <Navigation />
    <Router location={location} />
  </>
 }

function Navigation () {
  return (
    <nav>
      <h2>Navigation</h2>
      <ul>
        {
          [{
            children: 'Home',
            to: '/'
          }, {
            children: 'Contact',
            to: '/contact'
          }, {
            children: 'Login',
            to: '/login'
          }].map(props => <NavLink ...props key={to} />)
        }
     </ul>
    </nav>
  )
}

function NavLink ({ to, children }) {
  return <li><Link to={to}>{children}</Link></li>
}

function Router ({ location }) {
  return <main><Content location={location} /></main>
}

function Content ({ location }) {
  const isUserLoggedIn = useLoggedInState()
  if (!isUserLoggedIn || location === '/login') {
    return 'This is a login form'
  }
  if (location === '/profile') {
    return 'Contact us!'
  }
 return 'This is the homepage'
}

 

重複を徹底的に排除したおかげで、NavBarに別のリンクオブジェクトを追加し、Contentコンポーネントに新しいコンテンツを含むif文を追加するだけで済みました。

私たちのアプリは成長しており、現在はUXチームがあります。ユーザーテストの結果、ログイン画面にナビゲーションが表示されていると、どのリンクも機能していないように見えて、とても混乱することがわかりました。そこで、ログイン画面のNavBarにリンクを表示しないようにして、この問題を解決しましょう。

 

export function App () {
  const location = useLocation()
  return <>
    <Navigation location={location} />
    <Router location={location} />
  </>
 }

function Navigation ({ location }) {
 return (
    <nav>
      <h2>Navigation</h2>
      { location !== '/login' && <NavLinks /> }
   </nav>
  )
}

function NavLinks () {
  return <ul>
    {
      [{
        children: 'Home',
        to: '/'
      }, {
        children: 'Contact',
        to: '/contact'
      }, {
        children: 'Login',
        to: '/login'
      }].map(props => <NavLink ...props key={to} />)
    }
  </ul>
}

function NavLink ({ to, children }) {
  return <li><Link to={to}>{children}</Link></li>
}

function Router ({ location }) {
  return <main><Content location={location} /></main>
}

function Content ({ location }) {
  const isUserLoggedIn = useLoggedInState()
  if (!isUserLoggedIn || location === '/login') {
    return 'This is a login form'
  }
  if (location === '/contact') {
    return 'Contact us!'
  }
 return 'This is the homepage'
}

繰り返しになりますが、重複が少ないため、変更は小さくて簡単です。しかし、このコードには何か違和感を感じ始めています。次の機能の後にどうなるか見てみましょう。

マーケティングチームから電話がありました。彼らは、特別なイースタープロモーションのための素晴らしいアイデアを持っていました。連絡先ページのログインリンクをクリックすると、代わりにプロモーションサイトにリダイレクトされるという、特別なイースターエッグをページに隠したいというのです。リンクを変更するだけなので、そんなに難しいことではありません。

 

export function App () {
  const location = useLocation()
  return <>
    <Navigation location={location} />
    <Router location={location} />
  </>
 }

function Navigation ({ location }) {
 return (
    <nav>
      <h2>Navigation</h2>
      { location !== '/login' && <NavLinks location={location} /> }
   </nav>
  )
}

function NavLinks ({ location }) {
  return <ul>
    {
      [{
        children: 'Home',
        to: '/'
      }, {
        children: 'Contact',
        to: '/contact'
      }, {
        children: 'Login',
        to: location === '/contact'
          ? 'https:///our-special-promotion.com'
          : '/login'
      }].map(props => <NavLink ...props />)
    }
  </ul>
}

function NavLink ({ to, children }) {
  return <li><Link to={to}>{children}</Link></li>
}

function Router ({ location }) {
  return <main><Content location={location} /></main>
}

function Content ({ location }) {
  const isUserLoggedIn = useLoggedInState()
  if (!isUserLoggedIn || location === '/login') {
    return 'This is a login form'
  }
  if (location === '/contact') {
    return 'Contact us!'
  }
 return 'This is the homepage'
}

今回は、モイストコードチームの様子を見てみましょう。

 

モイストコード

 

一番最初に見たコードを使います。

 

export function App () {
  const location = useLocation()
  if (location === '/contact') {
    return <>
      <nav>
        <h2>Navigation</h2>
        <ul>
          <li><Link to='/'>Home</Link></li>
          <li><Link to='/contact'>Contact</Link></li>
        </ul>
      </nav>
      <main>
        This is the homepage
      </main>
    </>
  }

  return <>
    <nav>
      <h2>Navigation</h2>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/contact'>Contact</Link></li>
      </ul>
    </nav>
    <main>
      Contact us!
    </main>
  </>
}

 

多少の繰り返しは気にしないので、すぐにリファクタリングするのではなく、まずは次の機能であるログインページを追加しましょう。

 

export function App () {
  const location = useLocation()
  const isUserLoggedIn = useLoggedInState()
  if (!isUserLoggedIn) {
    return <>
      <nav>
        <h2>Navigation</h2>
      </nav>
      <main>
        This is a login form
      </main>
    </>
  }

  if (location === '/login') {
    return <>
      <nav>
        <h2>Navigation</h2>
      </nav>
      <main>
        This is a login form
      </main>
    </>
  }

  if (location === '/contact') {
    return <>
      <nav>
        <h2>Navigation</h2>
        <ul>
          <li><Link to='/'>Home</Link></li>
          <li><Link to='/contact'>Contact</Link></li>
          <li><Link to='/login'>Login</Link></li>
        </ul>
      </nav>
      <main>
        Contact us!
      </main>
    </>
  }

  return <>
    <nav>
      <h2>Navigation</h2>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/contact'>Contact</Link></li>
        <li><Link to='/login'>Login</Link></li>
      </ul>
    </nav>
    <main>
      This is the homepage
    </main>
  </>
}

あまり多くはないが、出てきそうなコンセプトをいくつか抽出してみよう。

 

export function App () {
  const location = useLocation()
  const isUserLoggedIn = useLoggedInState()
  if (!isUserLoggedIn) {
    return <LoginPage />
  }

  return <Router />
}

function Router ({ location }) {
  if (location === '/login') {
    return <LoginPage />
  }

  if (location === '/contact') {
    return <ContactPage />
  }

  return <HomePage />
}

function LoginPage () {
  return <>
    <nav>
      <h2>Navigation</h2>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/contact'>Contact</Link></li>
        <li><Link to='/login'>Login</Link></li>
      </ul>
   </nav>
    <main>
      This is a login form
    </main>
  </>
}

function ContactPage () {
  return <>
    <nav>
      <h2>Navigation</h2>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/contact'>Contact</Link></li>
        <li><Link to='/login'>Login</Link></li>
      </ul>
    </nav>
    <main>
      Contact us!
    </main>
  </>
}

function HomePage () {
  return <>
    <nav>
      <h2>Navigation</h2>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/contact'>Contact</Link></li>
        <li><Link to='/login'>Login</Link></li>
      </ul>
    </nav>
    <main>
      This is the homepage
    </main>
  </>
}

まだまだ繰り返しが残っていて、削除したくて指がうずうずするかもしれませんが、まずは次の機能を追加しましょう。ログインページにリンクを表示しないこと。

 

export function App () {
  const location = useLocation()
  const isUserLoggedIn = useLoggedInState()
  if (!isUserLoggedIn) {
    return <LoginPage />
  }

  return <Router />
}

function Router ({ location }) {
  if (location === '/login') {
    return <LoginPage />
  }

  if (location === '/contact') {
    return <ContactPage />
  }

  return <HomePage />
}

function LoginPage () {
  return <>
    <nav>
      <h2>Navigation</h2>
   </nav>
    <main>
      This is a login form
    </main>
  </>
}

function ContactPage () {
  return <>
    <nav>
      <h2>Navigation</h2>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/contact'>Contact</Link></li>
        <li><Link to='/login'>Login</Link></li>
      </ul>
    </nav>
    <main>
      Contact us!
    </main>
  </>
}

function HomePage () {
  return <>
    <nav>
      <h2>Navigation</h2>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/contact'>Contact</Link></li>
        <li><Link to='/login'>Login</Link></li>
      </ul>
    </nav>
    <main>
      This is the homepage
    </main>
  </>
}

それは簡単でした。文字通り、数行を削除するだけです。

 

Navigationがどのように変化するかという新しい知識を得て、今度はそれを取り出してみましょう。

 

export function App () {
  const location = useLocation()
  const isUserLoggedIn = useLoggedInState()
  if (!isUserLoggedIn) {
    return <LoginPage />
  }

  return <Router />
}

function Router ({ location }) {
  if (location === '/login') {
    return <LoginPage />
  }

  if (location === '/contact') {
    return <ContactPage />
  }

  return <HomePage />
}

function LoginPage () {
  return <>
    <Navigation />
    <main>
      This is a login form
    </main>
  </>
}

function ContactPage () {
  return <>
    <Navigation
      links={[{
        children='Home'
        to='/'
      }, {
        children='Contact'
        to='/contact'
      }, {
        children='Login'
        to='/login'
      }]}
    />
    <main>
      Contact us!
    </main>
  </>
}

function HomePage () {
  return <>
    <Navigation
      links={[{
        children='Home'
        to='/'
      }, {
        children='Contact'
        to='/contact'
      }, {
        children='Login'
        to='/login'
      }]}
    />
    <main>
      This is the homepage
    </main>
  </>
}

function Navigation ({ links }) {
  return (
    <nav>
      <h2>Navigation</h2>
      { links &&
        <ul>
          {
            links.map({ children, to } =>
              <li key={to}><Link to={to}>{children}</Link></li>)
          }
       </ul>
      }
    </nav>
  )
}

 

このコードには、前回のバージョンよりも多くの繰り返しがありますが(例えば、各ページで明示的にmainを使用しなければならない)、すでに少しすっきりして、より軽量になっているように感じます。

 

それでは、「イースターエッグハントのための特別なリンクを用意してほしい」というマーケティングのリクエストがどうなるか見てみましょう。

 

export function App () {
  const location = useLocation()
  const isUserLoggedIn = useLoggedInState()
  if (!isUserLoggedIn) {
    return <LoginPage />
  }

  return <Router />
}

function Router ({ location }) {
  if (location === '/login') {
    return <LoginPage />
  }

  if (location === '/contact') {
    return <ContactPage />
  }

  return <HomePage />
}

function LoginPage () {
  return <>
    <Navigation />
    <main>
      This is a login form
    </main>
  </>
}

function ContactPage () {
  return <>
    <Navigation
      links={[{
        children='Home'
        to='/'
      }, {
        children='Contact'
        to='/contact'
      }, {
        children='Login'
        to='https:///our-special-promotion.com'
      }]}
    />
    <main>
      Contact us!
    </main>
  </>
}

function HomePage () {
  return <>
    <Navigation
      links={[{
        children='Home'
        to='/'
      }, {
        children='Contact'
        to='/contact'
      }, {
        children='Login'
        to='/login'
      }]}
    />
    <main>
      This is the homepage
    </main>
  </>
}

function Navigation ({ links }) {
  return (
    <nav>
      <h2>Navigation</h2>
      { links &&
        <ul>
          {
            links.map({ children, to } =>
              <li key={to}><Link to={to}>{children}</Link></li>)
          }
       </ul>
      }
    </nav>
  )
}

1本のリンクを交換しました。それだけだ。

何が起こったのか?

私たちの2つのアプローチの主な違いは、湿ったコードのアプローチでは、コードが乾くまでの時間を長くとったことです。仮定のみに基づいて早い段階で特定の形に強制するのではなく、まずもう少し要件を集めてから、繰り返しを取り除き始めました。

その結果、ナビゲーションには継承ではなくコンポジションのパターンを採用することになりました。これにより、ナビゲーションをルートごとに個別化することが容易になり、新機能のために実際に重要な場所でコードを変更する際の柔軟性が高まりました。

おわりに

コードを書くとき、私たちは通常、先に来るすべての要求を最初から知っているわけではありません。特にWebアプリケーションでは、要件が完全に解決されることはなく、常に動き続けています。要件を発見しながら段階的にコードを最適化していけば、コードをしっとりとした柔軟なものにすることができます。完全にDRYなコードを目指すと、コードは脆くなり、変更に柔軟に対応できなくなります。

 

藤沢瞭介(Ryosuke Hujisawa)
  • りょすけと申します。18歳からプログラミングをはじめ、今はフロントエンドでReactを書いたり、AIの勉強を頑張っています。off.tokyoでは、ハイテクやガジェット、それからプログラミングに関する情報まで、エンジニアに役立つ情報を日々発信しています!

未整理記事

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です