我一直被告知如果你正在做一些 一个对象,那应该是一个外部事物,所以人们会Save(Class)
而不是让对象保存自己:{ {1}}。
我注意到在.Net库中,通常让类修改自己与Class.Save()
一样,或者将自己排序为String.Format()
。
我的问题是,在严格的OOP中,有一个类在调用时自己执行函数,或者这些函数应该是外部函数并调用类类型的对象吗?
答案 0 :(得分:3)
好问题。我刚刚在一个非常相似的问题上进行了反思,并最终在SO上提出了同样的问题。
在OOP教科书中,您有时会看到Dog.Bark()
或Person.SayHello()
等示例。我得出结论,这些都是不好的例子。当你打电话给那些方法时,你会吠叫,或者一个人打个招呼。然而,在现实世界中,你无法做到这一点;一只狗决定什么时候会吠叫。一个人决定何时会向某人打招呼。因此,这些方法更适合建模为 事件 (编程语言支持)。
你会...有一个函数Attack(Dog)
,PlayWith(Dog)
或Greet(Person)
会触发相应的事件。
Attack(dog) // triggers the Dog.Bark event
Greet(johnDoe) // triggers the Person.SaysHello event
只要您有多个参数,决定如何最好地编写代码就不那么容易了。假设我想将一个新项目(例如整数)存储到集合中。有很多方法来制定这个;例如:
StoreInto(1, collection) // the "classic" procedural approach
1.StoreInto(collection) // possible in .NET with extension methods
Store(1).Into(collection) // possible by using state-keeping temporary objects
根据上面提出的思路,最后一个变体将是首选变体,因为它不会强迫对象(1
)对自己做某事。但是,如果你遵循这种编程风格,很快就会发现这个 流畅的界面 类似的代码非常冗长,虽然它很容易阅读,但它可能很累人写或甚至难以记住确切的语法。
PS:关于全局函数:对于.NET (您在问题中提到),您没有太多选择,因为.NET语言不提供全局函数。我认为这些在技术上可以通过CLI实现,但语言不允许使用该功能。 F#具有全局函数,但只有在C#或VB.NET打包到模块中时才能使用它们。我相信Java也没有全局功能。
我遇到了这种缺乏可惜的情况(例如,使用流畅的界面实现)。但是 一般来说,如果没有全局函数,我们可能会更好 ,因为一些开发人员可能总是回归过去的习惯,并为OOP开发人员留下程序代码库来维护。让人惊讶。
但是,在VB.NET中,您可以使用模块模仿全局函数。例如: <强> Globals.vb
强>
Module Globals
Public Sub Save(ByVal obj As SomeClass)
...
End Sub
End Module
<强> Demo.vb
强>
Imports Globals
...
Dim obj As SomeClass = ...
Save(obj)
答案 1 :(得分:1)
我想答案是“它取决于......”对于一个对象的持久性我会支持在一个单独的存储库对象中定义该行为。所以使用Save()示例我可能会这样:
repository.Save(class)
然而,对于飞机物体,您可能希望班级知道如何使用如下方法飞行:
airplane.Fly()
这是我从Fowler看到的关于贫血数据模型的例子之一。我不认为在这种情况下你会想要一个像这样的单独服务:
new airplaneService().Fly(airplane)
使用静态方法和扩展方法,就像在List.Sort()示例中一样。所以这取决于你的使用模式。您不希望新建一个ListSorter类的实例,只是为了能够对这样的列表进行排序:
new listSorter().Sort(list)
答案 2 :(得分:1)
在 strict OOP(Smalltalk或Ruby)中,所有方法都属于实例对象或类对象。在“真正的”OOP(如C ++或C#)中,您将拥有基本上完全独立的静态方法。
回到严格的OOP,我对Ruby更熟悉,Ruby有几个“对”方法,可以返回修改后的副本或返回对象 - 以!
结尾的方法表示该消息修改了其接收者。例如:
>> s = 'hello'
=> "hello"
>> s.reverse
=> "olleh"
>> s
=> "hello"
>> s.reverse!
=> "olleh"
>> s
=> "olleh"
关键是要找到纯OOP和纯程序之间的一些中间立场,它们适用于您需要做的事情。 Class
应该只做一件事(做得好)。大多数情况下,这不包括将自己保存到磁盘,但这并不意味着Class
不应该知道如何序列化本身到流。例如。
答案 3 :(得分:1)
当你说“做一些 一个对象”时,我不确定你似乎在画什么区别。在许多情况下(如果不是大多数情况),类本身是定义其操作的最佳位置,因为在“严格OOP”下,它是唯一可以访问这些操作所依赖的内部状态的代码(信息隐藏,封装,...... )。
也就是说,如果你有一个适用于其他几个不相关的类型的操作,那么每个类型暴露一个接口可能是有意义的,这个接口允许操作以或多或少的标准方式完成大部分工作。为了将它绑定到您的示例,几个类可能实现一个接口ISaveable
,它在每个接口上公开Save
方法。单个Save
方法利用它们对内部类状态的访问权限,但是给定ISaveable
个实例的集合,一些外部代码可以定义一个操作,用于将它们保存到某种自定义存储中而无需知道凌乱的细节。
答案 4 :(得分:1)
这取决于完成工作所需的信息。如果工作与类无关(主要是等效的,可以使用几乎任何具有公共接口的类),例如std :: sort,那么使它成为一个自由函数。如果它必须知道内部,则使其成为成员函数。
编辑:另一个重要的考虑因素是表现。例如,就地排序比返回新的有序副本要快几英里。这就是为什么在绝大多数情况下quicksort比合并排序更快的原因,即使合并排序在理论上更快,这是因为quicksort可以就地执行,而我从未听说过就地合并排序。仅仅因为技术上可以在类的公共接口中执行操作,并不意味着你实际上应该这样做。