インターフェースをどう使えばいいのか分からない
オブジェクト指向プログラミングにおいて、インターフェースは「どう使えばよいのか分からない」と言われる筆頭です。
このサイトはVBAを扱っているサイトですが、今回はVBAに限らず、他言語でも使えるオブジェクト指向プログラミングでのインターフェースの考え方と設計手順について説明します。
もしあなたがJava等のオブジェクト指向言語の本を読んだのであれば、インターフェースについて以下のような説明を目にしたと思います。
- インターフェースを使うとクラスにメソッドの実装を強制することができる。
- 複数のクラスで扱うメソッド名はインターフェースを使うことで統一できる。
- どうやって動くかは実装クラスにまかせることができる。
- インターフェースを使うと分かりやすいコードになる。
- 大規模なプログラムを作成するときにはインターフェースは不可欠。
これらの説明は実際に私が持っている本から引用しました。
これを読んで、
「いや、そういうことじゃなくて、もうちょい具体的にというか、、、、うーん、なんて言えばいいんだろう、、、」
という人。あなたの感覚はとても正常です。そして実践的な感覚を持っています。
従来の説明では納得できない理由
上記の説明はプログラミング言語の言語仕様の説明に過ぎず、インターフェースを使うことの利点は全く分かりません。最後の2つの「分かりやすいコードになる」とか「大規模プログラムに必須」とかはもはや説明放棄で何を言いたいのかさっぱりです。
こういう説明を見るとこう言いたくなります。
- インターフェースでメソッドの実装を強制することにどういう利点があるのか?
- 複数クラスでメソッド名を統一して何がよくなるか?
- インターフェースを使わなくてもクラスで公開メソッドを実装すれば同じなのではないか?
- なぜインターフェースを使うと何が分かりやすくなるのか?
- インターフェースを使うのにプログラムの規模は関係あるのか? →これだけは先に答えを書きます。開発規模とインターフェースは全く関係ありません。誤解を招く説明です。
もしこのような疑問を持ってあるのであれば、この記事が疑問解消のヒントになると思います。
この記事では、「インターフェースを使うと○○できる」ではなく、「インターフェースを使うことで何が良くなるのか? どう便利なのか? そして、どうやればいいのか?」という疑問を解消していきます。
インターフェースはプログラミングをする前に考える
先に答えを書いてしまいます。インターフェースは設計時に決まります。プログラミングしながら決めるものではありません。
インターフェースは 「利用者側の必要性に合わせて先にインターフェースを定義し、それを基にクラスを実装する」 のが理想的な流れです。
インターフェースを使うことで何がいいのかというと、「業務を満たすために必要な処理が揃っているかインターフェースを見て判断できる」という点です。実はプログラミングの話は全く関係ありません。なので、JavaだろうがC++だろうがPythonだろうがVBAだろうが、インターフェースの用途は変わりません。
業務には何かしら業務で扱うデータがあり、それを必要に応じて使いたい、という業務要望があるわけです。例えば、請求書の元となるデータがありそこから、請求書を印刷したい、とか、請求書未作成のデータを確認したい、などの業務要件がそれに当たります。
ところが、実際の開発プロジェクトで作成される設計書では、機能がどういう処理をするのか、ということばかりを書いていることが少なくありません。「どうやるのか」ばかりで、「何をしたいのか」を書いていないのです。
設計書に「請求書データの78バイト目が0の場合で、かつ、○○○が××で、××が・・・のデータを抽出する」と書いてあった場合に、請求書未作成データの抽出を行っている、と一目で分かるでしょうか? 分かるわけがありません。でもこんな設計書を書く設計者は多数います。
こういう設計書を渡されたプログラマーは、機能の中の処理を素直に書いていくわけですが、機能の中の話しか書いていないため、
- ユーザーの要件は何なのか?
- この機能に何を求めているのか?
- この機能の目的は何なのか?
ということが当然分かりません。これで何が起こるかというと、設計書に書かれた処理を持つクラスだけを作って終わり、という状況が発生します。
これがプログラミングの段階でどういうインターフェースを作ればいいのかが分からない原因です。
つまり、インターフェースを作るには
「この処理を使う人が何をしたいのか」
を明確に定義する必要があります。開発者の都合ではなく、利用者の目的こそが出発点になります。
利用者がプログラムに何をしてほしいのか、どういう目的の機能なのか。それを業務要件→基本設計→詳細設計、と段階を踏んで明確にしていくと、プログラミングの段階になって必要なインターフェースが見えてきます。
インターフェースは利用者の視点で決めるものです。作成するプログラムには、画面プログラムなどの利用者に近いものと、サーバーサイドや共通機能のように利用者から遠いものとありますが、いずれのプログラムでもそのプログラムを使う側が何をしたいのか、という視点に立つと、インターフェースに何が必要なのかはいずれのプログラムでも明確になります。
インターフェースに何を書けばいいのか分からないのはあなたのせいではありません。「こういうことをやりたい」ということを知らされていない状況に問題があります。
もしあなたが業務分析や設計書を書く立場なのであれば、その機能の目的を設計書に書いて、後続フェーズの担当者の理解度を高めることで、インターフェースに限らずクラス設計もより高い品質になります。
目的から設計されたインターフェースは、誰が見ても「何をしたいのか」が伝わる設計につながります。
インターフェースを決める手順
実際にどういう流れでインターフェースを決めるのかの手順を説明します。
たとえば、請求書を扱う業務で「印刷」と「未作成一覧取得」が必要なら、インターフェースにはこう内容が書かれるべきです。
請求書インターフェース
- 印刷メソッド(Print)
- 未作成一覧取得メソッド(GetPendingList)
このように「業務で使う動作」だけを抜き出して並べたものがインターフェースです。
重要なのは、「印刷(Print)」 や 「未作成一覧取得(GetPendingList)」が実際に「どうやって処理するか」はこの段階では関係ないということです。
必要なのは「その機能があるかどうか」「名前は何か」という情報だけで、実装はあくまで後から業務ごとの実情に合わせて用意します。
問題は、どういう名前のインターフェースがふさわしいのか、そのインターフェースに何を作ればいいのか、ということをどうやって導き出すかです。次にその導き方を説明します。
インターフェースの名前と中身をどう導き出すか
インターフェースの命名や中身をどう決めればいいかですが、「何のための機能なのか?」という目的の明確化から始めます。
【ステップ1】利用者の目的をはっきりさせる
最初にすべきことは、「その機能は何をするためのものなのか?」を文章で明確に書き出すことです。設計書に書いてあるのであればいいのですが、そうでないのであれば関係者からヒアリングを行って機能の目的や要望の洗い出しを行います。
大抵は複数の要望があります。この際に、5W1Hを明確してヒアリングを行います。5W1Hとは以下です。
5W1H | 内容 |
---|---|
Who(誰が) | 業務を行う人は誰か |
What(何を) | 業務を行う際の対象データは何か |
When(いつ) | 業務を行う日時やタイミングはいつか |
Where(どこで) | 業務を行う場所や場面はどこか |
Why(なぜ) | なぜその業務を行うのか、目的は何か |
How(どうやる) | どうやって業務を行うのか |
例えば、以下のような要望があったとします。(この例は前回記事から引用しています。)
- No.1「請求書を印刷したい」
- No.2「まだ請求書を作成していないデータの一覧を確認したい」
これを5W1Hでヒアリングしなおすと、細かい内容が分かってきます。
5W1H | No.1 | No.2 |
---|---|---|
Who(誰が) | 経理担当者が | 経理担当者または経理課長が |
What(何を) | 請求書を | 請求書未作成の売上データを |
When(いつ) | 月末締め処理が完了した後に | 請求処理を行う前に |
Where(どこで) | 経理システムの請求書画面から | 請求業務管理画面から |
Why(なぜ) | 顧客に郵送またはPDF送信するため | 請求漏れを防止するため |
How(どうやって) | 印刷ボタンを押すことでPDFに出力できるようにする | 未作成一覧ボタンで表示できるようにする |
ここでは2つの要望を書いてシンプルにしていますが、実際の業務要望を5W1Hに細分化すると、2つどころかかなりの数になります。これを「業務フローの洗い出し」という言い方をすることもあります。
このように、業務の目的を自然言語で整理することが、インターフェースを決める第一歩になります。
この作業をすっ飛ばすと、設計漏れになります。
なお、経理課長の方がやれることが少ないので、「あれ? 課長って請求書出力できないの? 担当者の方が機能多くない?」と思われるかもしれませんが、コードを簡潔にするためあえて課長は請求書出力を行わず、担当者に任せる、という業務にしています。
【ステップ2】Who:誰が、を洗い出す(役割の洗い出し)
5W1Hで業務フローの洗い出しが終わったら、次に行うのが複数の業務で登場する人や役割の「Who(誰が)」を集約します。業務を実行する「Who(誰が)」のことを、「ロール(役割)」と言います。その人に与えられた「役割」を指します。
業務フローが増えてくると、必ず「Who(誰が)」が重複してきます。経理担当者があれもこれもやっている、というように、複数の業務フローで同じ人や役職が出てきます。これを集約します。
5W1Hでの「Who(誰が)」では、No.1「経理担当者」とNo.2「経理担当者または経理課長」の2つがあります。ここで登場するのは「経理担当者」「経理課長」の2つに集約されます。
集約した結果のこれらの具体的な固有名詞や、役職などが、作成するインターフェース自体の数に関わってきます。
【ステップ3】What:何をするか、を洗い出す(やることの洗い出し)
「Who」の洗い出しと集約が終わったら、次に行うのが「What」+「How」の洗い出しです。何をどうするか、というWhat+Howのことを「アクティビティ(やること)」と言います。
ヒアリング結果から「~を~する」の部分を抽出します。さらにそれを動詞一言で言い換えます。
- 「請求書を、印刷ボタンを押すことでPDFに出力できるようにする」→「請求書出力」
- 「請求書未作成の売上データを、未作成一覧ボタンで表示できるようにする」→「未作成一覧出力」
こうすることで、業務で行うことを集約することが出来ます。これが、インターフェースに作るメソッドの元ネタになります。
【ステップ4】役割とやることからインターフェースを決める
「役割」と「やること」を洗い出したら、あとはインターフェースの作成に入ります。ここでいうインターフェースとは、「特定の役割が担当する業務のまとまり」のことです。プログラミングでの「クラス構造」に当たりますが、クラス構造に必要なインターフェースの作成という位置づけになります。
最初に、「役割」をインターフェースにします。上の例であれば、
- 「経理担当者インターフェース」
- 「経理課長インターフェース」
の2つをインターフェースとして作成します。
そして、「やること」からメソッドを決めます。
事前に挙げたメソッドは「Who(誰が)」が無い状態で集約していますので、5W1Hを見てそのメソッドを必要としているインターフェースに割り当てます。
「経理担当者インターフェース」には「請求書出力」メソッドと「未作成一覧出力」メソッドが用意します。
「経理課長インターフェース」には「未作成一覧出力」メソッドを用意します。
これが、インターフェースの作成手順です。
「請求書出力」メソッドは「経理担当者インターフェース」にのみありますが、「未作成一覧出力」メソッドは両方のインターフェースに存在しています。
これを導き出すために、5W1Hでのヒアリングが必要だ、ということになります。そして、プログラミングは全く関係がないということも理解できたと思います。
おまけ:厳密にインターフェースを決める方法
インターフェースをしっかりと作るのであれば以下の手順になります。
- ロールとアクティビティの抽出後に、その組み合わせからユースケースを作成して「業務の流れ」と「システムがやること」を把握する。
- システムを構成する部品を「コンポーネント(上の例であれば”請求書管理”という部品)」という独立した部品化を行う。
- その部品に対するユースケースからインターフェースが確定する。
- さらにその部品に対してユースケースがやることを整理すると、それがインターフェースのメソッドになる。
、、、という流れになるんですが、まあ、面倒ですよね。ここまでやらなくてもいいです。業務設計だけでなくUML設計スキルを要求されるため、そもそも書ける設計者も多くはいないことと、時間も掛かるため実際の開発現場でもここまでやることは多くないでしょう。
「役割」と「やること」の2つでも十分に正しいインターフェース設計が可能です。
インターフェースを使う利点
インターフェースを使う利点は、人や役職などの業務に携わる「役割」が増えたときに、インターフェースを追加するだけでいい、という点です。
そして、「役割」が増える場合は大抵「やること」も増えますが、役割が増えても影響範囲が限定的になります。
このように影響範囲を局所化でき、かつ、業務に必要なものが揃っているかインターフェースを見れば分かるようになります。例えば「Aという役割のインターフェースと、それば持つ3つのメソッドだけをテストすればよい」ということになります。これがインターフェースを使う利点です。
言語仕様である「メソッドの強制」、「メソッド名の統一」、「インターフェースを使わなくてもクラスの公開メソッドを使えばいいのでは?」といった疑問は、業務分析をした上でのインターフェースを作成すると、あくまでも言語仕様に過ぎないことがはっきりと分かります。
インターフェースとメソッドの一覧があれば、業務に必要な処理がきちんと網羅できているかが分かります。これは、結合試験や総合試験の時に多いに役立つ資料になります。
VBAでの請求書機能のソースコード例
上の請求書機能の内容をVBAのクラスモジュールを使った場合のソースコード例が以下になります。
- 経理担当者インターフェース:請求書出力機能のみ。
- 経理課長インターフェース:請求書出力機能と未作成一覧出力機能の両方。
モジュール種別 | モジュール名 | 内容 |
---|---|---|
クラスモジュール | IAccountingStaff | 経理担当者インターフェース |
クラスモジュール | IAccountingManager | 経理課長インターフェース |
クラスモジュール | InvoiceManager | 請求書出力ロジック本体 |
クラスモジュール | AccountingStaffClass | 経理担当者クラス(IAccountingStaff を実装) |
クラスモジュール | AccountingManagerClass | 経理課長クラス(IAccountingManager を実装) |
標準モジュール | Main | 実行テスト用エントリーポイント |
■経理担当者インターフェース
1 2 3 4 5 6 7 8 9 10 11 |
'// 経理担当者インターフェース '// クラスモジュール名: IAccountingStaff.cls Option Explicit '// 請求書出力メソッド Public Sub OutputInvoice() End Sub '// 未作成一覧出力メソッド Public Sub OutputUnissuedInvoiceList() End Sub |
■経理課長インターフェース
1 2 3 4 5 6 7 |
'// 経理課長インターフェース '// クラスモジュール名: IAccountingManager.cls Option Explicit '// 未作成一覧出力メソッド Public Sub OutputUnissuedInvoiceList() End Sub |
■請求書管理クラス
1 2 3 4 5 6 7 8 9 10 11 12 13 |
'// 請求書管理クラス '// クラスモジュール名: InvoiceManager.cls Option Explicit '// 請求書出力メソッド Public Sub OutputInvoice(userRole As String) Debug.Print userRole & " が請求書をPDF出力しました。" End Sub '// 未作成一覧出力メソッド Public Sub OutputUnissuedInvoiceList(userRole As String) Debug.Print userRole & " が未作成請求一覧を表示しました。" End Sub |
■経理担当者クラス(経理担当者インターフェースを使用する)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
'// 経理担当者クラス '// クラスモジュール名: AccountingStaffClass.cls Option Explicit '// 経理担当者インターフェースを実装 Implements IAccountingStaff '// 請求書管理クラスを宣言 Private invoiceLogic As InvoiceManager '// クラス初期処理 Private Sub Class_Initialize() '// 請求書管理クラスの領域確保 Set invoiceLogic = New InvoiceManager End Sub '// 請求書出力(経理担当者インターフェース) Private Sub IAccountingStaff_OutputInvoice() invoiceLogic.OutputInvoice "経理担当者" End Sub '// 未作成一覧出力(経理担当者インターフェース) Private Sub IAccountingStaff_OutputUnissuedInvoiceList() invoiceLogic.OutputUnissuedInvoiceList "経理担当者" End Sub |
■経理課長クラス(経理課長インターフェースを使用する)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
'// 経理課長クラス '// クラスモジュール名: AccountingManagerClass.cls Option Explicit '// 経理課長インターフェースを実装 Implements IAccountingManager '// 請求書管理クラスの宣言 Private invoiceLogic As InvoiceManager '// クラス初期処理 Private Sub Class_Initialize() '// 請求書管理クラスの領域確保 Set invoiceLogic = New InvoiceManager End Sub '// 未作成一覧出力(経理課長インターフェース) Private Sub IAccountingManager_OutputUnissuedInvoiceList() invoiceLogic.OutputUnissuedInvoiceList "経理課長" End Sub |
■標準モジュールでの呼び出し:Main.bas
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Option Explicit '// 経理担当者処理 Sub RunAsAccountingStaff() Dim staff As IAccountingStaff Set staff = New AccountingStaffClass '// 経理担当者での請求書出力 staff.OutputInvoice '// 経理担当者での未作成一覧出力 staff.OutputUnissuedInvoiceList End Sub '// 経理課長処理 Sub RunAsAccountingManager() Dim manager As IAccountingManager Set manager = New AccountingManagerClass '// 経理課長での未作成一覧出力 manager.OutputUnissuedInvoiceList End Sub |
経理担当者のRunAsAccountingStaff()実行結果
経理担当者 が請求書をPDF出力しました。
経理担当者 が未作成請求一覧を表示しました。
経理課長のRunAsAccountingManager()実行結果
経理課長 が未作成請求一覧を表示しました。
ぱっと見た感じで、「長い・・・」と思いましたか? オブジェクト指向はインターフェースの数とそれを使うクラスと、さらに実処理を書くクラスの5つに分かれた上で、さらに呼び出し処理も必要になるので、どうしても部品が多くなります。
しかし、全体の長さはありますが、個別に見ると上の説明で書いたとおりに実装できています。設計と実装が同じ形式で書けるというのがオブジェクト指向設計/製造の強みです。
おまけ:C#で書いた場合
上のソースをC#で書いた場合が以下になります。重要なことは、設計が同じなのでモジュール構成もVBAとC#で同じになる、という点です。
クラス/インターフェース名 | 内容 |
---|---|
IAccountingStaff | 経理担当者向けインターフェース(請求書出力と未作成一覧出力) |
IAccountingManager | 経理課長向けインターフェース(未作成一覧出力) |
InvoiceManager | 請求処理の共通ロジック |
AccountingStaff | 経理担当者クラス(IAccountingStaff 実装) |
AccountingManager | 経理課長クラス(IAccountingManager 実装) |
Program | 実行用メインクラス |
■IAccountingStaff.cs
1 2 3 4 5 |
public interface IAccountingStaff { void OutputInvoice(); void OutputUnissuedInvoiceList(); } |
■IAccountingManager.cs
1 2 3 4 |
public interface IAccountingManager { void OutputUnissuedInvoiceList(); } |
■InvoiceManager.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System; public class InvoiceManager { public void OutputInvoice(string userRole) { Console.WriteLine($"{userRole} が請求書をPDF出力しました。"); } public void OutputUnissuedInvoiceList(string userRole) { Console.WriteLine($"{userRole} が未作成請求一覧を表示しました。"); } } |
■AccountingStaff.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class AccountingStaff : IAccountingStaff { private readonly InvoiceManager _invoiceManager = new InvoiceManager(); public void OutputInvoice() { _invoiceManager.OutputInvoice("経理担当者"); } public void OutputUnissuedInvoiceList() { _invoiceManager.OutputUnissuedInvoiceList("経理担当者"); } } |
■AccountingManager.cs
1 2 3 4 5 6 7 8 9 |
public class AccountingManager : IAccountingManager { private readonly InvoiceManager _invoiceManager = new InvoiceManager(); public void OutputUnissuedInvoiceList() { _invoiceManager.OutputUnissuedInvoiceList("経理課長"); } } |
■Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System; class Program { static void Main(string[] args) { Console.WriteLine("=== 経理担当者の操作 ==="); IAccountingStaff staff = new AccountingStaff(); staff.OutputInvoice(); staff.OutputUnissuedInvoiceList(); Console.WriteLine("\n=== 経理課長の操作 ==="); IAccountingManager manager = new AccountingManager(); manager.OutputUnissuedInvoiceList(); } } |
まとめ
インターフェースは「役割」と「やること」を整理することで設計できます。
設計時にこれが出来ていない場合は、プログラミングでいくら試行錯誤しても正しいインターフェースは作れません。
もしどういうインターフェースにするべきなのか分からない場合は、業務フローを5W1H(Who, What, When, Where, Why, How)の形式で洗い出して、「役割」と「やること」を集約することで、作成できるようになります。