Mediatorパターン

概要

相互に影響し合うオブジェクトを仲介者(Mediator)を介して疎結合にするパターン。

仲介者を用意することで、オブジェクト同士がお互いの存在を知らなくてもよくなる。

そのことで依存オブジェクトを増やしやすくなる

使い方

一番代表的な例のメッセージの送受信を例にする。

bad

type Message = {
  owner: MessageUser;
  content: string;
};
class MessageUser {
  messages: Message[] = [];
  constructor(public name: string) {}
  sendMessage(message: string, to: MessageUser): {
    this.messages.push({ owner: this, content: message });
    to.receiveMessage(message, this);
  }
  receiveMessage(message: string, from: MessageUser): {
    this.messages.push({ owner: from, content: message }); 
  }
}

const alice = new MessageUser("Alice");
const bob = new MessageUser("Bob");

const message = "Hello, Bob!";

// 2人の時ならいいが...
alice.sendMessage(message, bob);

const charlie = new MessageUser("Charlie");
// 3人目が増えたら、送信先を増やす必要がある
alice.sendMessage(message, bob);
alice.sendMessage(message, charlie);

チャットルームの人数が増えたときの送信処理が増えてしまう。

その上、送信処理の共通ルールが変わったときに、全てのsendMessageを修正する必要がある。

good

// mediator
class ChatRoom {
  private users: MessageUser[] = [];
  addUsers(users: MessageUser[]) {}
  sendMessage(message: string, from: MessageUser) {
    this.users.forEach((user) => {
      if (user !== from) {
        user.receiveMessage(message, from);
      }
    });
  }
}

type Message = {
  owner: MessageUser;
  content: string;
};

class MessageUser {
  messages: Message[] = [];
  constructor(public name: string, private room: ChatRoom) {}
  sendMessage(message: string): void {
    this.messages.push({ owner: this, content: message });
    chat.sendMessage(message, this);
  }
  receiveMessage(message: string, from: MessageUser): void {
    this.messages.push({ owner: from, content: message });
  }
}

const chat = new ChatRoom();
const alice = new MessageUser("Alice", chat);
const bob = new MessageUser("Bob", chat);
const charlie = new MessageUser("Charlie", chat);

alice.sendMessage("Hello, everyone!");

chatというmediaotorを媒介させることによってaliceは誰にメッセージを送るのかを気にしなくていい。

人数分のsendMessageを呼ぶ必要がなくなる。

代表例2: フォーム

メッセージ以外にも代表例があります

bad

const Button = ({ onClick, canSubmit }: { onClick: () => void; canSubmit: boolean }) => {
  const handleClick = () => {
    if (canSubmit) {
      onClick();
    }
  };
  return <button onClick={handleClick}>Submit</button>;
};
const EmailInput = (
  {
    value,
    onChange,
    setCanSubmit 
  }: {
    value: string;
    onChange: (value: string) => void;  
  }
) => {
  const isValidEmail = (email: string) => {
    // 簡易的なメールアドレスのバリデーション
    return /\S+@\S+\.\S+/.test(email);
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newValue = e.target.value;
    onChange(newValue);
    setCanSubmit(isValidEmail(newValue));
  };
  return <input type="email" value={value} onChange={(e) => handleChange(e.target.value)} />;
};

const form = () => {
  const [email, setEmail] = useState("");
  const [canSubmit, setCanSubmit] = useState(false);

  const handleSubmit = () => {
    console.log("Submitted:", email);
  };

  return (
    <div>
      <EmailInput value={email} onChange={setEmail} setCanSubmit={setCanSubmit} />
      <Button onClick={handleSubmit} canSubmit={canSubmit} />
    </div>
  );
};

Buttonのclickの状態管理をそれぞれのフォームで管理すると、フォームが増えるとButtonの状態を管理するためのコードが増えてしまう上に、整合性が取れなくなる。

good

const Button = ({ onClick, canSubmit }: { onClick: () => void; canSubmit: boolean }) => {
  const handleClick = () => {
    if (canSubmit) {
      onClick();
    }
  };
  return <button onClick={handleClick}>Submit</button>;
};
const EmailInput = (
  { value, onChange, }: { value: string; onChange: (value: string) => void; }
) => {
  return <input type="email" value={value} onChange={(e) => onChange(e.target.value)} />;
};

const PasswordInput = (
  { value, onChange, }: { value: string; onChange: (value: string) => void; }
) => {
  return <input type="password" value={value} onChange={(e) => onChange(e.target.value)} />;
};

const form = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const isValidEmail = () => {
    // 簡易的なメールアドレスのバリデーション
    return /\S+@\S+\.\S+/.test(email);
  };
  const isValidPassword = () => {
    return password.length >= 6; // 簡易的なパスワードのバリデーション
  };

  const canSubmit = isValidEmail(email) && isValidPassword(password);

  const handleSubmit = () => {
    console.log("Submitted:", { email, password });
  };

  return (
    <div>
      <EmailInput value={email} onChange={setEmail} />
      <PasswordInput value={password} onChange={setPassword} />
      <Button onClick={handleSubmit} canSubmit={canSubmit} />
    </div>
  );
};

formをmediatorとして扱い、Buttonの送信可能状態の管理を一括管理でき、入力欄を増やしやすくなる

使い所

  • 複数のオブジェクトが協調して相互作用する時
  • 複数のオブジェクトの共通のルールを定義したいとき

参考