ためすう

第14章 オブジェクトを組み立てやすくする: Builder

2019-06-30

「Rubyによるデザインパターン」の読書メモです。

ビルダ (Builder) クラスが担当するのは、ある複雑なオブジェクトのコンポーネントの組み立てです。各ビルダには新しいオブジェクトに対してステップごとに構成を指示するインターフェースを持っています。

test.rb

class Computer
  attr_accessor :display
  attr_accessor :motherboard
  attr_reader :drives

  def initialize(display=:crt, motherboard=Motherboard.new, drives=[])
    @motherboard = motherboard
    @drives = drives
    @display = display
  end
end

class CPU
end

class BasicCPU < CPU
end

class TurboCPU < CPU
end

class Motherboard
  attr_accessor :cpu
  attr_accessor :memory_size

  def initialize(cpu=BasicCPU.new, memory_size=10000)
    @cpu = cpu
    @memroy_size = memory_size
  end
end

class Drive
  attr_reader :type # :hard_disk, :cd, :dvd
  attr_reader :size # 単位は MB
  attr_reader :writable

  def initialize(type, size, writable)
    @type = type
    @size = size
    @writable = writable
  end
end

class ComputerBuilder
  attr_reader :computer

  def initialize
    @computer = Computer.new
  end

  def turbo(has_turbo_cpu=true)
    @computer.motherboard.cpu = TurboCPU.new
  end

  def display=(display)
    @computer.display = display
  end

  def memory_size=(size_in_mb)
    @computer.motherboard.memory_size = size_in_mb
  end

  def add_cd(writer=false)
    @computer.drives << Drive.new(:cd, 760, writer)
  end

  def add_dvd(writer=false)
    @computer.drives << Drive.new(:cd, 760, writer)
  end

  def add_hard_disk(writer=false)
    @computer.drives << Drive.new(:dvd, 4700, writer)
  end
end


builder = ComputerBuilder.new
builder.turbo
builder.add_cd(true)
builder.add_dvd
builder.add_hard_disk(true)

p builder.computer

出力結果

$ ruby test.rb
#<Computer:0x00007fd3da8b3c78 @motherboard=#<Motherboard:0x00007fd3da8b3c50 @cpu=#<TurboCPU:0x00007fd3da8b3bd8>, @memroy_size=10000>, @drives=[#<Drive:0x00007fd3da8b3bb0 @type=:cd, @size=760, @writable=true>, #<Drive:0x00007fd3da8b3b88 @type=:cd, @size=760, @writable=false>, #<Drive:0x00007fd3da8b3b60 @type=:dvd, @size=4700, @writable=true>], @display=:crt>

まとめ

Builderパターンの考え方は、オブジェクトを作り出すのが難しい場合や、オブジェクトを構成するのに大量のコードを書かなければいけない場合に、それらの生成のためのコードを別のクラス、すなわちビルダに分離するというものです。

第13章 正しいクラスを選び出す: Factory part2

2019-06-30

「Rubyによるデザインパターン」の読書メモです。

Abstract Factory

Factory Method パターンがTemplate Method パターンをオブジェクトの作成に適用したのと同じように、Abstract Factory パターンは同じ問題にStrategyパターンを適用したのです。

アブストラクトファクトリで大事なことは、どの製品の型が一緒になるのかという知識をカプセル化することなのです。

class Duck
  def initialize(name)
    @name = name
  end

  def eat
    puts "アヒル #{@name} は食事中です。"
  end

  def speak
    puts "アヒル #{@name} はガーガー鳴いています。"
  end

  def sleep
    puts "アヒル #{@name} は静かに眠っています。"
  end
end

class Frog
  def initialize(name)
    @name = name
  end

  def eat
    puts "カエル #{@name} は食事中です。"
  end

  def speak
    puts "カエル #{@name} はゲロゲロっと鳴いています。"
  end

  def sleep
    puts "カエル #{@name} は眠りません。一晩中ゲロゲロ鳴いています。"
  end
end

class Algae
  def initialize(name)
    @name = name
  end

  def grow
    p "藻 #{@name} は日光を浴びて育ちます。"
  end
end

class WaterLily
  def initialize(name)
    @name = name
  end

  def grow
    p "睡蓮 #{@name} は浮きながら日光を浴びて育ちます。"
  end
end

class Tree
  def initialize(name)
    @name = name
  end

  def grow
    p "樹木 #{@name} が高く育っています。"
  end
end

