一个班级的真正责任是什么?


42

我一直想知道在OOP中使用基于名词的动词是否合法。
我遇到了这篇精彩的文章,尽管我仍然不同意它的观点。

为了进一步解释该问题,文章指出,例如,不应有一个FileWriter类,但是由于编写是一种动作,因此它应该是该类的方法File。您将认识到它通常与语言相关,因为Ruby程序员可能会反对使用FileWriter类(Ruby使用方法File.open来访问文件),而Java程序员则不会。

我个人的观点(是的,非常谦虚)是,这样做会违反“单一责任”原则。当我用PHP编程时(因为PHP显然是OOP的最佳语言,对吗?),我经常会使用这种框架:

<?php

// This is just an example that I just made on the fly, may contain errors

class User extends Record {

    protected $name;

    public function __construct($name) {
        $this->name = $name;
    }

}

class UserDataHandler extends DataHandler /* knows the pdo object */ {

    public function find($id) {
         $query = $this->db->prepare('SELECT ' . $this->getFields . ' FROM users WHERE id = :id');
         $query->bindParam(':id', $id, PDO::PARAM_INT);
         $query->setFetchMode( PDO::FETCH_CLASS, 'user');
         $query->execute();
         return $query->fetch( PDO::FETCH_CLASS );
    }


}

?>

据我了解,后缀DataHandler 没有添加任何相关内容;但问题是单责任原则指示我们,用作包含数据的模型的对象(可能称为记录)也不应承担执行SQL查询和数据库访问的责任。这使Ruby on Rails例如使用的ActionRecord模式无效。

前几天,我遇到了这个C#代码(是,本文中使用的第四种目标语言):

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

我得说,Encodingor或Charsetclass实际上编码字符串对我来说没有多大意义。它仅应表示编码的真正含义。

因此,我倾向于认为:

  • File打开,读取或保存文件不是类的责任。
  • Xml序列化自己不是阶级责任。
  • User查询数据库不是类责任。
  • 等等

然而,如果我们推断这些想法,为什么ObjecttoString课吗?现在,将自身转换为字符串不是汽车或狗的责任吗?

我了解从务实的角度出发,toString放弃遵循严格的SOLID格式的美的方法是不可接受的选择,该方法通过使代码变得无用而使代码更易于维护。

我也理解,可能对此没有确切的答案(更多的是论文,而不是严肃的答案),或者可能是基于观点的。不过,我仍然想知道我的方法是否真正遵循单一责任原则。

班级有什么责任?


对象toString主要是方便,并且toString通常用于在调试过程中快速查看内部状态
棘手怪胎

3
@ratchetfreak我同意,但这是一个例子(几乎是个玩笑),它表明单项责任经常以我描述的方式被打破。
皮埃尔·阿洛德

8
尽管单一责任原则有其热心的支持者,但在我看来,这只是一个指导原则,而对于绝大多数设计选择而言,这是一个非常宽松的指导原则。SRP的问题在于,您最终得到了数百个名称奇异的类,这使得很难找到所需的内容(或找出所需的内容是否已经存在)并难以把握全局。我更喜欢使用较少命名的类,这些类可以立即告诉我对他们有什么期望,即使他们承担了很多责任。如果将来情况有所变化,那么我将拆分班级。
Dunk

Answers:


24

考虑到语言之间的某些差异,这可能是一个棘手的话题。因此,我以一种在OO领域内尽可能全面的方式编写以下评论。

首先,所谓的“单一责任原则”是对概念凝聚力的明确反映。阅读当时(大约70年代)的文献,人们(现在仍在)努力定义什么是模块,以及如何以保留良好属性的方式构造它们。因此,他们会说“这里有一堆结构和过程,我将用它们构成一个模块”,但是由于没有关于为何将这套任意事物打包在一起的标准,该组织可能最终没有任何意义-一点“凝聚力”。因此,出现了关于标准的讨论。

因此,这里首先要注意的是,到目前为止,辩论围绕组织及其对维护和易懂性的影响(如果模块“有意义”,对计算机而言无关紧要)。

然后,其他人(马丁先生)进来,将相同的思想应用于课堂单元,作为在考虑什么应该属于或不属于它时要使用的标准,并将这一标准推广为一项原则,正在讨论中这里。他提出的观点是“一个班级只有一个改变的理由”

好吧,从经验中我们知道,许多看起来在做“许多事情”的对象(和许多类)都有很好的理由。不受欢迎的情况将是那些功能泛滥到难以维护的类,等等。要理解后者,就是去看看先生。马丁在阐述这个主题时就瞄准了它。

