如何将代码拆分为组件......大类?小班?

时间:2009-06-25 21:32:18

标签: c# .net

这是非常基本的东西,但是这里有。我发现我无法同意自己是否将大型课程分成较小的课程使得事情更易于维护或更难维护。我熟悉设计模式,虽然不详细,但也熟悉面向对象设计的概念。抛开所有花哨的规则和指导方针,我希望通过一个非常简单的示例场景来挑选我所缺少的内容。基本上是这样的:“......这种设计会让它变得更加困难”......等等......由于缺乏经验,所以我没有预料到的所有事情。

假设您需要编写一个基本的“文件读取器/文件编写器”样式类来处理某种类型的文件。我们将文件称为 YadaKungFoo文件。 YadaKungFoo文件的内容基本上类似于INI文件,但有细微差别。

有部分和值:

[Sections]
Kung,Foo
Panda, Kongo

[AnotherSection]
Yada,Yada,Yada
Subtle,Difference,Here,Yes

[Dependencies]
PreProcess,SomeStuffToPreDo
PreProcess,MoreStuff
PostProcess,AfterEight
PostProcess,TheEndIsNear
PostProcess,TheEnd

好的,所以这可以产生3个基本类:

public class YadaKungFooFile
public class YadaKungFooFileSection
public class YadaKungFooFileSectionValue

后两个类本质上只是具有ToString()重写的数据结构,用于吐出使用几个通用列表存储的值的字符串列表。这足以实现YadaKungFooFile保存功能。

随着时间的推移,YadaYadaFile开始增长。几种重载以不同的格式保存,包括XML等,文件开始推向800行左右。 现在真正的问题:我们想要添加一项功能来验证YadaKungFoo文件的内容。首先想到的是添加:

var yada = new YadaKungFooFile("C:\Path");
var res = yada .Validate()

我们已经完成了(我们甚至可以从构造函数中调用该方法)。麻烦的是验证非常复杂,并使类非常大,所以我们决定创建一个这样的新类:

var yada = new YadaKungFooFile("C:\Path"); 
var validator = new YadaKungFooFileValidator(yada); 
var result = validator.Validate();

现在这个样本显然非常简单,非常简单,无关紧要。上述两种方式中的任何一种都可能不会产生太大的差异,但我不喜欢的是:

  1. YadaKungFooFileValidator 类和 YadaKungFooFile 类似乎与此设计非常紧密结合。这似乎是一个班级的变化,可能会引发另一个班级的变化。
  2. 我知道“Validator”,“Controller”,“Manager”等短语表示一个与其他对象的状态有关的类而不是“自己的业务”,因此违反了分离关注原则和信息发送。
  3. 总而言之,我想我觉得我没有经验可以理解设计为什么不好的所有方面,在什么情况下它并不重要,什么问题带来更多的重量:更小的课程或更多有凝聚力的课程。他们似乎是矛盾的要求,但也许我错了。也许验证器类应该是一个复合对象?

    基本上我要求就上述设计可能带来的好处/问题提出意见。有什么不同的做法? (基础FileValidator类,FileValidator接口等等。你的名字)。想想YadaKungFooFile功能随着时间的推移不断增长。

7 个答案:

答案 0 :(得分:15)

鲍勃·马丁写了一系列关于课堂设计的文章,他称之为SOLID原则:

http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

原则是:

  1. 单一责任原则:一个班级应该只有一个改变的理由。
  2. 开放封闭原则:您应该能够扩展类行为,而无需对其进行修改。
  3. Liskov替换原则:派生类必须可替代其基类。
  4. 接口隔离原则:制作客户特定的细粒度接口。
  5. 依赖性倒置原则:取决于抽象,而不是结核。
  6. 因此,鉴于这些,我们来看一些陈述:

      

    随着时间的推移,YadaYadaFile开始增长。几种重载以不同的格式保存,包括XML等,文件开始推向800行左右

    这是第一个大红旗:YadaYadaFile开始时有两个职责:1)维护一个部分/键/值数据结构,以及2)知道如何读取和写入类似INI的文件。所以这是第一个问题。添加更多文件格式会使问题更加复杂:当1)数据结构发生变化,或者2)文件格式发生变化,或者3)添加新文件格式时,YadaYadaFile会发生变化。

    同样地,对所有这三个职责都设置一个验证器会对该单个班级承担太多责任。

    一个大类是一个“代码味道”:它本身并不坏,但它通常是由一些非常糟糕的东西产生的 - 在这种情况下,是一个试图做太多事情的类。 p>

