GoF デザインパターン 入門 ~構造に関するパターン(Structural Patterns)~
アダプタパターン(Adapter)
アダプターパターンは3つの構成要素から成る
- Target(クライアントが期待するインターフェース)
- Adaptee(既存のクラスでTargetと合わないもの)
- Adapter(TargetとAdapteeを合わせるもの)
レガシーコードを変更したくないかつ、レガシコードを利用した新しい機能を追加したい時に使用できる
// 既存のクラス(変更できない)
class OldPrinter {
fun printOldFormat(text: String) {
println("Old format: $text")
}
}
// 新しいインターフェース
interface ModernPrinter {
fun print(message: String)
fun printWithHeader(header: String, message: String)
}
// Adapterクラス - OldPrinterをModernPrinterインターフェースに適応
class PrinterAdapter(private val oldPrinter: OldPrinter) : ModernPrinter {
override fun print(message: String) {
// 旧形式のメソッドを呼び出す
oldPrinter.printOldFormat(message)
}
override fun printWithHeader(header: String, message: String) {
// 新機能を旧メソッドで実現
oldPrinter.printOldFormat("[$header] $message")
}
}
ブリッジパターン(Bridge)
ブリッジパターンは実装を増やすパターンと機能を増やすパターンを独立して追加できることにある
ダメなパターン
abstract class Sorter {
abstract fun sort(items: List<String>)
}
abstract class TimeSorter: Sorter() {
fun timeSort(items: List<String>) {
sort(items)
println("Sorted by time: $items")
}
}
class QuickSorter : Sorter() {
override fun sort(items: List<String>) {
// クイックソートの実装(ここは簡略化)
println("QuickSortでソート: ${items.joinToString()}")
}
}
class BubbleSorter : Sorter() {
override fun sort(items: List<String>) {
// バブルソートの実装(ここは簡略化)
println("BubbleSortでソート: ${items.joinToString()}")
}
}
// クイックソートを拡張したクラスを追加しなくてはならなくなる
class TimeQuickSorter : TimeSorter() {
override fun sort(items: List<String>) {
// クイックソートの実装(ここは簡略化)
println("QuickSortでソート: ${items.joinToString()}")
}
}
TimeSorterを機能拡張として追加したいだけなのに、TimeSorterを継承したQuickSorterやBubbleSorterが必要になってしまう
正しい例
interface SortImpl {
fun sort(items: List<String>)
}
class QuickSorter : SortImpl {
override fun sort(items: List<String>) {
// クイックソートの実装(ここは簡略化)
println("QuickSortでソート: ${items.joinToString()}")
}
}
class BubbleSorter : SortImpl {
override fun sort(items: List<String>) {
// バブルソートの実装(ここは簡略化)
println("BubbleSortでソート: ${items.joinToString()}")
}
}
open class Sorter(private val sorter: SortImpl) {
open fun sort(items: List<String>) {
sorter.sort(items)
}
}
class TimeSorter(sortImpl: SortImpl) : Sorter(sortImpl) {
fun timeSort(items: List<String>) {
super.sort(items)
println("Sorted by time: $items")
}
}
実装(SortImpl)を抽象化しSorterの引数に持たせる(継承可能にする)
機能拡張するクラスTimeSorterはSorterを継承する、引数には実装クラスを持つ
そうすることでソートを増やしたい場合はSortImplを継承したクラスを作り、機能拡張したい時はSorterを継承したclassを作れば良い
機能と実装の橋渡しをする
コンポジットパターン(Composite)
コンポジットパターンは容器と中身を同じものとして扱うためのデザインパターン
ファイルシステムに対して使用できる
ファイルとフォルダなのかを意識せずに同じように削除できるようにする
// 共通インターフェース
interface DirectoryEntry {
fun remove()
}
// ファイルクラス
class File(private val name: String) : DirectoryEntry {
override fun remove() {
println("$name を削除しました")
}
}
// ディレクトリクラス
class Directory(private val name: String) : DirectoryEntry {
private val entries = mutableListOf<DirectoryEntry>()
fun add(entry: DirectoryEntry) {
entries.add(entry)
}
override fun remove() {
for (entry in entries) {
entry.remove()
}
println("$name を削除しました")
}
}
fun main() {
val file1 = File("file1")
val file2 = File("file2")
val file3 = File("file3")
val file4 = File("file4")
val dir1 = Directory("dir1")
val dir2 = Directory("dir2")
dir1.add(file1)
dir2.add(file2)
dir2.add(file3)
dir1.add(dir2)
dir1.add(file4)
dir1.remove()
// 出力例
// file1 を削除しました
// file2 を削除しました
// file3 を削除しました
// dir2 を削除しました
// file4 を削除しました
// dir1 を削除しました
}
dir1
├─ file1
├─ dir2
│ ├─ file2
│ └─ file3
└─ file4
ディレクトリのremoveが呼ばれた時ファイルから削除されていく
再帰的に葉→枝→根の順で削除メッセージが表示される
デコレータパターン(Decorator)
デコレータパターンはあらゆる組み合わせが考えうる場合に小クラスを増殖することなく実装できるパターン
共通のインターフェース
interface Icecream {
fun getName(): String
fun howSweet(): String
}
具体的な実装
class VanillaIcecream : Icecream {
override fun getName(): String = "バニラアイスクリーム"
override fun howSweet(): String = "バニラ味"
}
class GreenTeaIcecream : Icecream {
override fun getName(): String = "抹茶アイスクリーム"
override fun howSweet(): String = "抹茶味"
}
Decoratorの基底クラス
abstract class IcecreamDecorator(private val decoratedIcecream: Icecream) : Icecream {
override fun howSweet(): String = decoratedIcecream.howSweet()
}
Decoratorクラス
class CashewNutsToppingIcecream(icecream: Icecream) : IcecreamDecorator(icecream) {
override fun getName(): String = "カシューナッツ${super.getName()}"
}
class SliceAlmondToppingIcecream(icecream: Icecream) : IcecreamDecorator(icecream) {
override fun getName(): String = "スライスアーモンド${super.getName()}"
}
使い方
fun main() {
// バニラアイスクリーム
val ice1: Icecream = VanillaIcecream()
println(ice1.getName()) // バニラアイスクリーム
println(ice1.howSweet()) // バニラ味
// カシューナッツバニラアイスクリーム
val ice2: Icecream = CashewNutsToppingIcecream(VanillaIcecream())
println(ice2.getName()) // カシューナッツバニラアイスクリーム
println(ice2.howSweet()) // バニラ味
// カシューナッツ抹茶アイスクリーム
val ice3: Icecream = CashewNutsToppingIcecream(GreenTeaIcecream())
println(ice3.getName()) // カシューナッツ抹茶アイスクリーム
println(ice3.howSweet()) // 抹茶味
// カシューナッツ+スライスアーモンドバニラアイスクリーム
val ice4: Icecream = SliceAlmondToppingIcecream(CashewNutsToppingIcecream(VanillaIcecream()))
println(ice4.getName()) // スライスアーモンドカシューナッツバニラアイスクリーム
println(ice4.howSweet()) // バニラ味
}
ファサードパターン(Facade)
ファサードパターンはクラスが複雑かつ肥大化した際、正しい順序でクラスを使用するのが困難になる
それを窓口となるクラスを作ることで複雑な内部を知らなくても利用することができるパターン
図書館において蔵書リストと貸出帳があるとする
蔵書クラス
class BookList {
// 書名に対する所蔵場所のマップ(簡単のため固定データ)
private val bookLocation = mapOf(
"昆虫図鑑" to "右奥の棚",
"宇宙図鑑" to "左手前の棚"
)
fun searchBook(bookName: String): String? {
// 本があれば場所を返す、なければnull
return bookLocation[bookName]
}
}
貸出帳クラス
class LendingList {
// 貸出中リスト(簡単のため固定データ)
private val lentBooks = listOf("宇宙図鑑")
fun isLentOut(bookName: String): Boolean {
return lentBooks.contains(bookName)
}
}
Facadeクラス
class Librarian {
private val bookList = BookList()
private val lendingList = LendingList()
// "本の場所" or "貸出中です" or "その本は所蔵していません" を返す
fun searchBook(bookName: String): String {
val location = bookList.searchBook(bookName)
return if (location != null) {
if (lendingList.isLentOut(bookName)) {
"貸出中です"
} else {
location // 本の場所
}
} else {
"その本は所蔵していません"
}
}
}
クライアント
fun main() {
val librarian = Librarian()
val bookName = "昆虫図鑑"
val location = librarian.searchBook(bookName)
when (location) {
"貸出中です" -> println("貸出中かよ…")
"その本は所蔵していません" -> println("なんだ、ないのか")
else -> println("サンキュ!場所は:$location")
}
}
フライウェイトパターン(Flyweight)
フライウェイトパターンはインスタンスを共有することでリソースを無駄なく使うことができるパターン
人文字を作って文字を作りそれの写真を撮ることを想定する
ファクトリークラスを作りすでにインスタンスがmapにあればmapにあるインスタンスを返し、なければ新しくインスタンを生成し、mapに追加する
class HumanLetter(private val letter: String) {
fun getLetter(): String = letter
fun getPhoto(): String {
// ここでは写真を撮る処理を模擬
return "写真: $letter"
}
}
object HumanLetterFactory {
private val letters = mutableMapOf<String, HumanLetter>()
fun getLetter(letter: String): HumanLetter {
return letters.getOrPut(letter) { HumanLetter(letter) }
}
}
fun main() {
// 作りたいメッセージ(同じ文字が何度も登場)
val message = listOf("あ", "い", "は", "あ", "い", "よ", "り", "も", "あ", "お", "い")
// 各文字ごとに、人文字をファクトリー経由で取得して写真を撮る
for (char in message) {
val humanLetter = HumanLetterFactory.getLetter(char)
humanLetter.getPhoto()
}
}
プロキシパターン(Proxy)
プロキシパターンはその名の通り代理人となるオブジェクトを作り中間で何かの処理を挟みたい時に使用できるパターン
オフライン時はローカルキャッシュを返すProxy
interface ArticleService {
fun getArticles(): List<Article>
}
class RealArticleService : ArticleService {
override fun getArticles(): List<Article> {
// サーバーから取得処理
}
}
class OfflineCacheArticleService(
private val real: ArticleService,
private val cache: LocalCache
) : ArticleService {
override fun getArticles(): List<Article> {
return if (isOnline()) {
real.getArticles().also { cache.save(it) }
} else {
cache.load()
}
}
}
Discussion