BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル リファクタリングにありがちな誤解を解く

リファクタリングにありがちな誤解を解く

ブックマーク

.NETコミュニティにおいて、リファクタリングが遅いスタートを切ったと言っても間違いではないでしょう。.NET開発者向けの主要製品であるVisual Studioは、C#においては、現在もなお、Refactoring Rubicon(http://www.martinfowler.com/articles/refactoringRubicon.html)を渡っているとは言えません。Visual Basic、あるいはより最近のものであるC++では、状況は改善されていますが、それは無償のリファクタリング・アドイン、Refactor!をダウンロードし、Developer Expressで開発されたVBやC++にインストールした場合に限られます。

その後の選択肢は、すべて代金を払わなければならない製品となります。確かに、これらの製品は投資をするに値しますが、企業買収の歴史を理解すれば、「コード品質」などといった、非本質的で内部的な議論が懸念される場合、とても開発者の本流まで達するのは困難になります。リファクタリングはツールがなくても行えますが、手作業による処理は、困難であることが予想され、決して興味をそそられるものとは言えません。.NETコミュニティがリファクタリングの採用で遅れを取っている点や、リファクタリングやその有効性に関する一連の問題について、なおも多くの混乱がある点は、不思議なことではありません。

本稿では、頻繁に見かける誤解の一部をリストアップすることに努めました。従来のプログラミングにおける先入観の一部とともに、これらの誤解は、有効な手法を取り入れる段階で重大な障害を引き起こす場合が多々あります。続いて、これらの先入観による障害をリストアップします。また、その原因について説明し、間違いであることを証明するのに適した論拠を述べるつもりです。まだリファクタリングの本質に疑念を抱いている方や、リファクタリングを習得中、あるいはチームでこの手法を確立し広めようと取り組まれている方すべてにとって、この論拠が有効であると証明されることを望んでおります。

「壊れていないものを修理するな」

「壊れていないものを修理するな」という姿勢は、長きにわたるエンジニアリングの見識として、述べられことが多々ありますが、これは自己満足を助長するに過ぎません。リファクタリングは、これとは対照的に正当な理由を教えてくれます。

プログラミング人生の初期において、コードに関する極めて小さなことでも、非常に大きな相違が生じる場合があることを学びますが、この知識が大きな被害を引き起こすことも少なくありません。小さな変更は、意外な形で最悪な時にソフトウェアの破壊を引き起こすものなのです。このため、一度手にやけどを負ってしまうと、必ずしも必要ではない変更には、たいていの場合、どれも気が進まなくなるのです。このことは、少しの間は効果があります。しかし、必然的に、バグの解決が必要な場合や新機能の要請をこれ以上回避できないような状況がやって来ます。それまで必死に向き合わないようにしてきたコードと向き合うことになるのです。

「壊れていないものを修理するな」という姿勢をとる人は、リファクタリングを、既に目的を果たしたものへの余計なお世話であると見なしています。実際、この「現状維持」の体制順応的な態度は、コードに直面することの恐怖や、その恐怖をコントロールできない事実を理由付けする意思の表れなのです。

経験を積んだ多くのプログラマは、この姿勢を取りますが、それは、実際に必要ではない努力はすべて最小化するという正当な意思が働くためです。たとえば、アプリケーションがパフォーマンスの面で申し分なく稼動しているのなら、パフォーマンスのために最後のプロセッサ・サイクルを絞り出す必要などないということです。これと同様のことが、「いつかこの機能が必要になるかもしれない」と、主張されることの多い見込み的な設計にも当てはまります。

その意味で、リファクタリングとは、考え方が非常に合っていると言えます。リファクタリングをする場合、使用されていないコードを除去し、見込み的な設計や時期尚早な最適化を回避しなければなりません。

