我偶然发现了以下段落(在阅读PEP-3119时,但问题不是语言特定的)。强调我的。
特别是,通常需要以对象类的创建者未预料到的方式处理对象。构建满足该对象的每个可能用户需求的每个对象方法并不总是最佳解决方案。此外,有许多强大的调度哲学与经典的OOP行为要求形成鲜明对比,行为被严格封装在一个对象中,例如规则或模式匹配驱动的逻辑。
我熟悉OOP:围绕反映概念或现实世界实体的对象构建的代码,封装状态,并且可以通过方法采取行动。
规则或模式匹配驱动逻辑如何工作?它看起来像什么?
真实世界的例子(可能在Web应用程序后端域中?)将非常感激。 Here’s OOP中的相应示例。
答案 0 :(得分:2)
我认为PEP-3119文章描述了expression problem的解决方案。他们描述的解决方案是abstract base classes。
要理解抽象基类,首先要阐明抽象实体和具体实体之间的区别是有用的。抽象实体没有实现。具体实体具有实现。面向对象编程中的实体通常是属性或方法。
面向对象编程语言中的class是一组具体实体。一些面向对象的编程语言也有interfaces,它们是抽象实体的组。抽象基类是实体的混合包。默认情况下,它的所有实体都是抽象的,但可以通过为它们提供默认实现来使它们具体化,如果需要可以覆盖它们。
Java中的抽象基类的一个例子(如果我错了,请纠正我):
abstract class Equals<T> {
public boolean equals(T x) {
return !notEquals(x);
}
public boolean notEquals(T x) {
return !equals(x);
}
}
class Person extends Equals<Person> {
public firstname;
public lastname;
public Person(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public boolean equals(Person x) {
return x.firstname == firstname &&
x.lastname == lastname;
}
}
无论如何,继续表达问题。 Philip Wadler有以下说法:
表达式问题是旧问题的新名称。目标是按案例定义数据类型,其中可以在数据类型上添加新案例,在数据类型上添加新函数,而无需重新编译现有代码,同时保留静态类型安全性(例如,无强制转换)。
表达式问题是将所有切片和切割数据类型转换为可管理的部分,同时仍然允许数据类型任意扩展。数据类型可以被视为案例和函数的二维矩阵。例如,请考虑Document
数据类型:
Text Drawing Spreadsheet
+-----------+-----------+-----------+
draw() | | | |
+-----------+-----------+-----------+
load() | | | |
+-----------+-----------+-----------+
save() | | | |
+-----------+-----------+-----------+
Document
数据类型有三种情况(Text
,Drawing
和Spreadsheet
)和三种函数(draw
,load
和{ {1}})。因此,它已被切成片并切成九块,可以用Java等面向对象的语言实现,如下所示:
save
因此,我们将public interface Document {
void draw();
void load();
void save();
}
public class TextDocument implements Document {
public void draw() { /* draw text doc... */ }
public void load() { /* load text doc... */ }
public void save() { /* save text doc... */ }
}
public class DrawingDocument implements Document {
public void draw() { /* draw drawing... */ }
public void load() { /* load drawing... */ }
public void save() { /* save drawing... */ }
}
public class SpreadsheetDocument implements Document {
public void draw() { /* draw spreadsheet... */ }
public void load() { /* load spreadsheet... */ }
public void save() { /* save spreadsheet... */ }
}
数据类型切片并切成九个可管理的部分。但是,我们选择首先将数据类型切片为函数,然后根据情况对其进行切块。因此,添加新案例很容易(我们所做的就是创建一个实现Document
接口的新类)。但是,我们无法在界面中添加新功能。因此,我们的数据类型不是完全可扩展的。
但是,面向对象的方法并不是切片和切割数据类型的唯一方法。正如您强调的文字所说,还有另一种方式:
特别是,通常需要以对象类的创建者未预料到的方式处理对象。构建满足该对象的每个可能用户需求的每个对象方法并不总是最佳解决方案。此外,有许多强大的调度哲学与经典的OOP行为要求形成鲜明对比,行为被严格封装在一个对象中,例如规则或模式匹配驱动的逻辑。
在面向对象的方式中,行为被严格地封装在一个对象中(即每个类实现一组方法,在上面的例子中,是同一组方法)。另一种选择是规则或模式匹配驱动逻辑,其中数据类型首先按案例切片,然后切割成函数。例如,在OCaml中:
Document
同样,我们将type document
= Text
| Drawing
| Spreadsheet
fun draw (Text) = (* draw text doc... *)
| draw (Drawing) = (* draw drawing doc... *)
| draw (Spreadsheet) = (* draw spreadsheet... *)
fun load (Text) = (* load text doc... *)
| load (Drawing) = (* load drawing doc... *)
| load (Spreadsheet) = (* load spreadsheet... *)
fun save (Text) = (* save text doc... *)
| save (Drawing) = (* save drawing doc... *)
| save (Spreadsheet) = (* save spreadsheet... *)
数据类型切片并切成九个可管理的部分。但是,我们首先按案例切片数据类型,然后将其切割成函数。因此,添加新功能很容易,但无法添加新案例。因此,数据类型仍然不是完全可扩展的。
这是表达问题。如果我们首先将数据类型分割为函数,那么很容易添加新案例但很难添加新函数。如果我们首先根据案例对数据类型进行切片,那么添加新函数很容易,但很难添加新案例。
出现表达式问题是因为扩展数据类型的固有需求。如果数据类型永远不需要扩展,那么您可以使用这两种方法中的任何一种(我将称之为面向对象的方法和功能方法)。但是,对于大多数实际用途,确实需要扩展数据类型。
如果您只需要通过添加新案例来扩展数据类型,那么面向对象的方法就是好的(例如,在图形用户界面中,操作通常保持不变,但可以添加新的可视元素)。如果您只需要通过添加新函数来扩展数据类型,那么功能方法就是好的(例如,几乎所有我能想到的通用程序)。
现在,如果需要通过添加新案例和新函数来扩展数据类型,那么这将是一个问题。但是,可以使用检查(PEP-3119文章使用的单词)在JavaScript和Python等动态语言中完成。唯一的问题是因为它是一个动态解决方案,编译器无法保证你已经实现了数据类型的所有部分,如果你回到表达式问题的定义,最后一个子句是并保持静态类型安全。因此,动态语言仍然无法解决表达问题。
无论如何,PEP-3119文章讨论了调用和检查作为选择数据类型的方法。首选调用是因为如果可以调用函数,则也意味着它已实现。检查是一种动态解决方案,因此并不总是正确的。
如果您想知道抽象基类如何解决表达式问题,那么我建议您阅读PEP-3119文章的其余部分。有关表达问题的更多信息,我建议您阅读Bob Nystrom关于“Solving the Expression Problem”的博文。