当然,看了什么先生。马丁写道,应该清楚地指出,这些是指导和设计的标准,以避免出现有问题的情况,而不是以任何方式追求任何合规性,更不用说强力合规性,尤其是在“责任”定义不明确的情况下(以及诸如“是否违反了原则?”是普遍混乱的完美示例)。因此,我觉得很不幸,这被称为原则,误导人们尝试将其带到最后的后果,那将是无济于事的。马丁先生本人曾讨论过“做不止一件事情”的设计,应该这样做,因为分开会导致更糟的结果。同样,关于模块化存在许多已知的挑战(这个问题就是一个例子),即使对于一些关于它的简单问题,我们也没有一个好的答案。

但是,如果我们推断这些想法,为什么Object会有toString类?现在,将自身转换为字符串不是汽车或狗的责任吗?

现在,让我暂停一下,谈一谈toString:当人们将思想从模块过渡到类并思考应该属于类的方法时,通常会忽略一个基本的事情。事情就是动态调度(又称后期绑定,“多态”)。

在没有“覆盖方法”的世界中,在“ obj.toString()”或“ toString(obj)”之间进行选择仅是语法首选项的问题。但是,在当今世界中,程序员可以通过添加具有现有/重写方法的不同实现的子类来更改程序的行为,这种选择不再具有吸引力:将过程设为方法也可以使其成为替代对象,对于“自由程序”来说可能并非如此(支持多方法的语言可以摆脱这种二分法)。因此,它不再只是关于组织的讨论,而是关于语义的讨论。最后,方法绑定到哪一类也将成为一个有影响力的决定(而且,到目前为止,在许多情况下,我们仅靠准则来帮助我们确定事物的归属,

最后,我们面对的语言会做出糟糕的设计决策,例如,迫使人们为每一件事创建一个类。因此,曾经是根本没有对象(以及在类国家,因此是类)的规范理由和主要标准是什么,那就是使这些“对象”属于“行为也像数据一样” ,但是要不惜一切代价保护它们的具体表示形式(如果有的话)免于直接操纵(从客户的角度来看,这是对象界面的主要提示),变得模糊和混乱。


31

[注意:我将在这里谈论对象。对象毕竟是面向对象的编程,而不是类。]

对象的职责主要取决于您的域模型。通常有很多方法可以对同一域进行建模,并且您将根据系统的使用方式选择一种或另一种方法。

众所周知,在入门课程中经常教授的“动词/名词”方法很荒谬,因为它在很大程度上取决于您如何构造句子。您可以用主动或被动的声音来表达几乎所有的名词或动词形式。让您的域模型依赖于错误是正确的,无论是对象还是方法,这都是明智的设计选择,而不仅仅是制定需求的偶然结果。

但是,它的确表明,就像您有很多用英语表达同一事物的可能性一样,您也有在您的域模型中表达同一事物的许多可能性……而且,这些可能性天生就比其他方法更正确。这取决于上下文。

这是一个在OO入门课程中也很受欢迎的示例:银行帐户。通常将其建模为具有balance字段和transfer方法的对象。换句话说:帐户余额是data,而转帐是operation。这是模拟银行帐户的合理方法。

除此之外,这不是在现实世界中的银行软件中模拟银行帐户的方式(实际上,这不是现实世界中银行的工作方式)。取而代之的是,您有一个交易凭条,并且通过将一个帐户的所有交易凭条相加(减去)来计算帐户余额。换句话说:传输是数据,余额是操作!(有趣的是,这也使您的系统具有纯粹的功能,因为您不需要修改余额,因此帐户对象和交易对象都不需要更改,它们是不可变的。)

关于您的特定问题toString,我同意。我非常喜欢Showable 类型类的Haskell解决方案。(Scala告诉我们类型类很适合OO。)与相等相同。平等通常不是对象的属性,而是对象使用的上下文。只需考虑浮点数:epsilon应该是什么?同样,Haskell具有Eq类型类。


我喜欢您的haskell比较,它使答案更加……新鲜。那么,对于允许ActionRecord模型既是模型又是数据库请求者的设计,您会怎么说?无论上下文如何,它似乎都违反了单一责任原则。
皮埃尔·阿洛德

5
“ ...因为这在很大程度上取决于您如何构造句子。” 好极了!哦,我确实喜欢您的银行业示例。从来都不知道八十年代我就已经开始学习函数式编程了:)(以数据为中心的设计和与时间无关的存储,其中余额等是可计算的,不应存储,并且不应更改某人的地址,而应将其存储为一个新的事实)...
Marjan Venema 2013年

1
我不会说动词/名词方法是“荒谬的”。我想说,简单的设计将会失败,并且很难正确地进行设计。因为作为反例,RESTful API是否不是动词/名词方法?在极端困难的情况下,动词在哪里受到限制?
user949300 2013年

