0

I'm coding an API to import data into our software.

As you can see below my models hierarchy is broken down into different types of models (A & B).

Each model :

  • embed another model : "ImportModel" embed "Production" which embed "Parts"

     Ex : ImportModelA => ProductionModelA => List of PartModelA
    
  • has specific properties related to itself and some common properties

public class ImportModelA : ImportModelBase
{
    public string ImportModelAProp { get; set; }
}

public class ImportModelB : ImportModelBase
{
    public string ImportModelBProp { get; set; }
}

public abstract class ImportModelBase
{
    public ProductionModelBase Production { get; set; }
}

public class ProductionModelA : ProductionModelBase
{
    public int ProductionModelAProp { get; set; }
}

public class ProductionModelB : ProductionModelBase
{
    public int ProductionModelBProp { get; set; }
}

public abstract class ProductionModelBase
{
    public List<PartModelBase> Parts { get; set; }
}

public class PartModelA : PartModelBase
{
    public int PartModelAProp { get; set; }
}

public class PartModelB : PartModelBase
{
    public int PartModelBProp { get; set; }
}

public abstract class PartModelBase
{
    public short CommonProp { get; set; }
}

Here's my problem :
In my modelA specific service method, I have no choice but to downcast the "Production" and "parts" models to get access to their specific properties :

public class ImportModelAManagement
{
    public void DoModelASpecificStuff(ImportModelA importModel)
    {
        // First downcast
        var production = (ProductionModelA)importModel.Production;

        // Uses productionModelA specific property
        Console.WriteLine(production.ProductionModelAProp);

        // Second downcast
        var parts = production.Parts.ConvertAll(p => (PartModelA)p)
            .ToList();

        // Uses partModelA specific property        
        var firstPartModelAProp = parts[0].PartModelAProp;

        Console.WriteLine(firstPartModelAProp);
    }
}

This method is an "external" operation, so it can't be added to the model classes directly.

As an alternative, I've already tried to use generics instead as shown below :

public class ImportModelA : ImportModelBase<ProductionModelA, PartModelA>
{
    public string ImportModelAProp { get; set; }
}

public class ImportModelB : ImportModelBase<ProductionModelB, PartModelB>
{
    public string ImportModelBProp { get; set; }
}

public abstract class ImportModelBase<TProductionModel, TPartModel>
    where TProductionModel : ProductionModelBase<TPartModel>
    where TPartModel : PartModelBase
{
    public TProductionModel Production { get; set; }
}

public class ProductionModelA : ProductionModelBase<PartModelA>
{
    public int ProductionModelAProp { get; set; }
}

public class ProductionModelB : ProductionModelBase<PartModelB>
{
    public int ProductionModelBProp { get; set; }
}

public abstract class ProductionModelBase<TPartModel>
    where TPartModel : PartModelBase
{
    public List<TPartModel> Parts { get; set; }
}

public class PartModelA : PartModelBase
{
    public int PartModelAProp { get; set; }
}

public class PartModelB : PartModelBase
{
    public int PartModelBProp { get; set; }
}

public abstract class PartModelBase
{
    public short CommonProp { get; set; }
}

Downcasting problem is now solved.
Unfortunately, this design will quickly get unmaintainable as the code base grows.

I know that my design isn't correct so :

  1. What can I do to avoid downcasting or using "convertAll" on these nested models ?
  2. Is the "visitor" pattern an appropriate way to solve this problem ?

Edit :

Not sure but here could be a way to solve this problem by simplifying the initial hierarchy :

public class ImportModelA : ImportModelBase
{
    public string ImportModelAProp { get; set; }
    public ProductionModelA Production { get; set; }
}

public class ImportModelB : ImportModelBase
{
    public string ImportModelBProp { get; set; }
    public ProductionModelB Production { get; set; }
}

public abstract class ImportModelBase
{
    public string ImportModelBaseProp { get; set; }
}

public class ProductionModelA : ProductionModelBase
{
    public int ProductionModelAProp { get; set; }
    public PartModelA Parts { get; set; }
}

public class ProductionModelB : ProductionModelBase
{
    public int ProductionModelBProp { get; set; }
    public PartModelB Parts { get; set; }
}

public abstract class ProductionModelBase
{
    public string ProductionModelBaseProp { get; set; }
}

public class PartModelA : PartModelBase
{
    public int PartModelAProp { get; set; }
}

public class PartModelB : PartModelBase
{
    public int PartModelBProp { get; set; }
}

