Jak stworzyć idealną aplikację OOP [zamknięte]

98

Ostatnio starałem się o firmę „x”. Przysłali mi kilka pytań i kazali rozwiązać tylko jedno.

Problem jest taki -

Podstawowy podatek od sprzedaży wynosi 10% na wszystkie towary, z wyjątkiem książek, żywności i produktów medycznych, które są zwolnione.
Cło przywozowe to dodatkowy podatek od sprzedaży, który ma zastosowanie do wszystkich importowanych towarów i wynosi 5%, bez zwolnień.

Kiedy kupuję przedmioty, otrzymuję pokwitowanie, które zawiera nazwę wszystkich pozycji i ich cenę (wraz z podatkiem), kończąc na całkowitym koszcie towarów i łącznej kwocie zapłaconego podatku od sprzedaży.
Zasady zaokrąglania podatku od sprzedaży są takie, że dla stawki podatku n% cena półkowa p zawiera (np / 100 zaokrągloną w górę do najbliższej 0,05) kwotę podatku od sprzedaży.

„Powiedzieli mi, że są zainteresowani aspektem projektowym Twojego rozwiązania i chcieliby ocenić moje umiejętności programowania obiektowego ”.

Oto, co powiedzieli własnymi słowami

  • Do rozwiązania chcielibyśmy, abyś użył języka Java, Ruby lub C #.
  • Jesteśmy zainteresowani ASPEKTEM PROJEKTOWYM Twojego rozwiązania i chcielibyśmy ocenić Twoje umiejętności programowania obiektowego .
  • Możesz używać zewnętrznych bibliotek lub narzędzi do celów budowania lub testowania. W szczególności możesz użyć bibliotek testów jednostkowych lub narzędzi do budowania dostępnych dla wybranego języka (np. JUnit, Ant, NUnit, NAnt, Test :: Unit, Rake itp.)
  • Opcjonalnie możesz również dołączyć krótkie wyjaśnienie projektu i założeń wraz z kodem.
  • Prosimy pamiętać, że NIE oczekujemy aplikacji internetowej ani wszechstronnego interfejsu użytkownika. Spodziewamy się raczej prostej, opartej na konsoli aplikacji i interesuje nas Twój kod źródłowy.

Więc podałem poniższy kod - możesz po prostu skopiować kod wklej i uruchomić w 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
     }
 }

możesz rozpakować wejście i uruchomić dla różnych wejść.

Podałem rozwiązanie, ale zostałem odrzucony.

„Powiedzieli, że nie mogą brać pod uwagę moich obecnych otwartych pozycji, ponieważ kod nie jest zadowalający”.

Proszę, prowadź mnie, czego tu brakuje. Czy to rozwiązanie nie jest dobrym rozwiązaniem OOAD.
Jak mogę poprawić swoje umiejętności OOAD.
Moi seniorzy również mówią, że idealna aplikacja OOAD również nie zadziała praktycznie.

Dzięki

oddzielać się
źródło
2
Może spodziewali się, że będziesz rozróżniać typy produktów za pomocą hierarchii dziedziczenia, a nie wyliczenia? (Chociaż myślę, że to podejście byłoby raczej zawiłe dla danego scenariusza.)
Douglas,
Domyślam się, że odrzucili Twoje rozwiązanie msotly, ponieważ nie zdefiniowałeś żadnych interfejsów.
Chris Gessler,
28
Z reguły, jeśli ktoś prosi cię w trakcie rozmowy o zademonstrowanie umiejętności OOP, powinieneś spróbować unikać używania instrukcji switch - zamiast tego użyj hierarchii dziedziczenia.
Joe
4
Powinien zostać opublikowany w przeglądzie kodu.
Derek
Napisałem tam również, ale nie mogłem tam znaleźć dobrego rozwiązania. Ale każdy może zobaczyć moje nowe rozwiązanie, które stworzyłem po pomocy innych codeproject.com/Questions/332077/ ... tutaj możesz również znaleźć mój nowy kod.
sund

