What is the better way to reuse implementation: inheritance or generics?
The model is following: Script has Steps, Steps have Elements. Tree structure is double linked, i.e. Steps know their Script and Elements now their Step.
Now, there are 2 types of Scripts: Templates and Runs, where a Run is created at first as a copy of the Template. This results in 2 similar hierarchies ScriptTemplate->ScriptTemplateStep->ScriptTemplateElement and ScriptRun->ScriptRunStep->ScriptRunElement. Most of the functionality is common, but various classes may have some additional properties.
To reuse functionality I could develop abstract Script class which would be derived by ScriptRun and ScriptTemplate like:
abstract class Script { IList<Step> Steps; }
class ScriptRun : Script {}
class ScriptTemplate : Script {}
class Step { Script Script; IList<Element> Elements; }
class ScriptRunStep : Step {}
class ScriptTemplateStep : Step {}
or I could try generics:
abstract class Script<TScript, TStep, TElement>
where TScript:Script<TScript, TStep, TElement>
where TStep:Step<TScript, TStep, TElement>
where TElement:Element<TScript, TStep, TElement>
{ IList<TStep> Steps; }
abstract class Step<TScript, TStep, TElement>
where TScript:Script<TScript, TStep, TElement>
where TStep:Step<TScript, TStep, TElement>
where TElement:Element<TScript, TStep, TElement>
{ TScript Script; IList<TElement> Elements; }
class ScriptRun : Script<ScriptRun, ScriptRunStep, ScriptRunElement> {}
class ScriptRunStep : Step<ScriptRun, ScriptRunStep, ScriptRunElement> {}
class ScriptRunElement : Element<ScriptRun, ScriptRunStep, ScriptRunElement> {}
class ScriptTemplate : Script<ScriptTemplate, ScriptTemplateStep, ScriptTemplateElement> {}
class ScriptTemplateStep : Step<ScriptTemplate, ScriptTemplateStep, ScriptTemplateElement> {}
class ScriptTemplateElement : Element<ScriptTemplate, ScriptTemplateStep, ScriptTemplateElement> {}
The cons of generics approach:
- Seems a bit complicated at first. Especially wheres are awful.
- Seems not familiar at first.
- Brings a bit of fun when DataContractSerializing it.
- Assembly is larger.
The pros:
- Type security: you won't be able to add a ScriptTemplateElement to a ScriptRunStep.
- Doesn't require casting to a concrete type from collection items. Also - better intellisense support. ScriptTemplate.Steps are instantly of ScriptTemplateStep, not abstract Step.
- Abides by Liskov principle: in inheritance scenario, you have IList collection on ScriptRun, but you really shouldn't add ScriptTemplateStep to it, altough it is clearly a Step.
- You don't have to do overrides. E.g. suppose you want to have a NewStep method on the Script. In the former scenario you say
:
abstract class Script { abstract Step NewStep(); }
abstract class ScriptRun {
override Step NewStep(){
var step = new ScriptRunStep();
this.Steps.Add(step);
return step;
}
}
abstract class ScriptTemplate {
override Step NewStep(){
var step = new ScriptTemplateStep();
this.Steps.Add(step);
return step;
}
}
In the generics scenario you write:
abstract class Script<TScript, TStep, TElement>
where TScript:Script<TScript, TStep, TElement>
where TStep:Step<TScript, TStep, TElement>, new()
where TElement:Element<TScript, TStep, TElement>
{
TStep NewStep() {
var step = new TStep();
this.Steps.Add(step);
return step;
}
}
and ScriptRun and ScriptTemplate automatically have that method, or an even better one: with a return type of respectively ScriptRunStep and ScriptTemplateStep instead of simply a Step.