しかし、リファクタリングの達人からすれば、ソフトウェアは「内部で」壊れる可能性もあります。設計に欠陥がある場合、コードは分かりづらく構造化は不十分で、たとえ現在アプリケーションが正確に機能し、外側からは壊れているように見えなくても、やはりリファクタリングを行い、「安定した」設計がなされるべきなのです。この意味で、リファクタリングは、有形とは言えないまでもソフトウェアの決定的な特性(設計、簡潔性、ソースコード理解の向上、信頼性など)を貫いています。

リファクタリングは、コードに対する指揮権を取り戻すのに有効です。とはいえ、収拾がつかない状態になってしまったコード・ベースの場合、これは単純な作業ではありません。リファクタリングをしない場合、唯一頼ることのできる解決策は、コードを完全に書き換えることとなります。

リファクタリングは今に始まったことではない

この誤解については、次のように言い換えられるでしょう 「リファクタリングは、既に知っていることをすべて別の言葉にしたにすぎない」。これは、良質なコード、オブジェクト指向設計、スタイル、有効な手法など、すべてを習得しており、リファクタリングは、誰かが本を売るために発案した新たなバズワードにすぎないことを意味しています。

もちろん、リファクタリングは、オブジェクト指向、あるいはアスペクト指向プログラミングが初めて登場した時のように、新たな範例を急激に押し付けるようなことはしません。リファクタリングがすることと言えば、プログラミングのやり方を根底から変えることです。ボタンのクリック一つで、コードに複雑な変換を適用できるような規則を定義します。コード変更による影響を受けにくい、ある種の凍結された構造体だと考えてはいけません。代わりに、最適な形でコードを維持し、新たな課題への効果的な対応も可能で、恐れることなくコードの変更が行えることに気づくでしょう。

リファクタリングは高度な論理

プログラミングとは困難なものです。多くの知的成果を必要とする複雑な作業です。中には理解するのが非常に困難な知識もあります。Visual Basic .NETの場合、VBプログラマは、完全なオブジェクト指向言語の機能に取り組む能力を身に付けなければなりませんでした。多くの人は、当初このことに当惑しました。良かった点は、新しいスキルを身に付けるということは、確実にそれだけの価値が得られるということです。

リファクタリングの優れた点は、それがいかにシンプルであるかにあります。それは、着手のための一連の非常に小さく簡潔な規則を与えてくれます。優れたツールに加えて、これにより、リファクタリングにおける第一歩が容易になります。UMLやデザイン・パターンなど、今日、上級プログラマが知っておくべき他の手法に比べ、リファクタリングは、VB自体が他の言語と比較した場合、そうであるように、最も習得しやすい手法と言えるでしょう。

リファクタリング習得期間の間もなくして成果が見え始めます。もちろん、人生における他のいかなることとも同様に、熟知するには多くの時間と努力が必要となります。

リファクタリングは性能劣化を招く

次のことについて述べるには、これまでより多くの時間がかかるでしょう。「通常、リファクタリング後は、メソッドやクラスなどの要素は、より細分化され数も多くなるため、新しい設計では極めて多くの間接的な処理をともなうこととなり、多少の性能劣化を招くに違いない。」

少し時間を遡ったなら、この主張は、オブジェクト指向プログラミングに対する初期の懐疑論と同様、奇異なことのように思えるでしょう。

実は、リファクタリング済みのコードとそうでないものとの違いは、どんなによく見てもわずかなものです。一部の非常に特化されたシステムを除けば、これは実質的な問題ではありません。

経験上、一般的に、パフォーマンス・フローは、コード中のいくつかのピンポイントな場所で問題となります。最適化フェーズでこれらを修正すれば、必要とされるパフォーマンス・レベルを得られるでしょう。コードの致命的な部分を容易に特定できることは、非常に有効であることを示しています。重複や総サイズが最小限に抑えられ、変更は一箇所で行われるような分かりやすいコードを記述することにより、最適化プロセスにおいて、リファクタリングは非常に役立ちます。

CPUの馬力が絶えず増強されている昨今では、コードの他の側面(保守性、品質、スケーラビリティ、信頼性など)により、パフォーマンスは最優先のコーディング特性から外されています。それならば、これを性能の悪いコードを記述する言い訳に使用しないこと、それを誇張しすぎてはいけないということです。