Odpowiedzi:

246

Po pierwsze, niebiosa nie wykonują podwójnych obliczeń finansowych . Wykonuj obliczenia finansowe w systemie dziesiętnym ; do tego służy. Używaj podwójnego do rozwiązywania problemów fizycznych , a nie finansowych .

Główną wadą projektową twojego programu jest to, że polityka jest w niewłaściwym miejscu . Kto jest odpowiedzialny za obliczanie podatków? To produkt jest odpowiedzialny za obliczanie podatków, ale kiedy kupujesz jabłko, książkę lub pralkę, rzecz, którą zamierzasz kupić, nie jest odpowiedzialna za określenie, ile podatku zamierzasz zapłacić to. Za poinformowanie Cię o tym odpowiada polityka rządu . Twój projekt w znacznym stopniu narusza podstawową zasadę projektowania obiektów obiektowych, zgodnie z którą obiekty powinny być odpowiedzialne za własne sprawy , a nie za nic innego. Problemem pralki jest pranie ubrań, a nie pobieranie odpowiedniego cła przywozowego. Jeśli zmieniają się przepisy podatkowe, nie chcesz się zmieniaćpralka , chcesz zmienić obiekt zasad .

Jak więc podejść do tego rodzaju problemów w przyszłości?

Zacząłbym od podkreślenia wszystkich ważnych rzeczowników w opisie problemu:

Podstawowy podatek od sprzedaży ma zastosowanie w tempie 10% na wszystkie towary , z wyjątkiem książek , żywności i wyrobów medycznych , które są zwolnione z podatku. Cło przywozowe to dodatkowy podatek od sprzedaży, który ma zastosowanie do wszystkich importowanych towarów i wynosi 5%, bez zwolnień . Kiedy kupuję przedmioty , otrzymuję paragon, który zawiera nazwę wszystkich przedmiotów i ich cenę (w tym podatek ), kończąc na całkowitym koszciepozycji oraz łączne kwoty zapłaconych podatków od sprzedaży . Zasady zaokrąglania podatku od sprzedaży są takie, że dla stawki podatku n% cena półki p zawiera (np / 100 zaokrągloną w górę do najbliższej 0,05) kwotę podatku od sprzedaży .

Jakie są relacje między tymi wszystkimi rzeczownikami?

  • Podstawowy podatek od sprzedaży to rodzaj podatku od sprzedaży
  • Cło importowe jest rodzajem podatku od sprzedaży
  • Podatek od sprzedaży ma stawkę dziesiętną
  • Książki są rodzajem Przedmiotu
  • Jedzenie to rodzaj przedmiotu
  • Produkty medyczne są rodzajem Przedmiotu
  • Przedmioty mogą być towarami importowanymi
  • Przedmiot ma nazwę, która jest ciągiem
  • Przedmiot ma cenę półkową, która jest liczbą dziesiętną. (Uwaga: czy produkt naprawdę ma cenę? Dwie identyczne pralki mogą być sprzedawane po różnych cenach w różnych sklepach lub w tym samym sklepie w różnym czasie. Lepszym projektem może być stwierdzenie, że polityka cenowa odnosi się do produktu jego cena.)
  • Zasady zwolnienia z podatku od sprzedaży opisują warunki, w których podatek od sprzedaży nie ma zastosowania do towaru.
  • Paragon zawiera listę towarów, ich ceny i podatki.
  • Rachunek ma sumę
  • Paragon zawiera całkowity podatek

... i tak dalej. Po opracowaniu wszystkich relacji między wszystkimi rzeczownikami możesz rozpocząć projektowanie hierarchii klas. Istnieje abstrakcyjna klasa bazowa Item. Książka dziedziczy po nim. Istnieje abstrakcyjna klasa SalesTax; BasicSalesTax dziedziczy po nim. I tak dalej.

