1
\$\begingroup\$

I have finished with my product but I am not that trained in refactoring, cleaning up my code and spot redundancy so I would really appreciate some help.

namespace Product_manager
{
    // Product class represents a product with category, name, and price.
    class Product
    {
        public Product(string category, string name, decimal price)
        {
            Category = category;
            Name = name;
            Price = price;
        }

        public string Category { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }

    class ProductManager
    {
        // List of all products
        private static readonly List<Product> Products = new List<Product>();

        // Add a new product.
        public void AddProduct(string category, string name, decimal price)
        {
            // Checks if the name is similar to already stored products.
            if (Products.Any(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("A product with the same name already exists. Please enter a unique name.");
                Console.ResetColor();
                Console.WriteLine(new string('-', 50));
                return;
            }

            // Adding successfully!
            Products.Add(new Product(category, name, price));
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("The product was successfully added!");
            Console.ResetColor();
            Console.WriteLine(new string('-', 50));
        }

        // Method to display all products in the list.
        public void ShowProducts()
        {
            Console.WriteLine(new string('-', 50));

            // Print the header with column names.
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("{0,-20} {1,-20} {2,-20}", "Category", "Name", "Price");
            Console.ResetColor();

            foreach (var product in Products.OrderBy(p => p.Price))
            {
                Console.WriteLine($"{product.Category,-20} {product.Name,-20} {product.Price,-20}");
            }

            Console.WriteLine(new string('-', 50));

            // Print the total amount of all products' prices.
            Console.WriteLine("{0,-20} {1,-20} {2,-20}", "", "Total amount:", Products.Sum(p => p.Price));
        }

        // Method to add more products interactively.
        public void AddMoreProducts()
        {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("To enter a new product - follow the steps | To quit - Enter Q");
            Console.ResetColor();

            while (true)
            {
                Console.WriteLine("Enter product details (or 'q' to quit adding more products):");
                Console.Write("Category: ");
                string? category = Console.ReadLine();

                if (category?.ToLower() == "q")
                    break;

                string? name = null;
                while (string.IsNullOrWhiteSpace(name))
                {
                    Console.Write("Product Name: ");
                    name = Console.ReadLine();

                    if (string.IsNullOrWhiteSpace(name))
                        Console.WriteLine("Invalid input. Please enter a non-empty product name.");
                }

                if (name != null)
                {
                    decimal price;
                    while (true)
                    {
                        Console.Write("Price: ");
                        if (decimal.TryParse(Console.ReadLine(), out price))
                        {
                            break;
                        }
                        else
                        {
                            Console.WriteLine("Invalid price. Please enter a valid decimal number.");
                        }
                    }

                    AddProduct(category!, name, price);
                }
            }
        }

        // Search method
        public void SearchProduct(string searchTerm)
        {
            var results = Products
                .Where(p => p.Name.Equals(searchTerm, StringComparison.OrdinalIgnoreCase))
                .ToList();

            Console.WriteLine("\nSearch Results:");

            // Print the header with column names in green.
            Console.WriteLine(new string('-', 50));

            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("{0,-20} {1,-20} {2,-20}", "Category", "Name", "Price");
            Console.ResetColor();

            // Mark the search result in purple.
            foreach (var product in Products.OrderBy(p => p.Price))
            {
                if (results.Any(p => p.Name.Equals(product.Name, StringComparison.OrdinalIgnoreCase)))
                {
                    Console.ForegroundColor = ConsoleColor.Magenta;
                    Console.WriteLine($"{product.Category,-20} {product.Name,-20} {product.Price,-20}");
                }
                else
                {
                    Console.ResetColor();
                    Console.WriteLine($"{product.Category,-20} {product.Name,-20} {product.Price,-20}");
                }
            }

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

    class Program
    {
        static void Main()
        {
            
            ProductManager manager = new ProductManager();

            // Loop to add more products.
            while (true)
            {
                manager.AddMoreProducts();

                // Display all products in the list.
                manager.ShowProducts();

                Console.Write("Do you want to add more products? (y/n): ");
                string? choice = Console.ReadLine();

                if (choice?.ToLower() != "y")
                    break;
            }

            // Loop to search for products.
            while (true)
            {
                Console.Write("Enter the product name to search (or 'q' to quit searching): ");
                string? searchTerm = Console.ReadLine();

                if (searchTerm != null && searchTerm.ToLower() != "q")
                {
                    manager.SearchProduct(searchTerm);
                }
                else
                {
                    break;
                }
            }
        }
    }
}
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

The ProductManager class is doing too much: adding, displaying and searching. You can split that into different classes, something like:

  • ProductAdder
  • ProductSearcher
  • ProductGetter

Your methods are also doing multiple things which violates the SRP. Example: SearchProduct()

  • Performs the search query.
  • Prints the results table layout (like header).
  • Prints the actual table with products.

This actually leads you to repeat code like this:

        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine("{0,-20} {1,-20} {2,-20}", "Category", "Name", "Price");
        Console.ResetColor();

So, I would also create a class to manage the table rendering.

The method AddMoreProducts() has also a couple of problems:

  • It handles user input.
  • It handles input validation, (which I think you don't do when it is a single product add) - this may call for a validator class.
  • It holds program flow with while(true).

In Short:

  • Split logic into more classes to keep your classes with a single responsability.
  • Do not mix user input handling, presentation and validation with business logic. (If tomorrow you would need to use a different UI but keep the logic, with your current code it would be much harder to do). Also, much harder (or even impossible) to correctly unit test.
  • Your methods should also do a single thing.
  • IMO, you should not mix the program flow with business logic also, this makes it harder to read, debug and understand.
\$\endgroup\$
1
\$\begingroup\$

Class Singularity Yields Clarity

product manager that add products, sorts, lists and makes the list searchable

Products class will do all of these for itself; the precept of single responsibility. ProjectManager will tell (not ask) Products what to do.

The ProjectManager will have clean, simple code because Products and Product classes are full of Single Responsibility Principle

Products myProducts = new Products();

// prompt for product

myProducts.Add(new Product(aCategory, aName, aPrice));

// loop

myProducts.Sort();
console.WriteLine(myProducts);

Products toys = myProducts.FindByCategory('toys');
toys.SortByPrice();
console.WriteLine(toys);

The above code validates parameters, keeps the list unique by name, outputs the products with column headings, sorted, w/ the total price on the end.


.NET Collection Customization Basics

.NET collections, in cooperation with Object polymorphism (overriding), are designed for customizing these functions.

Product overrides

  • Equals for object identity, searching, and sorting
  • CompareTo for sorting
  • ToString for formatted textual output. P.S. Very helpful during development/troubleshooting.

.

class Product {
  public Product (string category, string name, decimal price) {
    this.Name     = name ?? String.Empty;
    this.Category = category ?? "no Category";
    this.Price    = price;

    // do ALL validation tests required to create a valid object
    // with valid state - all instance variables set, at least to some default.
    // client code should not have to check for null. Do it here.
    // HIGHLY recommend String.Empty for string default. Method calls on null
    // throw exceptions, string.Empty does not.
  }

  public bool override Equals (Object other) {
    if (other == null) return false;

    Product theOther = other as Product;
    if ( theOther == null ) return false;

    return Name.Equals(other.Name);
  }

  public int override CompareTo(Object other) {
    if (other == null) return 1;

    Product theOther = other as Product;
    if (theOther == null) return 1;

    return Name.CompareTo(other.Name);
  }

  public string override ToString() {
    return "{0,-20} {1,-20} {2,-20}", Category, Name, Price);
  }
}

Products collection

  • Composed with List. Per MSDN do not inherit List.
  • Add ensures unique Product objects. Internally calls Product.Equals
  • Sort internally calls Product.CompareTo
  • ToString calls Product.ToString.
    • console.WriteLine calls ToString implicitly

.

class Products {
  protected List<Product> Products {get; set;}

  public Products() { Products = new List<Product>(); }


  // return bool to detect duplicate reject
  public void Add(Product newProduct) {
    if (newProduct == null) return;
    if (Products.Contains(newProduct)) return;  // calls Equals
   
    Products.Add(newProduct)
  }

  public Product FindByName(string findMe) {
    return Products.Find(x -> x.Name == findMe);
  }

  public Products FindByCategory(string findMe) {
    return Products.Find(x -> x.Category == findMe);
  }

  public void Sort() { Products.Sort(); } // calls CompareTo

  public void SortByPrice() {
     Products.Sort( x -> x.Price );
  } 

  public decimal PriceTotal() {
    decimal total = 0;

    forEach(var product in Products) total += product.price;

    return total;
  }

  public string override ToString() {
    StringBuilder me = new StringBuilder();
    me.AppendLine("{0,-20} {1,-20} {2,-20}", "Category", "Name", "Price");

    forEach (var product in Products) {
      me.Append(product);    // calls ToString
      me.Append("/n");
    }

    me.AppendLine("/n Total Product Prices: {0}", Products.PriceTotal());

    return me.ToString();
  } 
} // Products

Read MSDN collection documentation. It is a treasure trove of OO goodness

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.