コンストラクタの拡張
CoddyのLuaジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 33/70。
child クラスが parent から継承するとき、通常は child の constructor で parent が行うすべての初期化に加えて、child 固有の追加の field も初期化したいと考えるでしょう。コードを重複させる代わりに、parent の constructor を直接呼び出すことができます。
このテクニックでは、parent クラス名を使用してその :new() メソッドを呼び出し、最初の引数として self を渡します。これにより、parent の初期化をすべて含む基本オブジェクトが作成され、それを child 用にカスタマイズします。
local Vehicle = {}
Vehicle.__index = Vehicle
function Vehicle:new(speed)
local obj = {speed = speed}
setmetatable(obj, Vehicle)
return obj
end
-- Car は Vehicle を継承する
local Car = {}
Car.__index = Car
setmetatable(Car, {__index = Vehicle})
function Car:new(speed, brand)
local obj = Vehicle.new(Vehicle, speed) -- 親コンストラクタを呼び出す
obj.brand = brand -- 子固有のフィールドを追加する
setmetatable(obj, Car) -- Car に再リンクする
return obj
end子コンストラクターの重要な手順に注目してください。まず、speed フィールドがすでに設定されたオブジェクトを取得するために、Vehicle.new(Vehicle, speed) を呼び出します。次に、子固有の brand フィールドを追加します。最後に、インスタンスが最初に子クラスでメソッドを検索するように、メタテーブルを Vehicle から Car に変更します。
親のコンストラクターを呼び出すときには、コロン構文も使用できます。Vehicle:new(speed) は、どちらも最初の引数として Vehicle を渡すため、Vehicle.new(Vehicle, speed) と同等です。ここではどちらの形式も正しく機能するので、読みやすいと思う方を自由に使ってください。
local myCar = Car:new(120, "Toyota")
print(myCar.speed) -- 出力: 120
print(myCar.brand) -- 出力: Toyotaこのパターンにより、コードを DRY に保てます。共有される初期化ロジックは parent に存在し、一方、各 child は固有の属性だけを処理します。
チャレンジ
簡単親クラスのコンストラクターを拡張する子クラスを使って、生き物の階層構造を作りましょう!Animal親クラスとBird子クラスを作成します。BirdのコンストラクターはAnimalのコンストラクターを呼び出して共通の初期化を処理してから、独自の属性を追加します。
コードは3つのファイルに分けて整理します。
Animal.lua:動物の名前を保存する:new(name)コンストラクターを持つ親クラスを定義します。動物の名前を出力する:describe()メソッドも含めます。__indexを使った標準的なクラスパターンを設定し、クラスを返します。Bird.lua:Animalを継承する子クラスを定義します。Birdの:new(name, wingspan)コンストラクターでは、Animal.new(Animal, name)を使って親コンストラクターを呼び出し、名前を初期化します。次にwingspanフィールドをオブジェクトに追加し、最後にメタテーブルをBirdに再リンクします。名前と翼幅を指定された形式で出力する:info()メソッドも含めます。main.lua:Birdモジュールをrequireし、2つの入力(名前と翼幅)を読み取り、Birdのインスタンスを作成して、継承した機能と子クラス固有の機能が連携して動作することを示すために両方のメソッドを呼び出します。
2つの入力を受け取ります。
- 鳥の名前
- 鳥の翼幅(数値)
mainファイルで、指定された名前と翼幅を使ってBirdを作成します。その後、別々の行で:describe()、続けて:info()を呼び出します。
:describe()メソッド(Animalから継承)は、次のように出力します。
Animal: {name}:info()メソッド(Birdで定義)は、次のように出力します。
{name} has a wingspan of {wingspan}たとえば、入力がEagleと200の場合、出力は次のようになります。
Animal: Eagle
Eagle has a wingspan of 200ここで重要なのは、Birdのコンストラクターが名前の初期化を重複して行わないことです。その処理をAnimalのコンストラクターに委ね、その後、鳥に固有のものだけを追加します。これにより、適切な継承チェーンを構築しながら、コードをDRYに保てます!
自分で試してみよう
-- main.lua: エントリーポイント
local Bird = require('Bird')
-- 入力を読み取る
local name = io.read()
local wingspan = tonumber(io.read())
-- TODO: 指定された name と wingspan で Bird インスタンスを作成する
-- TODO: bird で :describe() を呼び出す(Animal から継承)
-- TODO: bird で :info() を呼び出す(Bird で定義)
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
自分で練習してみよう: Luaオンラインコンパイラ