web-dev-qa-db-fra.com

Comment créer le parfait OOP application

Récemment, j’essayais de créer une société «x». Ils m'ont envoyé une série de questions et m'ont dit de n'en résoudre qu'une.

Le problème est comme ça -

La taxe de vente de base est applicable au taux de 10% sur tous les produits, à l'exception des livres, des produits alimentaires et des produits médicaux exonérés.
Le droit d'importation est une taxe de vente additionnelle applicable à tous les produits importés au taux de 5%, sans aucune exemption. 

Lorsque j'achète des articles, je reçois un reçu indiquant le nom de tous les articles et leur prix (taxes comprises), en indiquant le coût total des articles et le montant total des taxes de vente payées.
Les règles d’arrondi de la taxe de vente sont les suivantes: pour un taux de taxe de n%, un prix de revient de p contient (np/100 arrondi à 0,05 près) le montant de la taxe de vente.

"Ils m'ont dit qu'ils s'intéressaient au aspect de la conception de votre solution et souhaitaient évaluer mes compétences en programmation orientée objet."

Voici ce qu'ils ont dit avec leurs propres mots

  • Pour la solution, nous voudrions que vous utilisiez Java, Ruby ou C #.
  • Nous sommes intéressés par l’aspect design de votre solution et souhaitons évaluer votre compétences en programmation orientée objet.
  • Vous pouvez utiliser des bibliothèques ou des outils externes à des fins de construction ou de test. Plus précisément, vous pouvez utiliser des bibliothèques de tests unitaires ou créer des outils disponibles pour le langage choisi (par exemple, JUnit, Ant, NUnit, NAnt, Test :: Unit, Rake, etc.).
  • Facultativement, vous pouvez également inclure une brève explication de votre conception et de vos hypothèses avec votre code.
  • Veuillez noter que nous n'attendons PAS une application Web ou une interface utilisateur complète. Nous attendons plutôt une application simple, basée sur une console et intéressée par votre code source.

Donc, j'ai fourni ci-dessous le code - vous pouvez simplement copier le code coller et l'exécuter dans VS.

class Program
 {
     static void Main(string[] args)
     {
         try
         {
             double totalBill = 0, salesTax = 0;
             List<Product> productList = getProductList();
             foreach (Product prod in productList)
             {
                 double tax = prod.ComputeSalesTax();
                 salesTax += tax;
                 totalBill += tax + (prod.Quantity * prod.ProductPrice);
                 Console.WriteLine(string.Format("Item = {0} : Quantity = {1} : Price = {2} : Tax = {3}", prod.ProductName, prod.Quantity, prod.ProductPrice + tax, tax));
             }
             Console.WriteLine("Total Tax : " + salesTax);
             Console.WriteLine("Total Bill : " + totalBill);                
        }
         catch (Exception ex)
         {
             Console.WriteLine(ex.Message);
         }
         Console.ReadLine();
     }

    private static List<Product> getProductList()
     {
         List<Product> lstProducts = new List<Product>();
         //input 1
         lstProducts.Add(new Product("Book", 12.49, 1, ProductType.ExemptedProduct, false));
         lstProducts.Add(new Product("Music CD", 14.99, 1, ProductType.TaxPaidProduct, false));
         lstProducts.Add(new Product("Chocolate Bar", .85, 1, ProductType.ExemptedProduct, false));

        //input 2
         //lstProducts.Add(new Product("Imported Chocolate", 10, 1, ProductType.ExemptedProduct,true));
         //lstProducts.Add(new Product("Imported Perfume", 47.50, 1, ProductType.TaxPaidProduct,true));

        //input 3
         //lstProducts.Add(new Product("Imported Perfume", 27.99, 1, ProductType.TaxPaidProduct,true));
         //lstProducts.Add(new Product("Perfume", 18.99, 1, ProductType.TaxPaidProduct,false));
         //lstProducts.Add(new Product("Headache Pills", 9.75, 1, ProductType.ExemptedProduct,false));
         //lstProducts.Add(new Product("Imported Chocolate", 11.25, 1, ProductType.ExemptedProduct,true));
         return lstProducts;
     }
 }

