Làm thế nào điều này sẽ được lập trình trong không OO? [đóng cửa]


11

Đọc qua một bài viết đầy gay gắt về những nhược điểm của OOP để ủng hộ một số mô hình khác, tôi đã gặp một ví dụ mà tôi không thể tìm thấy quá nhiều lỗi.

Tôi muốn cởi mở với các lập luận của tác giả, và mặc dù về mặt lý thuyết tôi có thể hiểu được điểm của họ, một ví dụ cụ thể là tôi đang gặp khó khăn khi tưởng tượng làm thế nào nó sẽ được thực hiện tốt hơn bằng ngôn ngữ FP.

Từ: http://www.smashcompany.com/tĩ/object-oriented-programming-is-an-Exensive-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.

Lưu ý rằng tôi không tìm kiếm phản bác hoặc phản đối lập luận của người viết vì "Có bất kỳ lý do hợp lý nào tại sao 3 hành vi này nên được liên kết với hệ thống phân cấp dữ liệu không?".

Điều tôi đặc biệt hỏi là làm thế nào ví dụ này được mô hình hóa / lập trình bằng ngôn ngữ FP (Mã thực tế, không phải trên lý thuyết)?


44
Bạn không thể mong đợi một cách hợp lý để so sánh bất kỳ mô hình lập trình nào trên các ví dụ ngắn và thiếu sót như vậy. Bất cứ ai ở đây cũng có thể đưa ra các yêu cầu về mã làm cho mô hình ưa thích của riêng họ trông tốt hơn so với phần còn lại, đặc biệt nếu họ thực hiện không đúng cách khác. Chỉ khi bạn có dự án thực sự, lớn, thay đổi, bạn mới có thể hiểu rõ về điểm mạnh và điểm yếu của các mô hình khác nhau.
Euphoric

20
Không có gì về lập trình OO, điều này bắt buộc 3 phương thức đó phải đi cùng nhau trong cùng một lớp; tương tự như vậy, không có gì về lập trình OO bắt buộc hành vi đó phải tồn tại trong cùng một lớp với dữ liệu. Điều đó có nghĩa là, với Lập trình OO, bạn có thể đặt dữ liệu vào cùng một lớp với hành vi hoặc bạn có thể tách nó ra thành một thực thể / mô hình riêng biệt. Dù bằng cách nào, OO thực sự không có gì để nói về việc dữ liệu nên liên quan đến một đối tượng như thế nào, vì khái niệm về một đối tượng về cơ bản liên quan đến hành vi mô hình hóa bằng cách nhóm các phương thức liên quan đến logic vào một lớp.
Ben Cottrell

20
Tôi đã nhận được 10 câu trong bài viết đó và bỏ cuộc. Không chú ý đến người đàn ông đằng sau bức màn đó. Trong một tin khác, tôi không biết rằng True Scotsmen chủ yếu là lập trình viên OOP.
Robert Harvey

11
Một lời ca ngợi khác từ một người viết mã thủ tục bằng ngôn ngữ OO, sau đó tự hỏi tại sao OO không hoạt động cho anh ta.
TheCatWhisperer

11
Mặc dù chắc chắn rằng OOP là một thảm họa của những sai lầm trong thiết kế từ đầu đến cuối - và tôi tự hào là một phần của nó! - bài viết này không thể đọc được và ví dụ bạn đưa ra về cơ bản đưa ra lập luận rằng hệ thống phân cấp lớp được thiết kế kém được thiết kế kém.
Eric Lippert

Câu trả lời:


42

Trong kiểu FP, Productsẽ là một lớp bất biến, product.setPricesẽ không làm biến đổi một Productđối tượng mà thay vào đó trả về một đối tượng mới và increasePricehàm sẽ là một hàm "độc lập". Sử dụng một cú pháp tìm kiếm tương tự như của bạn (giống như C # / Java), một hàm tương đương có thể trông như thế này:

 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;
}

Như bạn thấy, lõi không thực sự khác biệt ở đây, ngoại trừ mã "soạn sẵn" từ ví dụ OOP bị tước bỏ được bỏ qua. Tuy nhiên, tôi không xem đây là bằng chứng cho thấy OOP dẫn đến mã cồng kềnh, chỉ là bằng chứng cho thực tế nếu người ta xây dựng một ví dụ mã đủ nhân tạo, có thể chứng minh bất cứ điều gì.


