I am trying to port some code from Delphi to C# and I have found a construction which I cannot implement in a reasonable manner, while complying with .NET Framework design guidelines (which I address at the end of my question).
Obviously C#, Java, C++ (and many other languages) provide meanings of method/constructor overloading, but Delphi constructors can additionally have multiple names. This way it's possible to write code which directly represents the intent:
var
Data, ParI, ParD, Locl: TDataElement;
begin
Data := TDataElement.Create('Element');
ParI := TDataElement.CreateParam('IntElement', 22);
ParD := TDataElement.CreateParam('DblElement', 3.14);
Locl := TDataElement.CreateLocal('LocalElement');
// ... use the above objects ...
end;
Simplified code below:
unit DataManager;
interface
TDataElement = class
FName: string;
FPersistent: Boolean;
public
constructor Create(AName: string);
constructor CreateParam(AName: string; DefaultInt: Integer); overload;
constructor CreateParam(AName: string; DefaultDouble: Double); overload;
constructor CreateLocal(AName: string);
property Name: string read FName;;
property Persistent: Boolean read FPersistent;
end;
implementation
constructor TDataElement.Create(AName: string);
begin
FName := AName;
FPersistent := True;
// ... other initialization ...
end;
constructor TDataElement.CreateParam(AName: string; DefaultDouble: Double);
begin
Create(AName);
// ... use DefaultInt ...
end;
constructor TDataElement.CreateParam(AName: string; DefaultInt: Integer);
begin
Create(AName);
// ... use DefaultDouble...
end;
constructor TDataElement.CreateLocal(AName: string);
begin
Create(AName);
FPersistent := False;
// ... other code for local (non-persistent) elements ...
end;
Specifically in C# constructor must have the same name as the class, so first I have tried to differentiate behavior with enumeration. Alas, I've stumbled upon several problems:
First attempt below:
public enum ElementKind
{
Regular, IntParam, DoubleParam, Local
}
public class DataElement
{
private string FName;
public string Name { get { return FName; } }
private bool FPersistent;
public bool Persistent { get { return FPersistent; } }
public DataElement(ElementKind kind, string name)
{
FName = name;
// ugly switch :-(
switch (kind)
{
case ElementKind.Regular:
case ElementKind.IntParam:
case ElementKind.DoubleParam:
FPersistent = true;
break;
case ElementKind.Local:
FPersistent = false;
break;
}
// ... other initialization ...
}
public DataElement(ElementKind kind, string name, int defaultInt)
: this(kind, name)
{
// ... use defaultInt ...
}
public DataElement(ElementKind kind, string name, double defaultDouble)
: this(kind, name)
{
// ... use defaultDouble ...
}
// Redundant "bool local" parameter :-(
public DataElement(ElementKind kind, string name, bool local)
: this(kind, name)
{
// What to do when "local" is false ???
// ... other code for local (non-persistent) elements ...
}
}
public class Program
{
public void Run()
{
DataElement data = new DataElement(ElementKind.Regular, "Element");
DataElement parI = new DataElement(ElementKind.IntParam, "IntElement", 22);
DataElement parD = new DataElement(ElementKind.DoubleParam, "DblElement", 3.14);
DataElement locl = new DataElement(ElementKind.Local, "LocalElement");
}
}
Then I have tried more object-oriented way to differentiate constructors by types, while keeping the same initialization code in Run() method:
public class ElementKind
{
public class RegularElement
{
internal RegularElement() { /* disallow direct creation */ }
}
public class IntParamElement
{
internal IntParamElement() { /* disallow direct creation */ }
}
public class DoubleParamElement
{
internal DoubleParamElement() { /* disallow direct creation */ }
}
public class LocalElement
{
internal LocalElement() { /* disallow direct creation */ }
}
public static readonly ElementKind.RegularElement Regular = new RegularElement();
public static readonly ElementKind.IntParamElement IntParam = new IntParamElement();
public static readonly ElementKind.DoubleParamElement DoubleParam = new DoubleParamElement();
public static readonly ElementKind.LocalElement Local = new LocalElement();
}
public class DataElement
{
private string FName;
public string Name { get { return FName; } }
private bool FPersistent;
public bool Persistent { get { return FPersistent; } }
protected DataElement(string name)
{
FName = name;
// ... other initialization ...
}
public DataElement(ElementKind.RegularElement kind, string name)
: this(name)
{
FPersistent = true;
}
public DataElement(ElementKind.IntParamElement kind, string name, int defaultInt)
: this(name)
{
FPersistent = true;
// ... use defaultInt ...
}
public DataElement(ElementKind.DoubleParamElement kind, string name, double defaultDouble)
: this(name)
{
FPersistent = true;
// ... use defaultDouble ...
}
public DataElement(ElementKind.LocalElement kind, string name)
: this(name)
{
FPersistent = false;
// ... other code for "local" elements ...
}
}
public class Program
{
public void Run()
{
DataElement data = new DataElement(ElementKind.Regular, "Element");
DataElement parI = new DataElement(ElementKind.IntParam, "IntElement", 22);
DataElement parD = new DataElement(ElementKind.DoubleParam, "DblElement", 3.14);
DataElement locl = new DataElement(ElementKind.Local, "LocalElement");
}
}
Everything compiles and works very well. So what is my problem here? .NET Framework design guidelines, and a tool named Microsoft FxCop. After running the last code through this tool I got multiple breaking problems (see below).
And the question is: how to design my classes to comply with .NET design guidelines and best practices?
Breaking - Certainty 90% - Nested types should not be visible - ElementKind+RegularElement Breaking - Certainty 90% - Nested types should not be visible - ElementKind+IntParamElement Breaking - Certainty 90% - Nested types should not be visible - ElementKind+DoubleParamElement Breaking - Certainty 90% - Nested types should not be visible - ElementKind+LocalElement
Breaking - Certainty 90% - Static holder types should not have constructors - ElementKind
Breaking - Certainty 75% - Identifiers should not contain type names - DataElement.#.ctor(ElementKind+IntParamElement,System.String,System.Int32)
Breaking - Certainty 75% - Identifiers should not contain type names - DataElement.#.ctor(ElementKind+DoubleParamElement,System.String,System.Double)
Non Breaking - Certainty 25% - Do not declare read only mutable reference types - ElementKind.#Regular
Non Breaking - Certainty 25% - Do not declare read only mutable reference types - ElementKind.#IntParam
Non Breaking - Certainty 25% - Do not declare read only mutable reference types - ElementKind.#DoubleParam
Non Breaking - Certainty 25% - Do not declare read only mutable reference types - ElementKind.#Local
For starters, I would replace your nested "ElementKind" class
with an enum
:
public enum ElementKind
{
RegularElement,
IntParamElement,
DoubleParamElement,
LocalElement
}
In addition, I don't think your Delphi code needs to be mapped to a constructor. You would probably be better off using static factory methods that return a DataElement
. For example:
public static DataElement Create(string name)
{
return new DataElement(ElementKind.Regular, name);
}
public static DataElement CreateParam(string name, int defaultInt);
{
return new DataElement(ElementKind.IntParam, name);
// ... use defaultInt ...
}
// similar to above
public static DataElement CreateParam(string name, double defaultDouble);
public static DataElement CreateLocal(string name);
Since you're using the factory functions to create your DataElement objects, you should make the constructors private.
You would then update your Run() function to use these as follows:
public void Run()
{
DataElement data = DataElement.Create("Element");
DataElement parI = DataElement.CreateParam("IntElement", 22);
DataElement parD = DataElement.CreateParam("DblElement", 3.14);
DataElement locl = DataElement.CreateLocal("LocalElement");
}
Update: I included the suggested changes to your Run()
function and corrected the basic Create()
factory method (I believe it's supposed to return a "Regular" DataElement
).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With