中途半端に優秀なプログラマが「正しいプログラミングテクニック」だと妄信しがちな3つポイント

「変数のスコープは狭いほど良い」と妄信する


変数でもメソッド名でもクラス名でも言えることだが、単純に「スコープは狭いほどよい」という方針でプログラムすると、逆に保守性も可読性も悪いプログラムができあがることがけっこうある*1。
実際、「あちこちから頻繁にアクセスするようなオブジェクトやメソッド」は、スコープをぐっと広くしてしまった方が(場合によってはグローバル変数やグローバル関数にしてしまった方が)、いちいちパラメータ渡しのバケツリレーをせずに、オブジェクトや機能を使うことができ、プログラムの可読性も保守性もずっと向上することがけっこうある。
たとえば、プログラムのいろいろな箇所から比較的頻繁にアクセスする必要があるようなオブジェクトや機能がバインド(格納)された変数やメソッドのスコープをクラスやメソッド内のローカルにして、それを使うときは、いちいち各クラスやメソッドにパラメータ渡しのチェーンを繰り返して使うようにコーディングする人がいる。とくに、オブジェクトが複数のオブジェクトから構成されていて、その階層構造が深かったり、メソッドがサブメソッドを呼び出して機能実装する階層が深かったりすると、メソッドのネストの底から、トップのオブジェクトにアクセスするときなどは、悪夢のように醜くノイズの多いコードになる。
広いスコープからアクセスできるようにした方が正しいケースの代表的な例は、標準入出力ストリーム(stdinやstdout)がバインドされたグローバル変数の形で実装されているシステム変数や、Rubyのprintやpメソッドだろう。(この点、JavaのSystem.out.println()は、Rubyよりもいささかブサイクな仕様だ。)
もちろん、これは名前空間やプログラムの担当分け問題とのトレードオフがあるから、状況を無視して不用意にグローバル変数、グローバル関数、グローバルクラスを使うとプロジェクトが大混乱になるので、末端の未熟なプログラマには「とりあえず、変数のスコープはできるかぎり狭くしておけ」という方針出しをするのはありだが、少なくとも、中級以上のプログラマが、単純になんでもかんでもスコープを狭くするのは、逆に犠牲が大きくなりすぎる。
それと、これはプログラムの担当分け問題にも依存する。比較的小規模のシステム開発で、たとえば一人がサーバ、一人がクライアントを担当する、というように、クライアントプログラム全体のコーディングを一人のプログラマが担当するような場合、広いスコープの変数を使うメリットがデメリットを遙かに凌駕するケースは多くなるだろう。
また、stdin/stdoutやRubyのprintなどは、言語仕様に近い組み込みのシステム変数やメソッドなので、別格としてグローバルにアクセスできるだけだ、と単純に思いこんでいる人もときどきいるが、それは違う。
デフォルトの言語仕様の上に、自分が開発しようとしているアプリケーションのドメインに特化した言語仕様を載せて創り出した「拡張言語仕様」の上でプログラムを書くと生産性が大きく向上する。プログラミングの上級者になると、自然とそういうプログラミングスタイルになる人がけっこういる。DSL(Domain Specific Language)のコンセプトに近い考え方だ。このプログラミングスタイルだと、アプリケーションロジックレイヤと独自DSLレイヤが明確に分離されて、非常に保守性も生産性も高いプログラミングスタイルになる。この「アプリケーションドメインに特化した言語仕様」の実装手段の一つが、あえてスコープを広くする変数やメソッドなのだ。もちろん、C#の「属性」やリフレクションを使ったメタレベルプログラミングと、そういうグローバル変数との組み合わせでDSLを実装するケースも多い。他の言語でも同様。
すなわち、変数のスコープを考えるときには、その変数がDSLで言うところの「準言語仕様レベルの変数になりうるかどうか?」をよく考える必要がある。それが単なるアプリケーションロジックであるなら、そのスコープは狭くした方がよいケースが多く、そのアプリケーションドメインでは汎用的な概念や機能であれば、準言語仕様とみなして、スコープを敢えて広くする方がよいケースが多い。もちろん、これは一般論であって、そのときの納期の都合や、開発チームの規模や役割分担にも依存するので、文脈を無視してこのルールを適用できるというほど単純ではないが。