7
Các cách để thực hiện "thêm FP" này: 1) Sử dụng các loại Có thể / Tùy chọn thay vì vô hiệu hóa để giúp viết tổng hàm dễ dàng hơn thay vì các hàm một phần và sử dụng các hàm trợ giúp bậc cao để trừu tượng hóa "if (x! = Null)" Hợp lý. 2) Sử dụng ống kính để xác định giá tăng cho một sản phẩm về mặt áp dụng mức tăng phần trăm trong bối cảnh ống kính trên giá của sản phẩm. 3) Sử dụng một phần ứng dụng / thành phần / currying để tránh lambda rõ ràng cho bản đồ / Chọn cuộc gọi.
Jack

6
Gotta nói rằng tôi ghét ý tưởng về một bộ sưu tập có thể là null thay vì chỉ đơn giản là trống rỗng bởi thiết kế. Các ngôn ngữ chức năng với bộ tuple / bộ sưu tập hỗ trợ hoạt động theo cách đó. Ngay cả trong OOP tôi ghét trở về nullnơi một bộ sưu tập là loại trả về. /
giận dữ

Nhưng đây có thể là một phương thức tĩnh như trong một lớp tiện ích bằng các ngôn ngữ OOP như Java hoặc C #. Mã này ngắn hơn một phần vì bạn yêu cầu vượt qua trong danh sách và không tự giữ nó. Mã ban đầu cũng giữ cấu trúc dữ liệu và chỉ cần di chuyển nó ra sẽ làm cho mã gốc ngắn hơn mà không thay đổi khái niệm.
Đánh dấu

@Mark: chắc chắn, và tôi nghĩ OP đã biết điều này rồi. Tôi hiểu câu hỏi là "làm thế nào để diễn đạt điều này một cách có chức năng", không bắt buộc bằng ngôn ngữ không OOP.
Doc Brown

@Mark FP và OO không loại trừ nhau.
Pieter B

17

Điều tôi đặc biệt hỏi là làm thế nào ví dụ này được mô hình hóa / lập trình bằng ngôn ngữ FP (Mã thực tế, không phải trên lý thuyết)?

Trong "một" ngôn ngữ FP? Nếu bất kỳ đủ, sau đó tôi chọn Emacs lisp. Nó có khái niệm về các loại (loại, loại), nhưng chỉ có những loại tích hợp. Vì vậy, ví dụ của bạn giảm xuống "làm thế nào để bạn nhân từng mục trong danh sách với một cái gì đó và trả về một danh sách mới".

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

Có bạn đi. Các ngôn ngữ khác sẽ tương tự, với sự khác biệt mà bạn nhận được lợi ích của các loại rõ ràng với ngữ nghĩa "khớp" chức năng thông thường. Kiểm tra Haskell:

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

(Hoặc một cái gì đó như thế, nó đã có từ lâu ...)

Tôi muốn cởi mở với các lập luận của tác giả,

Tại sao? Tôi đã cố gắng đọc bài báo; Tôi đã phải bỏ cuộc sau một trang và nhanh chóng quét phần còn lại.

Vấn đề của bài viết không phải là nó chống lại OOP. Tôi cũng không mù quáng "pro OOP". Tôi đã lập trình với các mô hình logic, chức năng và OOP, khá thường xuyên trong cùng một ngôn ngữ khi có thể, và thường không có bất kỳ một trong ba, hoàn toàn bắt buộc hoặc thậm chí ở cấp độ trình biên dịch. Tôi sẽ không bao giờ nói rằng bất kỳ mô thức nào trong số đó là cực kỳ to lớn đối với các khía cạnh khác. Tôi có thể tranh luận rằng tôi thích ngôn ngữ X hơn Y không? Tất nhiên rồi! Nhưng đó không phải là những gì bài báo đó là về.

Vấn đề của bài viết là ông sử dụng rất nhiều công cụ hùng biện (ngụy biện) từ câu đầu tiên đến câu cuối cùng. Nó là hoàn toàn vô ích để thậm chí bắt đầu mô tả tất cả các lỗi nó chứa. Tác giả nói rõ rằng anh ta không có hứng thú thảo luận, anh ta đang ở trong một cuộc thập tự chinh. Vậy tại sao phải bận tâm?

Vào cuối ngày, tất cả những thứ đó chỉ là công cụ để hoàn thành công việc. Có thể có những công việc mà OOP tốt hơn, và có thể có những công việc khác trong đó FP tốt hơn hoặc cả hai đều quá mức cần thiết. Điều quan trọng là chọn công cụ phù hợp cho công việc và hoàn thành nó.