ここで、ある程度の数字を見たいという方のために、ちょっとした実験を用意しました。2つのサンプル・コードを使用します。1つ目は、構造化が不完全なコードの例で、1つのMainメソッドが存在します。2つ目の例には、内部にMainメソッドをもつModuleと細分化された多数のメソッドをもつCircleクラスが存在します。当初は、非構造化コードと構造化コードの形式を示すためにこれらのサンプルを使用していたため、計測用のコードが不足しています。

単純な幾何学公式(円周の長さ)を実行するのにかかる時間を計測します。また、このコード演算がアプリケーションに影響を与えないようにするため、データベース用のクエリ・コードを追加します。計測処理を除外し、内部で10000回のループ処理を繰り返し実行します。この実験は必要以上に厳密に行うつもりはないため、System.Diagnostics.Stopwatchクラスを使用し時間を取得します。この場合、Stopwatchクラスの精度で十分です。

Code Sample 1: Unstructured Code

Option Explicit On
 Option Strict On

 Imports System.Diagnostics
 Imports System.Data.SqlClient

 Namespace RefactoringInVb.Chapter9 

     Structure Point
         Public X As Double
         Public Y As Double
     End Structure

     Module CircleCircumferenceLength

         Sub Main()
             Dim center As Point
             Dim pointOnCircumference As Point
             'read center coordinates
             Console.WriteLine("Enter X coordinate" + _
             "of circle center")
             center.X = CDbl(Console.In.ReadLine())
             Console.WriteLine("Enter X coordinate" + _
             "of circle center")
             center.Y = CDbl(Console.In.ReadLine())
             'read some point on circumference coordinates
             Console.WriteLine("Enter X coordinate" + _
             "of some point on circumference")
             pointOnCircumference.X = CDbl(Console.In.ReadLine())
             Console.WriteLine("Enter X coordinate" + _
             "of some point on circumference")
             pointOnCircumference.Y = CDbl(Console.In.ReadLine())
             'calculate and display the length of circumference
             Console.WriteLine("The lenght of circle" + _
             "circumference is:")
             'calculate the length of circumference
             Dim radius As Double
             Dim lengthOfCircumference As Double
             Dim i As Integer
             'use stopWatch to measure transcurred time
             Dim stopWatch As New Stopwatch()
             stopWatch.Start()
             'repeat calculation for more precise measurement
             For i = 1 To 10000
                 'add some IO
                 Dim connection As IDbConnection = New SqlConnection( _
                 "Data Source=TESLATEAM;" + _
                 "Initial Catalog=RENTAWHEELS;" + _
                 "User ID=RENTAWHEELS_LOGIN;" + _
                 "Password=RENTAWHEELS_PASSWORD_123")
                 connection.Open()
                 Dim command As IDbCommand = New SqlCommand( _
                 "SELECT GETDATE()")
                 command.Connection = connection
                 Dim reader As IDataReader = command.ExecuteReader()
                 reader.Read()
                 reader.Close()
                 connection.Close()
                 radius = ((pointOnCircumference.X - center.X) ^ 2 + _
                 (pointOnCircumference.Y - center.Y) ^ 2) ^ (1 / 2)
                 lengthOfCircumference = 2 * 3.1415 * radius
             Next
             stopWatch.Stop()
             Console.WriteLine(stopWatch.Elapsed)
             Console.WriteLine(lengthOfCircumference)
             Console.Read()
         End Sub
      End Module
  End Namespace

Code Sample 2: Structured Code