「同じロジックのコードを2度以上書くな」と妄信する


同じようなパターンがプログラムの複数箇所に現れる場合、それらを抽象化して一つの共通ロジックへのパラメータ渡しとして実装し、それを複数箇所から呼び出すように実装すると、プログラムコード量が小さくなり、保守性が良くなったような気がするので、未熟なプログラマが、なんでもかんでも共通ルーチン化しまくって、非常に保守性の悪いプログラムにしてしまうことがある。
レベルの高いエンジニアは、同じロジックのコードが複数箇所に現れている方が、適切な判断の結果であることはけっこうあるということをよく知っている。


まず第一に、将来的なプログラムパターンの変更の可能性がある。現在、複数箇所に同じようなプログラムパターンが現れたとしても、将来的には、それらは別の修正を受け、別進化していく可能性が予見されることがある。このような場合に、近視眼的に現在だけのパターンの共通性に着目してルーチンを共通化してしまうと、後の修正の時に、あちらを立てればこちらが立たずの現象があちこちに出てきて、必要以上にたくさんの箇所から呼び出されている共通ルーチンを頻繁に変更し、それがシステムを不安定にさせたり、デグレードにつながることも多い。
今後別々の進化をする可能性が高く、また、それぞれの進化の方向性が予測しにくいときは、同じパターンが複数箇所に現れても、あえて冗長なままにしておく方が、正しいこともよくある。もちろん、あくまでこれは、敢えてコストをかけてまで共通ルーチン化はしなくてもよい理由の一つ、という程度のものであって、コレが理由で現在共通ルーチン化されているものを、あわてていますぐコピペ化する必要はない。現在共通ルーチン化されている場合、将来的に別々に進化させる必要が来たときに、コピペにすればいいのだから。


第二に、抽象化レベルの話がある。抽象度を上げることで、共通ルーチン化はできるが、プログラムがコンパクトになるかわりに、直感的に理解しにくくなるケースがある。もちろん、単にそれだけが理由で同じようなパターンがあちこちにたくさん現れているのを放置してはダメだが、共通化のメリット・デメリットを総合的に判断して、共通化するかどうかの意志決定を下すとき、このファクターも考慮に入れるべきだ。


第三に、共通化の投資効果の話がある。とくに、短納期の単発の開発の場合、過度な共通化は、工数の増大につながる。似たようなパターンの共通部分を見いだして、正しく共通化しようとするより、単に同じようなパターンをコピペして使い回した方が、開発速度が速い場合も多い。たとえば、テレビCM、パンフレットの印刷などのメディアミックスでのイベントで使われるPCやケータイのサイト開発などで、お客さんのスケジュールが遅れると、巨大な損失が発生するようなときなどは、将来の保守性ためのプログラムルーチンの共通化などより、納期をしっかり守る方が、はるかに重要だ。


第四に、将来の保守性や拡張性、というのは、たびたび幻想でしかない。将来このように拡張されるだろう、ということを予測して汎用化し、共通ルーチン化しても、あらかじめ予想していたのと異なる方向での拡張が必要となり、せっかく苦労して汎用化していたルーチンが役に立たないケースも多い。その場合、遙かな未来のことまで考えて、がちがちに汎用的に作るより、毎回適当に、その場しのぎのプログラミングをして、将来、まったく予想外の仕様変更がきたら、さっさとプログラムごと作り直してしまった方が、トータルで見たら低コストで現実的なことも多い。ところが、中途半端に優秀なプログラマは、自分の未来の予見能力を過信しており、過度な汎用ルーチン化に走りがちだ。未来は自分が思っている以上に予測しにくいということを肝に銘じ、謙虚になることが必要だ。


