将大多数类划分为仅数据字段类和仅方法类(如果可能的话)是好的还是反模式的?


10

例如,一个类通常具有类成员和方法,例如:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public void printInfo(){
        System.out.println("Name:"+this.name+",weight:"+this.weight);
    }

    public void draw(){
        //some draw code which uses this.image
    }
}

但是在阅读了有关“单一职责原则”和“开放封闭原则”之后,我更喜欢仅使用静态方法将类分为DTO和辅助类,例如:

public class CatData{
    public String name;
    public int weight;
    public Image image;
}

public class CatMethods{
    public static void printInfo(Cat cat){
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat){
        //some draw code which uses cat.image
    }
}

我认为这符合单一职责原则,因为现在CatData的职责是仅保留数据,而不关心方法(对于CatMethods也是如此)。而且它也符合开放式封闭原则,因为添加新方法不需要更改CatData类。

我的问题是,这是好的还是反模式的?


15
因为您了解了一个新概念,所以到处盲目做任何事情始终是一种反模式。
加耶曼

4
如果将数据与修改它们的方法分开总是更好的话,类的意义是什么?这听起来像非面向对象的编程(当然有它的位置)。因为这被标记为“面向对象”,所以我假设您希望使用OOP提供的工具?
user1118321

4
另一个证明SRP严重误解的问题应予以禁止。将数据保存在一堆公共字段中不是责任
user949300

1
@ user949300,如果该类负责这些字段,那么将其移动到应用程序的其他位置当然将产生零影响。或者换句话说,您误会了:他们100%是责任。
David Arno

2
@DavidArno和R Schmitz:对于SRP而言,包含字段不算是责任。如果这样做的话,就不可能有OOP,因为一切都将是DTO,然后单独的类将包含处理数据的过程。单一责任与单个利益相关者有关。(尽管这在主体的每次迭代中似乎都略有变化)
user949300 '18

Answers:


10

您已经展示了两个极端(“在一个对象中的所有私有方法和所有(可能不相关的)方法”与“在对象中的所有公共方法而没有方法”)。恕我直言,良好的OO建模并不是其中一个,最有效的地方是中间。

对什么方法或逻辑属于一个类以及什么属于外部的一个试题测试是查看方法将引入的依赖关系。只要它们完全适合给定对象的抽象,不引入其他依赖项的方法就可以了。确实需要其他外部依赖项的方法(例如图形库或I / O库)很少适合。即使您通过使用依赖项注入使依赖项消失,但如果确实有必要在域类中放置此类方法,我仍然会三思而后行。

因此,您既不必公开每个成员,也不需要为类内对象的每个操作实现方法。这是一个替代建议:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public String getInfo(){
        return "Name:"+this.name+",weight:"+this.weight;
    }
    public Image getImage(){
        return image;
    }
}

现在,该Cat对象提供了足够的逻辑,以使周围的代码可以轻松实现printInfodraw,而无需公开所有属性。这两种方法的正确位置很可能不是上帝类CatMethods(因为printInfo并且draw很可能是不同的关注点,所以我认为它们不太可能属于同一类)。

我可以想象一个CatDrawingController实现draw(可能使用依赖注入来获取Canvas对象)。我还可以想象另一个实现某些控制台输出和使用的类getInfo(因此printInfo在这种情况下可能会过时)。但是为了对此做出明智的决定,需要了解上下文以及Cat该类的实际使用方式。

实际上,这就是我解释Fowler的贫血域模型批评者的方式 -对于一般可重用的逻辑(无外部依赖项),域类本身是个好地方,因此应使用它们。但这并不意味着在那里实现任何逻辑,恰恰相反。

还要注意,上面的示例仍然为做出(不)可变性做出决定留有余地。如果Cat该类不会公开任何设置方法,并且Image本身是不可变的,则此设计将允许使其Cat不可变(DTO方法不会)。但是,如果您认为不变性不是必需的或对您的案例没有帮助,那么您也可以朝这个方向发展。