public enum ProductType
 {
     ExemptedProduct=1,
     TaxPaidProduct=2,
     //ImportedProduct=3
 }

class Product
 {
     private ProductType _typeOfProduct = ProductType.TaxPaidProduct;
     private string _productName = string.Empty;
     private double _productPrice;
     private int _quantity;
     private bool _isImportedProduct = false;

    public string ProductName { get { return _productName; } }
     public double ProductPrice { get { return _productPrice; } }
     public int Quantity { get { return _quantity; } }

    public Product(string productName, double productPrice,int quantity, ProductType type, bool isImportedProduct)
     {
         _productName = productName;
         _productPrice = productPrice;
         _quantity = quantity;
         _typeOfProduct = type;
         _isImportedProduct = isImportedProduct;
     }

    public double ComputeSalesTax()
     {
         double tax = 0;
         if(_isImportedProduct) //charge 5% tax directly
             tax+=_productPrice*.05;
         switch (_typeOfProduct)
         {
             case ProductType.ExemptedProduct: break;
             case ProductType.TaxPaidProduct:
                 tax += _productPrice * .10;
                 break;
         }
         return Math.Round(tax, 2);
         //round result before returning
     }
 }

vous pouvez décompresser une entrée et exécuter pour différentes entrées.

J'ai fourni la solution mais j'ai été rejeté.

"Ils ont dit qu'ils ne pouvaient pas me considérer pour nos postes vacants actuels car la solution de code n'est pas satisfaisante."

S'il vous plaît guidez-moi ce qui manque ici. Est-ce que cette solution n'est pas une bonne solution OOAD?.
Comment puis-je améliorer mes compétences en OOAD?.
Mes aînés ont également déclaré que l’application OOAD parfaite ne fonctionnerait pas non plus.

Merci

98
sunder

Tout d'abord bon sang ne faites pas de calculs financiers en double _. Effectuer des calculs financiers en décimal; c'est ce que c'est pour. Utilisez double pour résoudre les problèmes physics, pas les problèmes financier

Le principal défaut de conception de votre programme est que policy est au mauvais endroit. Qui est responsable du calcul des taxes? Vous avez chargé le produit de calculer les taxes, mais lorsque vous achetez un Apple, un livre ou une machine à laver, ce que vous êtes sur le point d'acheter n'est pas responsable de vous dire combien l'impôt que vous allez payer dessus. La politique du gouvernement est chargée de vous le dire. Votre conception enfreint massivement le principe de base de la conception OO selon lequel les objets doivent être responsables de leurs propres problèmes, et non ceux de quelqu'un d'autre. Le souci d'une machine à laver est de laver vos vêtements, et non de facturer le droit d'importation approprié. Si les lois fiscales changent, vous ne souhaitez pas modifier l'objet de machine à laver, vous souhaitez modifier l'objet de stratégie.

Alors, comment aborder ce genre de problèmes à l’avenir?

J'aurais commencé par mettre en évidence chaque nom important dans la description du problème:

_ {Taxe de vente de base} _ est applicable à un taux de 10% sur tous les biens, sauf (livres), aliments, et produits médicaux qui sont exemptés. Droits d'importation est un taxe sur les ventes applicable sur tous les produits importés à un {taux} de 5%, sans exemptions. Lorsque j'achète articles, je reçois un reçu qui répertorie les nom de tous les articles et leurs prix ( y compris taxe), en terminant avec le coût total} des articles et le montant total de taxes de vente payés. Les règles d'arrondi pour taxe de vente sont les suivantes: pour un taux de taxe de n%, un prix de vente de p contient (np/100 arrondi à 0,05 près) un montant de taxe de vente.