第五に、抽象化し、共通ルーチン化しすぎたプログラムは、可読性が低くなり、スキルの低いプログラマに引き継ぎをしなければならなくなったときに、途方に暮れてしまうことがある。上級者にとっては高度に抽象化されたコンパクトなプログラムの方がはるかに理解しやすいし、保守もしやすいが、未熟なエンジニアにとっては必ずしもそうではない。もちろん、単にこれだけが理由で共通ルーチン化しないのはばかげているが、プロジェクトの長期的ライフサイクルや社内の体制まで含めた全体のトレードオフバランスを考えるときに、考慮すべきファクターの一つではある。


「プログラミング言語を極めるのが大切」と妄信する*2


プログラミング言語のスキルというのは、価値あるスキルのうちの一つにすぎないどころか、それほど優先順位の高いスキルではない。実際には、サーバ設定、ネットワーク、データベース、パフォーマンス設計、セキュリティ設計に関するスキルは、プログラミング言語の知識に負けず劣らず重要だ。プログラミング言語を極めることにこだわりすぎるあまり、それらシステム開発全体のスキルのバランスを欠くと、独りよがりで視野狭窄的なシステム設計に陥りがちだ。
さらに、要求仕様を実装仕様に落とし込むスキル、あるいは、実装の都合と顧客の都合を最大限に満たすような落としどころを洞察するスキルなど、より優れた美しいアーキテクチャ設計をするのに、単なるプログラミング言語のスキルよりもはるかに重要なスキルはたくさんある。
結局のところ、ほとんどのコンピュータプログラムは、人間や社会のニーズや欲望を満たすために存在し、人間や社会の利害や感情の構造の組み立て(これも一種のプログラミングだ)と無縁ではいられない。人間や社会の利害と感情の組み立てまで視野に入れたアーキテクチャ全体を、もっともシンプルに設計する方法を突き詰めることで、ほとんどプログラムを書かずに根本的な問題が解決する方法を見いだし、顧客も会社も同僚も自分も、関係者全員が幸せになるというようなケースも多いのである。

*1:変数のスコープの議論をするのと、メソッド名のスコープを議論するのと、クラス名のスコープの議論をするので、もちろん、別々に議論しなければならない部分もあるが、共通部分の議論もある。ここでは、その共通部分の議論をしている

*2:これはプログラミングテクニックではないのだけれど