class Tiger
  def initialize(name)
    @name = name
  end

  def eat
    puts "トラ #{@name} は食べたいものを何でも食べます。"
  end

  def speak
    puts "トラ #{@name} はガオーとほえています。"
  end

  def sleep
    puts "トラ #{@name} は眠くなったら眠ります。"
  end
end

class PondOrganismFactory
  def new_animal(name)
    Frog.new(name)
  end

  def new_plant(name)
    Algae.new(name)
  end
end

class JungleOrganismFactory
  def new_animal(name)
    Tiger.new(name)
  end

  def new_plant(name)
    Tree.new(name)
  end
end

class Habitat
  def initialize(number_animals, number_plants, organism_factory)
    @organism_factory = organism_factory

    @animals = []
    number_animals.times do |i|
      animal = @organism_factory.new_animal("動物#{i}")
      @animals << animal
    end

    @plants = []
    number_plants.times do |i|
      plant = @organism_factory.new_plant("植物#{i}")
      @plants << plant
    end
  end

  def simulate_one_day
    @plants.each {|plant| plant.grow}
    @animals.each {|animal| animal.speak}
    @animals.each {|animal| animal.eat}
    @animals.each {|animal| animal.sleep}
  end
end

jungle = Habitat.new(1, 4, JungleOrganismFactory.new)
jungle.simulate_one_day

pond = Habitat.new(2, 4, PondOrganismFactory.new)
pond.simulate_one_day

Factory の一段上のレイヤーで、組み合わせを担っています。

まとめ

「どのクラスを選ぶのか?」という問いに答えるための技術です。

pandas の DataFrame に変換する

2019-06-30

やったこと

pandas で 配列データ、辞書データを DataFrame に変換してみます。

確認環境

$ ipython --version
6.1.0
$ jupyter --version
4.3.0
$ python --version
Python 3.6.2 :: Anaconda custom (64-bit)
import pandas as pd
print(pd.__version__)
0.20.3

調査

from sklearn import datasets
iris = datasets.load_iris()
train = iris.data

print(type(train))

df = pd.DataFrame(train)
print(type(df))

data : ndarray (structured or homogeneous), Iterable, dict, or DataFrame Dict can contain Series, arrays, constants, or list-like objects

引数に渡すデータは ndarray だけでなくても良いようです。

リストを渡す

import pandas as pd
train = [2, 3]
print(type(train))

df = pd.DataFrame(train)
print(type(df))
<class 'list'>
<class 'pandas.core.frame.DataFrame'>

辞書を渡す

import pandas as pd
train = {'col1': [1, 2], 'col2': [3, 4]}
print(type(train))

df = pd.DataFrame(train)
print(type(df))
<class 'dict'>
<class 'pandas.core.frame.DataFrame'>

参考

pandas で任意の列を抽出する

2019-06-29

やったこと

pandas で任意の列を抽出してみます。

確認環境

$ ipython --version
6.1.0
$ jupyter --version
4.3.0
$ python --version
Python 3.6.2 :: Anaconda custom (64-bit)
import pandas
print(pd.__version__)
0.20.3

調査

アイリスデータの1 ~ 2列目を取得します

import pandas
import pandas as pd
from sklearn import datasets
iris = datasets.load_iris()
train = iris.data

df = pd.DataFrame(train)
df[[0, 1]]

また、下記でも同じものが取得できます。

df.loc[:, [0, 1]]

参考

NumPy の astype を使ってみる

2019-06-29

やったこと

NumPy で astype を使いデータをキャストしてみます。

確認環境

$ ipython --version
6.1.0
$ jupyter --version
4.3.0
$ python --version
Python 3.6.2 :: Anaconda custom (64-bit)
import numpy as np
np.__version__
'1.13.1'

調査

float -> int へ変換してみたいと思います。

x = np.array([1, 2, 2.5])
print(x)
print(x.dtype)
[ 1.   2.   2.5]
float64
x = x.astype(int)
print(x)
print(x.dtype)
[1 2 2]
int64

参考

第13章 正しいクラスを選び出す: Factory part1

2019-06-29

「Rubyによるデザインパターン」の読書メモです。

パラメータ化された Factory Method パターン

class Duck
  def initialize(name)
    @name = name
  end

  def eat
    puts "アヒル #{@name} は食事中です。"
  end

  def speak
    puts "アヒル #{@name} はガーガー鳴いています。"
  end

  def sleep
    puts "アヒル #{@name} は静かに眠っています。"
  end
end