Option Explicit On
 Option Strict On

 Imports System.Data.SqlClient

 Namespace RefactoringInVb.Chapter11
      Public Structure Point
         Public X As Double
         Public Y As Double
     End Structure

     Module CircleCircumferenceLength

         Sub Main()
             Dim circle As Circle = New Circle
             circle.Center = InputPoint("circle center")
             circle.PointOnCircumference = InputPoint( _
             "point on circumference")
             Console.WriteLine("The length of circle " + _
             "circumference is:")
             Dim circumference As Double
             Dim i As Integer
             'use stopWatch to measure transcurred time
             Dim stopWatch As New Stopwatch()
             stopWatch.Start()
             'repeat calculation for more precise measurement
             For i = 1 To 10000
                 circumference = circle.CalculateCircumferenceLength()
             Next
             stopWatch.Stop()
             Console.WriteLine(stopWatch.Elapsed)
             Console.WriteLine(circumference)
             WaitForUserToClose()
         End Sub

         Public Function InputPoint(ByVal pointName As String) As Point
             Dim point As Point
             Console.WriteLine("Enter X coordinate " + _
             "of " + pointName)
             point.X = CDbl(Console.In.ReadLine())
             Console.WriteLine("Enter Y coordinate " + _
             "of " + pointName)
             point.Y = CDbl(Console.In.ReadLine())
             Return point
         End Function

          Private Sub WaitForUserToClose()
             Console.Read()
         End Sub
      End Module
      Public Class Circle
         Private centerValue As Point
         Private pointOnCircumferenceValue As Point
         Public Property Center() As Point
             Get
                 Return centerValue
             End Get
             Set(ByVal value As Point)
                 centerValue = value
             End Set
         End Property
         Public Property PointOnCircumference() As Point
             Get
                 Return pointOnCircumferenceValue
             End Get
             Set(ByVal value As Point)
                 pointOnCircumferenceValue = value
             End Set
         End Property

         Public Function CalculateCircumferenceLength() As Double
             QueryDatabase()
             Return 2 * 3.1415 * CalculateRadius()
         End Function

         Private Function CalculateRadius() As Double
             Return ((Me.PointOnCircumference.X - Me.Center.X) ^ 2 + _
             (Me.PointOnCircumference.Y - Me.Center.Y) ^ 2) ^ (1 / 2)
         End Function

         Private Sub QueryDatabase()
             Dim connection As IDbConnection = New SqlConnection( _
                 "Data Source=TESLATEAM;" + _
                 "Initial Catalog=RENTAWHEELS;" + _
     "User ID=RENTAWHEELS_LOGIN;" + _ 
                 "Password=RENTAWHEELS_PASSWORD_123")
             connection.Open()
             Dim command As IDbCommand = New SqlCommand( _
 "SELECT GETDATE()")
             command.Connection = connection
             Dim reader As IDataReader = command.ExecuteReader()
             reader.Read()
             reader.Close()
             connection.Close()
         End Sub
     End Class
 End Namespace

数回実行すると、両サンプルの時間が非常に近いことが分かります。私のマシンでは、これらの値は、2.2秒から2.4秒付近にあります。1つ認められた小さな違いは、非構造化サンプルのベストタイムが1.9114800秒であったのに対し、構造化のほうは2.0398497秒であった点です。しかし、これは大きな差ではないと考えます。

リファクタリングは優れたオブジェクト指向設計を壊す

十分な構造化やリファクタリングが行われたコードは、素人の目には不自然に映ることもあります。メソッドは、非常に短く実体がないように見えることもよくあります。クラスは、数個のメンバから構成されているだけで、重要さに欠けているように見えます。これらは、コード上何も起こらないように見えます。

より多くの要素(クラスやメソッドなど)を管理する必要があるということは、処理するものがより複雑であることを意味します。

実際、この議論は誤った認識を与えかねません。実は、同様の複雑性は常に存在していたのです。ただし、リファクタリングされたコードでは、その複雑性はより簡潔で、より構造化された方法で表現されます。

リファクタリングから短期的なメリットは得られない

これには、実際にリファクタリングでプログラムミングの高速化を図るというリファクタリング転向者から圧倒的な同意があります。今までのところ、私が言ったことを証明するために頼りになるような調査結果を知りえていないのですが、経験上、これは事実です。それはただ論理的であるにすぎません。取るに足りない、非現実的なまでに小規模なコードを処理しているのでない限り、全体的にコード少量化、重複箇所の減少化、状態の明確化が進むため、リファクタリングのメリットは、すぐに明らかになります。

