Design a Generic Product Processor

Viewed 97

i'm trying to put some order into a large application that manage Products from multiple sources, all of them different from each other but mapped into a single table (NEEDED VALUES).

I'm looking to hear opinions, experiences, patterns or any kind of recommendations.

Let's say we have Product_A,Product_B,Product_C all mapped to "Product_Dto" and saved as Product into SQL (right now different methods, different classes and files)

/// <summary>
/// INACCESIBLE TO DEVELOPERS
/// </summary>
public class Product
{
    public int id { get; set; }
    public string sku { get; set; }
    public string name { get; set; }
    public int status_id { get; set; }
    public int category_id { get; set; }
    public string description { get; set; }
    public decimal price { get; set; }
    public List<int> quantity { get; set; }
}

public class Product_Dto
{
    public int id { get; set; }
    public string sku { get; set; }
    public string name { get; set; }
    public int? status_id { get; set; }
    public int category_id { get; set; }
    public string description { get; set; }
    public decimal price { get; set; }
}

The main idea is to reorganize (rewrite) the code and provide an abstract class that must be implemented every time they need to add a new Product source.

Why abstract class? Some of the points

  • We want to hide functionality that apply to all sources (Private methods)
  • We want to provide functionality that developers will need when implementing a new source (Public or Protected methods)
  • We want to ensure developers write some NEEDED code (Abstract methods)
  • We want to give the possibility to implement custom code in certain scenarios (Virtual methods)

As i mentioned, the application manage the products from multiple sources and some of the information must be processed, formatted and mapped by executing multiple PRIVATE methods that nobody can touch, override, etc (business logics and value assignment by lookup other lists and sources) and this is the biggest problem i found.

Resuming:

  • Create Abstract Class: ok
  • Add Private,Public and Abstract Methods: OK
  • Map Products_A to Product_Dto manually applying logic: OK => GetProduct_Dto(Product_A product)
  • Map Products_B to Product_Dto manually applying logic: OK => GetProduct_Dto(Product_B product)
  • Create a Generic Mapper: NOT OK => GetProduct_Dto(Product_Generic product);

If write the custom Dto for each case with real Product types work, take that way!

Ok, but for some reason i didn't like it.

We're talking about map at least 60 properties for each source. 90% of the properties had different database values than the original (status, type, category, etc etc) so the value comes from a private function (that will be the same for all scenarios). Also Product class (DB) changes frequently and properties must be added to each source / mapping and make sure the right function is being used for new ones, without interface its impossible. (ONLY CREATE, x2 FOR UPDATE that had the same properties / functionality with exception of some insert values like created_date, sku, that will never change).

Workaround => Generic Mapper: The Interface Way

Ok, to create a generic Mapper we can implement an Interface, let's call it IProduct and Product_A,Product_B & Product_C must implement it. Then, we can write a new GetProduct_Dto

public interface IProduct
{
    //INTERFACES CANNOT CONTAIN INSTANCE CONSTRUCTORS (to assign source on new)
    //public IProduct(Product_Source_Enum source)
    //{
    //    this.source = source;
    //}

    public Product_Source_Enum source { get; set; }
    public int id { get; set; }
    public string sku { get; set; }
    public string name { get; set; }
    public string status { get; set; }
    public string category { get; set; }
    public string description { get; set; }
    public decimal price { get; set; }

    public enum Product_Source_Enum
    {
        A,
        B,
        C
    }
}

public class Product_A : IProduct
{
    public string id { get; set; }
    public string sku { get; set; }
    public string title { get; set; }
    public ProductA_Status_Enum status { get; set; }
    public string category_name { get; set; }
    public string description_text { get; set; }
    public decimal price { get; set; }
    public int quantity { get; set; }