4
"Rất rõ ràng rằng anh ta không có hứng thú thảo luận, anh ta đang ở trong một cuộc thập tự chinh" Có một sự ủng hộ cho viên ngọc này.
Euphoric

Bạn không cần một ràng buộc Num trên mã Haskell của bạn? làm thế nào bạn có thể gọi (*) nếu không?
jk.

@jk., đã nhiều năm tôi làm Haskell, đó chỉ là để thỏa mãn sự ràng buộc của OP cho câu trả lời mà anh ấy đang tìm kiếm. ;) Nếu ai đó muốn sửa mã của tôi, hãy thoải mái. Nhưng chắc chắn, tôi sẽ chuyển nó sang Num.
AnoE

7

Tác giả đã đưa ra một quan điểm rất tốt sau đó chọn một ví dụ mờ nhạt để cố gắng sao lưu nó. Khiếu nại không phải với việc triển khai lớp, với ý tưởng là hệ thống phân cấp dữ liệu được kết hợp chặt chẽ với hệ thống phân cấp chức năng.

Sau đó, để hiểu quan điểm của tác giả, sẽ không chỉ giúp xem anh ta sẽ thực hiện lớp duy nhất này theo phong cách chức năng như thế nào. Bạn sẽ phải xem cách anh ta sẽ thiết kế toàn bộ bối cảnh dữ liệu và chức năng xung quanh lớp này theo kiểu chức năng.

Hãy suy nghĩ về các loại dữ liệu tiềm năng liên quan đến sản phẩm và giá cả. Để động não một vài: tên, mã upc, danh mục, trọng lượng vận chuyển, giá cả, tiền tệ, mã giảm giá, quy tắc giảm giá.

Đây là phần dễ dàng của thiết kế hướng đối tượng. Chúng tôi chỉ tạo một lớp cho tất cả các "đối tượng" ở trên và chúng tôi tốt, phải không? Làm một Productlớp để kết hợp một vài trong số họ với nhau?

Nhưng chờ đã, bạn có thể có các bộ sưu tập và tổng hợp của một số loại sau: Đặt [danh mục], (mã giảm giá -> giá), (số lượng -> số tiền chiết khấu), v.v. Những người phù hợp ở đâu? Chúng ta có tạo riêng CategoryManagerđể theo dõi tất cả các loại danh mục khác nhau không, hoặc trách nhiệm đó có thuộc về Categorylớp chúng ta đã tạo không?

Bây giờ những gì về chức năng cung cấp cho bạn giảm giá nếu bạn có một số lượng nhất định của các mặt hàng từ hai loại khác nhau? Điều đó đi trong Productlớp, Categorylớp, DiscountRulelớp, CategoryManagerlớp, hoặc chúng ta cần một cái gì đó mới? Đây là cách chúng tôi kết thúc với những thứ như DiscountRuleProductCategoryFactoryBuilder.

Trong mã chức năng, hệ thống phân cấp dữ liệu của bạn hoàn toàn trực giao với các chức năng của bạn. Bạn có thể sắp xếp các chức năng của mình theo bất cứ cách nào có ý nghĩa ngữ nghĩa. Ví dụ: bạn có thể nhóm tất cả các chức năng thay đổi giá của các sản phẩm lại với nhau, trong trường hợp đó sẽ hợp lý khi đưa ra chức năng phổ biến như mapPricestrong ví dụ Scala sau:

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)

Tôi có lẽ có thể thêm chức năng giá khác liên quan đến ở đây như thế decreasePrice, applyBulkDiscountvv

Vì chúng tôi cũng sử dụng một bộ sưu tập Products, phiên bản OOP cần bao gồm các phương thức để quản lý bộ sưu tập đó, nhưng bạn không muốn mô-đun này là về lựa chọn sản phẩm, bạn muốn nó có giá. Việc ghép dữ liệu chức năng buộc bạn cũng phải ném bản quản lý bộ sưu tập vào đó.

Bạn có thể cố gắng giải quyết điều này bằng cách đưa productsthành viên vào một lớp riêng, nhưng sau đó bạn kết thúc với các lớp kết hợp rất chặt chẽ. Các lập trình viên OO nghĩ rằng việc ghép dữ liệu chức năng là rất tự nhiên và thậm chí có lợi, nhưng có một chi phí cao liên quan đến việc mất tính linh hoạt. Bất cứ khi nào bạn tạo một hàm, bạn phải gán nó cho một và chỉ một lớp. Bất cứ khi nào bạn muốn sử dụng một chức năng, bạn phải tìm cách đưa dữ liệu được ghép nối của nó đến điểm sử dụng. Những hạn chế là rất lớn.


