はじめに
みなさんオブジェクト指向開発してますか?
大規模な開発であれば実装前にきっちりとクラス設計を行っていることでしょう。
また、大規模でなくても機能を跨いだり、複数の担当者で利用するクラスなどは、事前に慎重に設計すべきです。
開発現場にきちんとクラスの設計ルールがある場合は、この記事は読まなくて大丈夫です。
しかし、小規模の機能追加なんかの場合、クラス設計も実装も個人に任されることがあると思います。何ならオブジェクト指向で作らなくても全然構わない状況かもしれません。
でも、オブジェクト指向で作りたいですよね?
この記事では、オブジェクト指向について基本的な知識がある人向けに、実装の最初の取っ掛かりや、リファクタリングする際の観点とか、そういったヒントを紹介したいと思います。
(注意)チームの開発・設計・コーディング等、各種ルール・規約を遵守してくださいね。
サンプルケース
ここではカラーコードを取り扱うものをサンプルケースとしました。
HTMLなどで色を指定したりするのに出てくるカラーコードです。
256諧調のグレイスケールあるいは8色カラーコードからRGB24bitのカラーコードを求めるコードを実装します。
オブジェクト指向、手続き型のどちらで実装しても何の問題もないケースですが、あえてオブジェクト思考で書いてみます。
カプセル化
オブジェクト思考で開発するなら、まずはカプセル化から始めましょう。
中級者以上はバリューオブジェクトなどが良いのでしょうが、それらの説明はちょっと置いておいて、まずはカプセル化しましょう。
サンプルケースを1つのクラスで実装してみます。
RGB24bitのカラーコードを求めるということから、RGBの値をデータ構造として持つクラスを考えてみます。
#
# カラー
#
class RgbColor
def initialize(red, green, blue)
@red = red
@green = green
@blue = blue
end
# RGBの値からカラーコードを求める
def code
(@red * 256 * 256) + (@green * 256) + (@blue)
end
# 16進数のカラーコード
def to_s
'#' + format("%06x", code)
end
# グレイスケールで作成
def self.create_gray_scale(level)
# RGPを同値にする
new(level, level, level)
end
# 8色カラーで作成
def self.create_eight_color(color_code)
# color_code は、3bit
# 各桁は、green, red, blue を表す
# 000 : 黒
# 001 : 青
# 010 : 赤
# 011 : 紫(マゼンタ)
# 100 : 緑
# 101 : 水色(シアン)
# 110 : 黄色(イエロー)
# 111 : 白
green = color_code[2] * 255
red = color_code[1] * 255
blue = color_code[0] * 255
new(red, green, blue)
end
end
#
# 実行
#
puts '---------------------------------'
puts 'gray scale'
puts RgbColor.create_gray_scale(0).to_s
puts RgbColor.create_gray_scale(1).to_s
puts RgbColor.create_gray_scale(2).to_s
puts RgbColor.create_gray_scale(127).to_s
puts RgbColor.create_gray_scale(255).to_s
puts '---------------------------------'
puts 'eight color'
puts RgbColor.create_eight_color(0).to_s
puts RgbColor.create_eight_color(1).to_s
puts RgbColor.create_eight_color(2).to_s
puts RgbColor.create_eight_color(3).to_s
puts RgbColor.create_eight_color(4).to_s
puts RgbColor.create_eight_color(5).to_s
puts RgbColor.create_eight_color(6).to_s
puts RgbColor.create_eight_color(7).to_s
実行結果
---------------------------------
gray scale
#000000
#010101
#020202
#7f7f7f
#ffffff
---------------------------------
eight color
#000000
#0000ff
#ff0000
#ff00ff
#00ff00
#00ffff
#ffff00
#ffffff
ちなみに、このサンプルコードは、RGBというデータ構造について凝集度が高いコード。通信的凝集度が高いコードと言えます。
ファクトリークラス
値を求めるクラスと、クラスを生成するファクトリークラスに分割してみます。
サンプルコード2
#
# カラー
#
class RgbColor
def initialize(red, green, blue)
@red = red
@green = green
@blue = blue
end
# RGBの値からカラーコードを求める
def code
(@red * 256 * 256) + (@green * 256) + (@blue)
end
# 16進数のカラーコード
def to_s
'#' + format("%06x", code)
end
end
# グレイスケールで作成
class GrayScaleColorFactory
def self.create(level)
# RGPを同値にする
RgbColor.new(level, level, level)
end
end
# 8色カラーで作成
class EightColorFactory
def self.create(color_code)
# color_code は、3bit
# 各桁は、green, red, blue を表す
green = color_code[2] * 255
red = color_code[1] * 255
blue = color_code[0] * 255
RgbColor.new(red, green, blue)
end
end
#
# 実行
#
puts '---------------------------------'
puts 'gray scale'
puts GrayScaleColorFactory.create(0).to_s
puts GrayScaleColorFactory.create(1).to_s
puts GrayScaleColorFactory.create(2).to_s
puts GrayScaleColorFactory.create(127).to_s
puts GrayScaleColorFactory.create(255).to_s
puts '---------------------------------'
puts 'eight color'
puts EightColorFactory.create(0).to_s
puts EightColorFactory.create(1).to_s
puts EightColorFactory.create(2).to_s
puts EightColorFactory.create(3).to_s
puts EightColorFactory.create(4).to_s
puts EightColorFactory.create(5).to_s
puts EightColorFactory.create(6).to_s
puts EightColorFactory.create(7).to_s
RGBの値からカラーコードを求めるRGBカラークラス。
グレイスケールからRGBカラークラスを生成するクラス。
8色カラーコードからRGBカラークラスを生成するクラス。
ファクトリークラスがバリーエーションを生み出すパターン。
カラーコードを保持する責務、カラークラスを生成する責務。
それぞれのクラスの責務を分けてみました。
単一責任の原則に則っているように見えますが、私的には次のサンプルコードの方がより単一責任だと感じています。
クラスでバリエーションを表現する
最初に作ったクラスを別な観点でクラス分割してみます。
グレイスケールを表すクラスと、8色カラーを表すクラスに分割します。
さっきは、値を保持するクラスと、生成するクラスに分割しましたが、今回はバリエーションに着目して分割してみます。
サンプルコード3
#
# グレイスケールカラー
#
class GrayScaleColor
def initialize(level)
@level = level
end
# RGB毎にlevelを適用してラーコードを求める
def code
(@level * 256 * 256) + (@level * 256) + (@level)
end
# 16進数のカラーコード
def to_s
'#' + format("%06x", code)
end
end
# 8色カラー
class EightColor
def initialize(color_code)
@color_code = color_code
end
# RGBの値からカラーコードを求める
def code
green = @color_code[2] * 255
red = @color_code[1] * 255
blue = @color_code[0] * 255
(red * 256 * 256) + (green * 256) + (blue)
end
# 16進数のカラーコード
def to_s
'#' + format("%06x", code)
end
end
#
# 実行
#
puts '---------------------------------'
puts 'gray scale'
puts GrayScaleColor.new(0).to_s
puts GrayScaleColor.new(1).to_s
puts GrayScaleColor.new(2).to_s
puts GrayScaleColor.new(127).to_s
puts GrayScaleColor.new(255).to_s
puts '---------------------------------'
puts 'eight color'
puts EightColor.new(0).to_s
puts EightColor.new(1).to_s
puts EightColor.new(2).to_s
puts EightColor.new(3).to_s
puts EightColor.new(4).to_s
puts EightColor.new(5).to_s
puts EightColor.new(6).to_s
puts EightColor.new(7).to_s
ミックスイン
グレイスケールカラーを表すクラスと、8色カラーを表すクラスの2つのクラスで同じ処理を行うメソッドがあります。
このメソッドをモジュールとして共通化し、それぞれのクラスでインクルードします。
継承は極力使わないようにしたいですね。
スーパークラスはサブクラスの不要な責務を背負い込むことになりがちなので。
サンプルコード4
module Formatter
# 16進数のカラーコード
def to_s
'#' + format("%06x", code)
end
end
#
# グレイスケールカラー
#
class GrayScaleColor
include Formatter
def initialize(level)
@level = level
end
def code
(@level * 256 * 256) + (@level * 256) + (@level)
end
end
# 8色カラー
class EightColor
include Formatter
def initialize(color_code)
@color_code = color_code
end
def code
green = @color_code[2] * 255
red = @color_code[1] * 255
blue = @color_code[0] * 255
(red * 256 * 256) + (green * 256) + (blue)
end
end
#
# 実行
#
puts '---------------------------------'
puts 'gray scale'
puts GrayScaleColor.new(0).to_s
puts GrayScaleColor.new(1).to_s
puts GrayScaleColor.new(2).to_s
puts GrayScaleColor.new(127).to_s
puts GrayScaleColor.new(255).to_s
puts '---------------------------------'
puts 'eight color'
puts EightColor.new(0).to_s
puts EightColor.new(1).to_s
puts EightColor.new(2).to_s
puts EightColor.new(3).to_s
puts EightColor.new(4).to_s
puts EightColor.new(5).to_s
puts EightColor.new(6).to_s
puts EightColor.new(7).to_s
委譲
さっきはミックスインを使って実現しましたが、委譲をつかった例も紹介します。
継承よりミックスイン。ミックスインより委譲を使いたいところです。
継承とミックスインは静的に依存関係が決まりますが、委譲は依存関係を動的に変更することもできます。(サンプルコードは静的な依存関係です)
サンプルコード5
class HexFormatter
def self.to_s(color)
'#' + format("%06x", color.code)
end
end
#
# グレイスケールカラー
#
class GrayScaleColor
def initialize(level)
@level = level
end
def code
(@level * 256 * 256) + (@level * 256) + (@level)
end
def to_s
HexFormatter.to_s(self)
end
end
# 8色カラー
class EightColor
def initialize(color_code)
@color_code = color_code
end
def code
green = @color_code[2] * 255
red = @color_code[1] * 255
blue = @color_code[0] * 255
(red * 256 * 256) + (green * 256) + (blue)
end
def to_s
HexFormatter.to_s(self)
end
end
#
# 実行
#
puts '---------------------------------'
puts 'gray scale'
puts GrayScaleColor.new(0).to_s
puts GrayScaleColor.new(1).to_s
puts GrayScaleColor.new(2).to_s
puts GrayScaleColor.new(127).to_s
puts GrayScaleColor.new(255).to_s
puts '---------------------------------'
puts 'eight color'
puts EightColor.new(0).to_s
puts EightColor.new(1).to_s
puts EightColor.new(2).to_s
puts EightColor.new(3).to_s
puts EightColor.new(4).to_s
puts EightColor.new(5).to_s
puts EightColor.new(6).to_s
puts EightColor.new(7).to_s
最後に
サンプルコードはどれが正解というものではなくて、クラス設計やリファクタリングする際の考え方のヒントとして、引き出しに入れておくことができれば良いなと思っています。
一人でコードを書いている時「このクラス設計で良かったのか?」と自問する時に、ここで挙げたものを照らし合わせて考えてみてはどうでしょうか?
勿論、これだけでなく様々な概念や原則
SOLID、GRASP、DDD、Clean Architecture .etc
こういったものに照らし合わせて考えることができれば、さらに良いクラスとなっていくことでしょう。