在非OO中如何编程?[关闭]


11

在阅读有关OOP缺点的严厉文章以支持其他一些范例的过程中,我遇到了一个例子,我发现它没有太多的缺点。

我想对作者的论点持开放态度,尽管我从理论上可以理解他们的观点,但尤其是一个例子,我很难想象如何用FP语言更好地实现它。

来自:http : //www.smashcompany.com/technology/object-oriented-programming-is-an-expensive-disaster-which-must-end

// Consider the case where “SimpleProductManager” is a child of
// “ProductManager”:

public class SimpleProductManager implements ProductManager {
    private List products;

    public List getProducts() {
        return products;
    }

    public void increasePrice(int percentage) {
        if (products != null) {
            for (Product product : products) {
                double newPrice = product.getPrice().doubleValue() *
                (100 + percentage)/100;
                product.setPrice(newPrice);
            }
        }
    }

    public void setProducts(List products) {
        this.products = products;
    }
}

// There are 3 behaviors here:

getProducts()

increasePrice()

setProducts()

// Is there any rational reason why these 3 behaviors should be linked to
// the fact that in my data hierarchy I want “SimpleProductManager” to be
// a child of “ProductManager”? I can not think of any. I do not want the
// behavior of my code linked together with my definition of my data-type
// hierarchy, and yet in OOP I have no choice: all methods must go inside
// of a class, and the class declaration is also where I declare my
// data-type hierarchy:

public class SimpleProductManager implements ProductManager

// This is a disaster.

请注意,我并不是要对作者的论点提出反驳,即“这3种行为应与数据层次结构链接的任何合理理由吗?”。

我要特别问的是,如何用FP语言对这个示例进行建模/编程(实际代码,而不是理论上的代码)?


44
您不可能合理地期望在如此简短的示例中比较任何编程范例。这里的任何人都可以提出使他们自己的首选范例看起来比其他范例更好的代码需求,尤其是如果它们实施不正确的话。只有当您拥有真实,庞大且不断变化的项目时,您才能洞悉不同范式的优缺点。
欣快感'17

20
OO编程没有什么要求这三种方法应该在同一类中一起使用。同样,OO编程也没有什么规定行为必须与数据存在于同一类中。也就是说,通过OO编程,您可以将数据与行为放在同一个类中,或者可以将其拆分为单独的实体/模型。无论哪种方式,OO有没有什么要说的数据应该如何与一个对象,因为一个概念物体是从根本上与建模有关的行为进行分组逻辑相关的方法到一个类。
本·科特雷尔

20
我对那篇文章的咆哮说了十句话,然后放弃了。不要理会窗帘后面的那个人。在其他消息中,我不知道True Scotsmen主要是OOP程序员。
罗伯特·哈维

11
还有人用OO语言编写程序代码,然后又想知道为什么OO对他不起作用。
TheCatWhisperer

11
尽管毫无疑问,OOP是从头到尾设计失误的灾难,但我为成为其中的一员而感到自豪!-这是不可读的文章,您给出的示例基本上是在争论设计不良的类层次结构设计不良。
埃里克·利珀特

Answers:


42

在FP样式中,Product它将是一个不可变的类,product.setPrice不会变异一个Product对象,而是返回一个新的对象,并且该increasePrice函数将是一个“独立”函数。使用类似于您的语法(类似于C#/ Java),等效函数可能如下所示:

 public List increasePrice(List products, int percentage) {
    if (products != null) {
        return products.Select(product => {
                double newPrice = product.getPrice().doubleValue() *
                    (100 + percentage)/100;
                return product.setPrice(newPrice);     
               });
    }
    else return null;
}

正如您所看到的,这里的核心并没有什么不同,只是省略了人为的OOP示例中的“样板”代码。但是,我不认为这是OOP导致代码膨胀的证据,而只是作为以下事实的证据:如果人构建了足够人工的代码示例,就可以证明任何事情。


7
制作“更多FP”的方法:1)使用Maybe /可选类型而不是可空性,使编写全部函数(而不是部分函数)更加容易,并使用高阶辅助函数来抽象“ if(x!= null)”逻辑。2)使用镜片来定义单个产品的涨价,方法是在镜片价格上应用一定百分比的涨幅。3)使用部分应用程序/合成/计算来避免为地图/ Select调用使用明确的lambda。
杰克