    public IProduct.Product_Source_Enum source { get; set; } = IProduct.Product_Source_Enum.A;
    int IProduct.id { get { if (int.TryParse(this.id, out int ID)) { return ID; } return 0; } set => this.id = value.ToString(); }
    string IProduct.name { get => this.title; set => this.title = value; }
    string IProduct.status { get => this.status.ToString(); set { if (Enum.TryParse<ProductA_Status_Enum>(value, out ProductA_Status_Enum STATUS)) { this.status = STATUS; } } }
    string IProduct.category { get => this.category_name; set => this.category_name = value; }
    string IProduct.description { get => this.description_text; set => this.description_text = value; }


    public enum ProductA_Status_Enum
    {
        active,
        inactive
    }
}

public class Product_B : IProduct
{
    public int id { get; set; }
    public string sku { get; set; }
    public string name { get; set; }
    public string description { get; set; }
    public ProductB_Status_Enum general_status { get; set; }
    public string category { get; set; }
    public decimal price { get; set; }
    public int quantity { get; set; }


    public IProduct.Product_Source_Enum source { get; set; } = IProduct.Product_Source_Enum.B;
    int IProduct.id { get => this.id; set => this.id = value; }
    string IProduct.name { get => this.name; set => this.name = value; }
    string IProduct.status { get => this.general_status.ToString(); set { if (Enum.TryParse<ProductB_Status_Enum>(value, out ProductB_Status_Enum STATUS)) { this.general_status = STATUS; } } }
    string IProduct.category { get => this.category; set => this.category = value; }
    string IProduct.description { get => this.description; set => this.description = value; }
    decimal IProduct.price { get => this.price; set => this.price = value; }


    public enum ProductB_Status_Enum
    {
        enabled,
        disabled,
        paused
    }
}
  1. Create a IProduct Interface: OK
  2. Implement IProduct in Product_A,Product_B & Product_C: OK
  3. Write the GetProduct_Dto(IProduct product): OK, BUT CASTING

What's the problem about map the Interface IProduct to Product_Dto?

I will need all values of all sources merged into the interface in order to maintain the current functionality and map the values into the Product_Dto internally (private mapping)*

  • FALSE => Casting from IProduct to Product_A, Product_B, Product_C will solve the problem.

Resume

  • Implement IProduct Interface fits perfect to fill the Product_Dto because only basic needed properties will be included, keeping the interface small.

     /// <summary>
     /// Private Mapping between Interface object and Product_Dto
     /// </summary>
     /// <param name="product"></param>
     /// <returns></returns>
     private Product_Dto GetProduct_Dto(IProduct product)
     {
         return new Product_Dto
         {
             category_id = GetCategory(product.category),
             description = product.description,
             id = product.id,
             name = product.name,
             price = product.price,
             status_id = GetStatus(product.status)
         };
     }
    

FULL ABSTRACT CLASS

public abstract class ProductProcessor
{

    private Database _db = new Database();

    /// <summary>
    /// Private Mapping between Interface object and Product_Dto
    /// </summary>
    /// <param name="product"></param>
    /// <returns></returns>
    private Product_Dto GetProduct_Dto(IProduct product)
    {
        return new Product_Dto
        {
            category_id = GetCategory(product.category),
            description = product.description,
            id = product.id,
            name = product.name,
            price = product.price,
            status_id = GetStatus(product.status)
        };
    }

    protected Product_Dto Create(IProduct product)
    {
        var productDto = GetProduct_Dto(product);
        var productDb = new Product();
        _db.Product.Add(productDb);
        _db.SetValues(productDb, productDto);

        //private method, different logics for every type but still PRIVATE
        //DEVELOPERS CAN'T DECIDE WAT HAPPEN HERE
        Create_Depot_With_Stock(productDb, product); //=> CAST WILL BE NEEDED TO ACCESS FULL OBJECT

        //Create function has a lot of private functions, casting will be needed for each function

        //Other_1(productDb, product); //=> CAST WILL BE NEEDED TO ACCESS FULL OBJECT =====> EXCESIVE CASTING???
        //Other_2(productDb, product); //=> CAST WILL BE NEEDED TO ACCESS FULL OBJECT =====> EXCESIVE CASTING???
        //Other_3(productDb, product); //=> CAST WILL BE NEEDED TO ACCESS FULL OBJECT =====> EXCESIVE CASTING???

        _db.SaveChanges();
        return productDto;
    }

