我申请了应用程序开发人员职位。他们要求所有申请人完成3个编程任务中的1个。我选择了一个进行营业税计算。这很简单。


编写一个程序以提要价格。如果物品是书籍,食品或
医疗产品,则无需缴税,如果没有,则需缴纳10%的税(其他
物品)。如果该商品是进口商品,则需缴纳5%的税(
可以进口15%税(10 + 5)的进口商品和“其他商品”类型的商品)。
该程序需要打印这样的收据...


书:9.12(2 @ 4.56)
进口狗粮:6.52
香水: 6.23
营业税:1.65
总计:21.87



在这种情况下,您可以看到应用程序可以将项目“分组”并提供税收计算,包括总税。

要描述一下,我的应用程序有一个菜单,继续提示用户输入信息,直到他们退出菜单。产品信息已放入List中。将针对列表写入一个lambda表达式以分组并获取重复项。嵌套在if语句中的foreach语句会触发税收计算以打印出收据。公司表示这“不是面向对象的”。因此,我距离还很遥远,或者可以进行一些调整以满足他们的要求。无论如何,我在2天(限时)内完成了工作。

如何编写这些代码以遵循OOP概念?我不是要有人为我“重写”此内容。我认为我有正确的心态。我只需要有人向正确的方向“轻推”我即可。

评论

从您描述问题的方式来看,我怀疑他们想要Book,Food,Medical和Other子类,而没有它们是一个危险信号。

话虽这么说,我认为一个好的设计真的不需要那些类,而且我不确定我是否想与一家认为可以做到的公司合作...

老实说,我理解公司为什么这么说。该代码对我而言似乎是业余的。它未遵循任何常见的行业标准(命名等)。我真的建议您阅读amazon.es/Clean-code-Handbook-Software-Craftsmanship/dp / ...

如果他们没有真正说出面向对象操作,那么在此基础上让您失望似乎是不合理的。人为通信故障而不是编程故障的情况。是否在任何给定的项目中使用OO代码都是一个意见问题,并且有赞成和反对的理由。因此,实际上,由于缺乏有关此事的任何精确指导,它们实际上使您无法使用自己的判断。哪个不好。

我认为您不应该太重视“不是面向对象的”。他们不喜欢您的代码,而且可能要做的比向您详尽解释为什么不这样做要好得多。

#1 楼

我猜他们正在寻找的是类上的方法。另外,我建议您下载resharper并查看其对命名约定的说明,因为它们不是标准的。

我称之为“经典OO”的方法可能看起来有点像

即使没有真正的要求,继承一些继承也不会受到损害。这些面试测试的目的是向您证明您了解一些东西。它假定您将能够解决问题。

作为指导,我总是提供以下内容:


单元测试-不能太强调这一点!名称匹配测试用例,例如。
givenXThenY(TDD)
名称与业务术语(DDD)匹配的类
这些类(OO)上的方法
具有业务逻辑(SoA)的服务类
一切接口(更多的OO / DI)也显示了继承性
知识*
通过接口(DI)将服务注入到构造函数中
ViewModel对象或其他一些显示分离(MVVM) )
存储库对象(存储库模式/数据层分离)
至少使用一种通用集合
至少使用一种Linq
XML注释所有内容(代码可读性)
描述其功能的方法名称(干净编码)
每个解决方案有多个项目(不包括测试项目)
名称项目,如名称空间,例如YourCompany.Example5.Models
>如果您认为问题有待解决,可以投入其他内容

抽象类/更多继承
模数运算符%的一些测试角度这个,显示哟你知道
数学吗?
异步方法和任务,多线程

*当我说所有内容时,我指的是服务和模型,实际上您并不需要两者,但是有些人喜欢彼此。这些事情很多都是矛盾的,或者是模式的两倍。关键不是编写好的代码,它表明您了解许多模式和编码实践等

评论


\ $ \ begingroup \ $
如果您删除了有关“一切”接口的评论,您将获得+1 :)
\ $ \ endgroup \ $
– RobH
2015年5月1日在8:51

\ $ \ begingroup \ $
从来没有! :)(尽管我已经详细说明了,也许我可以得到1/2分吗?)
\ $ \ endgroup \ $
–伊万
2015年5月1日晚上8:58

\ $ \ begingroup \ $
这就是为什么删除“分配”很糟糕的原因。它评估候选人是否能够对或多或少的琐碎问题应用尽可能多的“良好实践”模式。它完全没有说明应聘者对这些模式的理解,解决问题的能力,标准制定方面的经验,业务要求等。
\ $ \ endgroup \ $
–miki
2015年5月1日18:27

\ $ \ begingroup \ $
...如果您未进行真正的TDD,则绝对不要将其称为TDD。很明显,“ TDD”是“写代码,然后测试”给已经完成TDD一段时间的人的。
\ $ \ endgroup \ $
– enderland
2015年5月1日23:49

