* 모든 클래스의 부모 클래스 구현
- 기본 설계
모든 클래스의 부모 클래스로 ClassBase라는 클래스가 존재한다. 내부에 public 함수로 여러 캐스팅 함수와 리플렉션 데이터 접근 함수를 지원하고 있다. 이 클래스는 기본적으로 인스턴스화 할 수 없다.
메소드를 제외한 기본 구조는 다음과 같다.
local ClassBase = {
public = {
},
private = {
reflectionData = {type = "ClassBase"},
},
IsClassBase = true
}
...
...
- 리플렉션 데이터 구현
ClassBase에는 private 멤버로 reflectionData를 가지고 있다. 이 데이터는 상속받은 클래스에서 SetType함수 등을 통해서 적절하게 초기화하여 사용한다. 예를 들어 SuperClass에서는 type = "SuperClass" 로 초기화하고 있다.
이를 통해서 실제로 가리키고 있는 객체 타입을 정확하게 판별할 수 있다. 현재는 타입만 다루고 있지만 추후 더 정보를 넣을 예정이다.
function ClassBase.public:SetType(inputString)
local typeString = type(inputString)
if typeString ~= "string" then
return
end
local this = CastToRawClassBase(self)
-- 이 메소드에 들어왔기 때문에 무조건 캐스팅이 보장된다.
--if not this then return end
this.private.reflectionData.type = inputString
end
function ClassBase.public:GetType()
local this = CastToRawClassBase(self)
return this.private.reflectionData.type
end
- 캐스팅 구현
이 클래스를 상속 받은 객체는 다른 클래스로 캐스팅을 시도해볼 수 있으며, 내가 해당 클래이인지, 클래스의 일종인지 확인할 수 있는 메소드를 제공한다. 제공 방식은 두 가지로 TypeString을 통하는 방법과 클래스 기본 객체인 CDO를 통하는 방법이 있다. ClassBase이상으로 올라가서 탐색하지 못하게 하기 위해서 ClassBase내부에 있는 IsClassBase 속성을 확인하여 적절하게 브레이크를 걸어준다. (ClassBase로 캐스팅하면 그 위 클래스가 없기 때문에 의미가 없다.)
또한 현재 위치하고 있는 클래스 타입을 판별하기 위해서(캐스팅할 때 어느 클래스로 캐스팅할 지 판별할 때 사용된다.) 각 클래스마다 GetClassTypeForInstance를 구현하고 있다. 오버라이딩되어 있는 것은 아니고 해당 클래스에 위치하면 해당 메소드가 호출되는 형태이다.(자식 클레스의 메소드에 의해 부모 클래스의 메소드가 가려지는 효과와 같다.) 이를 통해 현재 클래스를 확인하여 캐스팅 타입과 맞는지 판별할 수 있다.(실제 객체의 타입은 아니며 실제 타입은 리플렉션 데이터를 반환하는 GetType 메소드가 ClassBase에서 제공된다.)
function IsEmpty(target)
local rv = rawget(target, "IsClassBase")
if rv then
return true
end
return false
end
function ClassBase.public:CastByType(toClassType)
local classType = self.GetClassTypeForInstance()
if toClassType == classType then
return self
end
local fromClassBody = getmetatable(self)
if not fromClassBody or IsEmpty(fromClassBody) then return nil end
return fromClassBody:CastByType(toClassType)
end
function ClassBase.public:CastByCDO(toClassBody)
if not toClassBody or IsEmpty(toClassBody) then
return nil
end
return self:CastByType(toClassBody:GetClassTypeForCDO())
end
function ClassBase.public:IsAByType(toClassType)
-- CastByType 를 활용한다.
end
function ClassBase.public:IsAByCDO(toClassBody)
-- CastByCDO 를 활용한다.
end
function ClassBase.public:IsItselfByType(toClassType)
-- Cast와 로직이 비슷하다. 자기 자신의 클래스인지 확인한다.
end
function ClassBase.public:IsItselfByCDO(toClassBody)
-- Cast와 로직이 비슷하다. 자기 자신의 클래스인지 확인한다.
end
* 클래스 정의와 인스턴스
- 클래스 기본 객체(CDO)와 인스턴스 구현
클래스에 존재하는 멤버들과 이를 기본 값으로 가지고 있는 클래스 기본 객체(CDO)를 만들어서 사용하고 있다. 이를 기반으로 인스턴스를 생성한다. 기본적으로 CDO에서 Clone이라는 함수를 통해 인스턴스를 생성하는 방법을 제공한다.
local SuperClassCDO = {
public = {
value1 = 1,
GetClassTypeForInstance = function() return "SuperClass" end
},
private = {
value2 = 2}
}
...
...
function SuperClassCDO.Clone()
local baseCopy = SuperClassCDO.DeepCopy(SuperClassCDO)
return setmetatable({}, baseCopy)
end
...
...
위의 코드를 보면 빈 테이블 형식을 생성하고 그 테이블의 메타테이블을 CDO의 깊은 복사본으로 설정하여 반환하는 것을 알 수 있다. 테이블은 기본적으로 참조 기반으로 동작하기 때문에 깊은 복사를 수행해줘야 하고 public, private에 대한 접근 제한을 __index, __newindex를 통해서 컨트롤하기 위해서 빈테이블로 만들고 메타테이블을 연결하는 식으로 인스턴스를 구현하였다.
깊은 복사 코드는 다음과 같다. DeepCopy는 ClassBase의 함수로 DeepCopyDetail을 호출하고 있다.
function DeepCopyDetail(original)
local originalType = type(original)
local copy = {}
if originalType == 'table' then
for key, value in next, original, nil do
copy[DeepCopyDetail(key)] = DeepCopyDetail(value)
end
setmetatable(copy, DeepCopyDetail(getmetatable(original)))
else -- 테이블이 아닌 값들
copy = original
end
return copy
end
- 접근 제어 구현
접근 제어는 일반적으로 사용하는 public과 private을 구현하였다. 이는 메타테이블의 메타메소드인 __index와 __newindex를 통해서 구현된다. __index와 __newindex는 접근 제어 뿐 아니라 상속을 포함하여 부모 클래스의 함수를 가리는 효과나 오버라이딩 효과 등 여러 곳에서 활용된다.
모든 클래스의 __index, __newindex 는 다음과 같이 구현하였다.
function AccessProtection__index(table, key)
--print("AccessProtection__index => " .. tostring(key))
--print(table)
local classBody = getmetatable(table)
-- 있으면 반환한다.
local value = rawget(classBody.public, key)
if value then
return value
end
-- 부모 클래스를 확인한다.
return classBody[key]
end
function AccessProtection__newindex(table, key, value)
--print("AccessProtection__newindex => " .. tostring(key))
--print(table)
local alreadyExistKey = table[key] -- 부모 클래스에서 찾아보는 과정
if alreadyExistKey then
alreadyExistKey = value
else -- 찾아보고 없으면 그냥 테이블에 할당한다.
rawset(table, key, value)
end
end
하지만 이 방식에서 한 가지 문제점이 있는데 본인 클래스에서 자기 자신 멤버(private)에 접근할 수 없다는 점이다. 보통 모든 언어에서 본인 클래스안에서는 모든 멤버에 접근이 가능하다. 이를 해결하기 위해 인스턴스를 캐스팅하여 반환하는 함수인 GetThisByCDO를 제공한다.
인스턴스는 비어있는 테이블이고 메타테이블에 접근하기 때문에 public만 접근할 수 있지만, 이 함수가 반환하는 객체는 메타테이블이 아닌 클래스 그 자체 테이블을 반환한다. 메타테이블이 아니기 때문에 접근 제한이 걸리지 않는다. 다만 private, public 키워드를 붙여서 사용해야한다는 단점이 있다.
function ClassBase.public:GetThisByCDO(toClassBody)
local thisInstance = self:CastByCDO(toClassBody)
if not thisInstance then
-- 발생하면 원칙을 잘 지키지 않고 코드를 작성한 경우다.
return nil
end
-- 이렇게 하면 해당 클래스에서 접근 가능하다. 대신 public, private 과 같은 이름을 앞에 붙여줘야 한다.
local this = getmetatable(thisInstance)
return this
end
사용 예시는 다음과 같다.
function SubClassCDO.public:PrintDerived()
local this = self:GetThisByCDO(SubClassCDO)
print("SubClassCDO.public:PrintDerived() => " .. tostring(this.public.value3) .. tostring(this.private.value4))
this.private.PrintPrivateDerived(self)
this:PrintBase(self)
end
* 전체 기능 테스트
local SubClassCDO = require(script.SubClass)
local SuperClassCDO = require(script.SuperClass)
local subClassObject = SubClassCDO.Clone()
subClassObject.PrintDerived(subClassObject)
-- SuperClass에 있는 변수
print("value1 is in SuperClass public => " .. tostring(subClassObject.value1))
print("value2 is in SuperClass private => " .. tostring(subClassObject.value2))
-- SubClass에 있는 변수
print("value3 is in SuperClass public => " .. tostring(subClassObject.value3))
print("value4 is in SuperClass private => " .. tostring(subClassObject.value4))
local temp1 = subClassObject:CastByCDO(SuperClassCDO)
print("CastByCDO => SuperClassCDO => " .. temp1.GetClassTypeForInstance())
print("RealType(ref from reflection data) =>" .. temp1:GetType())
local temp2 = subClassObject:CastByType(SuperClassCDO:GetClassTypeForCDO())
print("CastByType => SuperClass Type => " .. temp2.GetClassTypeForInstance())
print("RealType(ref from reflection data) => " .. temp2:GetType())
local temp3 = subClassObject:CastByType("ClassBase")
print("CastByType => ClassBase Type => " .. temp3.GetClassTypeForInstance())
print("RealType(ref from reflection data) => " .. temp3:GetType())
-- SuperClass에 있는 함수
subClassObject.PrintBase(subClassObject)
print("GetPrivateValue2 => ".. tostring(subClassObject:GetPrivateValue2()))
SubClassCDO.public:PrintDerived() => 34
SubClassCDO.private:PrintPrivateDerived() =>
SuperClassCDO.public:PrintBase() => 12
value1 is in SuperClass public => 1
value2 is in SuperClass private => nil
value3 is in SubClass public => 3
value4 is in SubClass private => nil
CastByCDO => SuperClassCDO => SuperClass
RealType(ref from reflection data) =>SubClass
CastByType => SuperClass Type => SuperClass
RealType(ref from reflection data) => SubClass
CastByType => ClassBase Type => ClassBase
RealType(ref from reflection data) => SubClass
SuperClassCDO.public:PrintBase() => 12
GetPrivateValue2 => 2
'Lua in Roblox' 카테고리의 다른 글
[Lua / Roblox] 미니 배틀 로얄 게임 #6 : 구조 정리, 모듈 추가 (0) | 2022.07.15 |
---|---|
[Lua / Roblox] 미니 배틀 로얄 게임 #5 : 게임 데이터 매니저 (0) | 2022.07.14 |
[Lua] __index (0) | 2022.07.12 |
[Lua] __newindex (0) | 2022.07.12 |
[Lua] metatable (0) | 2022.07.12 |