Maintenant, quelles sont les relations entre tous ces noms?

  • La taxe de vente de base est une sorte de taxe de vente
  • Le droit d'importation est une sorte de taxe de vente
  • Une taxe de vente a un taux qui est une décimale
  • Les livres sont une sorte de point
  • La nourriture est une sorte d'article
  • Les produits médicaux sont une sorte d'article
  • Les articles peuvent être des marchandises importées
  • Un élément a un nom qui est une chaîne
  • Un article a un prix de vente qui est une décimale. (Remarque: un article a-t-il vraiment un prix? Deux machines à laver identiques peuvent être en vente à des prix différents dans des magasins différents ou dans le même magasin à des moments différents. Un meilleur design pourrait consister à dire qu'une politique de prix concerne un article son prix.)
  • Une politique d'exemption de la taxe de vente décrit les conditions dans lesquelles une taxe de vente est inapplicable sur un article.
  • Un reçu contient une liste d'articles, leurs prix et leurs taxes.
  • Un reçu a un total
  • Un reçu a une taxe totale

... etc. Une fois que vous avez défini toutes les relations entre tous les noms, vous pouvez alors commencer à concevoir une hiérarchie de classes. Il existe un élément de classe de base abstraite. Le livre en hérite. Il existe une classe abstraite SalesTax; BasicSalesTax en hérite. Etc.

242
Eric Lippert

Si la société parle de bibliothèques telles que NUnit, JUnit ou Test :: Unit est plus que probable que TDD leur importe réellement. Dans votre exemple de code, il n'y a pas de test du tout. 

J'essaierais de démontrer une connaissance pratique de:

  • Tests unitaires (ex. NUnit)
  • Se moquer (ex. RhinoMocks)
  • Persistance (par ex. NHibernate)
  • Conteneurs IoC (par exemple, NSpring)
  • modèles de conception 
  • Principe solide

Je voudrais recommander le www.dimecasts.net en tant que source impressionnante de captures d’écran gratuites et de bonne qualité couvrant tous les sujets susmentionnés.

38
Radek

Ceci est très subjectif mais voici quelques remarques sur votre code:

  • À mon avis, vous avez mélangé Product et ShoppingCartItem. Product devrait avoir le nom du produit, le statut fiscal, etc. mais pas la quantité. La quantité n'est pas la propriété d'un produit - ce sera différent pour chaque client de la société qui achète ce produit particulier.

  • ShoppingCartItem devrait avoir Product et la quantité. De cette manière, le client peut acheter librement plus ou moins du même produit. Avec votre configuration actuelle, ce n'est pas possible.

  • Le calcul de la taxe finale ne devrait pas non plus faire partie de la Product - il devrait faire partie de quelque chose comme ShoppingCart car le calcul de la taxe finale peut impliquer de connaître tous les produits du panier.

19
xxbbcc

Tout d’abord, c’est une très bonne question d’entrevue. C'est un bon indicateur de plusieurs compétences.

Il y a beaucoup de choses que vous devez comprendre pour donner une bonne réponse (il y a non réponse parfaite), à ​​la fois de haut niveau et de bas niveau. En voici un couple:

  • Domain Modeling -> comment créer un bon modèle de solution? Quels objets créez-vous? Comment vont-ils résoudre les exigences? Rechercher les noms est un bon début, mais comment décidez-vous si votre choix d’entités est bon? De quelles autres} entités avez-vous besoin? De quoi {connaissance du domaine} avez-vous besoin pour le résoudre?
  • Séparation des préoccupations, couplage lâche, cohésion élevée -> Comment séparez-vous les parties de la conception qui ont des préoccupations ou des taux de changement différents et comment les reliez-vous? Comment maintenez-vous votre conception flexible et à jour?
  • Test unitaire, refactorisation, TDD -> Quel est votre processus pour avoir trouvé une solution? Est-ce que vous écrivez des tests, utilisez des objets fictifs, un refactor, une itération?
  • Code propre, idiomes de langage -> Utilisez-vous les fonctionnalités de votre langage de programmation pour vous aider? Est-ce que vous écrivez du code compréhensible? Vos niveaux d'abstraction ont-ils un sens? Comment maintenable est le code?
  • Tools: Utilisez-vous le contrôle de source? Construire des outils? IDEs?