2

Đơn giản chỉ cần tách dữ liệu và chức năng như tác giả đã ám chỉ có thể trông như thế này trong F # ("ngôn ngữ 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 }

Bạn có thể thực hiện tăng giá trên danh sách các sản phẩm theo cách này.

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

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

Lưu ý: Nếu bạn không quen thuộc với FP, mọi hàm đều trả về một giá trị. Đến từ một ngôn ngữ giống như C, bạn có thể xử lý câu lệnh cuối cùng trong một hàm như thể nó có một ngôn ngữ returnphía trước nó.

Tôi đã bao gồm một số chú thích loại, nhưng chúng không cần thiết. getter / setter ở đây là không cần thiết vì mô-đun không sở hữu dữ liệu. Nó sở hữu cấu trúc của dữ liệu và các hoạt động có sẵn. Điều này cũng có thể được nhìn thấy List, nó sẽ hiển thị mapđể chạy một hàm trên mọi phần tử trong danh sách và trả về kết quả trong một danh sách mới.

Lưu ý rằng mô-đun Sản phẩm không phải biết bất cứ điều gì về vòng lặp, vì trách nhiệm đó thuộc về mô-đun Danh sách (người đã tạo ra nhu cầu lặp).


1

Hãy để tôi nói trước điều này với thực tế rằng tôi không phải là một chuyên gia về lập trình chức năng. Tôi là một người OOP hơn. Vì vậy, trong khi tôi khá chắc chắn dưới đây là cách bạn sẽ thực hiện cùng loại chức năng với FP, tôi có thể sai.

Đây là trong Bản in (do đó tất cả các chú thích loại). Bản đánh máy (như javascript) là một ngôn ngữ đa miền.

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 );

Cụ thể (và một lần nữa, không phải là chuyên gia về FP), điều cần hiểu là không có nhiều hành vi được xác định trước. Không có phương pháp "tăng giá" áp dụng tăng giá trong toàn bộ danh sách, vì tất nhiên đây không phải là OOP: không có lớp nào để xác định hành vi đó. Thay vì tạo một đối tượng lưu trữ danh sách các sản phẩm, bạn chỉ cần tạo một mảng sản phẩm. Sau đó, bạn có thể sử dụng các quy trình FP tiêu chuẩn để thao tác mảng này theo bất kỳ cách nào bạn muốn: lọc để chọn các mục cụ thể, bản đồ để điều chỉnh nội bộ, v.v ... Bạn kết thúc với việc kiểm soát chi tiết hơn danh sách sản phẩm của mình mà không bị giới hạn bởi API mà Simple SẢNtManager cung cấp cho bạn. Điều này có thể được coi là một lợi thế của một số. Cũng đúng là bạn không ' không phải lo lắng về bất kỳ hành lý nào liên quan đến lớp ProductManager. Cuối cùng, không phải lo lắng về "Set Products" hay "Get Products", bởi vì không có đối tượng nào đang che giấu sản phẩm của bạn: thay vào đó, bạn chỉ có danh sách các sản phẩm mà bạn đang làm việc. Một lần nữa, đây có thể là một lợi thế hoặc bất lợi tùy thuộc vào hoàn cảnh / người bạn đang nói chuyện. Ngoài ra, rõ ràng là không có hệ thống phân cấp lớp (đó là những gì anh ấy đã phàn nàn) bởi vì không có lớp học ở nơi đầu tiên. đây có thể là một lợi thế hoặc bất lợi tùy thuộc vào hoàn cảnh / người bạn đang nói chuyện. Ngoài ra, rõ ràng là không có hệ thống phân cấp lớp (đó là những gì anh ấy đã phàn nàn) bởi vì không có lớp học ở nơi đầu tiên. đây có thể là một lợi thế hoặc bất lợi tùy thuộc vào hoàn cảnh / người bạn đang nói chuyện. Ngoài ra, rõ ràng là không có hệ thống phân cấp lớp (đó là những gì anh ấy đã phàn nàn) bởi vì không có lớp học ở nơi đầu tiên.