リファクタリングはアジャイル・チームにのみ効果がある

リファクタリングは、アジャイル方法論の柱となる技術の一つとして言及されることが多いため、これらの原則に忠実なチームに限り、効果があるものと解釈されます。

リファクタリングはアジャイル・チームにとって不可欠である

たとえチームで違う方法論を使用するとしても、たいていの場合、コーディングの方法を任されているのは、あなたであり、リファクタリングが登場するのはこの段階です。他のチームメンバや管理者は、時々、あなたがIDEの「リファクタリング」オプションに手を伸ばしているという事実に気付きもしないかもしれません。したがって、チームでどのような方法論に同意されていようとも、あなたがコードのリファクタリングを行うのを阻止するものは何もありません。リファクタリングで得られる最良の成果というのは、リファクタリングを徐々に取り入れ、コーディング時に定期的に実施した場合に得られます。厳格なコード所有権やウォーターフォール・プロセスなど、一部の手法では、リファクタリングに対抗するものもあります。プログラミングの観点から、リファクタリングが意味をなすことを証明できるなら、まずは仲間と、続いて、残りのチームメンバにその話を広めることで、支持基盤の構築に着手できます。

リファクタリングは開発プロセスで独立した工程として適用され、別のチームにより実施される

通常、この考え方は管理者に支持されます。リファクタリングを独立した工程と見なし、実装およびテストフェーズの間のどこかに置くことは、管理者側の立場からすると、誤った統制感覚を与えることになります。そこでは、タスクとリソースがガント・チャート上で障害と見なされることが多く、容易に押し込まれたり移動させられたりするおそれがあります。

実のところ、リファクタリングを首尾よく行うには、問題の範囲を完全に理解し、要件定義、設計および実装の詳細までも意識する必要があったのです。あなたが初期段階からチームに参画していない、つまりクライアントとの作業、要求事項の分析、設計の考案に時間をかけなかったのであれば、当初のチームによって作成されたものを改善するため状況は困難になるでしょう。

製造工程のある本質を真似て後天的に記述されたコードで作られたモデルに従っても、通常、わずかなメリットしか得られません。実際にコードが何を行っているのか十分に理解しなければ、実際に自信を持ってリファクタリングを行ったとしても、わずかな改善しか得られないでしょう。そのような状況で、どんな本質的な変更を行おうとしても、達成されるはずのものとは全く反対の成果になる可能性は高くなります。問題の領域にコードを上手く関連付ける代わりに、状況を悪化させ、その結果、アプリケーションにバグを埋め込むことになるでしょう。

リファクタリングはユニット・テストをしないほうが機能する

一部の簡単なリファクタリングは、たとえユニット・テストをしなかったとしても、適切な箇所で実施できるでしょう。リファクタリング・ツールやコンパイラそのものは、単純な人的ミスを捕らえるために信頼できるような限定されたセーフティ・ネットを与えてくれます。デバッガの使用や機能テストの実施により、従来の方法でコードをテストすることも可能です。これらの手動によるテスト方法は、冗長で信頼性に欠ける傾向にあります。また、リファクタリングでは、今まで以上にコードの変更を受け入れやすくなります。余計な問題は避け、プロジェクトにNUnitテストを追加すれば、記述する小規模なステップ単位でエラーのテストができるでしょう。

コメントは当てにならない、などとはありえない

このような、奇妙なかたちでの混乱や不信感が生じるのも当然のことです。これまで何度となく、コードにコメントを書くべきだと言われてきたことと思います。これは、第三者がコードを理解する上で手助けとなる有効なプログラミング手法として考えられており、適切、体系的、かつ本格的なコーディングへのアプローチの現れとして解釈されることも少なくありません。したがって、今誰かが、コメントはそれほど有効なアイデアではない、などと言おうものなら、かなり驚きを生じさせるに違いありません。

