← 一覧に戻る
構造に関するパターン

Facade

シンプルな窓口

概要

複雑な手続き、複数オブジェクトの、それも順番があっていることが必須な呼び出しなどを「よく知っている窓口」であるファサードに任せる。 クライアントからは内部の状態が見えないのが嬉しいところ

マンガでわかる Facade

マンガでわかる Facade #デザインパターン - Qiita

でざぱたんで覚える Facade

ちびキャラは「ファサードたん」。闇市で店を構える商人の娘で、客が話す相手は店主ひとり、ややこしい仕入れや交渉は全部店の奥へ。実行順を知っているだけの「窓口」を用意してエントリポイントを単純に保つのがFacadeで、コツは複雑さをサブシステムへ追い出すことと、窓口を多機能にしないこと。「Mediatorは内向きのFacade、Facadeは外向きのMediator」という対比も面白い。

出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.129〜)

登場人物

  • Facade(正面):とてもシンプルな利用者にわかりやすいインタフェース
  • システムを構成するその他大勢のModule:Facadeのことは意識しないがFacadeから呼び出されて仕事を行う
  • Client: Facadeの利用者

クラス図

1920px-Facade_UML_class_diagram.svg.png (1920×960)

このサイトの実装(ページ生成の例)での対応関係:

classDiagram
  class Main {
    +main(args)
  }
  class PageMaker {
    +makeWelcomePage(mailaddr, filename)
  }
  class Database {
    +getProperties(dbname) Properties
  }
  class HtmlWriter {
    -writer Writer
    +title(title)
    +paragraph(msg)
    +link(href, caption)
    +mailto(mailaddr, username)
    +close()
  }
  Main ..> PageMaker
  PageMaker ..> Database
  PageMaker ..> HtmlWriter

やり方

  • Facadeにはクライアントからアクセスできるmainメソッドを置く
  • mainのなかで(privateな)インスタンスたちへの呼び出しを行う

メリット(用途)

  • インタフェースを少なくできる
  • 利用者は「まずAをして次にBをして,,,」という手順を覚える必要がなくなる

拡張して考える

  • 小さいFacadeをModuleとしてみなし、それらを束ねる大きいFacadeを実装して、大きなシステムを実装する (再帰的にFacadeパターンする)
Java
Main.java
import pagemaker.PageMaker;

public class Main {
    public static void main(String[] args) {
        PageMaker.makeWelcomePage("hyuki@hyuki.com", "welcome.html");
    }
}
pagemaker/PageMaker.java
package pagemaker;

import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;

