インターフェースという語は、USBの端子にも、画面のボタン配置にも、コードに書くinterface宣言にも同じように当てられる言葉です。指すものが違うのに同じ名前で呼ばれるため、設計の会話が噛み合わなくなる場面が出てきます。この記事では、境界面という原義から3つの層に切り分けたうえで、実装者が手を動かす2つの層——コード上の型としての契約と、システム間で交わす通信の契約——を、動くコードと公式仕様の記述で押さえていきましょう。Go固有の暗黙実装の詳細はGoのインターフェースと型制約の実装例に譲り、ここでは言語をまたいで通じる考え方に絞ります。
まとめ|インターフェースが指す3つの層と設計で先に決める判断の順序
インターフェースは「異なる2つのものが接する境界と、そこで交わす取り決め」を指す語です。層は3つに分かれます。機器同士をつなぐハードウェアインターフェース、人と機械が接するユーザーインターフェース、そしてソフトウェア同士が接するソフトウェアインターフェース。実装者が設計対象として扱うのは3つ目で、さらにコード内部の型としての契約と、プロセスをまたぐ通信の契約に分かれます。
設計で先に決める順序は3段階です。第一に境界の位置——どこからどこまでを1つの部品と見るか。第二に契約の内容——渡す値、返る値、失敗したときの表現。第三に契約の書き方——コード上のinterface宣言で表すのか、OpenAPIのような機械可読な定義ファイルで表すのか。この順で決めれば、実装の詳細が境界の形を歪める事態を避けられます。逆に実装を先に書いてから境界を後付けすると、内部都合の引数がそのまま契約に残ります。
置くかどうかの判断も先に済ませてください。実装が1つしかなく、差し替えの見込みもテストでの置き換えも要らない箇所にinterfaceを置くと、追う先が1段増えるだけです。条件は最後の章で線を引きます。
インターフェースとは何かを境界面という原義から3つの層に切り分ける
語の成り立ちと、IT分野で同じ語が3つの層に対して使われる理由
interface は inter(間の)と face(面)から成る語で、日本語では境界面や接点と訳されます。物理的にも論理的にも「2つのものが接する場所」を指すため、対象を限定せずに使える語です。IT分野で機器・画面・コードのどれにも同じ語が当たるのは、この汎用性が理由でしょう。表記は「インターフェース」と「インターフェイス」が併存し、現場では I/F や IF と略されます。
3つの層を分けておくと、会話の齟齬が減ります。「インターフェースを直したい」と言われたとき、端子の話なのか、画面の話なのか、メソッドの並びの話なのかで、動く人も工数も変わるためです。設計書のレビューでは、この語が出るたびにどの層かを明示する習慣をつけてください。
ハードウェア・ユーザー・ソフトウェアの3層それぞれの取り決めの置き場
ハードウェアインターフェースは、機器同士をつなぐ物理形状と電気的・論理的な約束です。USB、HDMI、Bluetooth、Ethernet などが該当します。この層の取り決めは業界団体が文書として公開しており、たとえばUSBの仕様書と適合性試験の文書はUSB Implementers Forum の文書配布ページから辿れます。個々の製品が勝手に決めず共通の文書に従うから、他社製品とつながる仕組みです。
ユーザーインターフェースは、人と機械が接する層です。画面のボタン配置やコマンドの打ち方がここに当たります。この層の設計論は範囲が広く、UI/UXの違いと設計の進め方にまとめてあるため、本記事では語の切り分けに留めます。
ソフトウェアインターフェースは、プログラム同士が接する層です。取り決めの置き場が2つに分かれるのがこの層の特徴で、同一プロセス内なら言語の型システム(interface宣言)に、プロセスをまたぐならHTTPやgRPCといった通信の上の定義ファイルに置きます。以降の章では、この2つの置き場をそれぞれ扱います。
APIやSDKとの関係|インターフェースを上位概念として整理しておく
APIはインターフェースの一種で、プログラムから呼ぶことを前提にした境界を指します。SDKはさらに別の層で、APIを呼ぶためのライブラリ・ツール・サンプルをまとめた配布物です。境界そのものがAPI、それを使うための道具一式がSDK、と区別してください。
コードのinterfaceが担う契約をJavaとC#の実装で確認する
Javaのinterfaceが表すのは実装ではなくメソッドの並びという約束
コード上のinterfaceが定義するのは、実装ではなく「どのメソッドが呼べるか」だけです。Java言語仕様 Java SE 21 の第9章 Interfacesでは、インターフェース宣言が抽象メソッド・既定メソッド・静的メソッド・定数・入れ子型を持てること、クラスがimplements節で複数のインターフェースを実装できることが規定されています。クラスの単一継承と違い、実装できる数に制限がありません。
// 通知の送信口を契約として切り出す
public interface Notifier {
void send(String to, String body);
}
// 実装その1: メール送信
public class MailNotifier implements Notifier {
@Override
public void send(String to, String body) {
System.out.println("mail to " + to + ": " + body);
}
}
// 実装その2: チャット送信(呼び出し側のコードは変わらない)
public class ChatNotifier implements Notifier {
@Override
public void send(String to, String body) {
System.out.println("chat to " + to + ": " + body);
}
}
public class OrderService {
private final Notifier notifier; // 具象クラスではなく契約に依存する
public OrderService(Notifier notifier) {
this.notifier = notifier;
}
public void complete(String userId) {
notifier.send(userId, "注文が確定しました");
}
}
OrderServiceが知っているのはsendという約束だけで、メールなのかチャットなのかを知りません。この「相手を知らずに使える」状態こそ、境界を切る目的そのものです。
C#の既定実装と静的抽象メンバーが契約の書き方に持ち込んだ変化
C#のinterfaceも出発点は同じ契約ですが、書ける内容が広がっています。Microsoft Learn の interface キーワード解説によれば、インターフェースはメンバーの既定の実装を持てて、実装型がオーバーライドしない場合はその既定が使われる仕組みです。さらにstatic abstractやstatic virtualメンバーを宣言し、実装型に演算子などの静的メンバーの定義を要求できる、とも書かれています。これらのディスパッチは実行時の型ではなく式のコンパイル時の型で解決されるため、基底型の変数に入れて呼ぶと基底側の静的メンバーが動く点にも同ページは触れています。
public interface INotifier
{
void Send(string to, string body);
// 既定の実装: 実装型が上書きしなければこれが使われる
void SendBulk(IEnumerable<string> targets, string body)
{
foreach (var t in targets) Send(t, body);
}
}
public class MailNotifier : INotifier
{
public void Send(string to, string body)
=> Console.WriteLine($"mail to {to}: {body}");
// SendBulk は既定の実装をそのまま使う
}
既定実装が効くのは、公開済みのインターフェースへメンバーを足したいときです。従来なら実装クラス全てを直す必要がありましたが、既定を付ければ既存の実装型を壊さずに追加できます。ただし同ページが注意している通り、既定のインターフェースメンバーを追加すると、そのインターフェースを実装するref structはそのメンバーを明示的に宣言しなければなりません。無条件に安全な追加ではない点は押さえてください。
なお、インターフェースはインスタンスフィールドを持てません。静的フィールドは許可されますが、インスタンスの状態を抱えられない性質は共通です。状態を持たせたくなったら、それは契約ではなく実装を切り出したい合図であり、抽象クラスや委譲の検討に移ります。型の設計全体の話はオブジェクト指向のカプセル化・継承・ポリモーフィズムを参照してください。
TypeScriptのinterfaceは形が合えば通る構造的な型付けで判定される
TypeScriptのinterfaceは、JavaやC#と同じ書き味に見えて判定の仕方が違います。TypeScript公式ハンドブックの Everyday Typesが説明するとおり、interfaceはオブジェクトの型に名前を付ける手段であり、実装したと宣言する必要がありません。形が合っていれば代入できます。
interface Notifier {
send(to: string, body: string): void;
}
// implements を書かなくても、形が合うので Notifier として通る
const mailNotifier = {
send(to: string, body: string) {
console.log(`mail to ${to}: ${body}`);
},
};
function complete(notifier: Notifier, userId: string) {
notifier.send(userId, "注文が確定しました");
}
complete(mailNotifier, "u-001");
この構造的な型付けは、外部ライブラリの戻り値や素のオブジェクトをそのまま契約に当てはめられる一方、意図しない型が偶然通ることもあります。同ハンドブックはinterfaceと型エイリアスの差にも触れており、実務上の分かれ目は既存の宣言へ後からメンバーを足せるかどうかです。拡張の余地を残したいならinterface、閉じた形で固定したいなら型エイリアスを選んでください。
Goの暗黙実装と型集合がインターフェース設計に与える違いを読む
実装宣言を書かない設計が、契約の所有者を提供側から使う側へ移す
Goのインターフェースは、実装側に何も書かせません。Go言語仕様(Language version go1.27・2026年5月26日版)は、インターフェース型が型集合(type set)を定義し、その型集合に属する型はインターフェースを実装しているとみなされる、と規定します。implementsに相当する構文が存在しないため、型は自分がどのインターフェースを満たしているかを知りません。
package main
import "fmt"
// 使う側のパッケージで契約を宣言する
type Notifier interface {
Send(to, body string) error
}
type MailNotifier struct{}
// implements を書かない。メソッドが揃った時点で Notifier を満たす
func (MailNotifier) Send(to, body string) error {
fmt.Printf("mail to %s: %s\n", to, body)
return nil
}
func Complete(n Notifier, userID string) error {
return n.Send(userID, "注文が確定しました")
}
func main() {
_ = Complete(MailNotifier{}, "u-001")
}
この仕組みが設計に与える差は、契約を誰が持つかにあります。宣言が要らないので、インターフェースを使う側のパッケージで、そのとき必要なメソッドだけを並べた小さな契約を宣言できます。提供側が用意した大きなインターフェースを受け取る必要がありません。結果として、契約は使う側の都合で最小化されます。Java系で同じことをすると、実装クラス側にimplementsを足す修正が必要になり、提供側の都合が入り込みます。
型集合と型制約|メソッドを1つも持たないインターフェースの使い道
Goの仕様が興味深いのは、インターフェースの要素にメソッドだけでなく型そのものを書ける点です。同仕様は、型項T、基礎型がTである全ての型を指す~T、そして和t1|t2を要素に取れると定めています。これにより、メソッドを1つも持たないインターフェースがジェネリクスの型制約として機能します。
// メソッドを持たず、型の集合だけを表すインターフェース
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
ただし制限があります。同仕様は、型要素を含む非基本インターフェースは型制約としてのみ使え、変数や構造体フィールドの型には使えないと明記しています。var x Numberは書けません。契約を表すインターフェースと型の範囲を絞るインターフェースが、同じ構文を共有しながら使える場所で分かれている構造です。Go側の実装パターンはGoのインターフェースの暗黙実装とダックタイピングの解説にまとめてあります。
システム間インターフェースをOpenAPIで機械可読な契約として書く
プロセスをまたぐ境界では、言語の型システムが契約を守ってくれない
同一プロセス内なら、契約違反はコンパイラが止めます。プロセスが分かれた瞬間に、その保証は消えてしまうでしょう。サーバが返すJSONの形が変わっても、クライアントは実行して初めて気づきます。ここで必要になるのが、通信の上に置く別の契約です。
土台になるのはHTTPの意味論で、メソッド・ステータスコード・ヘッダフィールドの意味はRFC 9110 HTTP Semanticsが規定しています。ただしRFCが決めるのは共通の語彙までで、「このパスに何を送ると何が返るか」は各サービスが決めます。その個別の取り決めを書き下したものが、システム間インターフェースの定義です。HTTP APIの設計原則そのものはRESTの6原則とSOAP/GraphQLとの違いで扱っています。
OpenAPI 3.2.0で契約を書く|人と機械の両方が読める形にする意味
HTTP APIの境界を記述する仕様として広く使われているのがOpenAPIです。OpenAPI Specification の最新版は執筆時点でバージョン3.2.0(2025年9月19日発行)で、言語に依存しないHTTP APIのインターフェース記述を定義すること、ソースコードや追加文書へアクセスせずにサービスの機能を人と機械の双方が把握できるようにすることを目的として掲げています。記述形式はJSONまたはYAMLです。
openapi: 3.2.0
info:
title: Notification API
version: 1.0.0
paths:
/notifications:
post:
summary: 通知を1件送信する
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationRequest'
responses:
'202':
description: 受理した(送信完了ではない)
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationAccepted'
'400':
description: 入力値が契約に合わない
components:
schemas:
NotificationRequest:
type: object
required: [to, body]
properties:
to:
type: string
description: 宛先の利用者ID
body:
type: string
maxLength: 500
NotificationAccepted:
type: object
required: [acceptedId]
properties:
acceptedId:
type: string
コード上のinterfaceと見比べると、対応関係がはっきりします。post /notificationsがメソッド名、NotificationRequestが引数の型、NotificationAcceptedが戻り値の型、400が例外に相当するでしょう。違うのは、この定義が実行環境から独立したファイルとして存在する点で、だからモックサーバを立てられ、クライアントの型を生成でき、差分を機械判定できます。定義を先に書いて実装を後から合わせる進め方はスキーマファーストによる並行開発と破壊的変更検知で扱っています。
失敗の表現を契約に含める|成功だけを書いた定義は境界として不完全
定義を書くときに落ちやすいのが失敗側です。成功時のスキーマは丁寧なのに、エラー時は「400が返ります」の1行で終わっている定義をよく見ます。呼ぶ側からすると、これは契約が無いのと同じで、どのフィールドにエラー理由が入るのか、再試行してよい失敗なのかを判別できません。
境界を切るなら、失敗の形も同じ粒度で決めてください。エラー応答のスキーマを1つ定義して全エンドポイントで共有し、再試行の可否が呼ぶ側に伝わる情報(エラー種別コードなど)を必ず含める。この2つを守るだけで、呼ぶ側のエラーハンドリングが場当たりでなくなります。
インターフェースを先に決める設計手順とIF一覧に書く項目の決め方
外部設計のIF一覧に書き出す項目と、実装前に埋めておくべき粒度
受託開発では、システム間インターフェースの取り決めを「IF一覧」「インターフェース定義書」といった成果物にまとめます。これは外部設計の成果物と発注者が承認すべき観点で扱う工程の一部で、実装に入る前に発注側と合意しておく対象です。
一覧に書く項目は、最低限この6つを埋めてください。第一に境界の識別子(連携名とID)。第二に方向(どちらが呼ぶか)。第三に契機(画面操作か、日次バッチか、イベント発生時か)。第四に授受するデータ項目と型と必須可否。第五に失敗時の扱い(再送か、滞留か、人の介入か)。第六に非機能の条件(想定件数、応答時間の目安、同時実行数)。このうち第五と第六が抜けた一覧が最も多く、結合テストで最も揉めます。
境界の位置を決める基準|変わる速さが違う場所どうしの間に線を引く
どこに境界を置くかは、変更の速さで決めるのが実務的です。頻繁に変わる部分とめったに変わらない部分の間に線を引くと、境界が変更の伝播を止めます。逆にいつも一緒に変わる2つの間へ境界を置くと、変更のたびに3か所を直すことになり、手間だけが増えます。
組織の切れ目も判断材料になります。担当チームが分かれる場所、外注と内製が分かれる場所、リリース周期が違う場所には、境界を置く価値があります。フロントエンドとバックエンドの役割分担と連携で扱った体制の分かれ目は、そのまま境界の候補です。逆に、同じ人が同じ周期で両側を直している箇所に境界を置いても、守られる約束がありません。
インターフェースを壊さずに変えるための互換性の判断と版の切り方
既存の利用者を壊す変更と壊さない変更を、境界の種類ごとに判定する
境界を切った後に必ず来るのが変更です。ここで効くのは、変更を「既存の利用者を壊すか壊さないか」で二分する見方です。ただし判定の線は境界の種類で変わります。
コード上のinterfaceでは、メソッドの追加が壊す変更になります。実装クラス全てが新メンバーを実装しなければコンパイルが通らないためです。ただしC#のように既定実装を付けられる言語では、既定を付けた追加は壊れません(前述のref structの例外を除く)。メソッドの削除・シグネチャ変更・引数の型変更は、どの言語でも壊す変更です。
HTTP APIでは判定が逆になる項目があります。レスポンスへのフィールド追加は通常は壊れません(未知フィールドを無視するクライアントが前提)。一方、リクエストの必須フィールド追加、レスポンスのフィールド削除、型変更、列挙値の追加(クライアントが網羅的に分岐している場合)は壊す変更です。とくに列挙値の追加は見落とされやすく、定義上は追加でも受け取る側で例外になります。
壊す変更を出すと決めたときの畳み方と、旧版を落とすまでの段取り
互換を保てないと判断したら、隠さずに版を切ってください。同じエンドポイントの挙動をこっそり変えるのが最も損害が大きく、呼ぶ側は気づけません。
段取りは4段階で組みます。第一に新版を別のパスまたは別のインターフェース名で公開し、旧版を残す。第二に旧版の利用状況を計測し、誰がまだ呼んでいるかを把握する。第三に移行期限を通知し、その間は両方を保守する。第四に利用がゼロになったのを確認してから旧版を落とす。この第二段階を飛ばして期限だけ切ると、期限後に想定外の呼び出し元が現れます。
壊す変更かどうかを人の目で判定し続けるのは持ちません。定義ファイルがあるなら差分検知をCIに入れ、利用側の期待を固定するならPactによる消費者駆動契約テストとCI組み込みで機械に判定させてください。境界が10を超えたあたりから、人手のレビューでは漏れます。
インターフェースを切るべき場面と切らないほうがよい場面の線引き
実装が1つしかない箇所にinterfaceを置く判断は3条件で決める
「将来差し替えるかもしれないから」という理由だけでinterfaceを置くのは勧めません。実装が1つしかない状態で契約だけ増えると、コードを追う人は宣言と実装を往復する羽目になります。
置く判断は、次の3条件のいずれかに当てはまるときに限ると決めておくと迷いません。第一に、実装が現時点で2つ以上ある、または半年以内に2つ目が確実に入る。第二に、外部サービスや時刻・乱数といった制御しづらい要素を含み、テストで置き換える必要がある。第三に、境界の向こう側が別チームの担当で、実装の詳細を見せたくない。逆に、3条件のどれにも当てはまらない社内の純粋な計算処理に契約を被せるのは、後から必要になった時点で切り出せば足ります。
システム間の境界を切らず、1つのまま束ねたほうがよい3つの条件
システム間でも同じ判断が要ります。境界を1つ増やすと、定義ファイルの保守、通信の失敗処理、再試行時の重複対策、監視、結合テストが付いてきます。これらのコストを払う価値があるかを先に見てください。
束ねたほうがよいのは、次の3つが揃う場合です。両側のリリースが常に同時、担当が同じチーム、そしてデータの整合を1つのトランザクションで守りたい。この状態で境界を切ると、分散した状態の整合を自前で担保する重い仕組みを抱え込むことになります。逆に、リリース周期が違う、担当が分かれている、片側の障害でもう片側を止めたくない、のいずれかが当てはまるなら、境界を切って通信の契約を明文化する価値があります。
判断がつかない場合は、いったん同一プロセス内でコード上のinterfaceとして境界を切り、通信を挟むのは後回しにする手が使えます。境界の位置さえ正しければ、後からプロセスを分けるのは容易です。位置が間違ったまま通信を挟むと、直すのに両側の改修と移行期間が要ります。設計と実装の判断で迷う箇所があれば、API開発・システム連携の相談窓口で、既存システムの構成を踏まえた線引きから一緒に検討できます。
よくある質問
インターフェースとAPIは何が違いますか?
インターフェースが上位概念で、APIはその一種です。インターフェースは機器・人・プログラムのいずれとの境界にも使う語で、APIはプログラムから呼ぶ前提の境界を指します。HTTP APIもライブラリの公開メソッド群もAPIですが、USB端子や画面のボタン配置はインターフェースであってAPIではありません。
「インターフェース」と「インターフェイス」はどちらが正しい表記ですか?
どちらも使われており、指すものは同じです。ベンダー資料では「インターフェイス」、技術記事では「インターフェース」がやや多く、JIS系の文書では「インタフェース」も見られます。社内文書では最初にどれかへ揃えておくと、検索や置換で困りません。
抽象クラスとインターフェースはどう使い分けますか?
共有したいものが「約束だけ」か「実装も」かで分けます。呼べるメソッドの並びだけを決めたいならインターフェース、共通の実装や状態を親に持たせたいなら抽象クラスです。加えて、多くの言語ではクラスの継承が1つに限られる一方、インターフェースは複数実装できるため、複数の性質を持たせたい型なら後者が向きます。C#のように既定実装を持てる言語では境目が曖昧になりますが、インスタンスの状態を持てるかどうかは変わらない差です。
OpenAPIの定義ファイルは誰が書いて、どこに置くべきですか?
境界を提供する側が書き、利用する側がレビューする形が基本です。置き場はサーバ実装と同じリポジトリか定義専用のリポジトリかの二択で、外部へ公開する境界や複数チームが参照する境界なら、専用リポジトリのほうが変更の申請経路を作りやすくなります。仕様の版は執筆時点で3.2.0(2025年9月19日発行)ですが、既存の3.0系・3.1系から段階的に移す運用が現実的でしょう。
インターフェースを設計するときに最初に決めるべきことは何ですか?
境界の位置です。どこからどこまでを1つの部品と見るかが決まらないうちにメソッド名や項目の議論を始めると、内部都合の引数が契約に混ざります。位置を決める基準は変更の速さと担当の切れ目で、頻繁に変わる部分とめったに変わらない部分の間に線を引きます。位置が決まってから、渡す値・返る値・失敗の表現の順で埋めてください。
関連記事
- Goのインターフェースとは?暗黙実装とダックタイピング・型制約:宣言を書かない契約の実装パターン
- オブジェクト指向とは?カプセル化・継承・ポリモーフィズム:契約を使う側の型設計の前提
- スキーマファーストとは?OpenAPI起点の並行開発と破壊的変更検知:定義を先に固定する進め方
- 外部設計とは?内部設計との違いと成果物、承認すべき観点:IF一覧を成果物として合意する工程
- UI/UXとは?UIデザインの基本とUXとの違い・設計の進め方:人と機械が接する層の設計論