はじめに
皆さんは、適切な設計が思いつかないことや、後々こんな設計で実装をしておけばよかったと思うことはありませんか?
新機能を実装する際に今後の事を考えて、ある程度拡張性のある設計をする必要があることは多いと思います。
私も上手な設計をうまく思いつくことが出来ず、先輩エンジニアにアドバイスを求めた経験があります。
その際に、デザインパターンを使えば上手に設計できるという事を教えて頂き、こんなにも綺麗に実装できるのかと感動しました。
デザインパターンについてこれまでも調べたことはありましたが、なかなか実装の際に意識したことはなかったため、せっかくのこの機会に記事を書いてみようと思います。
この記事ではデザインパターンとは何か、デザインパターンを使うメリット、Kotlinを使った実際のプログラム例を紹介します。
オブジェクト指向におけるデザインパターンとは
オブジェクト指向におけるデザインパターンとは、Gang of Fourと呼ばれる4人が共同し1995年に出版された『オブジェクト指向における再利用のためのデザインパターン』という書籍にて紹介された23個の設計手法です。
この23種類のデザインパターンは、それぞれ特定の目的を持ち、オブジェクト指向プログラミングにおける設計上の問題を解決するために使用されます。
たとえば、今回例として紹介するFactory Method パターンは、オブジェクトを生成する箇所をまとめ、柔軟性を高めるために使用されます。
オブジェクト指向に置けるデザインパターンを使うメリット
デザインパターンを使うことによって得られるメリットはいくつかあります。
たとえば、デザインパターンを使用することで、後に改修が必要になった場合でもプログラムを安全に変更することができます。
あるクラスを変更した場合、その変更が別のクラスに影響を与えることがあります。しかし、デザインパターンを使用することでプログラムの依存性を管理し、変更による影響を最小限に抑えることができます。
他にも「技術者同士の意思疎通が容易になる」というメリットがあります。
デザインパターンを習得している技術者同士であれば、パターン名で設計の概要の合意を取ることができるため、意思疎通が容易になりスムーズに開発に取り組むことができます。
デザインパターンの例
デザインパターンとそのメリットについて説明しましたが、実際にどのようにデザインパターンを使用するのか簡単なプログラムを使って説明します。
今回は図形を描画するシステムを例にデザインパターンの実装方法を紹介します。
まずはデザインパターンを考えずに実装してみます。
class Circle(private val name: String) { fun draw() { println("${name}を描画します") } } class Square(private val name: String) { fun draw() { println("${name}を描画します") } } fun main() { val circle = Circle("丸") circle.draw() val square = Square("四角形") square.draw() }
上の例ではクライアントはコンストラクタを使いそのままインスタンスを生成しています。
ですがコンストラクタを使うとコンストラクタに修正が必要になった場合、クライアントコードに影響を与える可能性があり、柔軟性が低下する可能性があります。また、ロジックが散らばったりコードの重複が発生し、保守性が低下する可能性があります。
ここでデザインパターンを使いリファクタリングをしてみます。今回はFactory Methodパターンを使用します。
interface Shape { fun draw() } class Circle(private val name: String) : Shape { override fun draw() { println("${name}を描画します") } } class Square(private val name: String) : Shape { override fun draw() { println("${name}を描画します") } } class ShapeFactory { fun createShape(shapeType: String,name: String): Shape { return when (shapeType) { "CIRCLE" -> Circle(name) "SQUARE" -> Square(name) else -> throw IllegalArgumentException("not support shape type") } } } fun main() { val factory = ShapeFactory(); val circle = factory.createShape("CIRCLE","丸") circle.draw() val square = factory.createShape("SQUARE","四角形") square.draw() }
Factory Methodパターンとはインスタンスの生成をFactoryクラスにまとめ、インスタンスの生成を容易にする設計手法です。
Factory Methodパターンを使うことでクライアントは直接コンストラクタを使ってCircleやSquareのインスタンスを作成する必要がなくなり、ShapeFactory経由でインスタンスを作成するようになりました。
これによりクライアントはShapeFactoryにのみ依存するようになり、Shapeオブジェクトの作成と描画の責任が分離され、Shapeオブジェクトの作成方法が変更されてもクライアントのコードには影響がなくなりました。
つまり、コードの変更による影響を最小限に抑えることができたと言えます。
では次に、それぞれの形に塗りつぶされた形が追加されるとします。今回はAbstract Factoryパターンを使いFactoryクラスを抽象化し、実装してみます。
interface Shape { fun draw() } open class Circle(private val name: String) : Shape { override fun draw() { println("${name}を描画します") } } open class Square(private val name: String) : Shape { override fun draw() { println("${name}を描画します") } } class FilledCircle(private val name: String): Circle(name) { override fun draw() { super.draw() println("${name}を塗りつぶします") } } class FilledSquare(private val name: String) : Square(name) { override fun draw() { super.draw() println("${name}を塗りつぶします") } } interface AbstractFactory { fun createShape(shapeType: String,name: String): Shape } class ShapeFactory : AbstractFactory { override fun createShape(shapeType: String,name: String): Shape { return when (shapeType) { "CIRCLE" -> Circle(name) "SQUARE" -> Square(name) else -> throw IllegalArgumentException("not support shape type") } } } class FilledShapeFactory : AbstractFactory { override fun createShape(shapeType: String,name: String): Shape { return when (shapeType) { "CIRCLE" -> FilledCircle(name) "SQUARE" -> FilledSquare(name) else -> throw IllegalArgumentException("not support filled shape type") } } } fun main() { val shapeFactory = ShapeFactory() val filledShapeFactory = FilledShapeFactory() val circle = shapeFactory.createShape("CIRCLE","丸1") circle.draw() val filledCircle = filledShapeFactory.createShape("CIRCLE","丸2") filledCircle.draw() }
AbstractFactoryパターンは関連するインスタンスの生成を抽象クラスに集約し、生成処理は抽象クラスを実装したファクトリークラスに任せる設計手法です。
上記の例では、AbstractFactoryを実装しShapeFactoryとFilledShapeFactoryを作成しました。
ShapeFactoryは通常の図形、FilledShapeFactoryは塗りつぶされた図形のインスタンスを作成します。
クライアントは必要な図形の種類を指定するだけで、その図形がどのように生成され、どのように塗りつぶされたかを気にする必要がありません。
つまりAbstractFactoryによって、インスタンスの生成方法を抽象化し、クライアントにはどの具象クラスが生成されているかを意識する必要がなくなりました。
この様にデザインパターンを知っていることによって再利用性や拡張性が高い実装方法を思いつくことができ、必要になった機能を既存を安全に適切な実装をすることができます。
おわりに
オブジェクト指向におけるデザインパターンについて、そのメリットや2つのパターンを用いた具体的な例について説明しました。23個すべてのデザインパターンを覚えるのは難しいと思いますが、デザインパターンを積極的に活用することで、保守性や可読性を向上させることができ、より柔軟なプログラムを実現できます。是非、実際の開発においてデザインパターンを活用してみてください。