'; break; case 'horizontal': if(a.imgUrl){ imgCode = ''; } code = '
'+ '
'; code += imgCode; if(a.label) code += ''+a.label+''; code += '
'; break; default: error('こんなview知らねえ。'+a.view); } return code; } function toImgUrl(super_kind){ var HFotoTbl = { tw: '20180803102713', fb: '20180803102707', hb: '20180803102710', line: '20180803102711', pocket: '20180803102712', feedly: '20180803102708', hate_blo: '20180803102709', note: '20180821103319' }; var hf_id = HFotoTbl[super_kind]; if(!hf_id){ log('このsuper kind:'+super_kind+'のはてなフォトIDが登録されてないぞ。'); return null; } return hfid2url(hf_id); } var HFDayRE = /^\d{8}/; function hfid2url(hf_id){ var sa = HFDayRE.exec(hf_id); return 'https://cdn-ak.f.st-hatena.com/images/fotolife/f/fromdusktildawn/'+ sa[0] + '/' + hf_id + '.png'; } function setButton(a){ var pos = isString( a.pos ) ? $(a.pos) : a.pos; a.imgUrl = toImgUrl(a.superKind); var html = makeButtonHtml(a); var meth = a.insertMethod; if(!meth) meth = 'after'; pos[meth](html); $('#'+a.id).on('click',function(){ window.location.href = a.url; }); } function asurePrefix(a){ if(!a.prefix){ var random = Math.floor( Math.random() * 110000000000000 ); a.prefix = 'random-'+random+'-'; } } function setSocialBts(a, defs){ var title = getPageTitle(a.title); var url = getPageUrl(); if(!a.insertMethod) a.insertMethod = 'after'; var b = $.extend(true, {}, a); asurePrefix(b); var i=0; b.title = encodeURIComponent(title); b.url = encodeURIComponent(url); var pos = b.pos; var id; defs.forEach(function(def){ var c = $.extend(true, {}, b); var kind = def[0]; c.label = def[1]; id = c.id = c.prefix + kind; c.pos = pos; c.kind = kind; var superKind = SuperKindTbl[kind]; c.superKind = superKind ? superKind : kind; var site = SITE_NAME; if(site == 'fromd.hateblo.jp') site = 'www.furomuda.com'; switch(kind){ case 'tw': c.url = 'http://twitter.com/share?url=' + c.url + '&text='+c.title+encodeURIComponent(' @fromdusktildawn ')+ '&related=fromdusktildawn'; break; case 'fb': c.url = 'https://www.facebook.com/sharer/sharer.php?u='+c.url; break; case 'hb': c.url = 'http://b.hatena.ne.jp/add?mode=confirm&url=' + c.url + '&title='+c.title; break; case 'line': c.url = 'https://social-plugins.line.me/lineit/share?url='+c.url; break; case 'pocket': c.url = 'http://getpocket.com/edit?url='+c.url+ '&title='+c.title; break; case 'twFollow': c.url = "https://twitter.com/intent/follow?screen_name=fromdusktildawn"; break; case 'hbFollow': c.url = "http://b.hatena.ne.jp/fromdusktildawn/"; break; case 'fbPage': c.url = 'https://www.facebook.com/furomuda/'; break; case 'fbGroup': c.url = "https://www.facebook.com/groups/426678561170457/"; break; case 'hate_blo': c.url = "https://blog.hatena.ne.jp/fromdusktildawn/"+ site + "/subscribe"; break; case 'note': c.url = 'https://note.mu/fromdusktildawn'; break; case 'feedly': c.url = 'https://feedly.com/i/subscription/feed/'+ encodeURIComponent('https://'+site+'/feed'); break; default: console.log('こんなkindしらねえ。:'+kind); } setButton(c); if(c.insertMethod == 'after'){ pos = '#'+id; } }); return id; } function make2ColButtons(a, ldef, rdef){ var col_ids = make2ColFrame(a); var lpos = '#'+col_ids.l; var rpos = '#'+col_ids.r; a.pos = lpos; setSocialBts(a, ldef); a.pos = rpos; setSocialBts(a, rdef); return col_ids.frame_id; } function make2ColFrame(a){ asurePrefix(a); var LEFT_SUFFIX='-left-cell'; var RIGHT_SUFFIX='-right-cell'; var prefix = a.prefix; var frame_id = prefix+'-frame'; var left_id = prefix+LEFT_SUFFIX; var right_id = prefix+RIGHT_SUFFIX; var frame = '
'+ '
'+ '
'+ '
'; $(a.pos).after(frame); return {l: left_id, r: right_id, frame: frame_id}; } // PC依存のコード========================================== var archiveEntries = $(".archive-entry"); if(archiveEntries.length){ archiveEntries.each(function(){ var archiveEntry = $(this) var entryTitleLink = archiveEntry.find(".entry-title-link"); var entryHref = entryTitleLink.attr("href") var entryDescription = archiveEntry.find(".entry-description"); var bookmarkHtml = " " entryTitleLink.append(bookmarkHtml); var readMoreHtml = "もっと読む"; entryDescription.append(readMoreHtml); var bookmarkCounter = archiveEntry.find(".bookmark-widget-counter"); bookmarkCounter.hide(); var starContainer = archiveEntry.find(".star-container"); var bigBookmarkCounter = archiveEntry.find(".big-bookmark-counter"); starContainer.insertAfter(bigBookmarkCounter); function makeStarCountBig(){ var starCount = archiveEntry.find(".hatena-star-inner-count"); if(starCount.length){ starCount.css('font-size','140%'); } else { setTimeout(makeStarCountBig,300); } } makeStarCountBig(); }); // フォローしてもらうボタンをサイドバーに設置 var btPos = '.profile-description'; var startPos = 'my-buttons-pos'; $(btPos).after( '
' ); twPos = '#'+startPos; var last_id = setSocialBts({pos: twPos}, [ ['twFollow', 'フォローする'], ['note', 'フォローする'], ['hate_blo', '読者になる'], ['feedly', '購読する'], ['hbFollow', 'フォローする'], ['fbPage', 'facebookページ'], ['fbGroup', 'facebookグループ'] ]); var paddingBottomId = 'my-padding-bottom-id'; $('#'+ last_id).after( '
' ); var pos = $('#main-inner'); var img_url = hfid2url('20201008224538'); //var img_url = hfid2url('20201003093324'); //var img_url = "https://storage.googleapis.com/hateblog/img/prj/book/learning/learning_blog_top.png"; pos.before( '
'+ ''+ ''+ ''+ '
' ); /* var free_a_begin = ''; var book_a_begin = ''; pos.before( ''+ ''+ ''+ ''+ ''+ ''+ ''+ '
'+ free_a_begin+ ''+ ''+ 'ふろむだ本のガッツリ5章分が無料のWeb記事として読めます!