Eric Lippert
źródło
12
potrzebujesz więcej niż to, co zostało dostarczone? Wygląda na to, że musisz dowiedzieć się więcej o tym, jak wdrażane jest dziedziczenie i czym jest polimorfizm.
Induster
27
@sunder: Ta odpowiedź jest więcej niż wystarczająca. Twoim obowiązkiem jest teraz rozwijanie swoich umiejętności, być może używając tego jako pierwszego przykładu. Zwróć uwagę, że twój przykład jest definicją przykładu z życia wziętego. Oblałeś prawdziwą rozmowę kwalifikacyjną, ponieważ ten kod z życia wzięty wymagał projektu z życia wziętego, którego nie dostarczyłeś.
Greg D
9
@Narayan: doublejest idealny w sytuacjach, w których znajdowanie się w granicach 0,00000001% prawidłowej odpowiedzi jest więcej niż wystarczające. Jeśli chcesz dowiedzieć się, jak szybko spada cegła po pół sekundy, wykonaj obliczenia w grze podwójnej. Kiedy wykonujesz arithemę finansową w grach podwójnych, otrzymujesz odpowiedzi, takie jak cena po opodatkowaniu wynosi 43,79999999999999 dolarów, a to po prostu wygląda głupio, mimo że jest bardzo bliskie poprawnej odpowiedzi.
Eric Lippert
31
+1 Podkreśliłeś niezwykłe ćwiczenie, które polega na zbadaniu każdego rzeczownika w zadaniu, a następnie wyliczeniu ich wzajemnych relacji. Świetny pomysł.
Chris Tonkinson,
3
@ Jordão: Dziesięciokrotne dodanie 0,10 daje 1,00. Ale dodanie 1,0 / 333,0 trzysta trzydzieści trzy razy niekoniecznie daje wynik w postaci dziesiętnej lub podwójnej. W systemie dziesiętnym ułamki, które mają potęgę dziesięciu w mianowniku, są dokładnie reprezentowane; w grze podwójnej są to ułamki o potęgach dwóch. Wszystko inne jest reprezentowane w przybliżeniu.
Eric Lippert
38

Jeśli firma powie coś o bibliotekach takich jak NUnit, JUnit lub Test :: Unit, jest więcej niż prawdopodobne, że TDD jest dla nich naprawdę ważne. W twoim przykładzie kodu nie ma żadnych testów.

Postaram się wykazać praktyczną znajomość:

  • Testy jednostkowe (np. NUnit)
  • Mocking (np. RhinoMocks)
  • Trwałość (np. NHibernate)
  • Kontenery IoC (np. NSpring)
  • wzorce projektowe
  • Zasada SOLID

Chciałbym zarekomendować stronę www.dimecasts.net jako imponujące źródło bezpłatnych, dobrej jakości screencastów, które obejmują wszystkie wyżej wymienione tematy.

Radek
źródło
19

Jest to wysoce subiektywne, ale oto kilka uwag, które chciałbym poruszyć na temat twojego kodu:

  • Moim zdaniem zmieszałeś Producti ShoppingCartItem. ProductPowinien zawierać nazwę produktu, status podatkowy itp., ale nie ilość. Ilość nie jest właściwością produktu - będzie inna dla każdego klienta firmy, która kupi dany produkt.

  • ShoppingCartItempowinien mieć a Producti ilość. W ten sposób klient może swobodnie kupić mniej więcej ten sam produkt. Przy obecnej konfiguracji nie jest to możliwe.

  • Obliczanie ostatecznego podatku również nie powinno być częścią Product- powinno być częścią czegoś podobnego, ShoppingCartponieważ ostateczne obliczenie podatku może obejmować znajomość wszystkich produktów w koszyku.