答案 1 :(得分:14)

我不认为课程的大小是一个问题。关注的是更多的凝聚力和耦合。您想要设计松散耦合和内聚的对象。也就是说,他们应该关注一个定义明确的事情。因此,如果事情发生得非常复杂,那么课程就会增长。

您可以使用各种设计模式来帮助管理复杂性。例如,您可以做的一件事是创建一个Validator接口,并让YadaKunfuFile类依赖于接口而不是Validator。这样,只要接口没有更改,就可以更改Validator类而无需更改YadaKungfuFile类。

答案 2 :(得分:4)

YadaKungFooFile不应该知道如何从磁盘中读取自己。它应该只知道如何穿越自己,透露其子女等。它还应该提供添加/删除孩子等的方法。

YadaKungFooFile应该在他的Load方法中使用IYadaKungFooReader接口,它将用于从磁盘加载自身。还有一些实现,比如AbstractKungFooReader,PlainTextYadaKungFooReader,XMLYadaKungFooWriter,它们知道如何阅读和相应的格式。

同样的写作。

最后,应该有一个YadaKungFooValidatingReader,它将带一个IYadaKungFooReader读取器,用它来读取并在读取时验证输入。然后,只要您希望它在从磁盘读取时进行验证,您就可以将验证读取器传递给YadaKungFooFile.Load。

或者,您可以使阅读器处于活动状态,而不是被动类。然后,您不会将其传递给YadaKungFooFile,而是将其用作使用后一种常规访问方法创建YadaKungFooFile的工厂。在这种情况下,您的读者还应该实现YadaKungFooFile界面,以便您可以链接普通读者 - >验证读者 - > YadaKungFooFile。有意义吗?

答案 3 :(得分:2)

我会解决这个问题

  

YadaKungFooFileValidator类和   YadaKungFooFile类似乎是   非常强烈地结合这种设计。   这似乎是一个班级的变化,会   可能会引发另一方的变化。

是的,所以鉴于此,弄清楚如何使其尽可能无缝。在最好的情况下,设计YadaKungFooFile类,以便验证器可以自己接收它。

首先,语法本身应该很简单。我不希望语​​法验证发生变化,因此您可能只需要硬编码即可。

就可接受的属性而言,您可以从Validator将检查的File类中公开枚举器。如果解析的值不在给定节的任何枚举器中,则抛出异常。

等等......

答案 4 :(得分:0)

在这种情况下我处理事物的个人方式是有一个YadaKungFooFile类,它将由YadaKungFooFileSections列表组成,它将是YadaKungFooFileValues的列表。

对于验证器,您将让每个类调用其下面的类的validate方法,层次结构中的最低类实现验证方法的实际内容。

答案 5 :(得分:0)

如果验证不依赖于整个文件,而只适用于单个节值,则可以通过指定一组对节值进行操作的验证器来分解YadaKungFooFile类的验证。

如果验证确实依赖于整个文件,则验证自然会耦合到文件类,您应该考虑将它放在一个类中。

答案 6 :(得分:0)

类大小不像方法大小那么大。类是一种组织方式,虽然有很好的方法来组织代码,但它并不直接与类的大小联系在一起。

方法大小不同,因为方法正在进行实际工作。方法应该有非常具体的工作,这将不可避免地限制他们的规模。

我发现确定方法大小的最佳方法是编写单元测试。如果您正在编写具有足够代码覆盖率的单元测试,那么当方法太大时就会变得很明显。

单元测试方法需要简单,否则您最终需要测试来测试单元测试以确保它们正常工作。如果您的单元测试方法变得复杂,可能是因为某个方法试图做得太多,应该分解成具有明确职责的小方法。