'+ free_a_begin+ '無料版を読むにはここをクリック'+ '
'+ book_a_begin+ '本で読みたい方はここをクリック'+ '
' ); */ } else { var title = $(".entry-header .entry-title a"); if(title) { var titleHref = title.attr("href") var bookmarkHtml = "" var entryTitle =title.parent(); entryTitle.append(bookmarkHtml); entryTitle.css('font-size','150%'); function addHeadStar(){ var starCountCheck = $(".entry-footer .hatena-star-star-container .hatena-star-inner-count"); if(starCountCheck.length){ var starContainer = $(".entry-footer .hatena-star-star-container"); if(starContainer){ var starCount = starContainer.find(".hatena-star-inner-count"); starCount.css('font-size','24px'); var starClone = starContainer.clone(true,true); var bigBookmarkCounter = title.parent().find(".big-bookmark-counter"); starClone.insertAfter(bigBookmarkCounter); starCount = starClone.find(".hatena-star-inner-count"); starCount.on('click',function(){ alert('☆を付けた人の一覧を見るには、この記事の下部の☆数をクリックしてください。'); $("html,body").animate({scrollTop:$('.entry-footer .hatena-star-star-container').offset().top-100}); }); var bt = starClone.find(".hatena-star-add-button"); bt.on('click',function(){ alert('☆を付けるには、この記事の下部の「☆+」ボタンをクリックしてください。'); $("html,body").animate({scrollTop:$('.entry-footer .hatena-star-star-container').offset().top-100}); }); } else{ setTimeout(addHeadStar,300); } } else { setTimeout(addHeadStar,300); } } addHeadStar(); procSpecialMarkers(); var param = { pos: 'footer.entry-footer p.entry-footer-section', title: title.text() } make2ColButtons(param, [ ['tw', 'この記事をツイートする'], ['hb', 'この記事をブックマークする'], ['pocket', 'この記事を後で読む'] ], [ ['fb', 'この記事をシェアする'], ['line', 'この記事をLINEで送る'] ] ); // フォローしてもらうボタンを下部に設置 param = { pos: 'footer.entry-footer div.social-buttons' }; var frame_id = make2ColButtons(param, [ ['twFollow', 'フォローする'], ['hate_blo', '読者になる'], ['hbFollow', 'フォローする'], ['fbGroup', 'facebookグループ'] ], [ ['note', 'フォローする'], ['feedly', '購読する'], ['fbPage', 'facebookページ'] ] ); } } // 記事一覧ページと、単体表示ページの両方に共通するコード ==================== var d = $('div.date a time'); d.css({ 'color': '#aaa', 'font-weight': 'normal' }); if(IS_ANNEX){ var link = $('h1#title a'); // ブログタイトルのリンク link.attr("href", 'https://www.furomuda.com/'); } })