Tôi đã không dành thời gian để đọc toàn bộ câu nói của anh ấy. Tôi sử dụng thực hành FP khi nó thuận tiện, nhưng tôi chắc chắn là một loại người OOP. Vì vậy, tôi đã tìm ra từ khi tôi trả lời câu hỏi của bạn, tôi cũng sẽ đưa ra một số nhận xét ngắn gọn về ý kiến ​​của anh ấy. Tôi nghĩ rằng đây là một ví dụ rất giả tạo làm nổi bật "nhược điểm" của OOP. Trong trường hợp cụ thể này, đối với chức năng được hiển thị, OOP có thể bị giết quá mức và có lẽ FP sẽ phù hợp hơn. Sau đó, một lần nữa, nếu điều này là cho một cái gì đó như một giỏ hàng, bảo vệ danh sách sản phẩm của bạn và hạn chế quyền truy cập vào nó (tôi nghĩ) là một mục tiêu rất quan trọng của chương trình, và FP không có cách nào để thực thi những điều đó. Một lần nữa, có thể tôi không phải là chuyên gia về FP, nhưng đã triển khai giỏ hàng cho các hệ thống thương mại điện tử, tôi thích sử dụng OOP hơn là FP.

Cá nhân tôi có một thời gian khó khăn với bất cứ ai nghiêm túc mà đưa ra một lập luận mạnh mẽ như vậy cho "X chỉ là khủng khiếp. Luôn luôn sử dụng Y". Lập trình có nhiều công cụ và mô hình vì có rất nhiều vấn đề cần giải quyết. FP có vị trí của nó, OOP có vị trí của nó, và không ai sẽ trở thành một lập trình viên tuyệt vời nếu họ không thể hiểu được những hạn chế và lợi thế của tất cả các công cụ của chúng tôi và khi nào nên sử dụng chúng.

** lưu ý: Rõ ràng có một lớp trong ví dụ của tôi: lớp Sản phẩm. Trong trường hợp này mặc dù nó chỉ đơn giản là một thùng chứa dữ liệu câm: Tôi không nghĩ việc sử dụng nó vi phạm các nguyên tắc của FP. Nó là nhiều hơn một người trợ giúp để kiểm tra loại.

** lưu ý: Tôi không nhớ trên đỉnh đầu và không kiểm tra xem cách tôi sử dụng chức năng bản đồ có sửa đổi các sản phẩm tại chỗ hay không, tức là tôi đã vô tình tăng gấp đôi giá sản phẩm trong các sản phẩm ban đầu mảng. Đó rõ ràng là loại tác dụng phụ mà FP cố gắng tránh và với một chút mã hơn tôi chắc chắn có thể chắc chắn rằng nó không xảy ra.


2
Đây không thực sự là một ví dụ OOP, theo nghĩa cổ điển. Trong OOP thực, dữ liệu sẽ được kết hợp với hành vi; Ở đây, bạn đã tách hai. Nó không hẳn là một điều xấu (tôi thực sự thấy nó sạch hơn), nhưng đó không phải là điều mà tôi sẽ gọi là OOP cổ điển.
Robert Harvey

0

Dường như đối với tôi, Simple ProducttManager không phải là con (mở rộng hoặc kế thừa) một thứ gì đó.

Nó chỉ thực hiện giao diện ProductManager, về cơ bản là một hợp đồng xác định những hành động (hành vi) mà đối tượng phải thực hiện.

Nếu nó là một đứa trẻ (hay nói tốt hơn, lớp hoặc lớp mở rộng chức năng lớp khác) thì nó sẽ được viết là:

class SimpleProductManager extends ProductManager {
    ...
}

Về cơ bản, tác giả nói:

Khi có một số đối tượng có hành vi là: set Products, tăngprice, get Products. Và chúng tôi không quan tâm nếu đối tượng cũng có hành vi khác hoặc cách hành vi được thực hiện.

Lớp Simple SẢNtManager thực hiện nó. Về cơ bản, nó thực hiện các hành động.

Nó cũng có thể được gọi là PercentagepriceIncreaser vì hành vi chính của nó là tăng giá theo một số giá trị phần trăm.

Nhưng chúng ta cũng có thể triển khai một lớp khác: ValuepriceIncreaser sẽ tuân theo:

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

Từ quan điểm bên ngoài, không có gì thay đổi, giao diện giống nhau, vẫn có ba phương thức giống nhau nhưng hành vi thì khác.

Vì không có giao diện nào trong FP nên sẽ khó thực hiện. Ví dụ, trong C, chúng ta có thể giữ các con trỏ tới các hàm và gọi một hàm thích hợp dựa trên nhu cầu của chúng ta. Cuối cùng, trong OOP, nó hoạt động theo cách rất giống nhau, nhưng nó được "tự động hóa" bởi trình biên dịch.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.