6
一定要说,我讨厌集合的想法可能为空,而不是设计为空。具有本地元组/集合支持的功能语言以这种方式运行。即使在OOP中,我也讨厌返回null集合是返回类型的地方。/ rant over
Berin Loritsch '17

但这可以是静态方法,例如在Java或C#之类的OOP语言中的实用程序类中。该代码之所以简短,部分原因是您要求传递列表而不是自己保存。原始代码还拥有一个数据结构,只需将其移出即可缩短原始代码,而无需更改概念。
标记

@Mark:可以,我认为OP已经知道这一点。我理解的问题是“如何以功能方式表达此问题”,而不是非OOP语言中的强制性问题。
布朗

@Mark FP和OO不会互相排斥。
Pieter B

17

我要特别问的是,如何用FP语言对这个示例进行建模/编程(实际代码,而不是理论上的代码)?

使用“ a” FP语言?如果足够的话,我选择Emacs lisp。它确实具有类型(种类,种类)的概念,但是只有内置的。因此,您的示例简化为“如何将列表中的每个项目乘以某项并返回新列表”。

(mapcar (lambda (x) (* x 2)) '(1 2 3))

你去。其他语言将是相似的,不同之处在于您可以通过具有通常的功能“匹配”语义的显式类型来获得好处。查看Haskell:

incPrice :: (Num) -> [Num] -> [Num]  
incPrice _ [] = []  
incPrice percentage (x:xs) = x*percentage : incPrice percentage xs  

(或者类似的东西,已经很久了...)

我想对作者的观点保持开放,

为什么?我试图阅读这篇文章。我不得不在一页后放弃,只是很快地扫描了其余的部分。

本文的问题不在于它反对OOP。我也不是盲目地“亲OOP”。我已经使用逻辑,功能和OOP范式进行编程,在可能的情况下,经常使用同一语言编写,并且经常没有这三种语言中的任何一种,纯粹是必须的,甚至在汇编程序级别也是如此。我永远不会说这些范例中的任何一个在各个方面都远远优于另一个。我会说我比X更喜欢X 语言吗?当然可以!但这不是该文章的目的。

文章的问题在于,从第一句话到最后一句话,他都使用了大量的修辞工具(谬误)。甚至开始描述它包含的所有错误是完全没有用的。作者非常清楚地表明,他对讨论不感兴趣,他正处于十字军东征中。那为什么要打扰呢?

归根结底,所有这些只是完成工作的工具。可能有一些工作的OOP更好,而其他一些工作的FP更好,或者两者都过头了。重要的是选择正确的工具来完成工作。


4
“非常清楚他对讨论不感兴趣,他正在十字军东征”对这颗宝石有赞誉。
欣快感'17

您是否不需要Haskell代码上的Num约束?否则怎么称呼(*)?
jk。

@jk。,我做Haskell已经很久了,这仅仅是为了满足OP对他正在寻找的答案的约束。;)如果有人想修复我的代码,请放心。但是可以肯定的是,我将其切换为Num。
AnoE

7

作者提出了一个很好的观点,然后选择了一个平淡无奇的示例来对其进行备份。抱怨不是与类的实现有关,而是与数据层次结构与功能层次结构密不可分的结合。

随之而来的是,要理解作者的观点,只看他如何以功能样式实现这个单一的类就无济于事。您将不得不看到他将如何以一种功能样式围绕该类设计数据和功能的整个上下文

考虑产品和定价中涉及的潜在数据类型。集思广益:名称,UPC代码,类别,运输重量,价格,货币,折扣代码,折扣规则。

这是面向对象设计的简单部分。我们只是为所有上述“对象”创建一个类,我们很好,对吧?上一Product堂课,将其中的一些结合在一起吗?