@ user949300:动词集固定的事实恰好避免了我提到的问题,即您可以用多种不同的方式来表达句子,从而使什么是名词和什么是动词取决于写作风格而非领域分析。
约尔格W¯¯米塔格

8

单一责任原则常常会成为零责任原则,最终您会得到无能为力的贫血阶级(除塞特犬和吸气剂外),这导致了名词王国灾难

您将永远无法获得完美的结果,但是对于一个班级来说,做太多而不是做得更好。

在IMO编码示例中,它绝对应该能够编码。您应该怎么做呢?仅使用名称“ utf”是零责任。现在,也许名称应该是Encoder。但是,正如康拉德所说,数据(哪种编码)和行为(进行编码)是在一起的


7

类的一个实例是闭包。而已。如果您以这种方式思考,那么您看的所有精心设计的软件都将看起来正确,而所有考虑周全的软件都不会。让我扩展。

如果要写一些东西来写文件,请考虑一下所有需要指定的内容(操作系统):文件名,访问方法(读,写,追加),要写的字符串。

因此,您可以使用文件名(和访问方法)构造一个File对象。File对象现在关闭了文件名(在这种情况下,它很可能已将其作为只读/ const值接收)。现在,File实例已准备好调用该类中定义的“ write”方法。这只需要一个字符串参数,但是在write方法的主体(实现)中,也可以访问文件名(或从文件名创建的文件句柄)。

因此,File类的实例将一些数据组合到某种复合Blob中,以便以后使用该实例更加简单。当您拥有File对象时,您不必担心文件名是什么,您只需要担心将哪些字符串作为参数放入write调用中即可。重申一下-File对象封装了您不需要知道要写入的字符串实际去向的所有内容。

您要解决的大多数问题都是以这种方式分层的:先创建的事物,在某种过程循环的每次迭代开始时创建的事物,然后在if的两半中声明不同的事物,然后在子循环的开始,然后在该循​​环中声明为算法的一部分的东西,等等。随着向堆栈中添加越来越多的函数调用,您正在执行更细粒度的代码,但是每次将堆栈中较低层中的数据装箱成某种漂亮的抽象,使其易于访问。看看您正在使用“ OO”作为一种功能编程方法的闭包管理框架。

解决某些问题的捷径涉及对象的可变状态,这不是那么有用-您是从外部操纵闭包。您正在使类中的某些内容“全局”化,至少在上述范围内。每次您写二传手时-尽量避免。我认为这是您对班级或其他班级负责的地方,这是错误的。但是不必太担心-有时会陷入一点易变的状态,以便在没有太多认知负担的情况下快速解决某些类型的问题并实际上完成任何事情,这是实用的。

因此,总而言之,回答您的问题-课堂的真正责任是什么?它将基于某种基础数据提供一种平台,以实现更详细的操作。它封装的数据涉及的一半操作比另一半数据的更改频率要低(请思考一下...)。例如,文件名与要写入的字符串。

通常,这些封装在表面上看起来像现实世界中的对象/问题域中的对象,但并不被愚弄。当您创建Car类时,它实际上并不是汽车,而是一个数据集合,这些数据构成一个平台,在该平台上可以实现您可能想在汽车上完成的某些事情。形成字符串表示形式(toString)就是其中之一-吐出所有内部数据。请记住,有时即使汽车的问题领域全都与汽车有关,但汽车课可能不是正确的课。与名词王国相反,它是一类应基于的操作,动词。


4

我也理解,可能对此没有确切的答案(更多的是论文,而不是严肃的答案),或者可能是基于观点的。但是,我仍然想知道我的方法是否真正遵循单一责任原则。

尽管确实如此,但它不一定能产生良好的代码。除了任何盲目遵循的规则都会导致错误代码这一简单事实之外,您还从一个无效的前提开始:(编程)对象不是(物理)对象。

对象是一组内聚的数据和操作包(有时只是两者之一)。尽管这些通常会模拟现实世界中的对象,但是某物的计算机模型与该物本身之间的差异需要差异。

通过在表示事物的“名词”和消耗事物的其他事物之间采取强硬路线,您将面临面向对象编程的主要好处(将函数和状态结合在一起,以便函数可以保护状态的不变性) 。更糟糕的是,您正在尝试用代码表示物理世界,正如历史所示,这种方法无法正常工作。


4

前几天,我遇到了这个C#代码(是,本文中使用的第四种目标语言):

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

我得说,Encodingor或Charsetclass实际上编码字符串对我来说没有多大意义 。它仅应表示编码的真正含义。

从理论上讲,是的,但是我认为C#为简单起见做出了合理的折衷(与Java更严格的方法相对)。

