
なぜコードは完全な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なコードを目指すと、コードは脆くなり、変更に柔軟に対応できなくなります。