xxbbcc
źródło
Jedyny problem, jaki mam z tą odpowiedzią, polega na tym, że opisuje ona, jak zbudować lepszy system płatności za produkt (który jest ważny), ale tak naprawdę nie opisuje metodologii OOP. Można to zaimplementować w dowolnym języku. Bez pokazania jakichś interfejsów, dziedziczenia, polimorfizmu itp. I tak nie zdałby testu.
Timeout
Odnosząc się do ostatniego punktu: IMO najlepszym miejscem do kalkulacji podatku jest odrębna klasa TaxCalculator ze względu na zasadę jednej odpowiedzialności.
Radek
dzięki za odpowiedź, ale jakie to praktyczne. czy każda firma pracuje w tak rozbudowanych i czystych modelach OOPS.
sund
@shyamsunder W mojej odpowiedzi nie ma nic naprawdę czystego. Nie używa interfejsów / dziedziczenia, które są ważnymi aspektami OOD, ale pokazuje najważniejszą zasadę - moim zdaniem - a mianowicie stawianie obowiązków tam, gdzie ich miejsce. Jak wskazywały inne odpowiedzi, głównym problemem związanym z projektem jest to, że pomieszałeś obowiązki różnych aktorów, co doprowadzi do problemów podczas dodawania funkcji. Większość dużych programów może się rozwijać tylko wtedy, gdy przestrzegają tych zasad.
xxbbcc,
Dobra odpowiedź, ale zgadzam się również, że wyliczenie podatku powinno być osobnym przedmiotem.
14

Przede wszystkim jest to bardzo dobre pytanie do wywiadu. To dobry miernik wielu umiejętności.

Jest wiele rzeczy, które musisz zrozumieć, aby udzielić dobrej odpowiedzi (nie ma idealnej odpowiedzi), zarówno na wysokim, jak i niskim poziomie. Oto kilka:

  • Modelowanie domen -> jak stworzyć dobry model rozwiązania? Jakie obiekty tworzysz? Jak rozwiążą wymagania? Szukanie rzeczowników to dobry początek, ale jak zdecydować, czy dobór jednostek jest dobry? Jakich innych podmiotów potrzebujesz? Jakiej wiedzy o domenie potrzebujesz, aby go rozwiązać?
  • Oddzielenie obaw, luźne powiązania, wysoka spójność -> Jak wyodrębnić części projektu, które mają różne obawy lub zmiany i jak je odnieść? W jaki sposób zapewniasz elastyczność i aktualność swojego projektu?
  • Testy jednostkowe, refaktoryzacja, TDD -> Jaki jest Twój proces tworzenia rozwiązania? Piszesz testy, używasz pozorowanych obiektów, refaktoryzujesz, iterujesz?
  • Czysty kod, idiomy językowe -> Czy korzystasz z funkcji swojego języka programowania, aby Ci pomóc? Piszesz zrozumiały kod? Czy twoje poziomy abstrakcji mają sens? Jak łatwy do utrzymania jest kod?
  • Narzędzia : czy używasz kontroli źródła? Tworzyć narzędzia? IDE?

Stamtąd możesz przeprowadzić wiele interesujących dyskusji, obejmujących zasady projektowania (takie jak zasady SOLID), wzorce projektowe, wzorce analizy, modelowanie domeny, wybory technologiczne, ścieżki przyszłej ewolucji (np. Co jeśli dodam bazę danych lub bogatą warstwę interfejsu użytkownika, co należy zmienić?), kompromisy, wymagania niefunkcjonalne (wydajność, łatwość konserwacji, bezpieczeństwo, ...), testy akceptacyjne itp.

Nie będę komentował, jak powinieneś zmienić swoje rozwiązanie, po prostu powinieneś bardziej skupić się na tych koncepcjach.

Ale mogę pokazać, jak (częściowo) rozwiązałem ten problem , na przykładzie (w Javie). Zajrzyj do Programklasy, aby zobaczyć, jak to wszystko składa się na wydruk tego potwierdzenia:

------------------ TO TWOJE ZAMÓWIENIE ------------------
(001) Projektowanie oparte na domenie ----- 69,99 USD
(001) Rosnące oprogramowanie zorientowane obiektowo ----- 49,99 USD
(001) House MD Season 1 ----- 29,99 $
(001) House MD sezon 7 ----- 34,50 USD
(IMD) Rosnące oprogramowanie zorientowane obiektowo ----- 2,50 USD
(BST) House MD sezon 1 ----- 3,00 USD
(BST) House MD sezon 7 ----- 3,45 USD
(IMD) House MD Season 7 ----- 1,73 USD
                                SUMA CZĘŚCIOWA ----- 184,47 USD
                                SUMA PODATKU ----- 10,68 USD
                                    RAZEM ----- 195,15 $
---------------- DZIĘKUJEMY ZA WYBRANIE NAS ----------------

Zdecydowanie powinieneś rzucić okiem na te książki :-)

Tylko jako zastrzeżenie: moje rozwiązanie jest nadal bardzo niekompletne, po prostu skupiłem się na scenariuszu szczęśliwej ścieżki, aby mieć dobre podstawy do budowania.

Jordão
źródło
Przejrzałem twoje rozwiązanie i uznałem je za całkiem interesujące. Chociaż uważam, że klasa Order nie powinna odpowiadać za wydrukowanie Reciept. Podobnie klasa TaxMethod nie powinna być odpowiedzialna za obliczanie podatku. Ponadto TaxMethodPractice nie powinien zawierać listy TaxMethod. Zamiast tego klasa o nazwie SalesPolicy powinna zawierać tę listę. Do klasy o nazwie SalesEngine należy przekazać SalesPolicy, Order i TaxCalculator. SalesEngine zastosuje SalesPolicy do towarów w zamówieniu i obliczy podatek za pomocą TaxCalculator
CKing
@bot: ciekawe spostrzeżenia… W tej chwili Orderdrukuje paragon, ale Receiptwie o własnym formatowaniu. Ponadto TaxMethodPractice jest rodzajem polityki podatkowej, zawiera wszystkie podatki, które mają zastosowanie w określonym scenariuszu. TaxMethods to kalkulatory podatkowe. Czuję, że brakuje ci tylko jakiejś klasy wiązania wyższego poziomu , takiej jak proponowany SalesEngine. To ciekawy pomysł.
Jordão,
Po prostu czuję, że każda klasa musi mieć jedną, dobrze zdefiniowaną odpowiedzialność, a klasy, które reprezentują przedmioty ze świata rzeczywistego, powinny zachowywać się w sposób zgodny z rzeczywistym światem. Z tego powodu TaxMethod można podzielić na dwie klasy. TaxCriteria i TaxCalculator. Podobnie Zamówienie nie może drukować paragonu. Aby wygenerować paragon, do generatora pokwitowań należy przekazać pokwitowanie.
CKing
@bot: Całkowicie się zgadzam! Dobre projekty są SOLIDNE ! TaxMethod to kalkulator podatkowy, a TaxEligibilityCheck to kryterium podatkowe. Są oddzielnymi bytami. Jeśli chodzi o paragon, tak, rozdzielenie części wytwarzającej dodatkowo poprawiłoby projekt.
Jordão
1
Ten pomysł pochodzi ze wzoru specyfikacji , spójrz!
Jordão
12

Oprócz tego, że używasz klasy o nazwie product, nie wykazałeś, że wiesz, czym jest dziedziczenie, nie utworzyłeś dziedziczenia wielokrotnego z klasy Product, żadnego polimorfizmu. Problem mógł zostać rozwiązany przy użyciu wielu koncepcji OOP (nawet po to, aby pokazać, że je znasz). To jest problem z rozmową kwalifikacyjną, więc chcesz pokazać, ile wiesz.

Nie zamieniłbym się jednak teraz w depresję. To, że ich tutaj nie zademonstrowałeś, nie oznacza, że ​​już ich nie znasz lub nie jesteś w stanie się ich nauczyć.

Potrzebujesz tylko trochę więcej doświadczenia z OOP lub wywiadami.

Powodzenia!