触发快乐的拒绝投票者,这很无聊。如果您有任何批评意见,请告诉我。如果您有任何误解,我将很高兴澄清。
布朗

尽管我通常都同意您的回答,但getInfo()对于有人问这个问题,我可能会产生误导。它巧妙地混合了演示文稿的职责printInfo(),从而DrawingController
破坏

@AdrianoRepetti我看不到它违反了的目的DrawingController。我不同意,getInfo()并且printInfo()是一个可怕的名字。考虑toString()printTo(Stream stream)
candied_orange

@candid不是(只是)名称,而是名称的作用。该代码仅涉及表示细节,我们实际上只是抽象了输出,而不是处理后的内容及其逻辑。
Adriano Repetti

@AdrianoRepetti:老实说,这只是一个人为的例子,适合原始问题并证明了我的观点。在真实的系统中,将会有周围的上下文,这些上下文将允许就更好的方法和名称做出决策。
布朗

6

回答迟了,但我无法抗拒。

X是进入Y的最多类还是反模式?

在大多数情况下,大多数规则,如果不加考虑,就会大为改错(包括这一规则)。

让我告诉你一个关于一个对象的诞生的故事,该对象是在一些正确的,快速而又肮脏的程序代码的混乱中发生的,这不是设计使然,而是出于绝望。

我和我的实习生结对编程,可以快速创建一些可丢弃的代码来抓取网页。我们绝对没有理由期望此代码可以使用很长时间,所以我们只是敲出一些可行的方法。我们以字符串的形式抓取整个页面,并以您能想象到的最脆弱的方式切碎我们所需的内容。不要判断 有用。

现在,在执行此操作的同时,我创建了一些静态方法来进行斩波。我的实习生创建了一个非常像您的DTO课程CatData

当我第一次查看DTO时,它困扰了我。多年来,Java对我的大脑造成的伤害使我在公共领域退缩了。但是我们正在使用C#。C#不需要过早的getter和setter来保留使数据不可变或稍后封装的权利。在不更改界面的情况下,您可以随时添加它们。也许只是这样,您可以设置一个断点。所有这些都没有告诉您的客户有关此事的事情。是的C#。Boo Java。

所以我握住了舌头。我看着他在使用之前用我的静态方法初始化了这个东西。我们大约有14个。丑陋,但是我们没有理由在意。

然后我们在其他地方需要它。我们发现自己想要复制并粘贴代码。14行初始化。它开始变得痛苦。他犹豫了一下,问我要点什么。

我无奈地问,“你会考虑一个物体吗?”

他回头看了看DTO,困惑地拧紧了脸。“这是一个对象”。

“我的意思是真实的对象”

“ Hu?”

“让我给你看一些东西。你决定是否有用”

我选择了一个新名称,然后迅速整理出一个看起来像这样的东西:

public class Cat{
    CatData(string catPage) {
        this.catPage = catPage
    }
    private readonly string catPage;

    public string name() { return chop("name prefix", "name suffix"); }
    public string weight() { return chop("weight prefix", "weight suffix"); }
    public string image() { return chop("image prefix", "image suffix"); }

    private string chop(string prefix, string suffix) {
        int start = catPage.indexOf(prefix) + prefix.Length;
        int end = catPage.indexOf(suffix);
        int length = end - start;
        return catPage.Substring(start, length);
    }
}

静态方法还没有做任何事情。但是现在,我将14种静态方法吸收到了一个类中,在这些类中它们可以与所处理的数据一起使用。

我没有强迫实习生使用它。我只是提供了它,让他决定他是否要坚持使用静态方法。我回到家里,以为他可能会坚持他已经从事的工作。第二天,我发现他在很多地方使用它。它使其余的代码变得杂乱无章,这些代码仍然很丑陋,而且程序繁琐,但是现在我们在一个对象后面隐藏了这一点复杂性。好一点了。