public abstract class PartModelBase
{
    public short CommonProp { get; set; }
}

Thanks

Daftom
  • 1
  • 2
  • The code you've shown is for example ofcourse, but why do you need hierarchy in your object here? It looks like your one big hierarchy tree of classes doesn't match the structure of your program structure. Downcasting could be a sign of overdesigned data structures. Keep it more simple, is my guess. – Jeroen van Langen Nov 17 '21 at 08:39
  • This code is for example, indeed. In my mind, this kind of hierarchy looked pretty obvious to me because each model is conceptually part of the parent model (ex : HeadModel or LegModel would really be part of BodyModel). That's why I used inheritance and nested models that way. What do you concretly mean by making it simpler ? Simplifying inheritance as shown in my "edit" section does the job but does it still make sense ? – Daftom Nov 17 '21 at 14:14

1 Answers1

0

You creating abstractions, but you didn't use their (and polymorphism) power. You may not need DoModelASpecificStuff or DoModelBSpecificStuff and explicit downcasting, but DoAnyModelStuff only and work with both ImportModels as with the same objects and only when you need some concrete model specific property - you can use pattern matching and is keyword:

public abstract class ImportBase
{
    public ProductionBase Production { get; set; }
    public string ImportCommonProp { get; set; } = "Common Import prop value";
}
public class ImportA : ImportBase
{
    public string ImportASpecificProp { get; set; } = "Specific Import A prop value";
}
public class ImportB : ImportBase
{
    public string ImportBSpecificProp { get; set; } = "Specific Import B prop value";
}

public abstract class ProductionBase
{
    public List<PartBase> Parts { get; set; } = new List<PartBase>();
    public string ProductionCommonProp { get; set; } = "Common Production prop value";
}
public class ProductionA : ProductionBase
{
    public string ProductionASpecificProp { get; set; } = "Specific Production A prop value";
}
public class ProductionB : ProductionBase
{
    public string ProductionBSpecificProp { get; set; } = "Specific Production B prop value";
}

public abstract class PartBase
{
    public string PartCommonProp { get; set; } = "Common Part prop value";
}
public class PartA : PartBase
{
    public string PartASpecificProp { get; set; } = "Specific Part A prop value";
}
public class PartB : PartBase
{
    public string PartBSpecificProp { get; set; } = "Specific Part B prop value";
}

So you creating some 2 ImportModels of A and B concrete types:

void Main(string[] args)
{
    ImportBase importModelA = new ImportA
    {
        Production = new ProductionA()
        {
            Parts = new List<PartBase>()
            {
                new PartA(),
                new PartA()
            }
        }
    };
    ImportBase importModelB = new ImportB
    {
        Production = new ProductionB()
        {
            Parts = new List<PartBase>()
            {
                new PartB(),
                new PartB()
            }
        }
    };

    DoAnyModelStuff(importModelA, importModelB);

    _ = Console.ReadKey();
}

And in DoAnyModelStuff work with them as with group of same (abstract, not concrete) objects and if needed access to specific type property - use pattern matching:

public void DoAnyModelStuff(params ImportBase[] importBases)
{
    foreach (var importBase in importBases)
    {
        Console.WriteLine(importBase.ImportCommonProp);

        if (importBase is ImportA importA)
            Console.WriteLine(importA.ImportASpecificProp);
        if (importBase is ImportB importB)
            Console.WriteLine(importB.ImportBSpecificProp);

        var production = importBase.Production;
        Console.WriteLine(production.ProductionCommonProp);

        if (production is ProductionA productionA)
            Console.WriteLine(productionA.ProductionASpecificProp);
        if (production is ProductionB productionB)
            Console.WriteLine(productionB.ProductionBSpecificProp);

        var parts = production.Parts;
        foreach (var part in parts)
        {
            Console.WriteLine(part.PartCommonProp);

            if (part is PartA partA)
                Console.WriteLine(partA.PartASpecificProp);
            if (part is PartB partB)
                Console.WriteLine(partB.PartBSpecificProp);
        }

        Console.WriteLine(new string('-', 25));
    }
}

Sample output:

enter image description here

Auditive
  • 1,607
  • 1
  • 7
  • 13
  • You're right, I'm currently not leveraging the full power of polymorphism. However, I can be wrong but doesn't your solution violate the Open-Closed principle? Adding a new model type would require adding a new class-type test everytime, thus, modifying the DoAnyModelStuff's class instead of adding a new type-related class. – Daftom Nov 17 '21 at 14:00