class Frog
  def initialize(name)
    @name = name
  end

  def eat
    puts "カエル #{@name} は食事中です。"
  end

  def speak
    puts "カエル #{@name} はゲロゲロっと鳴いています。"
  end

  def sleep
    puts "カエル #{@name} は眠りません。一晩中ゲロゲロ鳴いています。"
  end
end

class Algae
  def initialize(name)
    @name = name
  end

  def grow
    p "藻 #{@name} は日光を浴びて育ちます。"
  end
end

class WaterLity
  def initialize(name)
    @name = name
  end

  def grow
    p "睡蓮 #{@name} は浮きながら日光を浴びて育ちます。"
  end
end

class Pond
  def initialize(number_animals, number_plants)
    @animals = []
    number_animals.times do |i|
      animal = new_organism(:animal, "動物#{i}")
      @animals << animal
    end

    @plants = []
    number_plants.times do |i|
      plant = new_organism(:plant, "植物#{i}")
      @plants << plant
    end
  end

  def simulate_one_day
    @plants.each {|plant| plant.grow}
    @animals.each {|animal| animal.speak}
    @animals.each {|animal| animal.eat}
    @animals.each {|animal| animal.sleep}
  end
end

class DuckWaterLilyPond < Pond
  def new_organism(type, name)
    if type == :animal
      Duck.new(name)
    elsif type == :plant
      WaterLily.new(name)
    else
      raise "Unknown"
    end
  end
end

class FrogAlgaePond < Pond
  def new_organism(type, name)
    if type == :animal
      Frog.new(name)
    elsif type == :plant
      Algae.new(name)
    else
      raise "Unknown"
    end
  end
end

pond = FrogAlgaePond.new(3, 2)
pond.simulate_one_day

具象クラスも外から渡すパターン

class Duck
  def initialize(name)
    @name = name
  end

  def eat
    puts "アヒル #{@name} は食事中です。"
  end

  def speak
    puts "アヒル #{@name} はガーガー鳴いています。"
  end

  def sleep
    puts "アヒル #{@name} は静かに眠っています。"
  end
end

class Frog
  def initialize(name)
    @name = name
  end

  def eat
    puts "カエル #{@name} は食事中です。"
  end

  def speak
    puts "カエル #{@name} はゲロゲロっと鳴いています。"
  end

  def sleep
    puts "カエル #{@name} は眠りません。一晩中ゲロゲロ鳴いています。"
  end
end

class Algae
  def initialize(name)
    @name = name
  end

  def grow
    p "藻 #{@name} は日光を浴びて育ちます。"
  end
end

class WaterLily
  def initialize(name)
    @name = name
  end

  def grow
    p "睡蓮 #{@name} は浮きながら日光を浴びて育ちます。"
  end
end

class Pond
  def initialize(number_animals, animal_class,
                 number_plants, plant_class)
    @animal_class = animal_class
    @plant_class = plant_class

    @animals = []
    number_animals.times do |i|
      animal = new_organism(:animal, "動物#{i}")
      @animals << animal
    end

    @plants = []
    number_plants.times do |i|
      plant = new_organism(:plant, "植物#{i}")
      @plants << plant
    end
  end

  def simulate_one_day
    @plants.each {|plant| plant.grow}
    @animals.each {|animal| animal.speak}
    @animals.each {|animal| animal.eat}
    @animals.each {|animal| animal.sleep}
  end

  def new_organism(type, name)
    if type == :animal
      @animal_class.new(name)
    elsif type == :plant
      @plant_class.new(name)
    else
      raise "Unknown"
    end
  end
end


pond = Pond.new(3, Duck, 2, WaterLily)
pond.simulate_one_day

animals と plants の組み合わせを呼び出し元から決めることができます。

この組み合わせも構造化するのは次の part2 で実施してみます。

pandas で データ型を確認する

2019-06-29

やったこと

pandas のデータ構造である

  • Series
  • DataFrame

について調べたことを書きます。

確認環境

$ ipython --version
6.1.0
$ jupyter --version
4.3.0
$ python --version
Python 3.6.2 :: Anaconda custom (64-bit)
print(pd.__version__)
0.20.3

調査

DataFrame の各列の型を確認してみます。

data = {'state': ['Ohio', 'Nevada'],
       'year': [2000, 2001]}
df = DataFrame(data)
df.dtypes

出力結果

state    object
year      int64
dtype: object

参考

第12章 唯一を保証する: Singleton

2019-06-29

「Rubyによるデザインパターン」の読書メモです。

ただ1つのインスタンスしか持てないクラスで、その1つのインスタンスへのアクセスをグローバルに提供します。

シングルトンを使わないロガーの例

test.rb