现在确定每次访问此功能都会做很多工作。DTO是一个很好的快速缓存值。我对此感到担心,但是意识到我可以在需要时添加缓存,而无需接触任何使用代码。因此,在我们关心之前,我不会打扰。

我是说您应该始终在DTO上坚持OO对象吗?不。当您需要越过边界使您无法移动方法时,DTO会大放异彩。DTO占有一席之地。

但是OO对象也是如此。了解如何使用这两种工具。了解每笔费用。学会让问题,情况和实习生决定。Dogma不是您的朋友。


由于我的回答已经很荒谬了,所以让我在审查您的代码时避免一些误解。

例如,一个类通常具有类成员和方法,例如:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public void printInfo(){
        System.out.println("Name:"+this.name+",weight:"+this.weight);
    }

    public void draw(){
        //some draw code which uses this.image
    }
}

您的构造函数在哪里?这还不足以让我知道它是否有用。

但是在阅读了有关“单一职责原则”和“开放封闭原则”之后,我更喜欢仅使用静态方法将类分为DTO和辅助类,例如:

public class CatData{
    public String name;
    public int weight;
    public Image image;
}

public class CatMethods{
    public static void printInfo(Cat cat){
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat){
        //some draw code which uses cat.image
    }
}

我认为这符合单一职责原则,因为现在CatData的职责是仅保留数据,而不关心方法(对于CatMethods也是如此)。

您可以以“单一责任原则”的名义做很多愚蠢的事情。我可以争辩说Cat Strings和Cat int应该分开。该绘图方法和图像必须都具有自己的类。您正在运行的程序是一项责任,因此您应该只有一个班级。:P

对我而言,遵循“单一责任原则”的最好方法是找到一个好的抽象,让您将复杂性放在一个盒子中以便将其隐藏。如果您能给它起一个好名字,可以使人们对他们进入内部时所发现的东西不感到惊讶,那么您对它的了解就很好。期望它决定更多的决定,然后就会带来麻烦。老实说,您的两个代码清单都这样做,所以我看不到SRP在这里为什么重要。

而且它也符合开放式封闭原则,因为添加新方法不需要更改CatData类。

好吧 打开关闭原理与添加新方法无关。它是关于能够更改旧方法的实现,而无需进行任何编辑。什么都不会使用您,不会使用您的旧方法。相反,您在其他地方编写了一些新代码。某种形式的多态可以很好地做到这一点。在这里看不到。

我的问题是,这是好的还是反模式的?

好吧,我怎么知道?看,以任何一种方式进行都具有收益和成本。当您将代码与数据分开时,您可以更改其中一个而不必重新编译另一个代码。也许这对您至关重要。也许这只会使您的代码变得毫无意义。

如果这样会让你感觉更好,马丁•福勒(Martin Fowler)称之为参数对象的地方就差不多了。您不必仅将基元带入对象。

我希望您做的是对两种编码方式之间的分离有所了解。因为信不信由你,您不会被迫选择一种样式。您只需要选择就可以生活。


2

您偶然发现了一个争论话题,该话题在开发人员之间争论了十多年。2003年,马丁·福勒(Martin Fowler)创造了短语“厌氧域模型”(ADM)来描述数据和功能的这种分离。他和其他同意他的人认为,“丰富域模型”(混合数据和功能)是“适当的面向对象”,而ADM方法是非面向对象的反模式。

一直有一些人对此种说法不屑一顾,并且随着越来越多的开发人员采用功能性开发技术,这种观点的这一端近年来变得越来越大胆。这种方法积极鼓励将数据和功能问题分开。数据应尽可能不变,因此可变状态的封装变得无关紧要。在这种情况下,直接将功能附加到该数据没有任何好处。这样的人绝对对“是否不是OO”毫无兴趣。

无论你坐在围栏边(我坐得很用力。“马丁·福勒在谈论老胡说的负载”侧BTW),您使用的静态方法printInfodraw是接近普遍令人难以接受的。编写单元测试时,静态方法很难模拟。因此,如果它们有副作用(例如在某些屏幕或其他设备上打印或绘图),则它们不应是静态的,或者应将输出位置作为参数传递。