Andrei G.
źródło
właściwie to był mój pierwszy projekt, stworzyłem inny, ale nie mogę ci pokazać, ponieważ przekracza limit znaków.
sund
czy możesz to zademonstrować na jakimkolwiek przykładzie.
sunder
@sunder: Możesz po prostu zaktualizować pytanie o nowy projekt.
Bjarke Freund-Hansen
10

Osoby, które zaczęły uczyć się programowania z OOP, nie mają wielkich problemów ze zrozumieniem, co to znaczy, ponieważ jest tak jak w prawdziwym życiu . Jeśli masz umiejętności w zakresie programowania z innej rodziny niż OO, może to być trudniejsze do zrozumienia.

Przede wszystkim wyłącz ekran lub wyjdź z ulubionego środowiska IDE. Weź papier i ołówek i zrób listę bytów , relacji , ludzi , maszyn , procesów , rzeczy itp. Wszystko , co można napotkać w twoim ostatecznym programie.

Po drugie, spróbuj zdobyć różne podstawowe jednostki. Zrozumiesz, że niektórzy mogą mieć wspólne właściwości lub zdolności , musisz umieścić to w abstrakcyjnych obiektach . Powinieneś zacząć rysować ładny schemat swojego programu.

Następnie musisz podać funkcje (metody, funkcje, podprogramy, nazwij je tak, jak chcesz): na przykład obiekt produktu nie powinien mieć możliwości obliczenia podatku od sprzedaży . Obiekt silnika sprzedaży powinien.

Nie kłopocz się wszystkimi wielkimi słowami ( interfejsami , właściwościami , polimorfizmem , dziedzictwem itp.) I wzorcami projektowymi za pierwszym razem, nawet nie próbuj tworzyć pięknego kodu lub czegokolwiek ... Po prostu pomyśl o prostych obiektach i interakcje między nim jak w prawdziwym życiu .

Potem spróbuj przeczytać jakąś poważną, zwięzłą literaturę na ten temat. Myślę, że Wikipedia i Wikibooks to naprawdę dobry sposób na rozpoczęcie, a potem po prostu przeczytanie rzeczy o GoF i wzorcach projektowych oraz UML .

smonff
źródło
3
+1 dla „Przede wszystkim wyłącz ekran”. Myślę, że moc myślenia zbyt często mylona jest z mocą komputerów.
kontur
1
+1 za najprostsze podejście do używania ołówka i papieru. Wiele razy ludzie są zdezorientowani siedząc przed IDE :)
Neeraj Gulia
Niektórzy naukowcy powiedzieli, że nasz mózg nie zwraca uwagi na ekran. Kiedy studiuję projektowanie architektury oprogramowania, nasz nauczyciel zmusza nas do pracy na papierze. Nie przeszkadza mu potężne oprogramowanie UML. Ważne jest, aby najpierw zrozumieć rzeczy.
smonff
4

Najpierw nie mieszaj Productklasy z klasą Receipt ( ShoppingCart), quantitypowinna być częścią ReceipItem( ShoppingCartItem), a także Tax& Cost. TotalTaxI TotalCostpowinny być częścią ShoppingCart.

Moja Productklasa ma tylko Name& Price& kilka właściwości tylko do odczytu, takich jak 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); } }
}

Twoja część obliczania podatku jest połączona z Product. Produkt nie definiuje zasad podatkowych, jest to klasy podatkowe. Opierając się na opisie problemu, istnieją dwa rodzaje podatków od sprzedaży: Basici Dutypodatki. Możesz użyć, Template Method Design Patternaby to osiągnąć:

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; } }
}

I wreszcie klasa do naliczania podatków:

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));
        }

    }
}

Możesz je wypróbować na MyFiddle .

Daniel B.
źródło
2

Bardzo dobrym punktem wyjścia do zasad projektowania są zasady SOLID .

Na przykład zasada Open Closed mówi, że jeśli chcesz dodać nową funkcjonalność, nie musisz dodawać kodu do istniejącej klasy, ale raczej dodać nową klasę.