但是,等等,您可以拥有其中一些类型的集合和集合:Set [类别],(折扣代码->价格),(数量->折扣金额)等等。这些适合哪里?我们是否创建一个单独的CategoryManager跟踪所有不同种类的类别,或者该责任属于Category我们已经创建的类?

现在,如果您有一定数量的来自两个不同类别的项目,可以为您提供价格折扣的功能呢?这是在Product类,Category类,DiscountRule类,CategoryManager类中进行还是我们需要新的东西?这就是我们最终得到的结果DiscountRuleProductCategoryFactoryBuilder

在功能代码中,数据层次结构与功能完全正交。您可以以任何有意义的方式对函数进行排序。例如,您可以将改变产品价格的所有功能归为一组,在这种情况下,有必要像mapPrices下面的Scala示例中那样排除常见功能:

def mapPrices(f: Int => Int)(products: Traversable[Product]): Traversable[Product] =
  products map {x => x.copy(price = f(x.price))}

def increasePrice(percentage: Int)(price: Int): Int =
  price * (percentage + 100) / 100

mapPrices(increasePrice(25))(products)

我大概可以在这里添加其他价格相关的功能,如decreasePriceapplyBulkDiscount等。

由于我们还使用的集合Products,因此OOP版本需要包含管理该集合的方法,但是您不希望该模块与产品选择有关,而希望与价格有关。功能数据耦合迫使您也将收集管理样板丢在那里。

您可以尝试通过将products成员放在单独的类中来解决此问题,但是最终您会得到非常紧密耦合的类。OO程序员认为功能-数据耦合非常自然,甚至是有益的,但是由于灵活性的损失,成本很高。任何时候创建函数时,都必须将其分配给一个且只有一个类。每当您要使用功能时,都必须找到一种方法来将其耦合数据传递到使用点。这些限制是巨大的。


2

正如作者所暗示的那样,简单地将数据和功能分开就可以在F#(“ FP语言”)中看起来像这样。

module Product =

    type Product = {
        Price : decimal
        ... // other properties not mentioned
    }

    let increasePrice ( percentage : int ) ( product : Product ) : Product =
        let newPrice = ... // calculate

        { product with Price = newPrice }

您可以通过这种方式对产品清单执行提价。

let percentage = 10
let products : Product list = ...  // load?

products
|> List.map (Product.increasePrice percentage)

注意:如果您不熟悉FP,则每个函数都会返回一个值。来自类似C的语言,您可以将函数中的最后一条语句视为return前面的语句。

我包括了一些类型注释,但是它们应该是不必要的。由于模块不拥有数据,因此此处不需要getter / setter。它拥有数据的结构和可用的操作。同样可以看到List这一点,它公开map了在列表中的每个元素上运行一个函数,并在新列表中返回结果。

请注意,Product模块不必了解任何有关循环的知识,因为责任由List模块(创建了循环需求)负责。


1

首先,我不是函数式编程专家。我更像是一个面向对象的人。因此,尽管我非常确定以下是您如何使用FP实现相同功能的方法,但我可能是错的。

这是在Typescript中(因此,所有类型注释都在其中)。Typescript(如javascript)是一种多域语言。

export class Product extends Object {
    name: string;
    price: number;
    category: string;
}

products: Product[] = [
    new Product( { name: "Tablet", "price": 20.99, category: 'Electronics' } ),
    new Product( { name: "Phone", "price": 500.00, category: 'Electronics' } ),
    new Product( { name: "Car", "price": 13500.00, category: 'Auto' } )
];

// find all electronics and double their price
let newProducts = products
    .filter( ( product: Product ) => product.category === 'Electronics' )
    .map( ( product: Product ) => {
        product.price *= 2;
        return product;
    } );

console.log( newProducts );