À partir de là, vous pouvez avoir de nombreuses discussions intéressantes, impliquant des principes de conception (comme les principes SOLID), des modèles de conception, des modèles d'analyse, une modélisation de domaine, des choix technologiques, des chemins d'évolution future (par exemple, si j'ajoute une base de données ou interface utilisateur riche, que faut-il changer?), compromis, exigences non fonctionnelles (performances, maintenabilité, sécurité, ...), tests d'acceptation, etc.

Je ne commenterai pas la façon dont vous devriez changer votre solution, mais simplement que vous devriez vous concentrer davantage sur ces concepts.

Mais, je peux vous montrer comment j'ai (partiellement) résolu ce problème , juste à titre d'exemple (en Java). Regardez dans la classe Program pour voir comment tout se réunit pour imprimer ce reçu:

------------------ CECI IS VOTRE COMMANDE ------------------
 (001) Conception axée sur les domaines ----- 69,99 $ 
 (001) Logiciels grandissants orientés objet ----- 49,99 $ 
 (001) House MD Saison 1 ----- 29,99 $ 
 (001) House MD Saison 7 ----- 34,50 $ 
 (IMD) Logiciel logiciel orienté objet en pleine croissance ----- 2,50 $ 
 (BST) House MD Saison 1 ----- 3,00 $ 
 (BST) House MD Saison 7 ----- 3,45 $ 
 (IMD) House MD Saison 7 ----- 1,73 .__ $. SOUS-TOTAL ----- 184,47 $ 
 TOTAL FISCAL ----- 10,68 $ 
 TOTAL ----- 195,15 $ 
---------------- MERCI DE NOUS AVOIR CHOISI ----------------

Vous devriez certainement jeter un coup d'oeil à ces livres :-)

Juste une mise en garde: ma solution est encore très incomplète, je me suis concentrée sur le scénario du chemin du bonheur afin de disposer d'une bonne base sur laquelle construire.

14
Jordão

Hormis le fait que vous utilisez une classe appelée product, vous n'avez pas démontré que vous savez ce qu'est un héritage, vous n'avez pas créé plusieurs héritages classés de Product, aucun polymorphisme. Le problème aurait pu être résolu en utilisant plusieurs concepts OOP (même juste pour montrer que vous les connaissez). Il s’agit d’un problème d’entrevue, vous voulez donc montrer à quel point vous le savez.

Je ne voudrais cependant pas me transformer en dépression pour le moment. Le fait que vous ne les ayez pas démontrés ici ne signifie pas que vous ne les connaissez pas déjà ou que vous ne pouvez pas les apprendre.

Vous avez juste besoin d’un peu plus d’expérience avec OOP ou entrevues.

Bonne chance!

12
Andrei G

Les personnes qui ont commencé à apprendre à programmer avec OOP n’ont pas beaucoup de difficulté à comprendre ce que cela signifie, car c’est comme dans la vie réelle. Si vous avez des compétences dans une autre famille de programmation que OO, il pourrait être plus difficile à comprendre.

Tout d’abord, éteignez votre écran ou quittez votre IDE préféré. Prenez un paper et un pencil et dressez une liste de entités, relations, personnes, machines , processus, truc, etc. tout qui pourrait être rencontré dans votre programme final.

Deuxièmement, essayez d’obtenir les différentes entités basic. Vous comprendrez que certains peuvent partager propriétés ou capacités, vous devez le mettre dans des objets abstraits. Vous devriez commencer à dessiner un schéma de Nice de votre programme.

Ensuite, vous devez définir les fonctionnalités (méthodes, fonctions, sous-programmes, appelez-le comme vous le souhaitez): par exemple, un objet produit ne doit pas pouvoir utiliser calculer la taxe de vente. Un objet moteur de vente devrait l'être.

Ne vous inquiétez pas des gros mots (interfaces, propriétés, polymorphisme, héritage, etc.) et des modèles de conception dans une première fois, n'essayez même pas de faire du beau code ou quoi que ce soit ... Pensez simplement aux objets simples objets et interractions entre et comme dans la vie réelle

Après, essayez de lire une littérature sérieuse et concise à ce sujet. Je pense que Wikipedia et Wikibooks sont un très bon moyen de commencer et ensuite de lire des informations sur GoF et Design Patterns et UML .

9
smonff

Premièrement, ne mélangez pas les classes Product avec la classe Receipt (ShoppingCart), la quantity devrait faire partie de ReceipItem (ShoppingCartItem), ainsi que Tax & Cost. La TotalTax & TotalCost devrait faire partie de ShoppingCart.

Ma classe Product n'a que Name & Price & certaines propriétés en lecture seule comme IsImported:

class Product
{
    static readonly IDictionary<ProductType, string[]> productType_Identifiers = 
        new Dictionary<ProductType, string[]>
        {
            {ProductType.Food, new[]{ "chocolate", "chocolates" }},
            {ProductType.Medical, new[]{ "pills" }},
            {ProductType.Book, new[]{ "book" }}
        };

    public decimal ShelfPrice { get; set; }

    public string Name { get; set; }

    public bool IsImported { get { return Name.Contains("imported "); } }

    public bool IsOf(ProductType productType)
    {
        return productType_Identifiers.ContainsKey(productType) &&
            productType_Identifiers[productType].Any(x => Name.Contains(x));
    }
}

class ShoppringCart
{
    public IList<ShoppringCartItem> CartItems { get; set; }

    public decimal TotalTax { get { return CartItems.Sum(x => x.Tax); } }

    public decimal TotalCost { get { return CartItems.Sum(x => x.Cost); } }
}

class ShoppringCartItem
{
    public Product Product { get; set; }

    public int Quantity { get; set; }

    public decimal Tax { get; set; }

    public decimal Cost { get { return Quantity * (Tax + Product.ShelfPrice); } }
}

Votre calcul de taxe est associé à Product. Un produit ne définit pas les politiques fiscales, ce sont des classes de taxes. Selon la description du problème, il existe deux types de taxes de vente: les taxes Basic et Duty. Vous pouvez utiliser Template Method Design Pattern pour y parvenir:

abstract class SalesTax
{
    abstract public bool IsApplicable(Product item);
    abstract public decimal Rate { get; }

    public decimal Calculate(Product item)
    {
        if (IsApplicable(item))
        {
            //sales tax are that for a tax rate of n%, a shelf price of p contains (np/100)
            var tax = (item.ShelfPrice * Rate) / 100;

            //The rounding rules: rounded up to the nearest 0.05
            tax = Math.Ceiling(tax / 0.05m) * 0.05m;

            return tax;
        }

        return 0;
    }
}

class BasicSalesTax : SalesTax
{
    private ProductType[] _taxExcemptions = new[] 
    { 
        ProductType.Food, ProductType.Medical, ProductType.Book 
    };

    public override bool IsApplicable(Product item)
    {
        return !(_taxExcemptions.Any(x => item.IsOf(x)));
    }

    public override decimal Rate { get { return 10.00M; } }
}

class ImportedDutySalesTax : SalesTax
{
    public override bool IsApplicable(Product item)
    {
        return item.IsImported;
    }

    public override decimal Rate { get { return 5.00M; } }
}

Et enfin une classe pour appliquer les taxes:

class TaxCalculator
{
    private SalesTax[] _Taxes = new SalesTax[] { new BasicSalesTax(), new ImportedDutySalesTax() };

    public void Calculate(ShoppringCart shoppringCart)
    {
        foreach (var cartItem in shoppringCart.CartItems)
        {
            cartItem.Tax = _Taxes.Sum(x => x.Calculate(cartItem.Product));
        }

    }
}

Vous pouvez les essayer à MyFiddle .

4
Daniel B

Une mise en œuvre parfaite OOP est totalement discutable. D'après ce que je vois dans votre question, vous pouvez modéliser le code en fonction du rôle qu'il joue pour calculer le prix final, comme Produit, Taxe, ProduitDB, etc.

  1. Product pourrait être une classe abstraite et les types dérivés tels que Books, Food pourraient en être hérités. L’applicabilité fiscale peut être décidée par les types dérivés. Produit indiquerait si la taxe est applicable ou non en fonction de la classe dérivée.

  2. TaxCriteria peut être une énumération et cela peut être spécifié lors de l'achat (importé, applicabilité de la taxe de vente).

  3. La classe Tax calculera la taxe sur la base de TaxCriteria.

  4. Avoir une ShoppingCartItem comme suggéré par XXBBCC peut encapsuler les instances Product et Tax et constitue un excellent moyen de séparer les détails du produit avec la quantité, le prix total avec la taxe, etc.

Bonne chance.

2
Karthik

D'un point de vue strictement OOA/D, un problème majeur que je vois est que la plupart de vos attributs de classe ont le nom redondant de la classe dans le nom d'attribut. par exemple. produit Prix, typeDe produit. Dans ce cas, partout où vous utilisez cette classe, vous aurez un code excessivement verbeux et quelque peu déroutant, par exemple. product.productName. Supprimez le préfixe/suffixe de nom de classe redondant de vos attributs.

En outre, je n'ai vu aucune classe concernée par l'achat et la création d'un reçu, comme cela a été demandé dans la question.

1
Peter Cetinski

Voici un excellent exemple de modèle OO pour les produits, les taxes, etc. Notez l'utilisation d'interfaces, essentielle dans la conception de OO.

http://www.dreamincode.net/forums/topic/185426-design-patterns-strategy/

1
Chris Gessler

Les principes SOLID constituent un très bon point de départ pour les règles de conception.

Par exemple, le principe Open Closed indique que si vous souhaitez ajouter une nouvelle fonctionnalité, vous n'avez pas à ajouter de code à la classe existante, mais plutôt à ajouter une nouvelle classe.

Pour votre exemple d'application, cela signifierait que l'ajout d'une nouvelle taxe de vente nécessiterait l'ajout d'une nouvelle classe. Il en va de même pour différents produits qui sont des exceptions à la règle.

La règle d'arrondi va évidemment dans une classe séparée - le principe de responsabilité unique stipule que chaque classe a une responsabilité unique.

Je pense qu'essayer d'écrire le code vous-même apporterait bien plus d'avantages que d'écrire une bonne solution et de la coller ici. 

Un algorithme simple pour écrire le programme conçu parfait serait:

  1. Écrivez un code qui résout le problème 
  2. Vérifiez si le code est conforme aux principes SOLID 
  3. S'il y a des violations de règles que goto 1.
1
devdimi

Attaque du problème Coût avec impôt en utilisant un modèle de visiteur.

public class Tests
    {
        [SetUp]
        public void Setup()
        {
        }

        [Test]
        public void Input1Test()
        {
            var items = new List<IItem> {
                new Book("Book", 12.49M, 1, false),
                new Other("Music CD", 14.99M, 1, false),
                new Food("Chocolate Bar", 0.85M, 1, false)};

            var visitor = new ItemCostWithTaxVisitor();

            Assert.AreEqual(12.49, items[0].Accept(visitor));
            Assert.AreEqual(16.49, items[1].Accept(visitor));
            Assert.AreEqual(0.85, items[2].Accept(visitor));
        }

        [Test]
        public void Input2Test()
        {
            var items = new List<IItem> {
                new Food("Bottle of Chocolates", 10.00M, 1, true),
                new Other("Bottle of Perfume", 47.50M, 1, true)};

            var visitor = new ItemCostWithTaxVisitor();

            Assert.AreEqual(10.50, items[0].Accept(visitor));
            Assert.AreEqual(54.65, items[1].Accept(visitor));
        }

        [Test]
        public void Input3Test()
        {
            var items = new List<IItem> {
                new Other("Bottle of Perfume", 27.99M, 1, true),
                new Other("Bottle of Perfume", 18.99M, 1, false),
                new Medicine("Packet of headache pills", 9.75M, 1, false),
                new Food("Box of Chocolate", 11.25M, 1, true)};

            var visitor = new ItemCostWithTaxVisitor();

            Assert.AreEqual(32.19, items[0].Accept(visitor));
            Assert.AreEqual(20.89, items[1].Accept(visitor));
            Assert.AreEqual(9.75, items[2].Accept(visitor));
            Assert.AreEqual(11.80, items[3].Accept(visitor));
        }
    }

    public abstract class IItem : IItemVisitable
    { 
        public IItem(string name,
            decimal price,
            int quantity,
            bool isImported)
            {
                Name = name;
                Price = price;
                Quantity = quantity;
                IsImported = isImported;
            }

        public string Name { get; set; }
        public decimal Price { get; set; }
        public int Quantity { get; set; }
        public bool IsImported { get; set; }

        public abstract decimal Accept(IItemVisitor visitor);
    }

    public class Other : IItem, IItemVisitable
    {
        public Other(string name, decimal price, int quantity, bool isImported) : base(name, price, quantity, isImported)
        {
        }

        public override decimal Accept(IItemVisitor visitor) => Math.Round(visitor.Visit(this), 2);
    }

    public class Book : IItem, IItemVisitable
    {
        public Book(string name, decimal price, int quantity, bool isImported) : base(name, price, quantity, isImported)
        {
        }

        public override decimal Accept(IItemVisitor visitor) => Math.Round(visitor.Visit(this),2);
    }

    public class Food : IItem, IItemVisitable
    {
        public Food(string name, decimal price, int quantity, bool isImported) : base(name, price, quantity, isImported)
        {
        }

        public override decimal Accept(IItemVisitor visitor) => Math.Round(visitor.Visit(this), 2);
    }

    public class Medicine : IItem, IItemVisitable
    {
        public Medicine(string name, decimal price, int quantity, bool isImported) : base(name, price, quantity, isImported)
        {
        }

        public override decimal Accept(IItemVisitor visitor) => Math.Round(visitor.Visit(this), 2);
    }

    public interface IItemVisitable
    {
        decimal Accept(IItemVisitor visitor);
    }

    public class ItemCostWithTaxVisitor : IItemVisitor
    {
        public decimal Visit(Food item) => CalculateCostWithTax(item);

        public decimal Visit(Book item) => CalculateCostWithTax(item);

        public decimal Visit(Medicine item) => CalculateCostWithTax(item);

        public decimal CalculateCostWithTax(IItem item) => item.IsImported ?
            Math.Round(item.Price * item.Quantity * .05M * 20.0M, MidpointRounding.AwayFromZero) / 20.0M + (item.Price * item.Quantity)
            : item.Price * item.Quantity;

        public decimal Visit(Other item) => item.IsImported ?
            Math.Round(item.Price * item.Quantity * .15M * 20.0M, MidpointRounding.AwayFromZero) / 20.0M + (item.Price * item.Quantity)
            : Math.Round(item.Price * item.Quantity * .10M * 20.0M, MidpointRounding.AwayFromZero) / 20.0M + (item.Price * item.Quantity);
    }

    public interface IItemVisitor
    {
        decimal Visit(Food item);
        decimal Visit(Book item);
        decimal Visit(Medicine item);
        decimal Visit(Other item);
    }
0
LucidCoder