\ $ \ begingroup \ $
许多要点与“面向对象设计”无关。当然,他们会展示语言知识。但是要当心:不适当或任意使用语言功能表明编程技能很差。
\ $ \ endgroup \ $
– radarbob
15年5月4日在5:04



#2 楼

我认为他们可能正在寻找您来将职责划分为多个类。

您创建了product类来封装数据,但是到此为止。您已将所有与UI(编写/读取控制台)有关的内容塞进去,并在主Program类中计算税额。 >
public class TaxCalculator
{
    public decimal CalculateTax(Product product)
    {
         // Some code - everyone loves political humour...
         return isMultinationalOrganisation ? 0 : 10000m;
    }
} 


除此之外,还有一些样式点可以依靠一些工作来完成: product应该是Product

方法也应该是Pascal情况customRound应该是CustomRound

customRound到底在干什么。混合使用并杀死一只小狗。

    private decimal _Total_tax;
    public decimal Total_tax
    {
        get
        {
            return this._Total_tax;
        }

        set
        {
            this._Total_tax = value + Total_tax;
        }
    }


字段应为驼峰式。无论是string.Format还是totalTax

保持连续运行的设置器不是一个好主意。考虑我的示例:

 TotalTax = 10;
 TotalTax = 15;
 TotalTax == 15; // False!?!?!?!?


你不需要写:

