はじめに
この記事は「【マイスター・ギルド】本物のAdvent Calendar 2022」10日目の記事です。
最近、IT業界のトレンドや動向把握のため、面白そうなエンジニア雑誌があれば読むようになりました!
その雑誌の中で、12月24日に発売されるWEB+DB PRESS Vol.132が目に止まりました。その表紙には、「オブジェクト指向神話からの脱却」という文字が大きく掲載されています。
オブジェクト指向信者の私にとっては、脱却する必要はないだろうと思いながら、とても気になる内容で今から楽しみにしてます!
本を読む前に現時点での私のオブジェクト指向に対する考えについて整理しておきたいなと考え、本記事では個人的なオブジェクト指向のメリット・デメリットを書いていこうと思います。
オブジェクト指向歴
簡単に自身のオブジェクト指向歴を書いておきます。
オブジェクト指向の言語に触れたのはC#が初めてで、5年くらい書いてきました。
新人時代からどうすれば良いコードが書けるかというのがモチベーションだったので、コードの書き方や設計の勉強をしてきて今も継続中です。
こういった本や記事は、オブジェクト指向の言語で説明されていることが多く、自然と私もオブジェクト指向に傾倒していきました。
ただ客観的に考えてみても、良いコードを書くにはオブジェクト指向の考え方が現時点では合っていると思っています。
(良いコードという言葉は「分かりやすく、変更しやすいもの」という理解で使っています)
メリット
ミスを防げる
独自の型を定義し、それを明示することで、ミスに強い設計にすることができます。
例えば、以下のように製品を取得するメソッドの引数に文字列を指定していたとすると、文字列なら何でも渡せてしまいます。
/// <summary>
/// 製品データのリポジトリ
/// </summary>
class ProductRepository
{
/// <summary>
/// 製品取得
/// </summary>
/// <param name="id">製品ID</param>
/// <returns>製品</returns>
Product GetProduct(string id)
{
...
}
}
・・・
// 上のコードを使う部分
var orderId = "1";
var customerId = "8";
var productId = "0";
var productName = "aaa";
var productRepository = new ProductRepository();
// 関係ないIDを渡せてしまう
var product = productRepository.GetProduct(customerId);
ただ、以下のようにidをProductIDという型定義を導入することで、ProductID以外は渡せなくなり、ミスが減ります。
(ProductID以外を渡すとコンパイルエラーになる)
/// <summary>
/// 製品データのリポジトリ
/// </summary>
class ProductRepository
{
/// <summary>
/// 製品取得
/// </summary>
/// <param name="id">製品ID</param>
/// <returns>製品</returns>
Product GetProduct(ProductId id)
{
...
}
}
・・・
// 上のコードを使う部分
var orderId = new OrderId("1");
var customerId = new CustomerId("8");
var productId = new ProductId("0");
var productName = "aaa";
var productRepository = new ProductRepository();
// 型が違うのでコンパイルエラー
var product = productRepository.GetProduct(customerId);
表現力があがる
オブジェクトとして設計すると表現力があがり、コードの読みやすさ、理解のしやすさが上がります。
例えば、以下のように製品コードを単純に文字列としても扱えますが、
string productCode = "A-0001";
以下のようにオブジェクトとして設計すると、Aの部分は製品種別で、0001の部分は製品番号で、製品コードは製品種別と製品番号の組み合わせだなと一目でわかるようになります。
/// <summary>
/// 製品コード
/// </summary>
class ProductCode
{
private readonly ProductType type;
private readonly ProductNumber number;
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="type">製品種別</param>
/// <param name="number">製品番号</param>
public ProductCode(ProductType type, ProductNumber number)
{
this.type = type;
this.number = number;
}
public override string ToString()
{
return $"{type.Value}-{number.Value}";
}
}
/// <summary>
/// 製品種別
/// </summary>
class ProductType
{
private readonly string value;
public string Value { get { return value; } }
public ProductType(string type)
{
value = type;
}
}
/// <summary>
/// 製品番号
/// </summary>
class ProductNumber
{
private readonly string value;
public string Value { get { return value; } }
public ProductNumber(string number)
{
value = number;
}
}
加えて、完全コンストラクタを使うとさらに表現力が上がります。
/// <summary>
/// 製品種別
/// </summary>
class ProductType
{
private readonly string value;
public string Value { get { return value; } }
public ProductType(string type)
{
if(!(type == "A" || type == "B"))
{
throw new Exception("Type error");
}
value = type;
}
}
こうしておくと、製品種別に不正な値は設定できませんし、製品種別にはAとBだけがあるとコードから理解できます。
(本当を言うと、stringではなくenumを使うべきですが、簡略化のためそのままにしてます)
要件の理解が進む
オブジェクト指向で設計、実装していくには、実装したい機能を分割し、目的の単位でクラスを設計していきます。この作業では、クラスの責務の範囲や、クラス、変数やメソッドの命名などを考える必要があるので、やっているうちに仕様書で読んだ理解よりも深い理解を得られることがあります。
これは設計の勉強会で、現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法の著者である増田亨さんも似たようなことをおっしゃっていました。
何ができないのか一目でわかる
普段何かのクラスを使ってコードを実装するとき、そのクラスにはどんなメソッドがあり、何ができるのかを意識しますが、何ができないという情報も重要な表現です。定義されていないメソッドは、そのクラスの仕様上でできないこと、または必要のないことを意味します。つまり、そのクラスの関心外ということです。
例えば、下記のProgrammerのクラスを見てみると、コードを書くメソッドとコードを読むメソッドしかありません。
こう表現することで、このクラスはコードの読み書きをするクラスだと理解することができ、このクラスは料理をしたり、病人を治療したりできないということが明示できます。
また出来ないことを表現すると、足りてない仕様や要件につながることもあります。
下記の例だと、Programmerはコードの読み書きしかできない? ググったり、MTGなどで人とコミュニケーションも取れないとだめじゃないか?など、より良い形の足掛かりになることもあります。
class Programmer
{
void WriteCode()
{
}
void ReadCode()
{
}
}
コードに愛着が持てる
設計というのは一度したら終わりではありません。
以下のイベントのタイミングで、常により良い設計を考えるべきだと思っています。
- 機能の追加
- 機能の変更
- バグの修正
- 「要件の理解が進む」の項で言及したように深い理解を得る
こうして常に成長していく設計は生き物のようであり、自分が苦労して設計したクラスであれば、コードを公開したときには達成感を得て、自分で使うのも他の人に使われるのも嬉しく感じられると思います。
デメリット
記述量が多くなる
「表現力があがる」の項を見れば一目瞭然ですが、オブジェクト指向で設計すると記述量が膨れ上がります。
上記の例では、コメントも含め1行が60行くらいになってしまいました。
こればかりは仕方がないことです。(オブジェクト指向が好きな私でさえ、面倒くさいなと思うことがあります)
具体的な対策としては、コードスニペットを活用するのが一番だと考えています。
しかし、コード量が多くなっても、きちんと設計できていれば後のメリットは大きいと考えています。
基本的にコードは一度書いたら終わりではなく、機能追加や変更、不具合の修正など変更されていきます。
その際に、クラス化することによって読みやすく、責任の範囲が明確に分けられていれば、コードリーディングとコードの修正・変更作業の時間はかなり減ると思います。このようなメリットは人が多く、開発期間が長いほど恩恵が得られるので、最初に払う実装のコストは先行投資として払っても損はしないのではないでしょうか。
読むときに面倒、または複雑になる
また「表現力があがる」のサンプルコードを例にだします。
サンプルコードでは、1行で書いてたものが60行になったことで、製品コードは製品番号と製品種別の組み合わせであるという構造を読む必要があります。こうなると、読むときに、ただの文字列という情報から、製品番号と製品種別、製品コードの情報を認識する必要になり、情報量が増え、複雑性が増すということです。
上記の問題は、個人的にも長年の間、かなり気になっていました。
ただ、「きちんと設計したコードが複雑だということは、元々表現したいものが複雑だということ」という考え方(すみませんが、出典を忘れました)を知って、あまり気にならなくなりました。
さいごに
最近はReactを触る機会があり、関数型プログラミングも面白そうだと思ったので、また勉強してオブジェクト指向との違いみたいなものも知っていければと思ってます。
オブジェクト指向で書いてみたい!って人は下記のエクササイズがおすすめです!