コンストラクタの拡張
CoddyのLuaジャーニー「Object Oriented Programming」セクションの一部 — レッスン 33/70。
子クラスが親クラスから継承する場合、親クラスが行うすべての初期化に加えて、子クラス固有の追加フィールドも初期化したいことがよくあります。コードを重複させる代わりに、親クラスのコンストラクタを直接呼び出すことができます。
この手法では、親クラス名を使用してその :new() メソッドを呼び出し、最初の引数として親クラスを渡します。これはドット構文:Vehicle.new(Vehicle, speed)、または同等のコロン構文:Vehicle:new(speed) のいずれかで行うことができ、どちらも有効で同じ結果が得られます。これにより、すべての親の初期化が行われたベースオブジェクトが作成され、その後、子クラス用にカスタマイズします。
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)(または同等のVehicle:new(speed))を呼び出します。次に、子クラス固有のbrandフィールドを追加します。最後に、インスタンスが最初に子クラスのメソッドを検索するように、メタテーブルをVehicleからCarに変更します。
local myCar = Car:new(120, "Toyota")
print(myCar.speed) -- 出力: 120
print(myCar.brand) -- 出力: ToyotaこのパターンはコードをDRYに保ちます。共有の初期化ロジックは親に記述され、各子は自身に固有の属性のみを処理します。
チャレンジ
簡単子クラスが親クラスのコンストラクタを拡張する、クリーチャーの階層構造を構築しましょう!Animalという親クラスとBirdという子クラスを作成します。Birdのコンストラクタは、独自の属性を追加する前に、Animalのコンストラクタを呼び出して共通の初期化処理を行います。
コードは以下の3つのファイルに分けて構成します:
Animal.lua: 動物の名前を保存する:new(name)コンストラクタを持つ親クラスを定義します。動物の名前を表示する:describe()メソッドを含めてください。__indexを使用した標準的なクラスパターンを設定し、クラスを返します。Bird.lua: Animalを継承する子クラスを定義します。Birdの:new(name, wingspan)コンストラクタは、Animal.new(Animal, name)を使用して親のコンストラクタを呼び出し名前を初期化した後、オブジェクトにwingspanフィールドを追加し、最後にメタテーブルをBirdに再リンクする必要があります。名前と翼長(wingspan)を特定の形式で表示する:info()メソッドを含めてください。main.lua: Birdモジュールを読み込み、2つの入力(名前と翼長)を読み取り、Birdのインスタンスを作成します。そして、継承された機能と子クラス固有の機能が共に動作することを示すために、両方のメソッドを呼び出します。
以下の2つの入力を受け取ります:
- 鳥の名前
- 鳥の翼長(数値)
メインファイルで、与えられた名前と翼長を持つ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(重複を避ける)に保つことができます!
チートシート
子クラスから親クラスのコンストラクタを呼び出すには、親クラス名をドット構文で使用し、最初の引数として親クラス自体を渡します:
local obj = ParentClass.new(ParentClass, parentArgs)親コンストラクタを呼び出した後、子クラス固有のフィールドを追加し、メタテーブルを子クラスに再リンクします:
local Vehicle = {}
Vehicle.__index = Vehicle
function Vehicle:new(speed)
local obj = {speed = speed}
setmetatable(obj, Vehicle)
return obj
end
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
local myCar = Car:new(120, "Toyota")
print(myCar.speed) -- 出力: 120
print(myCar.brand) -- 出力: Toyotaこのパターンは初期化ロジックをDRY(Don't Repeat Yourself)に保ちます。共有のセットアップは親クラスに配置され、子クラスは独自の属性のみを処理します。
自分で試してみよう
-- main.lua: エントリポイント
local Bird = require('Bird')
-- 入力を読み込む
local name = io.read()
local wingspan = tonumber(io.read())
-- TODO: 指定された名前と翼長でBirdインスタンスを作成する
-- TODO: 鳥に対して :describe() を呼び出す (Animalから継承)
-- TODO: 鳥に対して :info() を呼び出す (Birdで定義)
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。