テストコードの用意されていないコードは全てレガシーコードと呼ぶそうです。(なので新規開発でもテストコードが用意されていなければレガシーコードになります。)
レガシーコードに手を入れる時は、手を入れる箇所 (と周辺のコード) にテストコードを用意しリファクタリングを行うと良い、という話はよく聞きますが、そうは言っても具体的にはどのようにテスト可能なコードに導いていけば良いのか、どの程度まで足を踏み込んで良いのか等、悩みの種はたくさんです。
しかし、本書を読めばきっとそういう悩みが解決されるだろうと僕は勝手に予想しています。これでレガシーコードも余裕です。
ちなみに Amazon には発売日がまだ表示されていないようですが、 (←勘違いだったようです) 発売日は 7/14 らしいです。
しかし、script タグの src 属性に関しては、チルダを使った仮想パスや相対パスで記述しても変換が一切行われません。
なので、Control.ResolveClientUrl メソッドや VirtualPathUtility.ToAbsolute メソッドを使用して変換する必要があります。幸い、HTML タグの属性値ならば <%= %> が使用できるので (コードのハイライトやインテリセンスは効きませんが) 簡単に対処できます。
Site.Master
<head runat="server">
<title><asp:Localize runat="server" Text="<%$ Resources:CommonResource, SystemName %>" /></title>
<link href="Common.css" type="text/css" rel="stylesheet" />
<link href="Site.css" type="text/css" rel="stylesheet" />
<script src="<%= this.ResolveClientUrl("~/Common.js") %>" type="text/javascript"></script>
<script src="<%= this.ResolveClientUrl("~/Site.js") %>" type="text/javascript"></script>
<asp:ContentPlaceHolder ID="HeadPlaceHolder" runat="server" />
</head>
なお、スタイルシートに関しては Thema 機能を使用すれば link タグを記述する必要はなくなりますが、全てのスタイルシートが読み込まれてしまうため、(スタイルシートの) クラス名の衝突の回避に一工夫必要だったりと管理が複雑化します。テーマの切り替えが不要ならば使用しない方が良いかと思います。
[関連]
C#と諸々 ルートディレクトリを示す ~ 演算子
ドメインレイヤで集約毎に名前空間を切る時、今まではエンティティ (集約ルート) の名前を複数形にしてたんだけど、これも不適切だなぁ。例えば Book というエンティティは Hoge.Fuga.Domains.Books という名前空間に。
# Domains も Domain の方が適切だ ^^;
別に Book が数種類あるわけでもないのに Books というのはおかしな話。でも、かといって単数形で Book にしちゃうとクラス名とかぶってしまうわけで…。
この名前空間は Book の集約を含めるための名前空間だから BookAggregate が妥当なのかなー。それとも、そこまで気にしないで Books にしちゃうか…。
まぁ無いものはしょうがないってことで、お遊びでこんなクラス作ってみました。
Int32Def(TSelf).cs
using System;
using System.Runtime.Serialization;
using System.Reflection;
public abstract class Int32Def<TSelf>
where TSelf : Int32Def<TSelf>
{
private readonly int _value;
public static implicit operator int(Int32Def<TSelf> value)
{
return value._value;
}
public static explicit operator Int32Def<TSelf>(int value)
{
TSelf obj = (TSelf)FormatterServices.GetUninitializedObject(typeof(TSelf));
FieldInfo valueField = typeof(Int32Def<TSelf>).GetField("_value", BindingFlags.NonPublic | BindingFlags.Instance);
valueField.SetValue(obj, value);
return obj;
}
public static TSelf operator +(Int32Def<TSelf> value)
{
return (TSelf)(+value._value);
}
public static TSelf operator -(Int32Def<TSelf> value)
{
return (TSelf)(-value._value);
}
public static TSelf operator ~(Int32Def<TSelf> value)
{
return (TSelf)(~value._value);
}
public static TSelf operator ++(Int32Def<TSelf> value)
{
int temp = value._value;
temp++;
return (TSelf)(temp);
}
public static TSelf operator --(Int32Def<TSelf> value)
{
int temp = value._value;
temp--;
return (TSelf)(temp);
}
public static TSelf operator +(Int32Def<TSelf> value1, Int32Def<TSelf> value2)
{
return (TSelf)(value1._value + value2._value);
}
public static TSelf operator -(Int32Def<TSelf> value1, Int32Def<TSelf> value2)
{
return (TSelf)(value1._value - value2._value);
}
public static TSelf operator *(Int32Def<TSelf> value1, Int32Def<TSelf> value2)
{
return (TSelf)(value1._value * value2._value);
}
public static TSelf operator /(Int32Def<TSelf> value1, Int32Def<TSelf> value2)
{
return (TSelf)(value1._value / value2._value);
}
public static TSelf operator %(Int32Def<TSelf> value1, Int32Def<TSelf> value2)
{
return (TSelf)(value1._value % value2._value);
}
public static TSelf operator &(Int32Def<TSelf> value1, Int32Def<TSelf> value2)
{
return (TSelf)(value1._value & value2._value);
}
public static TSelf operator |(Int32Def<TSelf> value1, Int32Def<TSelf> value2)
{
return (TSelf)(value1._value | value2._value);
}
public static TSelf operator ^(Int32Def<TSelf> value1, Int32Def<TSelf> value2)
{
return (TSelf)(value1._value ^ value2._value);
}
public static TSelf operator <<(Int32Def<TSelf> value1, int value2)
{
return (TSelf)(value1._value << value2);
}
public static TSelf operator >>(Int32Def<TSelf> value1, int value2)
{
return (TSelf)(value1._value >> value2);
}
}
Program.cs
using System;
class Hoge : Int32Def<Hoge>
{
}
class Fuga : Int32Def<Fuga>
{
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Test((Hoge)5, (Hoge)10)); //=> 15
Console.WriteLine(Test((Fuga)5, (Fuga)10)); //=> 50
}
static Hoge Test(Hoge a, Hoge b)
{
return a + b;
}
static Fuga Test(Fuga a, Fuga b)
{
return a * b;
}
}
あくまでもお遊びです。Int32 限定ですし、構造体ではなくクラス (従って null がありえますし現時点では対策してません) です。あと、演算子しか考慮してません。
ちなみに T4 Template 版もあります。まだ詰めが甘いですが。こちらは構造体としてコード生成するので、null を許容しません。あと、Int32 以外のプリミティブ型もたぶん行けます、たぶん。
こっちはもうちょい頑張れば実用的なものにできるかなと思います。
<#@ template language="C#" #>
<#@ output extension=".cs" #>
<#
string namespaceName = "Samples";
string newTypeName = "Hoge";
string sourceTypeName = "int";
string[] unaryOperators = new string[]
{
"+",
"-",
"~",
//"!",
};
string[] destructiveUnaryOperators = new string[]
{
"++",
"--",
};
string[] binaryOperators = new string[]
{
"+",
"-",
"*",
"/",
"%",
"&",
"|",
"^",
};
string[] booleanOperators = new string[]
{
"true",
"false"
};
string[] shiftOperators = new string[]
{
"<<",
">>"
};
string[] comparisonOperators = new string[]
{
"==",
"!=",
"<",
">",
"<=",
">="
};
#>
namespace <#= namespaceName #>
{
public struct <#= newTypeName #>
{
public <#= newTypeName #>(<#= sourceTypeName #> value)
{
this._value = value;
}
private readonly <#= sourceTypeName #> _value;
public static implicit operator <#= sourceTypeName #>(<#= newTypeName #> value)
{
return value._value;
}
public static explicit operator <#= newTypeName #>(<#= sourceTypeName #> value)
{
return new <#= newTypeName #>(value);
}
<#
foreach (string unaryOperator in unaryOperators)
{
#>
public static <#= newTypeName #> operator <#= unaryOperator #>(<#= newTypeName #> value)
{
return new <#= newTypeName #>(<#= unaryOperator #>value._value);
}
<#
}
#>
<#
foreach (string destructiveUnaryOperator in destructiveUnaryOperators)
{
#>
public static <#= newTypeName #> operator <#= destructiveUnaryOperator #>(<#= newTypeName #> value)
{
<#= sourceTypeName #> temp = value._value;
temp<#= destructiveUnaryOperator #>;
return new <#= newTypeName #>(temp);
}
<#
}
#>
<#
foreach (string binaryOperator in binaryOperators)
{
#>
public static <#= newTypeName #> operator <#= binaryOperator #>(<#= newTypeName #> value1, <#= newTypeName #> value2)
{
return new <#= newTypeName #>(value1._value <#= binaryOperator #> value2._value);
}
<#
}
#>
<#
foreach (string shiftOperator in shiftOperators)
{
#>
public static <#= newTypeName #> operator <#= shiftOperator #>(<#= newTypeName #> value1, int value2)
{
return new <#= newTypeName #>(value1._value <#= shiftOperator #> value2);
}
<#
}
#>
<#
/*
foreach (string booleanOperator in booleanOperators)
{
#>
public static bool operator <#= booleanOperator #>(<#= newTypeName #> value)
{
throw new System.NotImplementedException();
}
<#
}
*/
#>
}
}
頂いたコメントを参考に更なるリファクタリングを行いました。
class Cards
{
public string[] Deal(int numPlayers, string deck)
{
int needCount = deck.Length - (deck.Length % numPlayers);
string[] hands = Enumerable.Repeat(string.Empty, numPlayers).ToArray();
for (int cardIndex = 0; cardIndex < needCount; cardIndex++)
{
int playerIndex = cardIndex % numPlayers;
hands[playerIndex] += deck[cardIndex];
}
return hands;
}
}
かなりすっきりしました。
前回のリファクタリングで導入した yield return による順序のオブジェクト化は半分ネタだったのでともかくとしても、TrimDeck メソッドは今にして思うと過剰なリファクタリングだった気がします。(でも、順序のオブジェクト化に伴い for 文を foreach 文にしたくって TrimDeck という形を取った、というのもあるんですよ^^;)
CreateInitialHands メソッドも、Enumerable.Repeat の導入によって不要となりましたね。
ちなみに、メソッドの中身が一行だとしても、場合によってはメソッド化する意味があります。処理に名前を付けることで可読性の向上が期待できる場合があるからです。しかし、今回の場合は、メソッド化せずともローカル変数の名前で処理の目的を示すことができますので、インライン化を行いました。
needCount については、前回のようにまず needlessCount を求めてから needCount を求めるようにする方法と、今回のように一行にする方法と、どちらの方が良かったのか悩み所でしたが…。
最後に for 文に関してですが、前回の記事のコメント欄では hands[i % numPlayers] += deck[i]; と書きましたが、そうはせずにまず playerIndex を求めるようにしました。これは、i % numPlayers が何を求めているのかわかりづらいかなと思ってのことです。前述の通り、ローカル変数の名前によって可読性が向上したのではないかと思います。
ということで、かなりいい感じになったなぁと思いますがどうでしょ?
[どうでもよいこと]10分コーディング | Ryuzee.com
10分でコーディング|プログラミングに自信があるやつこい!!
問題読むのに2分ちょい、問題解くのに5分ちょいでした。
⊂(^ω^)⊃ セフセフ!!
class Cards
{
public string[] Deal(int numPlayers, string deck)
{
int count = deck.Length - deck.Length % numPlayers;
string[] result = new string[numPlayers];
for (int i = 0; i < numPlayers; i++)
{
result[i] = string.Empty;
}
int playerIndex = 0;
for (int i = 0; i < count; i++)
{
result[playerIndex] += deck[i];
playerIndex++;
if (numPlayers == playerIndex)
{
playerIndex = 0;
}
}
return result;
}
}
終了後に、リファクタリングもしてみました。
yield return による、順序のオブジェクト化が面白いかもです。
class Cards
{
public string[] Deal(int numPlayers, string deck)
{
string trimmedDeck = this.TrimDeck(deck, numPlayers);
string[] hands = this.CreateInitialHands(numPlayers);
IEnumerator<int> order = this.CreateDealOrder(numPlayers);
foreach (char card in trimmedDeck)
{
order.MoveNext();
hands[order.Current] += card;
}
return hands;
}
private string TrimDeck(string deck, int numPlayers)
{
int needlessCount = deck.Length % numPlayers;
int needCount = deck.Length - needlessCount;
return deck.Substring(0, needCount);
}
private string[] CreateInitialHands(int numPlayers)
{
string[] hands = new string[numPlayers];
for (int i = 0; i < numPlayers; i++)
{
hands[i] = string.Empty;
}
return hands;
}
private IEnumerator<int> CreateDealOrder(int numPlayers)
{
const int startIndex = 0;
int currentIndex = startIndex;
while (true)
{
yield return currentIndex;
int incrementedIndex = currentIndex + 1;
currentIndex = (incrementedIndex < numPlayers) ? incrementedIndex : startIndex;
}
}
}
続き → C#と諸々 Refactoring:10分でコーディング