    public Product_Dto Update(IProduct product)
    {
        var productDb = _db.Find(product.sku);
        var productDto = GetProduct_Dto(product);
        _db.SetValues(productDb, productDto);

        //private method, different logics for every type but still PRIVATE
        //DEVELOPERS CAN'T DECIDE WAT HAPPEN HERE
        Update_Source_Quantity(product, productDb); //=> CAST WILL BE NEEDED TO ACCESS FULL OBJECT

        //Other_1(productDb, product); //=> CAST WILL BE NEEDED TO ACCESS FULL OBJECT =====> EXCESIVE CASTING???
        //Other_2(productDb, product); //=> CAST WILL BE NEEDED TO ACCESS FULL OBJECT =====> EXCESIVE CASTING???
        //Other_3(productDb, product); //=> CAST WILL BE NEEDED TO ACCESS FULL OBJECT =====> EXCESIVE CASTING???


        Update_Source_Quantity_WORKAROUND(product); //MUST BE OVERRIDED AND CASTED BY DEVELOPERS

        _db.SaveChanges();
        return productDto;
    }

    /// <summary>
    /// Private method, developer must implement the interface properties and here we process the values
    /// </summary>
    /// <param name="status"></param>
    /// <returns></returns>
    private int GetCategory(string status)
    {
        var statuses = new Dictionary<string, int>
        {
            { "none",0 },
            { "CatA",1 },
            { "CatB" ,2},
            { "CatC",3 }
        };

        if (statuses.ContainsKey(status))
            return statuses.GetValueOrDefault(status);

        return 0;
    }

    private int? GetStatus(string status)
    {
        var statuses = new Dictionary<string, int> {
            { "other",0 },
            { "active",1 },
            { "inactive" ,2},
            { "enabled",3 },
            { "disabled",4 },
            { "paused",5 } };

        if (statuses.ContainsKey(status))
            return statuses.GetValueOrDefault(status);

        return null;
    }

    /// <summary>
    /// EVERY METHOD THAT NEED THE FULL PRODUCT WILL NEED TO CAST THE IProduct
    /// </summary>
    /// <param name="productDb"></param>
    /// <param name="product"></param>
    private void Create_Depot_With_Stock(Product productDb, IProduct product)
    {
        int product_stock = 0;// product.quantity doesnt exists in interface WE MUST CAST TYPES

        if (product.source == IProduct.Product_Source_Enum.A)
        {
            var product_a = (Product_A)product;
            product_stock = product_a.quantity;
            productDb.quantity = new List<int> { product_stock };
            return;
        }

        if (product.source == IProduct.Product_Source_Enum.B)
        {
            var product_a = (Product_A)product;
            product_stock = product_a.quantity;
            productDb.quantity = new List<int> { product_stock };
            return;
        }
    }

    /// <summary>
    /// OUTSIDE DEVELOPERS DONT NEED TO KNOW THIS IS HAPPENING
    /// </summary>
    /// <param name="product"></param>
    /// <param name="productDb"></param>
    private void Update_Source_Quantity(IProduct product, Product productDb)
    {
        if (product is Product_A)
        {
            var product_a = (Product_A)product;
            //THIS IF IS THE SAME FOR EVERY Product Type, EXTRACT FUNCTION AND USE VALUES?
            if (product_a.quantity != productDb.quantity.Sum())
            {
                //do something
            }
            return;
        }

        if (product is Product_B)
        {
            var product_b = (Product_B)product;
            //THIS IF IS THE SAME FOR EVERY Product Type, EXTRACT FUNCTION AND USE VALUES?
            if (product_b.quantity != productDb.quantity.Sum())
            {
                //do something
            }
            return;
        }
    }