详细地讲(同样不是FP专家),需要理解的是没有很多预定义的行为。没有一种“涨价”方法可以在整个列表中应用涨价,因为当然这不是面向对象的:没有类可以定义这种行为。您无需创建存储产品列表的对象,而只需创建产品数组即可。然后,您可以使用标准的FP程序以所需的任何方式来操纵该数组:过滤器以选择特定项目,映射以调整内部结构等。最后,您可以对产品列表进行更详细的控制,而不必受限于SimpleProductManager提供给您的API。有些人可能认为这是一种优势。您也确实是 不必担心与ProductManager类相关的任何负担。最后,不必担心“ SetProducts”或“ GetProducts”,因为没有任何对象可以隐藏您的产品:相反,您只拥有要使用的产品列表。再次,这可能是优点还是缺点,这取决于您正在与之交谈的环境/人员。另外,显然没有类层次结构(这是他所抱怨的),因为首先没有类。根据您所谈话的环境/人员,这可能是有利还是不利。而且,显然没有类层次结构(这是他所抱怨的),因为首先没有类。根据您所谈话的环境/人员,这可能是有利还是不利。而且,显然没有类层次结构(这是他所抱怨的),因为首先没有类。

我没有花时间阅读他的整本书。我会在方便时使用FP练习,但我绝对是OOP专家。因此,我想自从回答您的问题以来,我还会对他的观点做一些简短的评论。我认为这是一个非常人为的例子,突出了OOP的“缺点”。在此特定情况下,对于所示功能,OOP可能会过大,FP可能更适合。再说一次,如果这是用于购物车之类的东西,保护您的产品清单并限制对它的访问是该程序的一个非常重要的目标,并且FP无法执行此类操作。同样,可能我不是FP专家,但是为电子商务系统实现了购物车后,我宁愿使用OOP而不是FP。

就我个人而言,我很难认真对待任何人,因为这样一个强有力的论点是“ X太可怕了,请始终使用Y”。编程具有多种工具和范例,因为要解决的问题很多。FP占有一席之地,OOP占有一席之地,如果他们不能理解我们所有工具的弊端和优势以及何时使用它们,谁都不会成为一名优秀的程序员。

**注意:显然在我的示例中有一个类:Product类。在这种情况下,尽管它只是一个愚蠢的数据容器:我不认为我对它的使用违反了FP的原理。它更像是类型检查的助手。

**注意:我不记得我的脑袋了,也没有检查过我使用map函数的方式是否会修改产品,即我是否无意中使原始产品的价格翻了一番数组。显然,这是FP试图避免的一种副作用,而且我可以肯定的是,有了更多的代码,我可以确保它不会发生。


2
从经典意义上讲,这实际上不是一个面向对象的例子。在真正的OOP中,数据将与行为结合在一起。在这里,您将两者分开。这不一定是一件坏事(我实际上发现它更干净),但这并不是我所说的经典OOP。
罗伯特·哈维

0

在我看来,SimpleProductManager并不是某种东西的子代(扩展或继承)。

它只是ProductManager接口的公正实现,基本上是一种约定,用于定义对象必须执行的操作(行为)。

如果是孩子(最好是继承的类或扩展了其他类功能的类),它将被写为:

class SimpleProductManager extends ProductManager {
    ...
}

所以基本上,作者说:

他有一些对象的行为是:setProducts,increasePrice,getProducts。而且我们不在乎对象是否也具有其他行为或该行为如何实现。

SimpleProductManager类实现它。基本上,它执行动作。

也可以称为PercentagePriceIncreaser,因为它的主要行为是将价格提高一定的百分比值。

但是我们还可以实现另一个类:ValuePriceIncreaser,其行为将是:

public void increasePrice(int number) {
    if (products != null) {
        for (Product product : products) {
            double newPrice = product.getPrice() + number;
            product.setPrice(newPrice);
        }
    }
}

从外部的角度来看,什么都没有改变,接口是相同的,仍然具有相同的三种方法,但是行为是不同的。

由于FP中没有接口,因此很难实现。例如,在C语言中,我们可以保存指向函数的指针,并根据需要调用适当的函数。最后,在OOP中,它的工作方式非常相似,但是由编译器“自动”执行。

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.