public class PageMaker {
    private PageMaker() {   // インスタンスは作らないのでprivate宣言する
    }
    public static void makeWelcomePage(String mailaddr, String filename) {
        try {
            Properties mailprop = Database.getProperties("maildata");
            String username = mailprop.getProperty(mailaddr);
            HtmlWriter writer = new HtmlWriter(new FileWriter(filename));
            writer.title("Welcome to " + username + "'s page!");
            writer.paragraph(username + "のページへようこそ。");
            writer.paragraph("メールまっていますね。");
            writer.mailto(mailaddr, username);
            writer.close();
            System.out.println(filename + " is created for " + mailaddr + " (" + username + ")");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
pagemaker/Database.java
package pagemaker;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class Database {
    private Database() {    // newでインスタンス生成させないためにprivate宣言
    }
    public static Properties getProperties(String dbname) { // データベース名からPropertiesを得る
        String filename = dbname + ".txt";
        Properties prop = new Properties();
        try {
            prop.load(new FileInputStream(filename));
        } catch (IOException e) {
            System.out.println("Warning: " + filename + " is not found.");
        }
        return prop;
    }
}
pagemaker/HtmlWriter.java
package pagemaker;

import java.io.Writer;
import java.io.IOException;

public class HtmlWriter {
    private Writer writer;
    public HtmlWriter(Writer writer) {  // コンストラクタ
        this.writer = writer;
    }
    public void title(String title) throws IOException {    // タイトルの出力
        writer.write("<html>");
        writer.write("<head>");
        writer.write("<title>" + title + "</title>");
        writer.write("</head>");
        writer.write("<body>\n");
        writer.write("<h1>" + title + "</h1>\n");
    }
    public void paragraph(String msg) throws IOException {  // 段落の出力
        writer.write("<p>" + msg + "</p>\n");
    }
    public void link(String href, String caption) throws IOException {  // リンクの出力
        paragraph("<a href=\"" + href + "\">" + caption + "</a>");
    }
    public void mailto(String mailaddr, String username) throws IOException {   // メールアドレスの出力
        link("mailto:" + mailaddr, username);
    }
    public void close() throws IOException {    // 閉じる
        writer.write("</body>");
        writer.write("</html>\n");
        writer.close();
    }
}
maildata.txt
hyuki@hyuki.com=Hiroshi Yuki
hanako@hyuki.com=Hanako Sato
tomura@hyuki.com=Tomura
mamoru@hyuki.com=Mamoru Takahashi
Go

PageMaker(窓口)がDatabase(検索)とHtmlWriter(HTML生成)を協調させる構造をそのまま移植。maildata.txtの読み込みパスはカレントディレクトリに依存させず、実行ファイル自身の場所を基準に解決する。

実行: go run ./GoF/patterns/Facade/go

$ go run ./GoF/patterns/Facade/go
database.go
package main

import (
	"bufio"
	"os"
	"strings"
)

// LoadMailData は "メールアドレス=名前" 形式のテキストファイルを読み込み、
// メールアドレス→名前のmapを返す。
//
// Java版のDatabaseクラス(private コンストラクタ + static メソッドのみを持つ
// 「newさせないクラス」)に相当する。Goにはstaticクラスという概念が無いため、
// パッケージレベルの関数としてそのまま表現する(Singleton/goでの「型を非公開に
// する」対処とは違い、そもそも状態を持たないのでインスタンス化自体が不要)。
func LoadMailData(filename string) (map[string]string, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	data := make(map[string]string)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		key, value, ok := strings.Cut(line, "=")
		if !ok {
			continue
		}
		data[key] = value
	}
	return data, scanner.Err()
}
html_writer.go
package main

import (
	"fmt"
	"io"
)

// HtmlWriter はHTML文書を少しずつ書き出していく。Java版のHtmlWriterクラスに相当。
// Java版はjava.io.Writer(実体はFileWriter)を受け取るだけの薄いラッパーなので、
// Goでも同様にio.WriteCloserを受け取る構造体にする(*os.Fileへの書き込みは
// バッファリングを挟まず同期的に行われるので、Builder版のような文字列バッファは
// 経由せずそのままファイルへ流し込める)。
type HtmlWriter struct {
	w io.WriteCloser
}

// NewHtmlWriter はwへ書き込むHtmlWriterを返す。
func NewHtmlWriter(w io.WriteCloser) *HtmlWriter {
	return &HtmlWriter{w: w}
}

// Title はタイトルを出力する。
func (h *HtmlWriter) Title(title string) error {
	_, err := fmt.Fprintf(h.w, "<html>\n<head>\n<title>%s</title>\n</head>\n<body>\n<h1>%s</h1>\n", title, title)
	return err
}

// Paragraph は段落を出力する。
func (h *HtmlWriter) Paragraph(msg string) error {
	_, err := fmt.Fprintf(h.w, "<p>%s</p>\n", msg)
	return err
}

// Link はリンクを出力する。
func (h *HtmlWriter) Link(href, caption string) error {
	return h.Paragraph(fmt.Sprintf(`<a href="%s">%s</a>`, href, caption))
}

// Mailto はメールアドレスへのリンクを出力する。
func (h *HtmlWriter) Mailto(mailaddr, username string) error {
	return h.Link("mailto:"+mailaddr, username)
}

// Close は文書を閉じ、書き込み先(ファイル)も閉じる。
func (h *HtmlWriter) Close() error {
	if _, err := fmt.Fprint(h.w, "</body>\n</html>\n"); err != nil {
		return err
	}
	return h.w.Close()
}
page_maker.go
package main

import (
	"fmt"
	"os"
	"path/filepath"
	"runtime"
)

// mailDataPath はこのソースファイル自身のディレクトリを基準に maildata.txt の
// パスを組み立てる。runtime.Caller(0)で「このファイルの場所」を取得できるため、
// go run をどのディレクトリから実行してもmaildata.txtを見つけられる。
func mailDataPath() string {
	_, thisFile, _, _ := runtime.Caller(0)
	return filepath.Join(filepath.Dir(thisFile), "maildata.txt")
}

// MakeWelcomePage はこのパターンの窓口(Facade)そのもの。
// 呼び出し側は宛先メールアドレスと出力ファイル名しか知らなくてよく、
// 内部でDatabase(メール→名前の検索)とHtmlWriter(HTML組み立て)がどう協調して
// いるかはこの関数の中に閉じ込められている。Java版PageMaker.makeWelcomePageに
// 相当(Java版同様、newさせない前提でパッケージレベル関数として提供する)。
func MakeWelcomePage(mailaddr, filename string) error {
	mailData, err := LoadMailData(mailDataPath())
	if err != nil {
		// Java版もIOExceptionをcatchしてWarningを出すだけで処理を続ける。
		fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
	}

	username, ok := mailData[mailaddr]
	if !ok {
		// Java版はProperties#getPropertyがnullを返し、文字列結合の結果
		// 画面には文字通り"null"と表示されてしまう(見た目上のバグ)。
		// Goでは素直にフォールバック文字列にしておく。
		username = "(unknown)"
	}

	f, err := os.Create(filename)
	if err != nil {
		return err
	}

	writer := NewHtmlWriter(f)
	if err := writer.Title(fmt.Sprintf("Welcome to %s's page!", username)); err != nil {
		return err
	}
	if err := writer.Paragraph(username + "のページへようこそ。"); err != nil {
		return err
	}
	if err := writer.Paragraph("メールまっていますね。"); err != nil {
		return err
	}
	if err := writer.Mailto(mailaddr, username); err != nil {
		return err
	}
	if err := writer.Close(); err != nil {
		return err
	}

	fmt.Printf("%s is created for %s (%s)\n", filename, mailaddr, username)
	return nil
}
main.go
package main

import (
	"fmt"
	"os"
)

// 実行:
//
//	go run ./GoF/patterns/Facade/go                                  # デフォルト(hyuki@hyuki.com, welcome.html)
//	go run ./GoF/patterns/Facade/go hanako@hyuki.com welcome2.html   # 宛先・出力ファイル名を指定
//
// PageMaker(Facade)がDatabaseとHtmlWriterという2つのサブシステムを裏で協調させる。
// mainはMakeWelcomePageという「窓口」しか呼ばない。呼び出し側からはDatabaseや
// HtmlWriterの存在はまったく見えない(=複雑さをサブシステム側に追い出せている)。
func main() {
	mailaddr, filename := "hyuki@hyuki.com", "welcome.html"
	switch len(os.Args) {
	case 1:
		// デフォルトのまま
	case 3:
		mailaddr, filename = os.Args[1], os.Args[2]
	default:
		fmt.Fprintln(os.Stderr, "Usage: go run ./GoF/patterns/Facade/go [mailaddr filename]")
		os.Exit(1)
	}

	if err := MakeWelcomePage(mailaddr, filename); err != nil {
		fmt.Fprintln(os.Stderr, "Error:", err)
		os.Exit(1)
	}
}
maildata.txt
hyuki@hyuki.com=Hiroshi Yuki
hanako@hyuki.com=Hanako Sato
tomura@hyuki.com=Tomura
mamoru@hyuki.com=Mamoru Takahashi
PHP
index.php
<?php

/**
 * 窓口を統一して複雑度を追い出す
 * MVCのC
 * コツは
 * 複雑さはサブシステムに追い出すこと
 * 一つのファサードを多機能にしないこと
 */


/**
 * facade: 窓口。workerのdoMethodを管理
 * runで全て実行
 * worker: doMethodを持つ
 * 
 */


class Facade
{
  public function run()
  {
    $obj1 = new Worker1();
    $obj2 = new Worker2();
    $obj3 = new Worker3();

    if ($obj1->doMethod()) {
      return $obj2->doMethod();
    } else {
      return $obj3->doMethod();
    }
  }
}


interface IWorker
{
  public function doMethod();
}

class Worker1 implements IWorker
{
  public function doMethod()
  {
    return true;
  }
}

class Worker2 implements IWorker
{
  public function doMethod()
  {
    return "worker2";
  }
}



class Worker3 implements IWorker
{
  public function doMethod()
  {
    return "worker3";
  }
}


$obj = new Facade();

// workerが見えないのでスッキリする

print $obj->run();
TypeScript

Go版と同じ構成。maildata.txtのパス解決も同様にスクリプト自身のディレクトリ基準にしている。

実行: npx tsx GoF/patterns/Facade/typescript/main.ts

$ npx tsx GoF/patterns/Facade/typescript/main.ts
database.ts
// Database: "メールアドレス=名前" 形式のテキストファイルを読み込み、
// メールアドレス→名前のMapを返す。
//
// Java版のDatabaseクラス(privateコンストラクタ+staticメソッドのみを持つ
// 「newさせないクラス」)に相当する。TypeScriptでもクラス化はせず、
// exportするのは関数だけにする(Go版のパッケージレベル関数と同じ対応関係)。

import { readFileSync } from "node:fs";

export function loadMailData(filename: string): Map<string, string> {
  const data = new Map<string, string>();

  let text: string;
  try {
    text = readFileSync(filename, "utf-8");
  } catch {
    console.warn(`Warning: ${filename} is not found.`);
    return data;
  }

  for (const rawLine of text.split("\n")) {
    const line = rawLine.trim();
    if (line === "") continue;
    const idx = line.indexOf("=");
    if (idx === -1) continue;
    data.set(line.slice(0, idx), line.slice(idx + 1));
  }
  return data;
}
html_writer.ts
// HtmlWriter: HTML文書を少しずつ組み立てていく。Java版のHtmlWriterクラスに相当。
//
// Java版はjava.io.Writer(実体はFileWriter)へ逐次書き込むが、Node.jsでファイルへの
// 逐次書き込みをストリームで行うと非同期になり確認スクリプトとしては複雑になりすぎる
// ため、Builder版(HTMLBuilder)と同じくメモリ上の文字列バッファに組み立て、
// getResult()で返す方式にする。実際のファイル書き出しはPageMaker側がgetResult()の
// 結果をwriteFileSyncで同期的に書き出す形で行う。

export class HtmlWriter {
  private buffer = "";

  title(title: string): void {
    this.buffer += `<html>\n<head>\n<title>${title}</title>\n</head>\n<body>\n<h1>${title}</h1>\n`;
  }

  paragraph(msg: string): void {
    this.buffer += `<p>${msg}</p>\n`;
  }

  link(href: string, caption: string): void {
    this.paragraph(`<a href="${href}">${caption}</a>`);
  }

  mailto(mailaddr: string, username: string): void {
    this.link(`mailto:${mailaddr}`, username);
  }

  close(): void {
    this.buffer += "</body>\n</html>\n";
  }

  getResult(): string {
    return this.buffer;
  }
}
page_maker.ts
// PageMaker: このパターンの窓口(Facade)そのもの。
//
// 呼び出し側は宛先メールアドレスと出力ファイル名しか知らなくてよく、
// Database(メール→名前の検索)とHtmlWriter(HTML組み立て)がどう協調しているかは
// makeWelcomePage()の中に閉じ込められている。Java版PageMaker.makeWelcomePageに
// 相当(Java版同様、newさせない前提でクラス化はせず関数として提供する)。

import { writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { loadMailData } from "./database";
import { HtmlWriter } from "./html_writer";

// このファイル自身のディレクトリを基準にmaildata.txtを解決する。
// (Go版のruntime.Caller(0)、Python版の__file__に相当。tsxはESModuleとして
// 実行するため、import.meta.urlから絶対パスを求める。)
const MAIL_DATA_PATH = join(dirname(fileURLToPath(import.meta.url)), "maildata.txt");

export function makeWelcomePage(mailaddr: string, filename: string): void {
  const mailData = loadMailData(MAIL_DATA_PATH);
  // Java版はProperties#getPropertyがnullを返し、文字列結合の結果画面には
  // 文字通り"null"と表示されてしまう(見た目上のバグ)。ここでは素直に
  // フォールバック文字列にしておく。
  const username = mailData.get(mailaddr) ?? "(unknown)";

  const writer = new HtmlWriter();
  writer.title(`Welcome to ${username}'s page!`);
  writer.paragraph(`${username}のページへようこそ。`);
  writer.paragraph("メールまっていますね。");
  writer.mailto(mailaddr, username);
  writer.close();

  writeFileSync(filename, writer.getResult());
  console.log(`${filename} is created for ${mailaddr} (${username})`);
}
main.ts
// Facade パターン: お願いページ作成 (Java版と同じ題材)
//
// 実行:
//   npx tsx main.ts                                  # デフォルト(hyuki@hyuki.com, welcome.html)
//   npx tsx main.ts hanako@hyuki.com welcome2.html    # 宛先・出力ファイル名を指定
//
// PageMaker(Facade)がDatabaseとHtmlWriterという2つのサブシステムを裏で協調させる。
// mainはmakeWelcomePageという「窓口」しか呼ばない (Go版main.goと同じ構成)。

import { makeWelcomePage } from "./page_maker";

function usage(): void {
  console.error("Usage: npx tsx main.ts [mailaddr filename]");
}

function main(): void {
  const args = process.argv.slice(2);
  let mailaddr = "hyuki@hyuki.com";
  let filename = "welcome.html";

  if (args.length === 2) {
    [mailaddr, filename] = args;
  } else if (args.length !== 0) {
    usage();
    process.exit(1);
  }

  makeWelcomePage(mailaddr, filename);
}

main();
maildata.txt
hyuki@hyuki.com=Hiroshi Yuki
hanako@hyuki.com=Hanako Sato
tomura@hyuki.com=Tomura
mamoru@hyuki.com=Mamoru Takahashi
Python

Go/TS版と同じ構成。

実行: python3 GoF/patterns/Facade/python/main.py

$ python3 GoF/patterns/Facade/python/main.py
database.py
"""Database: メールアドレス→名前の対応をテキストファイルから読み込む。

"メールアドレス=名前" 形式の行を1行ずつ読み、dictにして返す。
Java版のDatabaseクラス(privateコンストラクタ+staticメソッドのみを持つ
「newさせないクラス」)に相当する。Pythonでもクラス化はせず、モジュールレベルの
関数としてそのまま表現する(Go版のパッケージレベル関数と同じ対応関係)。
"""

from __future__ import annotations


def load_mail_data(filename: str) -> dict[str, str]:
    data: dict[str, str] = {}
    try:
        with open(filename, encoding="utf-8") as f:
            for raw_line in f:
                line = raw_line.strip()
                if not line or "=" not in line:
                    continue
                key, _, value = line.partition("=")
                data[key] = value
    except OSError:
        print(f"Warning: {filename} is not found.")
    return data
html_writer.py
"""HtmlWriter: HTML文書を少しずつ組み立てていく。Java版のHtmlWriterクラスに相当。

Java版はjava.io.Writer(実体はFileWriter)へ逐次書き込むが、TS版(バッファ組み立て)に
倣い、ここでもメモリ上の文字列バッファに組み立ててget_result()で返す方式にする。
実際のファイル書き出しはPageMaker側がget_result()の結果を書き出す形で行う。
"""

from __future__ import annotations


class HtmlWriter:
    def __init__(self) -> None:
        self._buffer = ""

    def title(self, title: str) -> None:
        self._buffer += f"<html>\n<head>\n<title>{title}</title>\n</head>\n<body>\n<h1>{title}</h1>\n"

    def paragraph(self, msg: str) -> None:
        self._buffer += f"<p>{msg}</p>\n"

    def link(self, href: str, caption: str) -> None:
        self.paragraph(f'<a href="{href}">{caption}</a>')

    def mailto(self, mailaddr: str, username: str) -> None:
        self.link(f"mailto:{mailaddr}", username)

    def close(self) -> None:
        self._buffer += "</body>\n</html>\n"

    def get_result(self) -> str:
        return self._buffer
page_maker.py
"""PageMaker: このパターンの窓口(Facade)そのもの。

呼び出し側は宛先メールアドレスと出力ファイル名しか知らなくてよく、
Database(メール→名前の検索)とHtmlWriter(HTML組み立て)がどう協調しているかは
make_welcome_page()の中に閉じ込められている。Java版PageMaker.makeWelcomePageに
相当する(Java版同様、newさせない前提でクラス化はせず関数として提供する)。
"""

from __future__ import annotations

import os

from database import load_mail_data
from html_writer import HtmlWriter

# このファイル自身のディレクトリを基準にmaildata.txtを解決する。
# (Go版のruntime.Caller(0)、TS版のimport.meta.urlに相当。__file__を使う。)
_MAIL_DATA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "maildata.txt")


def make_welcome_page(mailaddr: str, filename: str) -> None:
    mail_data = load_mail_data(_MAIL_DATA_PATH)
    # Java版はProperties#getPropertyがnullを返し、文字列結合の結果画面には
    # 文字通り"null"と表示されてしまう(見た目上のバグ)。ここでは素直に
    # フォールバック文字列にしておく。
    username = mail_data.get(mailaddr, "(unknown)")

    writer = HtmlWriter()
    writer.title(f"Welcome to {username}'s page!")
    writer.paragraph(f"{username}のページへようこそ。")
    writer.paragraph("メールまっていますね。")
    writer.mailto(mailaddr, username)
    writer.close()

    with open(filename, "w", encoding="utf-8") as f:
        f.write(writer.get_result())

    print(f"{filename} is created for {mailaddr} ({username})")
main.py
"""Facade パターン: お願いページ作成 (Java版と同じ題材)

実行:
    python3 main.py                                  # デフォルト(hyuki@hyuki.com, welcome.html)
    python3 main.py hanako@hyuki.com welcome2.html    # 宛先・出力ファイル名を指定

PageMaker(Facade)がDatabaseとHtmlWriterという2つのサブシステムを裏で協調させる。
mainはmake_welcome_pageという「窓口」しか呼ばない (Go/TS版main.go/main.tsと同じ構成)。
"""

from __future__ import annotations

import sys

from page_maker import make_welcome_page


def usage() -> None:
    print("Usage: python3 main.py [mailaddr filename]", file=sys.stderr)


def main() -> None:
    mailaddr, filename = "hyuki@hyuki.com", "welcome.html"

    if len(sys.argv) == 3:
        mailaddr, filename = sys.argv[1], sys.argv[2]
    elif len(sys.argv) != 1:
        usage()
        sys.exit(1)

    make_welcome_page(mailaddr, filename)


if __name__ == "__main__":
    main()
maildata.txt
hyuki@hyuki.com=Hiroshi Yuki
hanako@hyuki.com=Hanako Sato
tomura@hyuki.com=Tomura
mamoru@hyuki.com=Mamoru Takahashi