if (is_import == true) {


你可以写:

if (is_import) {


断开键盘上的下划线键。只需将camelCase用作变量即可。

编辑

我认为说它不是非常面向对象的另一个原因之一是您的CalcTax方法(应为CalculateTax)接受一个参数列表,如果可以,最好使用_totalTax对象。

评论


\ $ \ begingroup \ $
我认为这最好地体现了责任分离。计算自己的税金实际上不应该是产品的工作,但是将税金计算通常与用户交互隔离到一个单独的类中很有意义。这样可以使税费计算的单元测试与UI分离,并隔离税费计算(也许将来会转移到远程运行的服务中,该服务管理更复杂的规则,并经常通过税率变化进行集中更新?)
\ $ \ endgroup \ $
–丹·布莱恩特
2015年5月4日13:17

#3 楼

该公司可能正在寻找能证明面向对象建模的代码。您有产品对象,但是描述中也提到了税收和收据。更完整的模型可能如下所示:重要的是(其他审阅者/评论者也要注意):在此模型中,ProductTax对象负责确定它们是否适用于给定的产品,并且“产品”不了解“税”。通过ReceiptCalculator的Taxes属性可以在计算中添加/删除税款,并且可以使用不同的税收考虑因素(例如,针对不同的州或国家)创建不同的ReceiptCalculators。

基于税收分类不是一个好方法。如果诸如IsImportIsBookFoodOrMedical之类的标志属性经常更改或增长为太多,则最好将其概括为某种标志类型的ISet<...>属性。在真实的系统中,您不需要每次引入或更改税费都必须更改类层次结构;您想通过更改数据来解决。如果引入了需要不同分类的新税种,那么您要做的就是标记适用的产品并创建ProductTax实例来查找它。 (实施此操作将是值得进行的练习。)

评论


\ $ \ begingroup \ $
我将使用枚举ProductType而不是布尔值IsFoodBookOrMedical。
\ $ \ endgroup \ $
– ANeves认为SE是邪恶的
15年7月22日在13:03

\ $ \ begingroup \ $
Microsoft惯例是接口名称应以大写I开头,例如IProduct,IReceiptItem等。
\ $ \ endgroup \ $
–里克·戴文(Rick Davin)
15年7月22日在14:29

#4 楼

我要坦率地说,因为我认为这对您找工作最有好处。您说自己“有正确的心态”,“只是需要有人朝正确的方向'推'[您]”。您可以领先大多数产品,因为您的功能比FizzBu​​zz难得多。但是,此代码需要大量工作和大量重组。学习C#的文化将需要时间,而有效地学习OO原理也将需要时间。如果我是您,那么我会在空闲时间开始用C#编写很多OO代码并将其发布到codereview。

这是C#。您不需要在字符串上调用.Equals。相反,请执行以下操作:

if (men1 == "0")


您不需要? true : false或多余的括号。取而代之的是:

is_other = men1.Equals("2");


正如其他人所说,您并没有遵循C#命名约定:


用于局部变量,第一个单词小写,之后的每个单词的首字母大写。不使用下划线。 (例如isImport
方法名,类名和属性名都以大写字母开头,与本地语言有所不同。没有任何东西可以访问TotalTax文件,因此,如果要访问该文件,也可能只是一个实例变量:

private decimal _TotalTax;


如果需要添加到它....

_TotalTax += ...;


Program中,您执行的操作与else中的很多操作相同。同样,if (item.count > 1)的格式字符串有重载。使用那个而不是将一串字符串和变量连接在一起。此外,当它们全部放在一行上时,很难阅读:

Console.Write("{0}: {1:0.00}",
    item.prod_name,
    CalcTax(item.sum, item.is_other, item.is_import));

if (item.count > 1)
    Console.Write(" ({0} @ {1})", item.count, item.prod_price);

Console.WriteLine();


Console.WriteLine返回价格加税。如果要这样做,我将其命名为CalcTax。该方法也有很多重复之处。找出不重复的部分,然后首先执行:

public decimal AddTax(decimal price, bool is_other, bool is_import)
{
    int percentage = 0;
    if (is_other) percentage += 10;
    if (is_import) percentage += 5;

    var tax = price * percentage / 100;
    Total_Tax = tax; //have to because of the value + in the property

    return price + tax;
}


我删除了AddTax,因为我认为这不是必需的。

我还将检查输入是否格式错误,因为customRound可能因错误而失败,并且Convert.ToDecimal可能不是men101。在进入该行业之前,如果您按照程序员的意图使用它,就会遇到一些不稳定的UI。有很多方法可以滥用它并破坏程序。我在工作中学到的第一件事就是那不会飞起来。

Ewan的回答对于OO原理非常有用,因此我将忽略它。

#5 楼

使用对象与进行面向对象的编程不同。您的代码具有很高的过程性,并且没有利用面向对象设计的固有灵活性。您应该在此处应用的主要概念是,您应该封装各种变化。您可以采用两种方法来实现此目的:装饰或注入。


装饰

我们可以从装饰的角度看待这一问题。我们有一个基础对象,然后将其附加到其他行为上。装饰器模式是用于在基础对象上分层附加行为的一种很好的模式。我们可以分解每个概念并将其封装到代表该概念的对象中。 (我的C#可能不是完全惯用,但它应该可以工作并举例说明该概念)。首先,我们需要一个产品:

public class Product
{
    public string Name { get; private set; }
    protected decimal Price { get; private set; }

    public Product(string name, decimal price) {
        Name = name;
        Price = price;
    }

    public decimal GetPrice() {
      return Price;
    }

    // Since we are taxing things, we probably would want the original price too.
    public decimal GetBasePrice() {
        return Price;
    }
}


现在要使用Decorator模式,我们需要提取一个接口:

public class IProduct
{
    string Name { get; }
    decimal GetPrice();
    decimal GetBasePrice();
}


我们将接口添加到Product类中:

class Product : IProduct { ...


现在,我们封装了产品的概念。我们没有涉及任何税费或进口费,因为它们是完全独立的概念,并且各不相同。由于我们封装了变化,因此我们知道应该将它们分开。现在,让我们为需要征税的Product创建一个装饰器:
class TaxableProduct : IProduct
{
    const decimal TAX_RATE = 0.10m;

    public string Name { get { return BaseProduct.Name; } }
    private IProduct BaseProduct { get; set; }

    public TaxableProduct(IProduct baseProduct) {
        BaseProduct = baseProduct;
    }

    public decimal GetPrice() {
        return BaseProduct.GetPrice() + BaseProduct.GetBasePrice() * TAX_RATE;
    }

    public decimal GetBasePrice() {
        return BaseProduct.GetBasePrice();
    }
}


现在我们可以创建未征税,未进口的产品:

class ImportedProduct : IProduct
{
    const IMPORT_FEE = 0.05m;

    public string Name { get { BaseProduct.Name } }
    private IProduct BaseProduct { get; set; }

    public ImportedProduct(IProduct baseProduct) {
        BaseProduct = baseProduct;
    }

    public decimal GetPrice() {
        return BaseProduct.getPrice() + BaseProduct.GetBasePrice() * IMPORT_FEE);
    }

    public decimal GetBasePrice() {
        return BaseProduct.GetBasePrice();
    }
}


新的征税,未进口的产品: >
new Product("Milk", 2.50m);


新进口的免税产品:

new TaxedProduct(new Product("Wrench", 8.00m));


新进口的免税产品:

new ImportedProduct(new Product("French cheese", 25.00m));


我们能够在运行时将产品及其税费和进口费计算组合在一起,同时通过ImportedProduct界面使其透明。我们已经将应纳税和进口状态的变化行为封装到它们自己的对象中,并且它们可以独立变化。如果出现另一种费用,我们可以添加一个新的装饰器来封装它。 (顺便说一句,此代码中有重复项,可以很容易地将其排除。)


注入

我们可以从注入的角度看待这个问题。我们有一个具有某种行为的对象,并且我们希望将更多行为推入其中。我们可以使用依赖注入将这种行为推入对象。我们可以分解每个概念并将其封装到表示该概念的对象中(我的C#可能不是完全惯用的,但它应该可以工作并举例说明该概念)。首先,我们需要一种产品:

new ImportedProduct(new TaxedProduct(new Product("Ferrari", 250000.00m)));


现在,税款和进口费都是产品的费用,所以让我们创建一个接口来封装费用的概念:

class Product
{
    public string Name { get; private set; }
    private decimal Price { get; set; }

    public Product(string name, decimal price) {
        Name = name;
        Price = price;
    }

    public decimal GetPrice() {
        return Price;
    }
}


现在让我们创建两个收费实现,IProduct

interface IFee
{
    decimal CalculateFee(decimal price);
}


TaxFee

class TaxFee : IFee
{
    const decimal TAX_RATE = 0.10m;

    public decimal CalculateFee(decimal price) {
        return price * TAX_RATE;
    }
}


我们希望将行为注入到产品类中,最简单的方法是通过构造函数传递参数。由于我们要传递任意数量的费用,因此我们可以传递一个ImportFee

class ImportFee : IFee
{
    const decimal IMPORT_FEE = 0.05m;

    public decimal CalculateFee(decimal price) {
        return price * IMPORT_FEE;
    }
}


现在我们可以创建一个免税,未进口的产品:

class Product
{
    public string Name { get; private set; }
    private decimal Price { get; set; }
    private IEnumerable<IFee> Fees { get; set; }

    public Product(string name, decimal price) : this(name, price, new List<IFee>()) {
    }

    public Product(string name, decimal price, IEnumerable<IFee> fees) {
        Name = name;
        Price = price;
        Fees = fees;
    }

    public decimal GetPrice() {
        decimal fullPrice = Price;

        foreach(IFee fee in Fees) {
            fullPrice += fee.CalculateFee(Price);
        }

        return fullPrice;
    }
}


一种新的免税进口产品:

new Product("Milk", 2.50m);


一种新的免税进口进口产品:

List<IFee> fees = new List<IFee>();
fees.Add(new TaxFee());
new Product("Wrench", 8.00m, fees);


新征税的进口产品:

List<IFee> fees = new List<IFee>();
fees.Add(new ImportFee());
new Product("French cheese", 25.00m, fees);


我们能够在运行时将产品及其税费和进口费计算组合在一起,同时通过IEnumerable<IFee>界面使其透明。我们已经将应纳税和进口状态的变化行为封装到它们自己的对象中,并且它们可以独立变化。如果出现其他类型的费用,我们可以添加一个新的IFee对其进行封装。 (顺便说一句,此代码中有重复项,可以很容易地将其排除。)

#6 楼

答案中的树太多

各个答案中都有很多有效的点,但它大部分都错过了大的OO森林。遵循OOP概念?


创建连贯的类。

一致的类具有适当的属性来描述/定义它是什么,并具有适当的方法来描述/定义它的作用。无论具体的总体实现方式如何,这个设计目标都会发挥作用。

OP static void Main()会执行Product自己应该做的事情:发出其属性值-ToString();和CalculateTax()

最小化耦合并最大化一致性


那么,为什么代码不是面向对象的呢?

我希望方法是“处于适当的抽象级别”。 main应该读得非常高级。请计算Product以计算税金。请不要对产品执行此操作。
请对Product显示其详细信息,请不要对产品进行此操作。
请对ProductList进行纳税计算。请不要为列表执行此操作。
告诉ProductList以显示其详细信息。请勿为产品执行此操作。我们真的不在乎它是什么,而是它的税率是多少。

我不仅考虑树,链接列表等经典事物,而且我倾向于考虑创建任何使数据(类属性)更易于操纵的类。数据结构大大降低了代码复杂度。




税率字典-每个应税项目类型都有一个条目。并且taxable-item-type将是字典键。

taxable-item-type-一个enum


显然,每个应税项目都有自己的税率。即使它与其他项目相同。



ProductList-该人员将知道如何打印列表,如何计算列表的总税以及如何在自身内部找到副本。再说一次,告诉[您的课程在这里],不要为[您的课程在这里]

ItemType

默认值为“无效”或“未知”或“未定义”非常非常有用。

税表

public static void main() {
    bool continueEntry = true;
    int userEntry = 0;

    while(continueEntry) {
       continueEntry = PrompUser(out userEntry);

       if(!continueEntry) break;

       CreateProduct(userEntry);
     }

     CalculateTax();
     PrintResults();
}




具有适当责任的产品类别

我不想缠在轴上,关于publicprivate,命名约定等。这与Object的较大点无关面向。

public enum ItemType {Invalid, Book, Food, Medical, Import, Other}


具有适当责任的产品列表

public class TaxTable {
    public static Dictionary<ItemType, decimal> Taxes = new Dictionary<ItemType, decimal>(){
    {ItemType.Invalid, 0},
    {ItemType.Book, 0},
    {ItemType.Food, 0},
    {ItemType.Medical, 0},
    {ItemType.Import, .05},
    {ItemType.Other, .10}
   }

   public static decimal GetTaxFor(ItemType) { // lookup in Taxes}
}


客户代码

这是面向对象的

public class Product {
   ItemType WhatIAm {get; set;}

   // EDIT: Add "imported" as a separate concept. See @cbojar comment
   public bool Imported {get; protected set;}

   // a product knows what a valid item code is
   public Product(int itemType) {
       // convert to an ItemType

       // even if it is "Invalid" you can still have an Invalid
       // product type with a tax of zero, price of zero, etc.
       // Client code will be none the wiser.
       // The ProductCollection could simply elect to ignore it.
   }

   public override ToString(){
       StringBuilder me = new StringBuilder();
       me.AppendFormat("WhatIAm : {0}", WhatIAm);
       me.AppendFormat("Price : {0}", Price);
       // etc.
       return me.ToString();
   }

   // EDIT: added "imported" as an additional tax. See @cbojar comment
   public decimal CalculateTax() {
       decimal tax = 0.0;
       tax = Price * TaxTable.GetTaxFor(WhatIAm);
       tax = Imported ? tax + Price * TaxTable.GetTaxFor(ItemType.Imported) : tax;
   }

   public decimal TotalCost(){
       return Price + CalculateTax();
   }
}


面向对象的良好代码的气味


客户端告诉类做某事。
代码在适当的抽象级别上读取。
方法往往很短!


将细节“压入”适当的类中。 br />
定义和分配主体部分nct


数据结构。学习,生活,爱它。



避免不必要的耦合


任何人都可以使用TaxTableItemType

每个类都为自己做。我们不依赖其他类来做到这一点。


代码是可重用的。


类更改最好​​没有副作用。 >更改Product不会导致TaxTable发生变化。


对对象进行相同的处理。Product.ToString()产品没有特殊情况。客户代码将不知道或不在乎。 ProductList.ToString()不能将无效的产品添加到您想要的列表中,但是即使这样,客户还是很无知的。 ,但到处都强调创建ItemType.Invalid s太多了。

评论


\ $ \ begingroup \ $
我发现您将面向对象代码的所需属性称为“气味”会产生误解。起初,我以为您将要列出那些标志性很差的面向对象代码的东西。
\ $ \ endgroup \ $
–mkrieger1
2015年5月4日9:36



\ $ \ begingroup \ $
我对这个答案的一个主要问题是,它不允许对产品实施实施征税和进口。创建另一个枚举值可能会解决此问题,但是如果税种数量增加,则会创建重复项并导致组合数字枚举。此外,如果税收的计算不仅仅涉及价格乘以价格(例如,对第一个$ x不征税,等等),则将需要不同的策略。
\ $ \ endgroup \ $
–cbojar
2015年5月4日14:57

\ $ \ begingroup \ $
@radarbob无需更改Product类以实现新的赋税算法,对我来说既不会感到封闭,也不会感到单一责任。我什至不知道您将如何更改Product类以适应此类更改。您是否将多个ItemTypes传递给构造函数?如果允许多个类型,您将如何处理传递的不兼容类型?
\ $ \ endgroup \ $
–cbojar
2015年5月4日15:54



\ $ \ begingroup \ $
@cbojar-如果可以导入任何(某种)项目,那么是的,我的示例未实现。并且感谢您有机会说明OO代码的“类更改避免副作用”属性。请参阅产品类别修改。
\ $ \ endgroup \ $
– radarbob
2015年5月4日15:56

\ $ \ begingroup \ $
@radarbob随着需求的变化,我们的应用程序应具有强大的变化能力。尽管可能需要更改设计,但是如果我能花相同的时间和精力来创建一个健壮的设计,即使付出额外的一两个接口的代价,我也会做出健壮的设计,因为当不可避免的变更发生时路,我会为此做好准备的。作为一个实际问题,税费一直在变化。产品可能也一直在变化。它们也可能独立变化,因此仅出于方便而将它们混在一起可能会很危险。
\ $ \ endgroup \ $
–cbojar
15年5月4日在16:23

#7 楼

“它不是面向对象的”是一个奇怪的回答。这会让我立即担心这家公司,因为它暗示他们过去生活,所以这里不是我想要工作的地方。就像您声明一个接口和类来处理每种税种。像这样:

public interface IProduct
{
    string Name { get; set; }
    decimal Price { get; set; }
    decimal Tax { get; }
    bool Import { set; }
}

public class BookFoodOrMedical : IProduct
{
    public string Name { get; set; }
    public decimal Price { get; set; }

    public decimal Tax
    {
        get { return customRound(Price * (Import ? 5 : 0) / 100); }
    }

    public bool Import { set; private get; }
}

public class Other : IProduct
{
    public string Name { get; set; }
    public decimal Price { get; set; }

    public decimal Tax
    {
        get { return customRound(Price * (10 + (Import ? 5 : 0)) / 100); }
    }

    public bool Import { set; private get; }
}


然后,您的菜单方法最后更改为:

if (is_other)
{
    prod_list.Add(new Other { Import = is_import, Name = prod_name, Price = prod_price });
}
else
{
    prod_list.Add(new BookFoodOrMedical { Import = is_import, Name = prod_name, Price = prod_price });
}


CalcTax方法变为:

    public decimal CalcTax(IProduct product)
    {
        return product.Price + product.Tax;
    }


这是否比您写的还要好。您的解决方案有效。通过使它“更面向对象”,我可以说使代码难以维护。因此,尽管我已经展示了如何编写这些代码以遵循OOP概念,但您最好还是坚持自己的方法,而只需遵循其他答案中关于如何更好地遵循C#约定以及如何改进代码的建议即可。

评论


\ $ \ begingroup \ $
我认为该产品不应该了解其税率。如果一本书有资格获得税收怎么办?现在很难更改代码。
\ $ \ endgroup \ $
– RobH
2015年5月1日晚上8:41

\ $ \ begingroup \ $
绝对是,这就是为什么我认为“还不够面向对象”的评论很讨厌的原因。通过将税额计算从CalcTax方法移到一系列类中,我使代码更加难以维护。
\ $ \ endgroup \ $
– David Arno
2015年5月1日在8:44

\ $ \ begingroup \ $
但是,您可以在不将税额计算到产品中的情况下进行OOed。您可以让产品负责报告其税率,或者产品报告其税种,并有一个单独的组件来跟踪每个类别的税率。我认为您没有证明OO会弄乱项目,但您肯定已经证明添加更多的OO并不一定会改善项目,您必须添加正确的OO。因此,“ OO不够”不应该被认为是“盲目添加更多的OO”,就像“文档不足”不应该被认为是“添加不正确的文档” :-)
\ $ \ endgroup \ $
–史蒂夫·杰索普(Steve Jessop)
2015年5月1日9:08



\ $ \ begingroup \ $
我认为让BookFoodOrMedical不是产品的原因使我很难。书是产品。食物是一种产品。医疗是一种产品。但不是BookFoodOrMedical。如果每页要对书籍加税,则现在需要创建Book类和FoodOrMedical类。另外,如果我们有一堆产品类别并且有新的税款可用,我们将不得不将该税款计算添加到所有类别中,而不是在一处更改根据产品类型进行计算的税款计算。 Ewan的解决方案使用枚举解决了所有这些问题,并为以后节省了复杂性。
\ $ \ endgroup \ $
–米莉·史密斯(Millie Smith)
15年5月2日在16:32



\ $ \ begingroup \ $
创建基类/接口并从基类/接口继承并不容易实现,将税率硬编码为产品描述在任何范式中都不是好主意。税率和产品以及分类仅仅是数据。除非实际计算税收或确定税收状态的算法发生变化,否则您不必更改代码。
\ $ \ endgroup \ $
–user2313838
15年5月2日在17:08



#8 楼

这种情况下的步骤1是识别描述中的参与者和行为。我们并不是在做一个证券交易所,它每隔一纳秒就监控公司与其分项财务状况相关的价值,我们只是在准确地描述所描述的内容。

编写一个程序以输入价格。演员?行为?价格上涨是一种行为,因此尚无参与者。不幸的是,“程序”一词在程序类型中并不清楚,因此我们将其称为注册程序,这就是价格的输入地。书籍,食品或医疗产品无需缴税,如果没有,则需缴纳10%的税(其他项目)。演员?行为?这里有四个演员,书籍,食品,医疗和其他。他们每个人都有税收行为。它们都是产品。以前我们知道我们也在处理这些行为者的价格。

如果该项目是进口商品,则需缴纳5%的税(可能有进口商品和“其他商品”类型的商品15%的税(10 + 5))。演员?行为?存在产品导入的行为。这是对现有Actor的修改。

程序需要打印收据Actor?行为?这可以追溯到没有一个程序名称的原因,因此我将假定程序再次是我的Register类。打印是一种操作,收据显示带有格式的名称。

这是解析需求的结尾。到目前为止,我们需要一个注册类,一个用于每种产品类型的类,一个用于共享行为的基类以及一个用于修改税收的枚举的要求。收银机类应该能够计算价格并打印。让我们对此稍作讨论。

税项和基本产品类别的枚举

public enum TaxModificationPercent
{
    Import  = 5
}

public abstract class Product
{
    public decimal Price { get; protected set; }
    public string Name { get; protected set; }
    protected decimal _Tax = 0;
    public virtual decimal Tax 
    { 
        get { return _Tax; } 
        protected set { _Tax = value; }
    }

    public Product(string Name, decimal Price)
    {
        this.Name = Name;
        this.Price = Price;
    }

    public Product(string Name, decimal Price, TaxModificationPercent taxMod) 
        : this(Name,Price)
    {
        this.Tax += (decimal)taxMod/100;
    }
}


产品类别集

public class Book : Product
{
    public Book(string Name, decimal Price) : base(Name, Price){}
    public Book(string Name, decimal Price,TaxModificationPercent taxMod) 
        : base (Name, Price, taxMod){}
}

public class Food : Product
{
    public Food(string Name, decimal Price) : base(Name, Price){}
    public Food(string Name, decimal Price,TaxModificationPercent taxMod) 
        : base (Name, Price, taxMod){}
}

public class Medical : Product
{
    public Medical(string Name, decimal Price) : base(Name, Price){}
    public Medical(string Name, decimal Price,TaxModificationPercent taxMod) 
        : base (Name, Price, taxMod){}
}

public class Other : Product
{
    private decimal _tax = 0.1M;
    public override decimal Tax 
    { 
        get { return _tax; } 
        protected set { _tax = value; }
    }

    public Other(string Name, decimal Price) : base(Name, Price){}

    public Other(string Name, decimal Price,TaxModificationPercent taxMod) 
        : base (Name, Price, taxMod){}
}


以及用于存放产品和打印收据的注册类别

public class Register
{
    private List<Product> products = new List<Product>();

    public void Feed(Product product)
    {
        this.products.Add(product);
    }

    public void Print()
    {
        decimal total = 0;
        foreach(var nameGroup in this.products.GroupBy( p => p.Name ))
        {
            decimal item = nameGroup.Sum( p => p.Price );
            total += item;
            StringBuilder lineItem = new StringBuilder();
            lineItem
                .Append(nameGroup.Key)
                .Append(": " )
                .Append(string.Format("{0:C}",item));
            if( nameGroup.Count() > 1 ) 
                lineItem.Append(" (")
                    .Append(nameGroup.Count())
                    .Append(" @ ")
                    .Append(string.Format("{0:C}",nameGroup.First().Price))
                    .Append(")");
            Console.WriteLine(lineItem.ToString());
        }
        decimal tax = this.products.Sum( p => p.Price*p.Tax );
        Console.WriteLine("Sales Taxes: " + string.Format("{0:C}",tax));
        Console.WriteLine("Total: " + string.Format("{0:C}",total));
    }
}


最后,此操作的用途是简单地添加这些项目的新实例
var register = new Register();

register.Feed(new Book("Robinson Crusoe",4.56M));
register.Feed(new Book("Robinson Crusoe",4.56M));
register.Feed(new Food("Imported dog food",6.52M,TaxModificationPercent.Import));//Was uncertain if this was Food or Other
register.Feed(new Other("Perfume",6.23M));

register.Print();


这里是一个演示:https://dotnetfiddle.net/gCTYOtback返回顶部

#9 楼

在文本中描述了几种类别。 OOD将尝试将这些类别捕获为类。有进口商品和未进口商品的类别。有免税商品和含税商品的类别。
如果只喝一种香水或一种狗食,这种方法就太致命了。主要思想是显示即使分类相交时也要进行分类的方法。可以有多个变体,可以子类化

var book = new Product("book", 4.56m);


类hierachi可能看起来像这样

#10 楼

重构

我使用了您拥有的代码,花了大约10分钟的时间进行重构,因为我认为这是获得OOD和OOP的好方法。重构只是重新排列代码(开始)。这个想法是使易于理解。

下面是一个重构示例。关于VS2013重构的几件我不在乎的事情:1)当您生成方法时,它们有时会变为静态,而有时在您不希望的时候出现; 2)如果您具有全局var,并且您生成了一个方法,它将命名输入parm同样的事情。我没有更改这两件事来向您显示发生了什么。我本人这样做是为了提醒需要初始化内容,并确保始终在以后重新初始化它们。

每个if语句调用customRound(两次),当您看到重复的模式时,可以考虑抽象它们。

我想到的一件事是传入产品或具有税项的超类产品。所有产品都有价格,并且即使价格为零,所有产品都将征税。税收通常不是产品本身的一部分,而可能是产品的装饰。

namespace Cash
{
 public class product
  {
    public bool is_import { get; set; }

    public bool is_other { get; set; }

    public string prod_name { get; set; }

    public decimal prod_price { get; set; }
}


这是主要的切入点,现在看起来更像是控制器而不是控制器一大堆代码。

internal class Program
{
    private decimal _Total_tax = 0;

    private bool isImport = false;

    private bool isOther = false;

    private List<product> productList = new List<product>();

    private string productName = string.Empty;

    private decimal productPrice = 0;

    private string selectedMenuItem = string.Empty;

    public decimal Total_tax
    {
        get
        {
            return this._Total_tax;
        }

        set
        {
            this._Total_tax = value + Total_tax;
        }
    }


这就是重构使我陷入需要传递产品的方式:

public decimal CalcTax(decimal price, bool is_other, bool is_import)
{
    decimal _price = 0;

    if (is_import == true && is_other == true)
    {
        _price = customRound((price * 15) / 100) + price;
        Total_tax = customRound((_price - price));
    }

    if (is_import == true && is_other == false)//
    {
        _price = customRound((price * 5) / 100) + price;
        Total_tax = customRound((_price - price));
    }

    if (is_import == false && is_other == true)
    {
        _price = customRound((price * 10) / 100) + price;
        Total_tax = customRound((_price - price));
    }

    if (is_import == false && is_other == false)
    {
        _price = price;
        Total_tax = customRound((_price - price));
    }

    return _price;
}

public decimal customRound(decimal num)
{
    return Math.Round(num * 20.0M, MidpointRounding.AwayFromZero) / 20.0M;
}

public void Print(List<product> prod_list)
{
    var result = prod_list.GroupBy(x => new { x.prod_name, x.prod_price, x.is_other, x.is_import })
                                     .Select(x => new
                                     {
                                         prod_name = x.Key.prod_name,
                                         sum = x.Sum(z => z.prod_price),
                                         count = x.Count(),
                                         prod_price = x.Key.prod_price,
                                         is_other = x.Key.is_other,
                                         is_import = x.Key.is_import,
                                     });

    foreach (var item in result)
    {
        if (item.count > 1)
        {
            Console.WriteLine(item.prod_name + ": " + string.Format("{0:0.00}", CalcTax(item.sum, item.is_other, item.is_import)) + " (" + item.count + " @ " + item.prod_price + ")");
        }
        else
        {
            Console.WriteLine(item.prod_name + ": " + string.Format("{0:0.00}", CalcTax(item.sum, item.is_other, item.is_import)));
        }
    }

    Console.WriteLine("Sales Taxes: " + string.Format("{0:0.00}", Total_tax));
    Console.WriteLine("Total: " + string.Format("{0:0.00}", prod_list.Sum(item => item.prod_price) + Total_tax));
    Console.ReadLine();
}


这些是Visual Studio自动生成的方法,请注意,它们都是静态的(不一定是好东西)。像这样重构为小块,可以让您开始考虑关注点分离。随着时间的流逝,您会自动执行此操作,因为它更容易理解。

public void ShowMenu()
{
    while (true)
    {
        Initialize();

        selectedMenuItem = GetAnswers(selectedMenuItem);

        if (UserSelectedEnd()) break;

        isImport = CheckImport(isImport);

        isOther = CheckOther(selectedMenuItem, isOther);

        productPrice = EnterPrice(productPrice);

        productName = EnterProductName(productName);

        AddProduct(productName, isImport, isOther, productPrice, productList);
    }
}


注意:以下每种方法都可以将product实例作为参数。我发现在处理许多属性时传递类实例通常会更好。这将是对程序的重新设计,在问题循环顶部实例化一个新产品。

public void ShowMenu()
{
    while (true)
    {
        Initialize();

        selectedMenuItem = GetAnswers(selectedMenuItem);

        if (UserSelectedEnd()) break;

        var product = new product();

        GetImportAnswer(product);

        GetOtherAnswer(product);

        GetPriceAnswer(product);

        GetProductName(product);

        productList.Add(product);
    }
}


如果我想将产品用作参数,则上面的代码如下所示:

private static void AddProduct(string productName, bool isImport, bool isOther, decimal productPrice, List<product> productList)
{
    productList.Add(new product { is_import = isImport, prod_name = productName, prod_price = productPrice, is_other = isOther });
}


无论如何,我认为您显示出一些不错的C#技能。你会找到一份好工作的!

#11 楼

从问题陈述中,

我们可以布置如下所示的类: >
class Product {
    private String name;
    private doublt price;
    //Getter setter, constructor etc
}
class Book extends Product implements NonTaxable { ... }
class Food extends Product implements NonTaxable { ... }


或者也可以是(特殊情况)

interface Taxable {
    float taxRate();
}


必须能够通过查看输入文字:

interface NonTaxable extends Taxable {
    default float taxRate(){
        return 0.0f;
    }
}


对于特殊情况,我们有一些“额外”计算:

interface ProductFactory {
    Taxable parseProduct(String desc);
}