コメントを記述することの目的は、多くの場合、コードをリファクタリングする時のものと似ています。コードは、人が読みやすいように記述しなければなりません。当初のツールでは、コメントに使用する識別文字の長さに制限があることが多く見受けられたため、コメントは第三者へ何らかの意味を伝えるための唯一の選択肢でした。リファクタリングの観点での違いは、適切なメソッド、クラス、変数および他の識別子を選択し、コードそのものを使用して意味を伝える必要があるという点です。また、同様の目的で、コメントの使用は避けるべきです。コメントは実行されないため、内容が古くなりやすいのです。慌しく典型的な日常業務において、コードの変更が極めて容易に行える一方で、コメント、ドキュメント、図表、その他二次的な成果物の更新は非常に忘れやすくなります。

ハンガリアン記法のどこがいけないのか?

ハンガリアン記法という用語を作り出した人は、思いついた時にCharles Simonyiの母国語だけを知らなかったに違いありません。仮に、a_crszkvc30LastNameColやlpszFileといった名前が彼らにとってハンガリー語のように聞こえたとしても驚きはしません。今日では、コンパイラが型の安全性を管理し型情報を追跡します。最新のIDEでは、必要なすべての型情報は、マウス・ポインタの先で見つけられます。C#やVB.NETなどの言語では、ハンガリアン記法の有効性は、もはや失われています。

再度、言わせていただくと、いくつかの古い習慣を変えるということは、骨が折れるものです。ハンガリアン記法は、有効なプログラミング手法と見なされることも多くあり、それは、何の努力もせずに実装されるものではありません。従来のネーミング体系が非常に時代遅れだと気づき、すべての人がそれに満足しなかったとしても不思議ではありません。

読みやすく、人間の自然な言語に類似する名前を使用することは、コードの明瞭さを大いに向上させるでしょう。奇妙な接頭辞はやめて第三者が理解しやすい用語を使用するのです。

結論

リファクタリングがプログラミングに革命をもたらしているというのは、言い過ぎかもしれません。しかし、いくつかの古い習慣を根底から変えつつあります。これは、経験の乏しい者に多少の抵抗と相当な混乱をもたらすはずです。

リファクタリングの習得に着手する際、本稿が役に立つことを願っております。中には、初めて読む時に意欲を削がれるような記事もありますが、驚く必要はありません。また、リファクタリングを奨励するのであれば、あるいは、リファクタリングについて第三者に指導しようとするのであれば、質問を受けるための心構えをしておかなければなりません。そのような場合に、リファクタリングの背後にある真の論法とその採用の必要性の弁明に十分使用できるような論拠を本稿から提供できれば幸いに思います。

本稿は、Danijel Arsenovsk氏の著書「Professional Refactoring in Visual Basic」(出版社:Wrox)より借用いたしました。

著者について

Danijel Arsenovski氏は、Professional Refactoring in Visual Basic(出版社:Wrox)の著者です。最近では、Excelsys S.Aにて、プロダクトおよびソリューション・アーキテクトとして、地域の数多くのクライアントに向けてインターネット・バンキング・ソリューションの設計に取り組んでいます。氏は、大規模バンキング・システムの全面的な見直しを行う一方で、リファクタリングの試行を開始しました。以来、氏のリファクタリングに対する関心が失われることはなく、VB 6で書かれたコードをVB .NET用にアップグレードする手段として、リファクタリングの使用方法を開拓しました。Arsenovski氏は、注目に値するさまざまな出版物の寄稿者であり、マイクロソフト認定ソリューション デベロッパー(MCSD)の認定を所有しています。また、2005年には、Visual Basic MVPに指名されました。Arsenovski氏への連絡はdanijel.arsenovski at empoweragile.comまでお願いします。また、http://blog.vbrefactoring.comにて、ブログの閲覧が可能です。

 

原文はこちらです:http://www.infoq.com/articles/RefactoringMyths
(このArticleは2008年7月16日に原文が掲載されました)

この記事に星をつける

おすすめ度
スタイル

BT