所以你可以有一个接口:

public interface CatMethods {
    void printInfo(Cat cat);
    void draw(Cat cat);
}

一个在运行时注入到系统其余部分的实现(其他实现用于测试):

internal class CatMethodsForScreen implements CatMethods {
    public void printInfo(Cat cat) {
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public void draw(Cat cat) {
        //some draw code which uses cat.image
    }
}

或添加额外的参数以消除这些方法的副作用:

public static class CatMethods {
    public static void printInfo(Cat cat, OutputHandler output) {
        output.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat, Canvas canvas) {
        //some draw code which uses cat.image and draws it on canvas
    }
}

但是,为什么您要尝试将像Java这样的OO语言变成一种功能语言,而不是从一开始就使用功能语言呢?是某种形式的“我讨厌Java,但每个人都使用Java,所以我必须这样做,但至少我要以自己的方式编写它”。除非您声称OO范例已过时且已过时。我赞成“如果您想要X,您知道从哪里获得”的思路。
加耶曼

@Kayaman,“ 我订阅了“如果您想获得X,您知道从何处获得“思想流派 ”。我不。我认为这种态度既短视又有点冒犯。我不使用Java,但是使用C#,并且忽略了某些“真正的OO”功能。我不在乎继承,有状态对象等,我写了很多静态方法和密封(最终)类。围绕C#的工具和社区规模是首屈一指的。对于F#,它会变得更穷。因此,我同意“如果您想要X,请要求将其添加到您当前的工具中”的想法。
David Arno

1
我想说,忽略语言的各个方面与尝试混合和匹配其他语言的功能有很大的不同。我也不尝试写“真正的面向对象”,因为试图在不精确的科学(如编程)中遵循规则(或原则)是行不通的。当然,您需要了解规则,以便可以打破规则。盲目地使用功能可能不利于语言的发展。没有冒犯,但是恕我直言,FP场景的一部分似乎感觉到“ FP是切成薄片以来的最伟大的东西,为什么人们不理解它”。我永远不会说(或认为)OO或Java是“最好的”。
加耶曼

1
@Kayaman,但是“试图混合和匹配其他语言的功能”应该是什么意思?您是否在争论不应该将功能功能添加到Java,因为“ Java是一种OO语言”?如果是这样,我认为这是一个短视的论点。让语言随着开发人员的需求而发展。我觉得强迫他们改用另一种语言是错误的方法。当然,某些“ FP倡导者”将FP视为有史以来最好的事情。一些“面向对象的拥护者”对面向对象具有“赢得了胜利,因为它显然是优越的”的看法。忽略它们两者。
David Arno

1
我不是反FP。我在有用的Java中使用它。但是,如果程序员说“我想要这个”,那就像客户说“我想要这个”。程序员不是语言设计师,客户不是程序员。我赞成您的回答,因为您说OP的“解决方案”不被接受。但是,问题中的评论更为简洁,即,他以OO语言重新发明了过程编程。正如您所展示的,总体思路确实有一点。非贫富域模型并不意味着数据和行为是排他的,并且将禁止外部渲染器或演示者。
加耶曼

0

DTO-数据传输对象

对此很有用。如果要在程序或系统之间分流数据,则最好使用DTO,因为它们提供了对象的可管理子集,该子集仅与数据结构和格式有关。优点是您不必在多个系统上将更新同步到复杂的方法(只要基础数据不变)。

OO的重点是将数据和作用于该数据的代码紧密地绑定在一起。将逻辑对象分成不同的类通常是一个坏主意。


-1

许多设计模式和原理会发生冲突,最终只有作为一个优秀程序员的判断才能帮助您决定哪种模式应适用于特定问题。

在我看来,除非您有充分的理由使设计变得更复杂,否则“ 可能做的最简单的事情”原则应该默认为赢。因此,在您的特定示例中,我将选择第一种设计,这是一种更为传统的面向对象的方法,在同一类中将数据和功能结合在一起。

您可能会问自己一些有关该应用程序的问题:

  • 我是否需要提供另一种渲染Cat图像的方式?如果是这样,继承将不是一种方便的方法吗?
  • 我的Cat类是否需要从另一个类继承或满足一个接口?

对以上任何一个回答是,可能会导致您使用单独的DTO和功能类进行更复杂的设计。


-1

这是一个好模式吗?

您可能会在这里得到不同的答案,因为人们遵循不同的思想流派。因此,在我开始之前:这个答案是根据罗伯特·C·马丁(Robert C. Martin)在“清洁代码”一书中描述的面向对象的编程得出的。


“真实”面向对象

据我所知,OOP有两种解释。对于大多数人来说,它可以归结为“一个对象就是一个类”。

但是,我发现另一种思想流派更有用:“一个对象就是可以做某事的东西”。如果您使用的是类,则每个类将是以下两种情况之一:“ 数据持有人 ”或对象。数据持有者持有数据(duh),对象负责填充。这确实不是意味着一个数据持有人不能有方法!想一想list:A list并没有真正任何事情,但是它有一些方法来强制其保存数据的方式

资料持有人

Cat是数据持有者的典范。当然,在程序外部,猫是可以执行某些操作的东西,但是在程序内部,a Cat是String name,int weight和Image image

这些数据持有人没有任何事情也并不意味着它们不包含业务逻辑!如果你的Cat类有需要的构造nameweight并且image,你已经成功地封装业务规则,每一个猫有那些。如果您可以在创建类weight后更改Cat,则这是封装的另一个业务规则。这些是非常基本的东西,但这仅意味着要弄明白它们是非常重要的-这样,您可以确保不可能出错。

对象

对象做某事。如果您要清理 *对象,我们可以将其更改为“对象做件事”。

打印猫信息是对象的工作。例如,您可以CatInfoLogger使用公共方法将该对象称为“ ” Log(Cat)。这就是我们需要从外部知道的所有内容:该对象记录猫的信息,为此,它需要一个Cat

在内部,该对象将引用其履行对猫的单方面职责所需的所有内容。在这种情况下,这只是一个参考SystemSystem.out.println,但在大多数情况下,这将是对其他对象的私有引用。如果在打印之前格式化输出的逻辑变得过于复杂,CatInfoLogger则只需引用一个新对象即可CatInfoFormatter。如果以后需要将每个日志也写入文件,则可以添加一个执行CatToFileLogger该操作的对象。

数据持有人与对象-摘要

现在,为什么要那样做?因为现在更改类仅更改一件事(示例中记录猫的方式)。如果要更改对象,则仅更改完成某项操作的方式。如果更改数据持有人,则仅更改保存的数据和/或保存方式。

更改数据可能也意味着必须更改某些事情的方式,但是只有通过更改负责的对象使它自己发生,更改才会发生。

过度杀伤力?

此解决方案似乎有些过大。坦率地说:。如果您的整个程序仅与示例一样大,则不要执行上述任何操作-只需选择抛硬币的方法之一即可。有没有危险混淆其他代码,如果有没有其他的代码。

但是,任何“严肃的”(大于脚本或2个)软件都可能足够大,可以从这种结构中受益。


回答

将大多数班级划分为DTO和辅助班级是好还是反模式?

将“大多数类”(没有任何进一步的资格证明)变成DTO /数据持有者,不是反模式,因为它实际上不是任何模式-它或多或少是随机的。

但是,将数据和对象分隔开是保持代码整洁*的好方法。有时,您只需要一个平坦的“贫血” DTO,仅此而已。有时,您需要一些方法来强制保留数据的方式。只是不要将程序的功能与数据放在一起。


*这是“ Clean”,带有大写字母C,如书名,因为-记住第一段-这是关于一个思想流派,但同时也存在其他思想流派。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.