W przypadku przykładowej aplikacji oznaczałoby to, że dodanie nowego podatku od sprzedaży wymagałoby dodania nowej klasy. To samo dotyczy różnych produktów, które są wyjątkami od reguły.

Zasada zaokrąglania oczywiście dotyczy oddzielnych klas - zasada pojedynczej odpowiedzialności mówi, że każda klasa ma jedną odpowiedzialność.

Myślę, że próba samodzielnego napisania kodu przyniosłaby znacznie więcej korzyści niż zwykłe napisanie dobrego rozwiązania i wklejenie go tutaj.

Prosty algorytm do napisania idealnie zaprojektowanego programu to:

  1. Napisz kod, który rozwiązuje problem
  2. Sprawdź, czy kod jest zgodny z zasadami SOLID
  3. Jeśli występują naruszenia zasad niż goto 1.
devdimi
źródło
2

Doskonała implementacja OOP jest całkowicie dyskusyjna. Z tego, co widzę w Twoim pytaniu, możesz modularyzować kod w oparciu o rolę, jaką pełnią w obliczaniu ostatecznej ceny, takiej jak produkt, podatek, baza danych produktu i tak dalej.

  1. Productmoże być klasą abstrakcyjną, a typy pochodne, takie jak Books, Food, mogą być z niej dziedziczone. O zastosowaniu podatku można decydować na podstawie typów pochodnych. Produkt wskazywałby, czy podatek ma zastosowanie, czy nie na podstawie klasy pochodnej.

  2. TaxCriteria może być wyliczeniem, które można określić podczas zakupu (import, zastosowanie podatku od sprzedaży).

  3. Taxclass obliczy podatek na podstawie TaxCriteria.

  4. Posiadanie, ShoppingCartItemzgodnie z sugestią XXBBCC, może zawierać przypadki produktu i podatku i jest to świetny sposób na segregację szczegółów produktu z ilością, całkowitą ceną z podatkiem itp.

Powodzenia.

Karthik
źródło
1

Z punktu widzenia ściśle OOA / D, jednym z głównych problemów, które widzę, jest to, że większość atrybutów klas ma nadmiarową nazwę klasy w nazwie atrybutu. np. cena produktu , typ produktu . W takim przypadku wszędzie tam, gdzie używasz tej klasy, będziesz mieć zbyt rozwlekły i nieco mylący kod, np. Product.productName. Usuń zbędne przedrostki / sufiksy nazw klas z atrybutów.

Nie widziałem też żadnych zajęć związanych z zakupem i tworzeniem paragonu, o który pytano w pytaniu.

Peter Cetinski
źródło
1

Oto świetny przykład wzorca OO dla produktów, podatków, itp ... Zwróć uwagę na użycie interfejsów, które jest niezbędne w projektowaniu OO.

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

Chris Gessler
źródło
3
Wolałbym uczynić produkt (abstrakcyjną) klasą niż uczynić z niego interfejs. Nie uczyniłbym też każdego produktu osobną klasą. Co najwyżej utworzyłbym jedną klasę na kategorię.
CodesInChaos
@CodeInChaos - W większości przypadków potrzebujesz obu, ale jeśli próbujesz zdobyć pracę jako architekt, zdecydowałbym się zaimplementować interfejsy zamiast klasy abstrakcyjnej.
Chris Gessler
1
Interfejsy w tym przykładzie nie mają żadnego sensu. Prowadzą tylko do powielania kodu w każdej implementującej je klasie. Każda klasa implementuje to w ten sam sposób.
Piotr Perak
0

Zaatakowano problem kosztów z podatkiem przy użyciu wzorca gości.

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);
    }
LucidCoder
źródło
Witamy w stackoverflow. Prosimy o wyjaśnienie swojej odpowiedzi w odpowiedzi na pytanie. PO nie tylko szuka rozwiązania, ale także dlaczego rozwiązanie jest lepsze / gorsze.
Simon.SA