    protected abstract void Update_Source_Quantity_WORKAROUND(IProduct product);

}
  • What i didn't like: when need to find the database value for a property (via private function) the function will accept a string generic parameter for status instead of the enum (but nobody will die).

     private int? GetStatus(string status)
     {
         var statuses = new Dictionary<string, int> {
             { "other",0 },
             { "active",1 },
             { "inactive" ,2},
             { "enabled",3 },
             { "disabled",4 },
             { "paused",5 } };
    
         if (statuses.ContainsKey(status))
             return statuses.GetValueOrDefault(status);
    
         return null;
     }
    
  • I can't find an easy way to transport full Product_A,B & C to the base to manage them in his right type (this would make everything easier) Ex. Product.status = Product_A.Status_Enum.active and not a status in string.

  • FALSE AGAIN => Casting from IProduct to Product_A, Product_B, Product_C will solve the problem.

      /// <summary>
      /// EVERY METHOD THAT NEED THE FULL PRODUCT WILL NEED TO CAST THE IProduct
      /// </summary>
      /// <param name="productDb"></param>
      /// <param name="product"></param>
      private void Create_Depot_With_Stock(Product productDb, IProduct product)
      {
          int product_stock = 0;// product.quantity doesn't exists in interface WE MUST CAST TYPES
    
          if (product.source == IProduct.Product_Source_Enum.A)
          {
              var product_a = (Product_A)product;
              product_stock = product_a.quantity;
              productDb.quantity = new List<int> { product_stock };
              return;
          }
    
          if (product.source == IProduct.Product_Source_Enum.B)
          {
              var product_a = (Product_A)product;
              product_stock = product_a.quantity;
              productDb.quantity = new List<int> { product_stock };
              return;
          }
      }
    

Some unresolved questions:

  1. Im using the source attribute only to cast the Product into its original type. since INTERFACES CANNOT CONTAIN INSTANCE CONSTRUCTORS assign the source is in hands of developers. Any workaround?

     //public IProduct(Product_Source_Enum source)
     //{
     //    this.source = source;
     //}
    

AUTO RESPONSE 1: Converting the interface into an abstract class may work. any ideas?

  1. What about cast the Interface every time i need the concrete class? There is a lot of IFS for every method only for casting purposes. Any ideas how to handle?

     /// <summary>
     /// EVERY METHOD THAT NEED THE FULL PRODUCT WILL NEED TO CAST THE IProduct
     /// </summary>
     /// <param name="productDb"></param>
     /// <param name="product"></param>
     private void Create_Depot_With_Stock(Product productDb, IProduct product)
     {
         int product_stock = 0;// product.quantity doesnt exists in interface WE MUST CAST TYPES
    
         if (product.source == IProduct.Product_Source_Enum.A)
         {
             var product_a = (Product_A)product;
             product_stock = product_a.quantity;
             productDb.quantity = new List<int> { product_stock };
             return;
         }
    
         if (product.source == IProduct.Product_Source_Enum.B)
         {
             var product_a = (Product_A)product;
             product_stock = product_a.quantity;
             productDb.quantity = new List<int> { product_stock };
             return;
         }
     }
    
     /// <summary>
     /// OUTSIDE DEVELOPERS DONT NEED TO KNOW THIS IS HAPPENING
     /// </summary>
     /// <param name="product"></param>
     /// <param name="productDb"></param>
     private void Update_Source_Quantity(IProduct product, Product productDb)
     {
         if (product is Product_A)
         {
             var product_a = (Product_A)product;
             //THIS IF IS THE SAME FOR EVERY Product Type, EXTRACT FUNCTION AND USE VALUES?
             if (product_a.quantity != productDb.quantity.Sum())
             {
                 //do something
             }
             return;
         }
    
         if (product is Product_B)
         {
             var product_b = (Product_B)product;
             //THIS IF IS THE SAME FOR EVERY Product Type, EXTRACT FUNCTION AND USE VALUES?
             if (product_b.quantity != productDb.quantity.Sum())
             {
                 //do something
             }
             return;
         }
     }
    
0 Answers
Related