class SimpleLogger
  attr_accessor :level

  ERROR = 1
  WARNING = 2
  INFO = 3

  def initialize
    @level = WARNING
  end

  def error(msg)
    p "#{self.class}##{__method__}"
    p msg
  end

  def warning(msg)
    p "#{self.class}##{__method__}" if @level >= WARNING
    p msg
  end

  def info(msg)
    p "#{self.class}##{__method__}" if @level >= INFO
    p msg
  end
end

logger = SimpleLogger.new
logger.level = SimpleLogger::INFO

logger.info('1つめ')
logger.info('2つめ')

logger2 = SimpleLogger.new

p '---'
p logger.object_id
p logger2.object_id

出力結果

$ ruby test.rb
"SimpleLogger#info"
"1つめ"
"SimpleLogger#info"
"2つめ"
"---"
70311664013160
70311664012640

シングルトンを適用したロガーの例

test.rb

class SimpleLogger
  attr_accessor :level

  @@instance = SimpleLogger.new

  ERROR = 1
  WARNING = 2
  INFO = 3

  def initialize
    @level = WARNING
  end

  def error(msg)
    p "#{self.class}##{__method__}"
    p msg
  end

  def warning(msg)
    p "#{self.class}##{__method__}" if @level >= WARNING
    p msg
  end

  def info(msg)
    p "#{self.class}##{__method__}" if @level >= INFO
    p msg
  end

  def self.instance
    return @@instance
  end

  private_class_method :new
end

logger = SimpleLogger.instance
logger.level = SimpleLogger::INFO

logger.info('1つめ')
logger.info('2つめ')

logger2 = SimpleLogger.instance

p '---'
p logger.object_id
p logger2.object_id

出力結果

$ ruby test.rb
"SimpleLogger#info"
"1つめ"
"SimpleLogger#info"
"2つめ"
"---"
70234052873820
70234052873820

シングルトンモジュール

Singleton モジュールを使って、実装することができます。

include Singleton

Ruby でシングルトンを使ってみる – ためすう

シングルトンとしてのクラス

class ClassBasedLogger
  ERROR = 1
  WARNING = 2
  INFO = 3

  def self.error(msg)
    p "#{self.class}##{__method__}"
    p msg
  end
end

シングルトンとしてのモジュール (自作)

module ClassBasedLogger
  ERROR = 1
  WARNING = 2
  INFO = 3

  def self.error(msg)
    p "#{self.class}##{__method__}"
    p msg
  end
end

モジュールは、インスタンス化するためにあるのではないと少しは示すことができます。

Need-to-Know原則に基づくシングルトン

Need-to-Know原則とは > 「必要な人にだけ情報を開示する」という情報セキュリティにおける基本的な考え方です。

require 'singleton'

class DatabaseConnectionManager
  include Singleton

  def get_connection
    p "#{self.class}##{__method__}"
  end
end

class PreferenceManager
  def initialize
    @reader = PrefReader.new
    @writer = PrefWriter.new

    @preferences = {
      :display_splash => false,
      :background_color => :blue
    }
  end

  def save_preferences
    @writer.write(DatabaseConnectionManager.instance, @preferences)
  end

  def get_preferences
    @preferences = @reader.read(DatabaseConnectionManager.instance)
  end
end

class PrefWriter
  def write(manager, preferences)
    p "#{self.class}##{__method__}"
    p preferences
    manager.get_connection
  end
end

class PrefReader
  def read(manager)
    p "#{self.class}##{__method__}"
    manager.get_connection
    p '読み取ったもの'
  end
end

PreferenceManager.new.get_preferences
PreferenceManager.new.save_preferences

シングルトンは実装詳細なので、シングルトンであると知っているクラスを少なくした方が良いようです。

第11章 オブジェクトを改良する: Decorator

2019-06-27

「Rubyによるデザインパターン」の読書メモです。

Decoratorパターンについて

既存のオブジェクトに対して簡単に機能を追加するためのパターンです。Decoratorパターンを使うとレイヤ状に機能を積み重ねていくことができ、それぞれの状況でまさに必要なだけの機能をもつオブジェクトを作ることができます。

今回、ファイルに下記の形式で書き出すことができるクラスを作ります。

  • プレーンテキスト
  • チェックサム付き
  • タイムスタンプ付き
  • 行番号付き

継承ベースのアプローチ

継承ベースのアプローチでは、あり得るすべての機能の組み合わせを前もって設計時に考えなければならない,ということです。

クラスの数が増えすぎるので、不採用とします。

Decoratorパターンで実装した例

