本記事対象者
- C#初学者
- 中堅あたりになってきて新人の頃の気持ちを忘れてしまった方
こんにちは。都内のソフトウェア開発会社でエンジニアをやっているものです。2020年度新卒社員として入社して約半年C#を書いてきて、アウトプットとしてAdventCalenderに投稿しようと思いました。しかし、C#に関して学んだこと一つを掘り下げて書いたとしても、ネットに転がっている記事の何番煎じにもなってしまうと感じたので、新人がつまづきやすいC#に関すること(一般的なコード実装に関することもあります)のまとめ、おまけとして心構えとして学んだことを書くことにしました。
技術面
変数のスコープに気を配る
学生の頃までは、個人または小規模な製品の開発(当時は大規模だと思ってました。。)にしか携わったことがなかったので正直「クラスの変数をどの範囲で持つべきなのか」というところまでしっかりと気を配ったことがありませんでした。気を配らなくても大体このクラスはこういう機能を持っているんだなというのが分かるし、大人数で開発するということもないので、変数のスコープに気を遣う必要性を把握していながらも、面倒で気を配っていなかったというのが実情です。
しかし、大規模製品を扱うようになってから、膨大な量のソースコード、ファイルを見て保守的な面、パフォーマンスの面両方から鑑みて、かなり気を配ってあげる必要があるということを感じました。
では、どのように考えるようになったか
- この変数はクラス内のフィールドとして持つべきかどうか
- クラス内のフィールドで持つことを選択したとき、どのように実装するか
- どのクラスがどの変数を持つべきなのか。
です。
1. この変数はクラス内のフィールドとして持つべきかどうか
例えば、100個のログを持つリストオブジェクトを格納する変数accessLogs
という変数と、時刻を格納する変数time
、メールアドレスtoAddress
があるとします。
これをアクセスログを管理する役割を持つクラスLogManageクラス
で使うときどのように持たせるべきでしょうか。ここでは、ログを元に現在時刻が入ったPDFを出力するメソッド、ログを元にメールを送信するメソッドが実装される予定だとします。
public class LogManage{
private readonly List<string> _accessLogs;
public LogManage(List<string> accessLogs){
_accessLogs = accessLogs;
}
private void OutputPdf(){
var time = DateTime.Now;
//_accessLogsとtimeを使ってPDFを出力する処理を記述
}
private string SendMail(){
var toAddress = ~~~~;
//_accessLogsを添付したメールをToAddressに送信する処理
}
}
このように書くことで、100個もの要素がある_accessLogsはコンストラクタ時に格納されて、それ以降はこのクラス内では新しくメモリを使うことなく使うことが出来ます。これはパフォーマンスの面でも重要なことです。また、このとき、このクラスがログを管理するという機能であることはLogManageというクラス名からも分かるので、クラス内のフィールドとして持つのは理にかなっていると言えます。
一方でtimeとtoAddressはOutputPdfメソッド、SendMailメソッド内でしか使われません。このときにフィールドを持ってしまうとこのクラスを作成したときに使わないかもしれない情報をインスタンスが持ってしまうことになるため無駄になってしまうケースがあります。そのため、メソッド内のローカル変数として定義するのが適切でしょう。
もしローカル変数として定義するべき変数がクラス内のフィールドとして定義されていた場合、必要のないところで見れてしまう、触れてしまうということが、予期せぬことで変数の値を変更してしまったり、ここでこの変数触っていいのか?など余計な思考を開発中に生んでしまう原因になり、保守性の観点から好ましくありません。
大切なことは、
- 定義する変数が、全体で持つことが適切か、メソッドごとに持つのが適切かどうかを常に考える
ということです。
余談ですが、少し違ってきますが、ニュアンスとしては、変数名の規則でも同じようなことが言えると考えていて、例えば抽象クラスを型に持つ変数を定義する場合、変数名を抽象的なものにするか、限定的なものにするかは「その変数がどの範囲で使われるのか」(全体に適用されるか、クラス内だけで使われるか)ということを念頭において考えるべきでしょう。
2. クラス内のフィールドで持つことを選択したとき、どのように実装するか
これはその変数を外部(クラスの外)に公開したいのか、そうでないのか(外部からの変更を許したくないか)という点で変わってくると思います。
クラス内の変数を外部に公開したい場合は変数をpublic
として実装する必要があります。しかし、publicとして直接公開するのは少し抵抗があるため、C#特有のプロパティという機能を用いて作成します。これは基本的な書き方だと思うのでここでは省略します。
これは、クラスの外部から見るとメンバ変数のように扱い、クラス内部からするとメソッドとして振る舞ってくれるという便利機能です。
では、外部に公開したくない場合はというと、private
として実装します。問題はここからで、このprivate変数にどのように値を格納するか。です。(今回はクラス内のフィールドとして実装することを前提としています)
結論から言うと、
- 単なる
private
としてコンストラクタ内,それ以外でも値を入れるか - コンストラクタでしか値を入れられない
private readonly
とするか - インスタンスによらないクラス共通の変数として作るなら
privte static
とするか
の3つだと考えます。
単なるprivate
だけを変数につけた場合は、そのクラス内ではどこからでも値を変更することができます。
private readonly
とした場合は、コンストラクタでしか値を格納できないためより安全なクラス設計をすることができます。また、private static
とした場合はインスタンス固有の情報でない(クラス共通)変数として扱うことができ、インスタンスを作成しなくても外部から呼び出せると言うメリットがあります。具体例としてHumanクラスを実装してみましょう。
public class Human{
// Humanクラス内のどのメソッドからもageに代入できる。産まれた後も同一人物でも毎年変わっていきます。
private string age;
// コンストラクタでしか代入できない。基本的に人は産まれてからずっと同じ名前で生きていきます。
private readonly string _name;
// インスタンスによらない変数を定義できる。人類みんなホモサピエンス。
private static string scienceName;
}
どの方法を選択するかはその都度考えることが必要でしょう。
3. どのクラスがどの変数を持つべきか
最初に、クラス内の変数をクラス内のフィールドで持つのか、はたまたローカル変数として持つのか考える必要があると書きました。しかし、C#がオブジェクト思考の言語である以上、より考える必要があるのは、 どのクラスでどの変数、どのメソッドを持つのかということです。
例えば車インスタンスを作成するCarクラスでcreateScrewsというネジを作るメソッドを実装するべきではありません。Carクラスでは車という一つのインスタンスを作成する役割や車の機能を実装する役割を担うだけで、他の細かな部品を作るメソッドは違うクラスに任せるべきということです。(もっと正確にいうならCarインスタンスを作成するfactoryクラスを作ってそこからインスタンスを作成すべきという話もあると思いますがここでは置いておきます。)
つまり、
- 今実装しているものの役割が何なのか
を常に気にかけるのが大切ということです。
ロジックのネストの深さ
大人数が携わるシステムにおいて、読んだだけで理解しやすい、流れを追いやすいコードを書くということは大切なことです。
しかし、ロジックを書いていると知らぬ間に条件分岐が増えていき、ネストがどんどん深くなっていってしまうというのは初心者あるあるだと思います。そこで、ここではネストを浅くしてコードを読みやすくする簡単なテクニックを紹介します。
- 複雑になりそうなロジックはキリの良いところでprivateメソッドとして切り分ける
- if文での条件分岐はfalseの条件から考える
- LINQ文を有効に使う
1. 複雑になりそうなロジックはキリの良いところでprivateメソッドとして切り分ける
例えば、例外処理を実装していて、かつその中でif文などが入ってくる場合はどうしても{}のネストが深くなってしまいがちです。そういう時は、実際の処理ロジックをprivateメソッドとして切り分けてそのメソッドに任せましょう。そうするとすっきりと書くことができます。
public string Load(){
try{
return LoadInner();
}
catch{
///例外処理
}
}
private string LoadInner(){
//具体処理内容
if(){
hogehoge
}else{
foofoo
}
}
2. if文での条件分岐はfalseの条件から考える
これは、長い処理をネストに入れたくないという発想からきます。
一見、何を言ってるんだという感じだと思うので、具体例で見ていきましょう。
例えば、Enabled(boolean)の値がtrueの時はとても長い処理をする。falseのときは何もしないで例外を上位に投げるというメソッドがあるとします。この時素直にif文を書くと
private int getNumber(){
if(Enabled){
hogehoge;
fugafuga;
foobar;
if(~~){
hogehoge;
fugafuga;
foobar;
}
hogehoge;
fugafuga;
foobar;
hogehoge;
} else{
throw new Exception("例外が発生しました。");
}
}
となります。ここで題名のテクニックを使うと
private int getNumber(){
if(!Enabled) throw new Exception("例外が発生しました。");
hogehoge;
fugafuga;
foobar;
if(~~){
hogehoge;
fugafuga;
foobar;
}
hogehoge;
fugafuga;
foobar;
hogehoge;
}
と書くことができます。ネストが一個減っているのがわかります。つまり、falseの時の処理を早めにしてあげてif文を抜けることで長く処理の追いにくいメインロジックがネストに入るのを防ぐことができます。基本if文ではtrueのときにメインのロジックを実行するという考えを逆手に取った書き方です。複雑な処理になったときにより力を発揮するので頭の片隅に置いておくくらいでいてください。
3. LINQ文を有効に使う
例えば、foreach文の中にif文を書くとき、これもネストが深くなってしまうよくあるパターンです。
こういったときに役立つのがLINQ文です。
では、ユーザーリストをforeachで回して、会員資格(Enabled)がtrueのユーザーにはメールを送るという処理があるとします。
これを素直にC#で書くと
foreach(var user in userList){
if(user.Enabled){
SendMail();
}
}
こうなります。これをLINQ文を使うと、
userList.Where(_ => _.Enabled).ToList().ForEach(_ => _.SendMail();
となります。有用な時もあるので覚えておいて損はないでしょう。
変数の命名
これはとても大切なことです。その変数、メソッドを見て、これが何を意味するのか、を直感的に理解できることは大切なことこの上ありません。では、僕が開発をする上で迷った時、先輩からいただいた考え方を記します。
- boolean型の変数をつける時: その値が何をしたときにtrueになるのかを考える。
- メソッド名をつける時: 返り値があるメソッドなのか、そうでないか(その場合は、そのメソッド内で何が行われるか)をメソッド名から推測できるようにする
- 冗長になってしまうのを避ける(Humanクラスで名前に関する変数を作る時はhumanNameという命名にしない)
他にも命名規則はいろいろありますがわかりやすく書くことと、以上のことを踏まえることがまず大切なのではないかと感じています。
クラス作成時の設計の重要さ
最後にクラスを新しく作成するときに、まず設計をしっかりすることの大切さについてです。
会社のソフトウェアを作っていると**「こういった機能を追加して欲しい」という要望が入ってくることが多々ある** ということをこの半年で実感しました。その要望全てに答えることが正解ではないですが、容易にその要望に対応することができるように、クラスを設計する段階から変更に強い設計にしておくことはソフトウェアエンジニアとしてのスキルが試されるのではないかと思います。
例えば、新しくVehicleクラスを作るとします。このとき、現段階での要望はCarクラスを内部で持つことしかありません。このとき以下のようにVehicleクラスを実装したらどうなるでしょうか。
public class Vehicle
{
public Car car;
public void DriveCar(){
hogehoge;
}
}
Vehicleクラスなのに車しか運転できなくなってしまいますね。。
では、以下のように実装したらどうでしょう。
public class Vehicle
{
public enum vehicleType;
public void Operate(){
if(vehicleType == vehicleType.car ){
//車運転する
}
else if(vehicleType == vehicleType.bike){
//自転車運転する
}
}
}
public enum vehicleType{
car = 0,
bike = 1
}
このようにすれば後から船も追加したい、飛行機も追加したいという要望があっても容易に答えることができそうです。これはほんの一部ですが、インターフェイスを使って実装したりすることもできるでしょう。とにかく
- 変更に強い設計を考える
ことが大切だということです。
その他
- 例外処理の大切さ
- IEnumerable, List<>によるパフォーマンスの違い
など書きたいなと思ったのですが、他にもたくさんの記事があると思うのでこの二つは省略します!
あくまでも、新人エンジニアとして最初は??と思っていたが大切だと自分なりに感じたことをメインにしたかったので上二つはまたの機会でまとめたいと思っています。
メンタル面
おまけとして新卒のエンジニアとして考えていたことを、何年後かに見返して、懐かしむように備忘録として残しておきたいと思います。
- なるだけレビューに早く回す
- 調べて試行錯誤して学ぶこともあるが、指摘されて見えてくるものは多い
- しかし無駄な指摘を先輩に多くさせて時間を余計に取らせてしまうのはダメ
- 自分の中でチェック、レビューに回すという二つのバランスをしっかり取ること。
- ソースコードの否定は人格の否定ではない
- 本質を知ろうとするのは大事
- しかし末端を知ること(とりあえず実装してみるとか)で初めて全体が見えることも多い
- 本質を理解する意識を持った状態で末端を知っていくことが大事なのかもしれない
後書き
入社して、会社の製品に携わって約半年ですが、振り返り、自分の思考の整理として記事を書きました。間違っているところもあると思うのでもし何かありましたらコメントなどでご指摘していただけると嬉しいです。1年後にこれを見返して、どのような感情になるのか、今から楽しみです。最後まで読んでくださりありがとうございました。