如果Encoding仅表示(或确定)某种编码(例如UTF-8),那么显然Encoder您也需要一个类,以便您可以GetBytes在其上实现-但是您需要管理Encoders和Encodings 之间的关系,因此我们最后以我们的好

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

在您链接到的文章中如此精彩地讽刺(顺便说一句,非常读)。

如果我对您的了解很好,那么您在问电话线在哪里。

好吧,与频谱相反的是过程方法,使用静态类在OOP语言中不难实现:

HelpfulStuff.GetUTF8Bytes(myString)

最大的问题是当HelpfulStuff叶子的作者和我坐在监视器前时,我无法知道在哪里GetUTF8Bytes实现。

难道我寻找它HelpfulStuffUtilsStringHelper

如果该方法实际上是在所有这三个方法中独立实现的,请上帝帮我...这经常发生,所要做的只是我前面的那个人也不知道该在哪里寻找该功能,并且认为没有任何功能。因此,他们又抢购了另一个,现在我们有很多。

对我来说,适当的设计不是要满足抽象标准,而是要让我坐在您的代码前面之后能够轻松进行。现在如何

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

在这方面评分?太差了。当我向同事大喊“我如何将字符串编码为字节序列?”时,这不是我想听到的答案。:)

因此,我会采取温和的态度,对单一责任持开放态度。我希望Encoding该类既可以识别编码类型,又可以进行实际编码:将数据与行为分开。

我认为它是作为立面图案通过的,有助于润滑齿轮。此合法模式是否违反SOLID?一个相关的问题:Facade模式是否违反了SRP?

请注意,这可能是内部(在.NET库中)实现编码字节的方式。这些类只是不公开可见,只有此“著名的” API对外公开。验证这是否正确并不难,我只是现在有点懒惰而已:)可能不是,但这不会使我的观点无效。

为了方便起见,您可以选择简化公开可见的实现,同时在内部维护更严格的规范实现。


1

在Java中,用于表示文件的抽象是流,有些流是单向的(只读或只写),而另一些是双向的。对于Ruby,File类是代表OS文件的抽象。因此,问题变成了它的单一责任是什么?在Java中,FileWriter的职责是提供连接到OS文件的单向字节流。在Ruby中,File的职责是提供对系统文件的双向访问。他们俩都履行SRP,只是职责不同。

现在,将自身转换为字符串不是汽车或狗的责任吗?

当然可以吗?我希望Car类能够完全代表Car抽象。如果在需要字符串的地方使用car抽象是有意义的,那么我完全希望Car类支持转换为字符串。

直接回答更大的问题,类的责任是充当抽象。这项工作可能很复杂,但是这很重要,抽象应该将复杂的东西隐藏在易于使用,不太复杂的界面后面。SRP是一种设计指南,因此您关注“这个抽象代表什么?”这个问题。如果需要多种不同的行为来完全抽象该概念,那就这样。


0

班级可以承担多个职责。至少在以数据为中心的经典OOP中。

如果您要全面运用“单一职责”原则,那么您将获得以职责为中心的设计,基本上每个非辅助方法都属于其自己的类。

我认为从File和FileWriter的情况下从基类提取复杂性没有错。这样做的好处是,您可以清楚地区分负责某种操作的代码,并且可以仅覆盖该代码段(子类),而不是覆盖整个基类(例如File)。但是,对我来说,系统地应用它似乎是过大的。您只需要处理更多的类,就需要为每个操作调用其各自的类。灵活性需要付出一定的代价。

这些提取类应该具有基于它们所包含的操作,例如非常描述性的名称<X>Writer<X>Reader<X>Builder。如姓名<X>Manager<X>Handler根本不能说明操作,如果他们被称为一样,因为它们包含多个操作,那么目前尚不清楚,你甚至取得的成就。您已经将功能与其数据分开,即使在该提取的类中,您仍然违反了单一职责原则,并且如果您正在寻找某种方法,则可能不知道在哪里寻找(如果在<X>或中<X>Manager)。

现在,您使用UserDataHandler的情况有所不同,因为没有基类(包含实际数据的类)。此类的数据存储在外部资源(数据库)中,并且您正在使用api访问和操作它们。您的User类代表一个运行时用户,这实际上与持久性用户有所不同,在这里(尤其是如果您有许多与运行时用户相关的逻辑)责任分离(问题)可能会派上用场。

您可能这样命名UserDataHandler,因为该类中基本上只有功能,而没有实际数据。但是,您也可以从该事实中抽象出来,并考虑数据库中的数据属于该类。有了这个抽象可以命名基于类它是什么,而不是它做什么。您可以给它起一个类似UserRepository的名称,它很漂亮,也建议使用存储库模式

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.