class SimpleWriter
  def initialize(path)
    @file = File.open(path, 'w')
  end

  def write_line(line)
    @file.print(line)
    @file.print("\n")
  end

  def pos
    @file.pos
  end

  def rewind
    @file.rewind
  end

  def close
    @file.close
  end
end

class WriterDecorator
  def initialize(real_writer)
    @real_writer = real_writer
  end

  def write_line(line)
    @real_writer.write_line(line)
  end

  def pos
    @real_writer.pos
  end

  def rewind
    @real_writer.rewind
  end

  def close
    @real_writer.close
  end
end

class NumberingWriter < WriterDecorator
  def initialize(real_writer)
    super(real_writer)
    @line_number = 1
  end

  def write_line(line)
    @real_writer.write_line("#{@line_number}: #{line}")
    @line_number += 1
  end
end

class CheckSummingWriter < WriterDecorator
  attr_reader :check_sum

  def initialize(real_writer)
    @real_writer = real_writer
    @check_sum = 0
  end

  def write_line(line)
    line.each_byte {|byte| @check_sum = (@check_sum + byte) % 256}
     # check_sum の計算...
    @real_writer.write_line("#{line} (checksum: #{@check_sum})")
  end
end

class TimeStampingWriter < WriterDecorator
  def write_line(line)
    @real_writer.write_line("#{Time.new}: #{line}")
  end
end

writer = CheckSummingWriter.new(
  TimeStampingWriter.new(
    NumberingWriter.new(SimpleWriter.new('final.txt'))
  )
)
writer.write_line('Hello out there')

出力された final.txt

1: 2019-06-23 13:28:36 +0900: Hello out there (checksum: 164)

Forwardable を使った例

class SimpleWriter
  def initialize(path)
    @file = File.open(path, 'w')
  end

  def write_line(line)
    @file.print(line)
    @file.print("\n")
  end

  def pos
    @file.pos
  end

  def rewind
    @file.rewind
  end

  def close
    @file.close
  end
end

出力された final2.txt

2019-06-23 13:34:25 +0900: Hello out there

ラッピングする例

class SimpleWriter
  def initialize(path)
    @file = File.open(path, 'w')
  end

  def write_line(line)
    @file.print(line)
    @file.print("\n")
  end

  def pos
    @file.pos
  end

  def rewind
    @file.rewind
  end

  def close
    @file.close
  end
end

w = SimpleWriter.new('final3.txt')

class << w
  alias old_write_line write_line

  def write_line(line)
    old_write_line("#{Time.new}: #{line}")
  end
end

w.write_line('write_line')
w.old_write_line('old_write_line')

出力された final3.txt

2019-06-23 13:42:53 +0900: write_line
old_write_line

モジュールを使った例

class SimpleWriter
  def initialize(path)
    @file = File.open(path, 'w')
  end

  def write_line(line)
    @file.print(line)
    @file.print("\n")
  end

  def pos
    @file.pos
  end

  def rewind
    @file.rewind
  end

  def close
    @file.close
  end
end

module TimeStampingWriter
  def write_line(line)
    super("#{Time.new}: #{line}")
  end
end

w = SimpleWriter.new('final4.txt')
w.extend(TimeStampingWriter)
w.write_line('hello')

出力された final4.txt

2019-06-23 13:45:02 +0900: hello

第10章 オブジェクトに代理を立てる: Proxy part2

2019-06-27

「Rubyによるデザインパターン」の読書メモです。

Proxy で重複を取り除いたパターン

test.rb

class BankAccount
  attr_reader :balance

  def initialize(starting_balance=0)
    @balance = starting_balance
  end

  def deposit(amount)
    @balance += amount
  end

  def withdraw(amount)
    @balance -= amount
  end
end

class AccountProxy
  def initialize(real_account)
    @subject = real_account
  end

  def method_missing(name, *args)
    p "Delegating #{name} message to subject."
    @subject.send(name, *args)
  end
end

ap = AccountProxy.new(BankAccount.new(100))
ap.deposit(25)
ap.withdraw(50)
p ap.balance

出力結果

$ ruby test.rb
"Delegating deposit message to subject."
"Delegating withdraw message to subject."
"Delegating balance message to subject."
75

method_missing をオーバーライドして、委譲メソッドを別々に定義しなくても良くなりました。

Proxy パターンの注意

method_missingの使いすぎは、継承の使いすぎと同様、あなたのコードをわかりにくくしてしまう代表的な原因になるということです。

次に来る実装者には、実装により意図が伝わるようにしてください。