Search code examples
c#genericsinterfacederived-class

How to access derived class members from an interface?


I have three classes; Stamp, Letter and Parcel that implement an interface IProduct and they also have some of their own functionality.

public interface IProduct
{
    string Name { get; }
    int Quantity { get; set; }
    float Amount { get; }
}

public class Stamp : IProduct
{
    public string Name { get { return "Stamp"; } }
    public int Quantity { get; set; }
    public float Amount { get; set; }
    public float UnitPrice { get; set; }
}

public class Letter : IProduct
{
    public string Name { get { return "Letter"; } }
    public int Quantity { get; set; }        
    public float Amount { get; set; }
    public float Weight { get; set; }
    public string Destination { get; set; }
}

public class Parcel : IProduct
{
    public string Name { get { return "Parcel"; } }
    public int Quantity { get; set; }        
    public float Amount { get; set; }
    public float Weight { get; set; }
    public string Destination { get; set; }
    public int Size { get; set; }
}

public static class ShoppingCart
{
    private static List<IProduct> products = new List<IProduct>();
    public static List<IProduct> Items { get { return products; } }
}

Why can't I access the additional members of derived classes from a List<IProduct> ?

ShoppingCart.Items.Add(new Stamp { Quantity = 5, UnitPrice = 10, Amount = 50 });
ShoppingCart.Items.Add(new Letter { Destination = "US", Quantity = 1, Weight = 3.5f });
ShoppingCart.Items.Add(new Parcel { Destination = "UK", Quantity = 3, Weight = 4.2f, Size = 5 });

foreach (IProduct product in ShoppingCart.Items)
{
    Console.WriteLine("Name: {0}, Quantity: {1}, Amount: {2}", product.Name, product.Quantity, product.Amount);
}

I thought of using generics, but in that case I will have to write separate code for each specific type of product.

public static class ShoppingCart<T> where T : IProduct
{
    private static List<T> items = new List<T>();
    public static List<T> Items { get { return items; } }
}


ShoppingCart<Stamp>.Items.Add(new Stamp { Quantity = 5, Amount = 10, UnitPrice = 50 });
ShoppingCart<Letter>.Items.Add(new Letter { Destination = "US", Quantity = 1, Weight = 3.5f });

foreach (Stamp s in ShoppingCart<Stamp>.Items)
{
    Console.WriteLine("Name: {0}, Quantity: {1}, Amount: {2}", s.Name, s.Quantity, s.Amount); 
}

foreach (Letter l in ShoppingCart<Letter>.Items)
{
    Console.WriteLine("Name: {0}, Destination: {1}, Weight: {2}", l.Name, l.Destination, l.Weight);      
}

Isn't there any kind of design pattern for this kind of problem. Factory Pattern?


Solution

  • This is because you are casting each Item in the shopping cart as IProduct in your foreach loop. What you would need to do is something like:

    foreach(IProduct product in ShoppingCart.Items)
    {
        if (product is Stamp)
        {
            var stamp = product as Stamp;
            Console.WriteLine("Name: {0}, Quantity: {1}, Amount: {2}, UnitPrice: {3}", stamp.Name, stamp.Quantity, stamp.Amount, stamp.UnitPrice);
        }
        else if (product is Letter)
        {
            var letter = product as Letter;
            Console.WriteLine("Name: {0}, Quantity: {1}, Amount: {2}, Weight: {3}, Destination: {4}", letter.Name, letter.Quantity, letter.Amount, letter.Weight, letter.Destination);
        }
        else if (product is Parcel)
        {
            var parcel = product as Parcel;
            Console.WriteLine("Name: {0}, Quantity: {1}, Amount: {2}, Weight: {3}, Destination: {4}, Size: {5}", parcel.Name, parcel.Quantity, parcel.Amount, parcel.Weight, parcel.Destination, parcel.Size);
        }
    }
    

    Alternatively, this more modern syntax now available in C#, which combines the is operator with the variable declaration:

    foreach(IProduct product in ShoppingCart.Items)
    {
        if (product is Stamp stamp)
        {
            Console.WriteLine("Name: {0}, Quantity: {1}, Amount: {2}, UnitPrice: {3}", stamp.Name, stamp.Quantity, stamp.Amount, stamp.UnitPrice);
        }
        else if (product is Letter letter)
        {
            Console.WriteLine("Name: {0}, Quantity: {1}, Amount: {2}, Weight: {3}, Destination: {4}", letter.Name, letter.Quantity, letter.Amount, letter.Weight, letter.Destination);
        }
        else if (product is Parcel parcel)
        {
            Console.WriteLine("Name: {0}, Quantity: {1}, Amount: {2}, Weight: {3}, Destination: {4}, Size: {5}", parcel.Name, parcel.Quantity, parcel.Amount, parcel.Weight, parcel.Destination, parcel.Size);
        }
    }
    

    Also you are repeating unnecessary properties Name, Quantity and Amount. You should derive each of your classes from Product:

    public class Stamp: Product, IProduct
    {
        public double UnitPrice { get; set; }
    }
    
    public class TransitProduct: Product, IProduct
    {
        public double Weight { get; set; }
        public string Destination { get; set; }   
    }
    
    public class Letter: TransitProduct, IProduct
    {
    }
    
    public class Parcel: TransitProduct, IProduct
    {
        public double Size { get; set; }
    }