Akim Khalilov
Akim Khalilov

Reputation: 1039

How to separate levels of abstractions w/ Interfaces?

I'm automation the warehouse and I'm trying to create the domain model for next task:

A warehouse has a lot of products. Products can be either liquid, or grocery, or piece by piece. There are two packing lines in the warehouse to pack either liquid products or all other products. Piece by piece products does not require packing.

Here are my models:

enum ProductType
{
    Liquid,
    Grossery
}

interface IProduct
{
    ProductType ProductType { get; }
}

interface IPackedProduct : IProduct
{
    bool IsPacked { get; }
}

interface ILiquidProduct : IProduct
{

}

interface IGrosseryProduct : IProduct
{

}

interface IPackingLine
{
    IPackedProduct Pack(IProduct product);
}

interface IGrosseryPackingLine : IPackingLine
{
    IPackedProduct Pack(IGrosseryProduct p);
}

class ProductPackingLine : IPackingLine
{
    public IPackedProduct Pack(IProduct product)
    {
        Console.WriteLine("Packing {0} default packing line", product);
        return new PackedProduct(product);
    }
}

class LiquidsPackingLine : IPackingLine
{
    public IPackedProduct Pack(ILiquidProduct product) // I want this <=======================
    {
        Console.WriteLine("Packing {0} by liquid packing line", product);
        return new PackedProduct(product);
    }
}

class GrosseryPackingLine : IPackingLine
{
    public IPackedProduct Pack(IProduct product)
    {
        Console.WriteLine("Packing {0} by grossery packing line", product);
        return new PackedProduct(product);
    }
}

I'm using it like this:

IProduct milk = new LiquidProduct(ProductType.Liquid);
IProduct pasta = new GrosseryProduct(ProductType.Grossery);

var packer = new PackingManager();

IPackedProduct packedMilk = packer.Pack(milk);
IPackedProduct packedPasta = packer.Pack(pasta);

Here is the PackingManager

class PackingManager
{
    public IPackedProduct Pack(IProduct product)
    {
        IPackingLine pl = GetPackingLineByProduct(product);

        return pl.Pack(product);
    }

    private IPackingLine GetPackingLineByProduct(IProduct product)
    {
        switch (product.ProductType)
        {
            case ProductType.Liquid:
                return new LiquidsPackingLine();

            case ProductType.Grossery:
                return new GrosseryPackingLine();

            default:
                throw new InvalidOperationException();
        }
    }
}

The problem is that if I'll use IPackingLine.Pack(IProduct p) I can pass an object of ILiquidProduct by mistake to a wrong packing line. But I need all my packing lines to implement IPackingLine to be able to use them in a more common way.

How to avoid that?

Upvotes: 0

Views: 74

Answers (1)

plalx
plalx

Reputation: 43718

I think there are 3 main ways to solve your problem:

  1. Work with IProduct everywhere and drop compile-time type safety in favor of runtime checks. If you go down that road then you should at least make it explicit that an IPackingLine may reject to pack a product.

    E.g.

    public interface IPackingLine {
        IPackedProduct pack(IProduct product);
        bool canPack(IProduct);
    }
    
  2. Use some kind of double-dispatch (the dynamic keyword with overloaded methods makes this easier in C#):

       public interface IPacker {
           IPackedProduct pack(IProduct product);
           IPackedProduct packLiquid(ILiquidProduct product);
           IPackedProduct packGrossery(IGrosseryProduct product);
       }
    
       public interface IProduct {
           IPackedProduct packWith(IPacker packer)
       }
    
       class LiquidProduct implements IProduct {
           IPackedProduct packWith(IPacker packer) {
               return packer.packLiquid(this);
           }
       }
    
       //...
    
  3. If possible, introduce new abstractions that would allow a packing line to treat any kind of product the same way. For instance, imagine you had to build an application that paints squares and triangles. You could have a specialized painter for each, but you could also have a single painter that works with abstract shapes. E.g. painter.paint(triangle.getShape()).

Upvotes: 1

Related Questions