thảo luận OOP hay FP

  • Người tạo chủ đề Người tạo chủ đề Quynh 123
  • Ngày bắt đầu Ngày bắt đầu

Các Voz Dev thuộc trường phái nào ?


  • Tổng số người bình chọn
    684
Nhìn vào code mình có 1 số vấn đề:
1 class trong oop nhìn vào là biết ngay:
  • Nó chứa những gì (data)
  • Nó có thể làm được những gì. (behavior)

Còn code của thím chia tách nó ra làm 3 phần rời rạc:
  • data (struct)
  • behavior (trait)
  • data - behavior association (impl)
Vậy 1 số vấn đề đặt ra như sau:
  • Compiler có đảm bảo cái mớ association phủ hết không (ví dụ trong trường hợp tôi không cung cấp impl thì compiler có báo lỗi không.
  • Cohesion: 3 phần ở mỗi nơi như thế, tôi nhìn vào customer không biết nó thể làm được gì. Nhìn vào 2 loại customer khác nhau không biết nó share những trait gì.
    Ví dụ trong Java tôi có 2 class A và B, tôi chỉ cần nhìn nó impl những interface nào là biết nó share những behaviour nào ngay. Nói đơn giản trong Java data + behavior là 1 mớ đi liền. Điều này làm giảm cognitive overhead thấy rõ, nhìn vào 1 class tôi biết nó chứa gì trong đó và tôi có thể làm được gì với mớ data đó.
  • Compile time: Việc không khai báo explicit chắc chắn sẽ làm tăng compile time. Vì 3 cái mớ đó là riêng biệt hoàn toàn, giả sử tôi gọi customer.print_info(), compiler phải xác định impl của thằng customer nào match với trait nào. Trong khi anh Java compiler nó biết ngay class này impl interface nào, việc xác định là vô cùng dễ.
Sao lúc tạo anh biết anh phải tạo loại khách hàng nào
 
Sao lúc tạo anh biết anh phải tạo loại khách hàng nào

WTF. Hỏi gì vậy trời?
oh2.png


via theNEXTvoz for iPhone
 
Thật ra bài toán engine gì đó cũng chỉ là expression problem cũ rích mà phe FP đem typeclass ra hype. Anh kia chưa hiểu rõ nên làm cái đề nát bét.
Expression Problem thì oop cổ điển sẽ có visitor pattern để giải (ngoài ra còn có thể dùng generic, mà cách này dài ngoằng nên anh nào thích thì tự google). Còn thật tế thì ngôn ngữ nào hỗ trợ multiple dispatch như groovy, c# thì đơn giản hơn nhiều.
// groovy là multi paradigm, trait là tính năng của oop nhé
 
Ok, vì bài này hơi khó hiểu nên mình xin giải thích luôn. Ý đồ của bài này là như sau. 😆

Đó là thím nghĩ như này:

Xem tệp đính kèm 229295

Nhưng các anh rồ FP lại nghĩ như này cơ: :beat_brick:

Xem tệp đính kèm 229296
Như bạn Thích Màu Hường nói, đúng là mình nghĩ theo cách thứ hai thật. Đối với Procedural, cải tiến là việc đóng gói và tái sử dụng code theo hàm phương thức. Đối với OOP là đóng gói và tái sử dụng hàm và phương thức theo cấu trúc dữ liệu. Đối với Functional sẽ là đóng gói và tái sử dụng phương thức theo từng kiểu cấu trúc dữ liệu và từng loại chức năng khác nhau.

p/s: tôi đọc qua cái solution của bạn vẫn thấy dùng class cho class Visualizer customerType where, bạn dùng class thì chẳng phải đang dùng polymorphism,overloading của OOP đó sao?

Ref: https://www.haskell.org/tutorial/classes.html
Mình chưa code FP,hashkell bao giờ, liệu mình có nhầm gì không. Mong được thông não?
Ok, đến đoạn thứ hai polymorphism được chia ra thành ba loại:
  • Ad-hoc polymorphism
  • Sub-type polymorphism
  • Parametric polymorphism
Đối với Parametric polymorphism ở trong cả ngôn ngữ OOP và FP đều có, nó chính là Generic. Ngoài Parametric, loại polymorphism được dùng chủ yếu trong OOP là Sub-type (chính là chức năng kế thừa của C# và Java). Còn polymorphism chính còn lại của các ngôn ngữ Functional như Haskell, Elixir, Rust là Ad-hoc polymorphism.

Đối với OOP Sub-type đúng như các bạn mention ở trên Poll: class A lai có class B class C kế thừa, class C lại có class D class E kế thừa. Do đó, mô hình kế thừa có cấu trúc dạng cây như sau:
drawing.png

Đối với FP thì khác, FP chia riêng cấu trúc ra thành hai loại:
  • type/struct định nghĩa kiểu dữ liệu.
  • typeclass/trait/protocol/category định nghĩa chức năng và có thể chứa implementation.
Việc một type đi implement một typeclass tương ứng với việc chúng ta đã tạo ra một adapter giữa kiểu dữ liệu và chức năng của nó.

Mô hình về type system của FP có thể được mô phỏng bằng đồ thị hai phía như sau:

1602401253819.png

Vậy điểm khác nhau giữa hai loại polymorphism này là gì. Mình xin trả lời là tính đóng gói.

Quay lại lịch sử của ngôn ngữ lập trình bắt đầu từ Procedural. Đối với Procedural chỉ cần có hai kiểu cấu trúc một là struct hai là hàm là đủ.

Vậy tại sao OOP phải nghĩ ra class. Đó là vì nếu để các struct và các method tách biệt một cách tùy tiện thì sẽ không thể biết được struct đó thực hiện nhiệm vụ gì. Ngoài ra nhiều hàm kết hợp với nhau mới tạo một chức năng và chức năng thường gắn với một cấu trúc dữ liệu chính. Do đó gắn các phương thức với cấu trúc dữ liệu là hợp lý nhất.

Tuy nhiên do đặc điểm của các ngôn ngữ high abstraction là nếu bên dưới class implementation sai, tính đóng gói không tốt thì bên trên dùng kiểu gì dùng cũng sai. Do đó để tránh người dùng truy cập thay đổi nhưng phương thức và thuộc tính ngoài ý muốn, OOP sử dụng hai access modifier là public và private.

Ngoài việc muốn đóng gói tốt các chức năng theo cấu trúc dữ liệu, để có thể tái sử dụng và customize các chức năng sẵn có, OOP sinh ra tính kế thừa và một access modifier nữa là protected. Tuy nhiên do việc chia sẻ chung thuộc tính và phương thức giữa các chức năng khác nhau khi kế thừa khá là nguy hiểm. Cho nên nhiều ngôn ngữ OOP như Java, C# không cho phép đa kế thừa. Thậm chí Golang còn loại bỏ kế thừa ra khỏi OOP do việc customize lại chức năng cũng có thể dẫn đến sai sót trong implementation bên dưới. Thay vào đó Golang chỉ cho phép tái sử dụng chức năng bằng cách sử dụng embedded struct. Ở trong Python cho phép sử dụng đa kế thừa nhưng không phát triển cũng vì antipattern như trên.

Sang tới FP việc đóng gói đã cải tiến hơn bởi phân tách được kiểu dữ liệu và chức năng ra thành hai cấu trúc riêng biệt. Còn việc xử lý vấn đề conflict ntn thì mình đã đề cập ở trên mình xin phép không đề cập lại. Do tính đóng gói của FP khá là tốt nên thậm chí việc implement các principle như SOLID hay Clean Architecture ở trong FP thậm chí còn tốt hơn OOP rất nhiều.

Trong bài toán thứ nhất của bạn Thích màu hường, tại sao mình biết là FP có thể cover được các bài toán của OOP. Đơn giản là vì Sub-Type polymorphism của OOP là dạng cây còn Ad-hoc Polymorphism của mình là dạng network. Tuy là dạng đồ thị hai phía tuy nhiên, do typeclass của FP có tính năng subclass tương tự như inheritence của OOP. Do đó mình có thể tự tin cover các bài toán của OOP bằng các ngôn ngữ FP.

Tính đa hình của FP có thể cover tính đa hình của OOP, vậy tính đa hình của OOP có thể cover FP hay không? Các bạn có thể thấy ở trong OOP cũng có tính năng gần gần tương tự như typeclass và ad-hoc polymorphism của FP, đó là interface. Vậy interface khác với typeclass chỗ nào. Trong bài toán thứ 2 mình viết nhằm mục đích là nêu ra điều đó.

Điểm khác nhau thứ nhất là name collision, tên các phương thức ở trong interface có thể bị name collision với nhau nhưng typeclass thì không. Một class không thể implement hai interface có cùng tên method nhưng lại khác nhau về input output, tuy nhiên typeclass có thể làm được điều này.

Điểm khác nhau thứ hai ví dụ như trong bài toán của mình bạn sẽ thấy ba interface ICar, ISubmarine, IAircraft vì có chung một phương thức là simulate. Do đó việc implement hàm simulate cho cả ba interface này trong cùng một class là vô nghĩa. Còn đối với FP đương nhiên việc này lại vô cùng có nghĩa. Ví dụ bạn có một base typeclass sử dụng chung giữa các typeclass khác nhau, sau đó một type đi implement cả ba typeclass này thì ba typeclass này vẫn có thể hoạt động độc lập mà không ảnh hưởng đến nhau. Do đó khi bạn run hàm simulate với hybrid vehicle trong OOP n lần đi nữa thì hàm simulate vẫn thể hiện một cách giống nhau, còn đương nhiên khi simulate Car hoặc simulate Aircraft hay simulate Submarine trên vehicle này trong FP thì với mỗi chức năng vehicle sẽ hoạt động một cách khác nhau. Ở đây tính polymorphism của interface trong OOP đó là với cùng một view interface có thể có nhiều kiểu dữ liệu khác nhau, tuy nhiên đối với một kiểu dữ liệu dù là với interface nào đi nữa thì biểu hiện của nó cũng phải giống nhau. Còn tính polymorphism của typeclass trong FP đó là ngoài việc một view có thể view nhiều kiểu dữ liệu thì đối với các view khác nhau, kiểu dữ liệu cũng có thể có các behavior khác nhau. Ví dụ như trong OOP có n class và m interface, mỗi interface có l hàm thì cuối cùng bạn cũng chỉ có nhiều nhất n * l phương thức khác nhau. Còn đối với FP thì bạn sẽ có tối n * m * l phương thức. Điều này khác hẳn với OOP và thể hiện được ý tưởng của Category Theory trong toán học.

drawing (3).png


Vậy giờ mọi người có thể thắc mắc là việc implement n * m * l phương thức sẽ phức tạp hơn việc implement n * l phương thức hay không thì mình xin giải thích là các chức năng ở trong typeclass có thể tái sử dụng được. Do đó nếu bạn tái sử dụng một chức năng đến cấp thứ 3 thôi thì số lượng phương thức và utilities sinh ra cũng khá nhiều. Và FP cũng không bị vấn đề về name collision như trong OOP nên số lượng chức năng mà một kiểu dữ liệu có thể tải trong FP là rất là lớn - ví dụ như Rust hay Elixir số lượng ultilities cho các kiểu dữ liệu khác nhau như String, List, Set rất là nhiều, ngược lại với một số ngôn ngữ OOP như Golang rất thiếu hụt về List comprehension và các ultilities xử lý cấu trúc dữ liệu khác - Ruby và JS là một trong số những ngoại lệ. Đó là chưa tính đến việc typeclass có thể parametric, generic cho nhiều tham số về kiểu khác nhau. Do đó khi sử dụng FP mình được lợi nhiều hơn là mất.

Ok, giờ mình giải thích xong rồi. Không biết bên OOP có đưa ra bài toán nào thử thách không nhỉ hay mình sẽ đưa tiếp bài toán của mình.

cc @Thích Màu Hường
 
Sửa lần cuối:
Ok, vì bài này hơi khó hiểu nên mình xin giải thích luôn. Ý đồ của bài này là như sau. 😆


Như bạn Thích Màu Hường nói, đúng là mình nghĩ theo cách thứ hai thật. Đối với Procedural, cải tiến là việc đóng gói và tái sử dụng code theo hàm phương thức. Đối với OOP là đóng gói và tái sử dụng hàm và phương thức theo cấu trúc dữ liệu. Đối với Functional sẽ là đóng gói và tái sử dụng phương thức theo từng kiểu cấu trúc dữ liệu và từng chức năng khác nhau.


Ok, đến đoạn thứ hai polymorphism được chia ra thành ba loại:
  • Ad-hoc polymorphism
  • Sub-type polymorphism
  • Parametric polymorphism
Đối với Parametric polymorphism ở trong cả ngôn ngữ OOP và FP đều có, nó chính là Generic. Ngoài Parametric, loại polymorphism được dùng chủ yếu trong OOP là Sub-type hay chính là chức năng kế thừa của C# và Java. Còn polymorphism chính còn lại của các ngôn ngữ Functional như Haskell, Elixir, Rust chính là Ad-hoc polymorphism.

Đối với OOP Sub-type đúng như các bạn mention ở trên Poll: class A lai có class B class C kế thừa, class C lại có class D class E kế thừa. Do đó, mô hình kế thừa có cấu trúc dạng cây như sau:
Xem tệp đính kèm 234273
Đối với FP thì khác, FP chia riêng cấu trúc ra thành hai loại:
  • type/struct định nghĩa kiểu dữ liệu.
  • typeclass/trait/protocol/category định nghĩa chức năng và có thể chứa implementation.
Khi một type implement một một typeclass là chúng ta đã thêm một adapter giữa kiểu dữ liệu và chức năng của nó.
Mô hình về type system của FP có thể được mô phỏng bằng đồ thị hai phía như sau:
Xem tệp đính kèm 234276
Vậy điểm khác nhau giữa hai loại polymorphism này là gì. Mình xin trả lời là tính đóng gói.

Quay lại lịch sử của ngôn ngữ lập trình bắt đầu từ Procedural. Đối với Procedural chỉ cần có hai kiểu cấu trúc một là struct hai là hàm là đủ.

Vậy tại sao OOP phải nghĩ ra class. Đó là vì nếu để các struct và các method tách biệt một cách tùy tiện thì sẽ không thể biết được struct đó thực hiện nhiệm vụ gì. Ngoài ra nhiều hàm kết hợp với nhau mới tạo một chức năng và chức năng thường gắn với một cấu trúc dữ liệu chính. Do đó gắn các phương thức với cấu trúc dữ liệu là hợp lý nhất.

Tuy nhiên do đặc điểm của các ngôn ngữ high abstraction là nếu bên dưới class implementation sai, tính đóng gói không tốt thì bên trên dùng kiểu gì dùng cũng sai. Do đó để tránh người dùng truy cập thay đổi nhưng phương thức và thuộc tính ngoài ý muốn, OOP sử dụng hai access modifier là public và private.

Ngoài việc muốn đóng gói tốt các chức năng theo cấu trúc dữ liệu, để có thể tái sử dụng và customize các tính năng sẵn có, OOP sinh ra tính kế thừa và một access modifier nữa là protected. Tuy nhiên do việc chia sẻ chung một cấu trúc dữ liệu giữa các chức năng khác nhau khi kế thừa khá là nguy hiểm. Cho nên nhiều ngôn ngữ OOP như Java, C# không cho phép đa kế thừa. Thậm chí Golang còn loại bỏ kế thừa ra khỏi OOP do việc customize lại tính năng cũng có thể dẫn đến sai sót trong implementation bên dưới. Thay vào đó Golang chỉ cho phép tái sử dụng chức năng bằng cách sử dụng embedded struct. Ở trong Python cho phép sử dụng đa kế thừa nhưng không phát triển cũng vì antipattern như trên.

Sang tới FP việc đóng gói đã cải tiến hơn bởi phân tách được kiểu dữ liệu và chức năng ra với nhau. Còn việc xử lý vấn đề conflict ntn thì mình đã đề cập ở trên mình xin phép không nhắc lại. Do tính đóng gói của FP khá là tốt nên thậm chí việc implement các principle như SOLID hay Clean Architecture ở trong FP thậm chí còn tốt hơn OOP rất nhiều.

Trong bài toán thứ nhất của bạn Thích màu hường, tại sao mình biết là FP có thể cover được các bài toán của OOP. Đơn giản là vì Sub-Type polymorphism của OOP là dạng cây còn Ad-hoc Polymorphism của mình là dạng network. Tuy là dạng đồ thị hai phía tuy nhiên đối với typeclass của mình có thể subclass tương tự như inheritence của OOP. Do đó mình có thể tự tin cover các bài toán của OOP bằng các ngôn ngữ FP.

Tính đa hình của FP có thể cover tính đa hình của OOP, vậy tính đa hình của OOP có thể cover các bài toán của FP không? Các bạn có thể thấy ở trong OOP cũng có tính năng gần gần tương tự như typeclass và ad-hoc polymorphism của FP, đó là interface. Vậy interface khác với typeclass chỗ nào. Trong bài toán thứ 2, bài toán của mình nhằm mục đích là nêu ra điều đó.

Điểm khác nhau thứ nhất là name collision, tên phương thức ở trong interface có thể bị name collision còn trong typeclass thì không. Một class không thể implement hai interface có cùng tên method nhưng lại khác nhau về input output, tuy nhiên typeclass có thể làm được điều này.

Điểm khác nhau thứ hai ví dụ như trong bài toán của mình bạn sẽ thấy ba interface ICar, ISubmarine, IAircraft vì có chung một phương thức là simulate. Do đó việc implement hàm simulate cho cả ba interface này trong cùng một class là vô nghĩa. Còn đối với FP đương nhiên việc này lại vô cùng có nghĩa. Ví dụ bạn có một base typeclass sử dụng chung giữa các typeclass khác nhau, sau đó một type đi implement cả ba typeclass này thì ba typeclass này vẫn có thể hoạt động độc lập mà không ảnh hưởng đến nhau. Do đó khi bạn run hàm simulate với hybrid vehicle trong OOP n lần đi nữa thì hàm simulate vẫn thể hiện một cách giống nhau, còn đương nhiên khi simulate Car hoặc simulate Aircraft hay simulate Submarine trên vehicle này trong FP thì với mỗi chức năng vehicle sẽ hoạt động một cách khác nhau. Ở đây tính polymorphism của interface trong OOP đó là với cùng một view interface có thể có nhiều kiểu dữ liệu khác nhau, tuy nhiên đối với một kiểu dữ liệu dù là với interface nào đi nữa thì biểu hiện của nó cũng phải giống nhau. Còn tính polymorphism của typeclass trong FP đó là ngoài việc một view có thể view nhiều kiểu dữ liệu thì đối với các view khác nhau, kiểu dữ liệu cũng có thể có các behavior khác nhau. Ví dụ như trong OOP có n class và m interface, mỗi interface có l hàm thì cuối cùng bạn cũng chỉ có nhiều nhất n * l behavior khác nhau. Còn đối với FP thì bạn sẽ có tối n * m * l behavior. Điều này khác hẳn với OOP và thể hiện được ý tưởng của Category Theory trong toán học.

Xem tệp đính kèm 234401

Vậy giờ mọi người có thể thắc mắc là việc implement n * m * l behavior sẽ phức tạp hơn việc implement n * l behavior thì mình xin giải thích là các chức năng ở trong typeclass sẽ tái sử dụng được do đó nếu bạn tái sử dụng một chức năng đến cấp thứ 3 thôi thì số lượng phương thức và utilities thôi cũng khá nhiều. Và do không bị vấn đề về name collision như trong OOP nên số lượng chức năng mà một kiểu dữ liệu có thể tải trong FP là rất là nhiều - ví dụ như Rust hay Elixir, ngược lại với một số ngôn ngữ OOP như Golang rất thiếu hụt về List comprehension và các ultilities xử lý cấu trúc dữ liệu khác - Ruby và JS là một trong số những ngoại lệ. Đó là chưa tính đến việc typeclass có thể parametric, generic cho nhiều tham số về kiểu khác nhau. Do đó khi sử dụng FP mình được lợi nhiều hơn là mất.

Ok, giờ mình giải thích xong rồi. Không biết bên OOP có đưa ra bài toán nào thử thách không nhỉ hay mình sẽ đưa tiếp bài toán của mình.

cc @Thích Màu Hường

Có, đợi chút nhé đợt 1 mới là "khúc dạo đầu", đợt 2, 3 sẽ là "mơn trớn" và "dood deed" nhé. Còn nhiều case khá hay để xem FP implement thế nào. :look_down:
 
Wall of text nhưng 1 dòng code demo xem thử thế nào vẫn chưa thấy.
Vẫn đang chờ cái 'type classes' giải quyết bài toán trên như nào vẫn chưa thấy đâu.
 
Đề Bài (phần 1):
https://voz.vn/t/oop-hay-fp.137397/page-10#post-4525643

Đề Bài (phần 2):

Giả sử phần custom print (business logic để custom cái step 2 cho hàm Print) đã được làm và chạy ổn định, việc sửa code trong module này cực kỳ nguy hiểm, đòi hỏi nhiều chi phí để test lại. Trong đề bài phần này xem như không được thay đổi. Như vậy hiện tại có 2 modules fixed là Visualizer (phần core) và CustomerInfoManager (chứa loại khách hàng và custom print).

Với điều kiện trên Implement thêm 2 yêu cầu sau:
4. Đối với khách hàng có Age <= 30, lúc in Name không được in tên thật mà phải mask thành các dấu "******".

5. Cần ghi log trước và sau khi gọi custom print cho từng khách hàng. Format như sau, xem phần output cho rõ:
[Log]: Begin CustomPrint for customer name = <Name>
// In phần custom như lúc đợt 1
[Log]: End CustomPrint for customer name = <Name>

OOP Implementation
Module 3: CrossCuttingConcerns.dll :
module mới chứa các logic cho yêu cầu mới 4 và 5
C#:
using CustomerInfoManager;

namespace CrossCuttingConcerns
{
    public class CustomerWrapper : Customer
    {
        protected Customer customer;

        public CustomerWrapper(Customer customer)
        {
            this.customer = customer;
            this.Name = customer.Name;
            this.Age = customer.Age;
        }
    }

    public class CustomerMasker : CustomerWrapper
    {
        public CustomerMasker(Customer customer) : base(customer) { }

        public override void CustomPrint()
        {
            // Mask if Age <= 30
            if (this.customer.Age <= 30)
            {
                this.customer.Name = "******";
            }

            // CustomPrint with masked data
            this.customer.CustomPrint();

            // Unmask
            this.customer.Name = this.Name;
        }
    }

    public class CustomerLogger : CustomerWrapper
    {
        public CustomerLogger(Customer customer) : base(customer) { }

        public override void CustomPrint()
        {
            System.Console.WriteLine(string.Format($"[Log]: Begin CustomPrint for customer name = {this.customer.Name}"));
            this.customer.CustomPrint();
            System.Console.WriteLine(string.Format($"[Log]: End CustomPrint for customer name = {this.customer.Name}"));
        }
    }
}

Program: CustomerManagement.exe: Module Program, add thêm các logic mới trước khi bỏ vào hàm Print cho máy chạy:
C#:
using CrossCuttingConcerns;
using CustomerInfoManager;
using System.Collections.Generic;

namespace CustomerManagement
{
    class Program
    {
        static void Main(string[] args)
        {
            IList<CustomerInfoManager.Customer> customers = CustomerRepo.GetCustomers();

            Visualizer.Visualizer visualizer = new Visualizer.Visualizer();
            foreach (CustomerInfoManager.Customer customer in customers)
            {
                // Add masker
                CustomerMasker customerMasker = new CustomerMasker(customer);

                // Add logger
                CustomerLogger customerLogger = new CustomerLogger(customerMasker);

                visualizer.Print(customerLogger);
            }
        }
    }
}

Output:
Mã:
#######################################################
[Log]: Begin CustomPrint for customer name = John Smith
Name = ******
Age = 20
$$$$$$$$$$$$$$$$$$$$$$$
[Log]: End CustomPrint for customer name = John Smith

#######################################################
[Log]: Begin CustomPrint for customer name = Tim Batte
Name = ******
Age = 30
VipPoints = 100
@@@@@@@@@@@@@@@@@@@@@@@
[Log]: End CustomPrint for customer name = Tim Batte

#######################################################
[Log]: Begin CustomPrint for customer name = Jenny Lars
Name = Jenny Lars
Age = 40
Department = IT
&&&&&&&&&&&&&&&&&&&&&&&
[Log]: End CustomPrint for customer name = Jenny Lars

P/s: mình đã rename cái CustomerVisualzer thành Visualizer bên code ở phần 1 để dễ so sánh cái này không làm thay đổi bản chất của đề bài.
 
Sửa lần cuối:
Ok, vì bài này hơi khó hiểu nên mình xin giải thích luôn. Ý đồ của bài này là như sau. 😆


Như bạn Thích Màu Hường nói, đúng là mình nghĩ theo cách thứ hai thật. Đối với Procedural, cải tiến là việc đóng gói và tái sử dụng code theo hàm phương thức. Đối với OOP là đóng gói và tái sử dụng hàm và phương thức theo cấu trúc dữ liệu. Đối với Functional sẽ là đóng gói và tái sử dụng phương thức theo từng kiểu cấu trúc dữ liệu và từng loại chức năng khác nhau.


Ok, đến đoạn thứ hai polymorphism được chia ra thành ba loại:
  • Ad-hoc polymorphism
  • Sub-type polymorphism
  • Parametric polymorphism
Đối với Parametric polymorphism ở trong cả ngôn ngữ OOP và FP đều có, nó chính là Generic. Ngoài Parametric, loại polymorphism được dùng chủ yếu trong OOP là Sub-type (chính là chức năng kế thừa của C# và Java). Còn polymorphism chính còn lại của các ngôn ngữ Functional như Haskell, Elixir, Rust là Ad-hoc polymorphism.

Đối với OOP Sub-type đúng như các bạn mention ở trên Poll: class A lai có class B class C kế thừa, class C lại có class D class E kế thừa. Do đó, mô hình kế thừa có cấu trúc dạng cây như sau:
Xem tệp đính kèm 234273
Đối với FP thì khác, FP chia riêng cấu trúc ra thành hai loại:
  • type/struct định nghĩa kiểu dữ liệu.
  • typeclass/trait/protocol/category định nghĩa chức năng và có thể chứa implementation.
Việc một type đi implement một typeclass tương ứng với việc chúng ta đã tạo ra một adapter giữa kiểu dữ liệu và chức năng của nó.

Mô hình về type system của FP có thể được mô phỏng bằng đồ thị hai phía như sau:

Xem tệp đính kèm 234276
Vậy điểm khác nhau giữa hai loại polymorphism này là gì. Mình xin trả lời là tính đóng gói.

Quay lại lịch sử của ngôn ngữ lập trình bắt đầu từ Procedural. Đối với Procedural chỉ cần có hai kiểu cấu trúc một là struct hai là hàm là đủ.

Vậy tại sao OOP phải nghĩ ra class. Đó là vì nếu để các struct và các method tách biệt một cách tùy tiện thì sẽ không thể biết được struct đó thực hiện nhiệm vụ gì. Ngoài ra nhiều hàm kết hợp với nhau mới tạo một chức năng và chức năng thường gắn với một cấu trúc dữ liệu chính. Do đó gắn các phương thức với cấu trúc dữ liệu là hợp lý nhất.

Tuy nhiên do đặc điểm của các ngôn ngữ high abstraction là nếu bên dưới class implementation sai, tính đóng gói không tốt thì bên trên dùng kiểu gì dùng cũng sai. Do đó để tránh người dùng truy cập thay đổi nhưng phương thức và thuộc tính ngoài ý muốn, OOP sử dụng hai access modifier là public và private.

Ngoài việc muốn đóng gói tốt các chức năng theo cấu trúc dữ liệu, để có thể tái sử dụng và customize các chức năng sẵn có, OOP sinh ra tính kế thừa và một access modifier nữa là protected. Tuy nhiên do việc chia sẻ chung thuộc tính và phương thức giữa các chức năng khác nhau khi kế thừa khá là nguy hiểm. Cho nên nhiều ngôn ngữ OOP như Java, C# không cho phép đa kế thừa. Thậm chí Golang còn loại bỏ kế thừa ra khỏi OOP do việc customize lại chức năng cũng có thể dẫn đến sai sót trong implementation bên dưới. Thay vào đó Golang chỉ cho phép tái sử dụng chức năng bằng cách sử dụng embedded struct. Ở trong Python cho phép sử dụng đa kế thừa nhưng không phát triển cũng vì antipattern như trên.

Sang tới FP việc đóng gói đã cải tiến hơn bởi phân tách được kiểu dữ liệu và chức năng ra thành hai cấu trúc riêng biệt. Còn việc xử lý vấn đề conflict ntn thì mình đã đề cập ở trên mình xin phép không đề cập lại. Do tính đóng gói của FP khá là tốt nên thậm chí việc implement các principle như SOLID hay Clean Architecture ở trong FP thậm chí còn tốt hơn OOP rất nhiều.

Trong bài toán thứ nhất của bạn Thích màu hường, tại sao mình biết là FP có thể cover được các bài toán của OOP. Đơn giản là vì Sub-Type polymorphism của OOP là dạng cây còn Ad-hoc Polymorphism của mình là dạng network. Tuy là dạng đồ thị hai phía tuy nhiên, do typeclass của FP có tính năng subclass tương tự như inheritence của OOP. Do đó mình có thể tự tin cover các bài toán của OOP bằng các ngôn ngữ FP.

Tính đa hình của FP có thể cover tính đa hình của OOP, vậy tính đa hình của OOP có thể cover FP hay không? Các bạn có thể thấy ở trong OOP cũng có tính năng gần gần tương tự như typeclass và ad-hoc polymorphism của FP, đó là interface. Vậy interface khác với typeclass chỗ nào. Trong bài toán thứ 2 mình viết nhằm mục đích là nêu ra điều đó.

Điểm khác nhau thứ nhất là name collision, tên các phương thức ở trong interface có thể bị name collision với nhau nhưng typeclass thì không. Một class không thể implement hai interface có cùng tên method nhưng lại khác nhau về input output, tuy nhiên typeclass có thể làm được điều này.

Điểm khác nhau thứ hai ví dụ như trong bài toán của mình bạn sẽ thấy ba interface ICar, ISubmarine, IAircraft vì có chung một phương thức là simulate. Do đó việc implement hàm simulate cho cả ba interface này trong cùng một class là vô nghĩa. Còn đối với FP đương nhiên việc này lại vô cùng có nghĩa. Ví dụ bạn có một base typeclass sử dụng chung giữa các typeclass khác nhau, sau đó một type đi implement cả ba typeclass này thì ba typeclass này vẫn có thể hoạt động độc lập mà không ảnh hưởng đến nhau. Do đó khi bạn run hàm simulate với hybrid vehicle trong OOP n lần đi nữa thì hàm simulate vẫn thể hiện một cách giống nhau, còn đương nhiên khi simulate Car hoặc simulate Aircraft hay simulate Submarine trên vehicle này trong FP thì với mỗi chức năng vehicle sẽ hoạt động một cách khác nhau. Ở đây tính polymorphism của interface trong OOP đó là với cùng một view interface có thể có nhiều kiểu dữ liệu khác nhau, tuy nhiên đối với một kiểu dữ liệu dù là với interface nào đi nữa thì biểu hiện của nó cũng phải giống nhau. Còn tính polymorphism của typeclass trong FP đó là ngoài việc một view có thể view nhiều kiểu dữ liệu thì đối với các view khác nhau, kiểu dữ liệu cũng có thể có các behavior khác nhau. Ví dụ như trong OOP có n class và m interface, mỗi interface có l hàm thì cuối cùng bạn cũng chỉ có nhiều nhất n * l phương thức khác nhau. Còn đối với FP thì bạn sẽ có tối n * m * l phương thức. Điều này khác hẳn với OOP và thể hiện được ý tưởng của Category Theory trong toán học.

Xem tệp đính kèm 234401

Vậy giờ mọi người có thể thắc mắc là việc implement n * m * l phương thức sẽ phức tạp hơn việc implement n * l phương thức hay không thì mình xin giải thích là các chức năng ở trong typeclass có thể tái sử dụng được. Do đó nếu bạn tái sử dụng một chức năng đến cấp thứ 3 thôi thì số lượng phương thức và utilities sinh ra cũng khá nhiều. Và FP cũng không bị vấn đề về name collision như trong OOP nên số lượng chức năng mà một kiểu dữ liệu có thể tải trong FP là rất là lớn - ví dụ như Rust hay Elixir số lượng ultilities cho các kiểu dữ liệu khác nhau như String, List, Set rất là nhiều, ngược lại với một số ngôn ngữ OOP như Golang rất thiếu hụt về List comprehension và các ultilities xử lý cấu trúc dữ liệu khác - Ruby và JS là một trong số những ngoại lệ. Đó là chưa tính đến việc typeclass có thể parametric, generic cho nhiều tham số về kiểu khác nhau. Do đó khi sử dụng FP mình được lợi nhiều hơn là mất.

Ok, giờ mình giải thích xong rồi. Không biết bên OOP có đưa ra bài toán nào thử thách không nhỉ hay mình sẽ đưa tiếp bài toán của mình.

cc @Thích Màu Hường

Nhiều chữ quá cho nên lười đọc. Đề nghị post ít chữ và súc tích hơn. Nhưng có vài lời thế này:

1. Đa kế thừa vừa là điểm mạnh vừa là điểm yếu. Nếu như ai từng code C++, MFC sẽ thấy cái disadvantage của nó là sự nhập nhằng khi các class cha có cùng tên phương thức nhưng ngược lại cực kỳ flexible vì có thể tạo ra 1 class rồi kế thừa tùy ý nhiều class cha khác nhau để xài các method mình mong muốn. Đây là 2 mặt vấn đề cũng là bài toán design thế nào thôi.

2. Ad-hoc polymorphism hay cái quái gì đó bên FP có lại tạo vấn đề "khỉ, rừng và chuối" như OOP? Trong khi các anh rồ FP tự hào là pureimmutable.

3. Việc tách data và method bên FP lại lặp lại vấn đề trước khi có OOP là ko biết được có thể làm gì với data. Cái này thấy rõ qua cái code đợt 1, chưa kể vụ trùng lặp code vì ko có kế thừa ~~~> để lại shit cho người làm maintenance.

4. Hóng code FP đợt 2 như nào. :look_down:
 
Đề Bài (phần 1):
https://voz.vn/t/oop-hay-fp.137397/page-10#post-4525643

Đề Bài (phần 2):

Giả sử phần custom print (business logic để custom cái step 2 cho hàm Print) đã được làm và chạy ổn định, việc sửa code trong module này cực kỳ nguy hiểm, đòi hỏi nhiều chi phí để test lại. Trong đề bài phần này xem như không được thay đổi. Như vậy hiện tại có 2 modules fixed là Visualizer (phần core) và CustomerInfoManager (chứa loại khách hàng và custom print).

Với điều kiện trên Implement thêm 2 yêu cầu sau:
4. Đối với khách hàng có Age <= 30, lúc in Name không được in tên thật mà phải mask thành các dấu "******".

5. Cần ghi log trước và sau khi gọi custom print cho từng khách hàng. Format như sau, xem phần output cho rõ:
[Log]: Begin CustomPrint for customer name = <Name>
// In phần custom như lúc đợt 1
[Log]: End CustomPrint for customer name = <Name>

OOP Implementation
Module 3: CrossCuttingConcerns.dll :
module mới chứa các logic cho yêu cầu mới 4 và 5
C#:
using CustomerInfoManager;

namespace CrossCuttingConcerns
{
    public class CustomerWrapper : Customer
    {
        protected Customer customer;

        public CustomerWrapper(Customer customer)
        {
            this.customer = customer;
            this.Name = customer.Name;
            this.Age = customer.Age;
        }
    }

    public class CustomerMasker : CustomerWrapper
    {
        public CustomerMasker(Customer customer) : base(customer) { }

        public override void CustomPrint()
        {
            // Mask if Age <= 30
            if (this.customer.Age <= 30)
            {
                this.customer.Name = "******";
            }

            // CustomPrint with masked data
            this.customer.CustomPrint();

            // Unmask
            this.customer.Name = this.Name;
        }
    }

    public class CustomerLogger : CustomerWrapper
    {
        public CustomerLogger(Customer customer) : base(customer) { }

        public override void CustomPrint()
        {
            System.Console.WriteLine(string.Format($"[Log]: Begin CustomPrint for customer name = {this.customer.Name}"));
            this.customer.CustomPrint();
            System.Console.WriteLine(string.Format($"[Log]: End CustomPrint for customer name = {this.customer.Name}"));
        }
    }
}

Program: CustomerManagement.exe: Module Program, add thêm các logic mới trước khi bỏ vào hàm Print cho máy chạy:
C#:
using CrossCuttingConcerns;
using CustomerInfoManager;
using System.Collections.Generic;

namespace CustomerManagement
{
    class Program
    {
        static void Main(string[] args)
        {
            IList<CustomerInfoManager.Customer> customers = CustomerRepo.GetCustomers();

            Visualizer.Visualizer visualizer = new Visualizer.Visualizer();
            foreach (CustomerInfoManager.Customer customer in customers)
            {
                // Add masker
                CustomerMasker customerMasker = new CustomerMasker(customer);

                // Add logger
                CustomerLogger customerLogger = new CustomerLogger(customerMasker);

                visualizer.Print(customerLogger);
            }
        }
    }
}

Output:
Mã:
#######################################################
[Log]: Begin CustomPrint for customer name = John Smith
Name = ******
Age = 20
$$$$$$$$$$$$$$$$$$$$$$$
[Log]: End CustomPrint for customer name = John Smith

#######################################################
[Log]: Begin CustomPrint for customer name = Tim Batte
Name = ******
Age = 30
VipPoints = 100
@@@@@@@@@@@@@@@@@@@@@@@
[Log]: End CustomPrint for customer name = Tim Batte

#######################################################
[Log]: Begin CustomPrint for customer name = Jenny Lars
Name = Jenny Lars
Age = 40
Department = IT
&&&&&&&&&&&&&&&&&&&&&&&
[Log]: End CustomPrint for customer name = Jenny Lars

P/s: mình đã rename cái CustomerVisualzer thành Visualizer bên code ở phần 1 để dễ so sánh cái này không làm thay đổi bản chất của đề bài.
Ok, đề của bạn cũng khó đấy, cũng làm mình suy nghĩ một hồi ko biết cái trick của bạn là gì và nên xử lý cái trick của bạn ntn. Tối nay mình sẽ đăng kết quả! =))
 
bài toán phải có input chứ phải có list data khách hàng dưới dạng json , rồi làm gì thì làm chứ , ra đề cũng thiếu

Lúc mới tranh luận tôi tưởng là các anh expert kinh nghiệm dày dặn đã kinh qua nhiều dự án làm OOP rồi thấy được cái dở của nó mới đâm ra rồ FP.

Nhưng giờ thấy các a hỏi vài câu ngô nghê thì mới biết là các a tư duy rất cụ thể, cục bộ. Tôi dám chắc anh còn ko hiểu tôi với 2 bạn làm FP kia đang tranh luận cái gì đâu. :feel_good:

P/s: à còn đòi data dạng json nữa chứ. Cái đề bài của tôi là về phân tích thiết kế giữa 2 trường phái OOP vs FP chứ ko phải ngôn ngữ hay framework, platform cụ thể nào.
 
Lúc mới tranh luận tôi tưởng là các anh expert kinh nghiệm dày dặn đã kinh qua nhiều dự án làm OOP rồi thấy được cái dở của nó mới đâm ra rồ FP.

Nhưng giờ thấy các a hỏi vài câu ngô nghê thì mới biết là các a tư duy rất cụ thể, cục bộ. Tôi dám chắc anh còn ko hiểu tôi với 2 bạn làm FP kia đang tranh luận cái gì đâu. :feel_good:

P/s: à còn đòi data dạng json nữa chứ. Cái đề bài của tôi là về phân tích thiết kế giữa 2 trường phái OOP vs FP chứ ko phải ngôn ngữ hay framework, platform cụ thể nào.
Vậy đề sai rồi bài toán phải có input output chứ, ko có data từ db hoặc các nguồn khác thì bói thông tin từ trong không khí à , tư duy cho gì biết đó dính với cái với oop cả đời là hợp lý rồi , sai đề nên làm lài đi
 
Vậy đề sai rồi bài toán phải có input output chứ, ko có data từ db hoặc các nguồn khác thì bói thông tin từ trong không khí à , tư duy cho gì biết đó dính với cái với oop cả đời là hợp lý rồi , sai đề nên làm lài đi

Anh có thấy ở đây không ai nói đề sai ko? 2 bạn làm FP cũng đã có lời giải mà ko vấn đề gì. Thấy a vẫn chưa hiểu nên tôi trả lời a đây. Data là do anh tự tạo ra nhé, làm sao cho giống cái output là được như code của tôi thì là tạo trong hàm GetCustomers đó.

Câu hỏi của anh rất vô duyên và éo liên quan gì. Tại vì sao? Nó tương đương như đề bài yêu cầu anh viết cái hàm sum(a, b, c) trả về tổng a + b + c. Anh lại đi hỏi tôi ủa thế a, b, c bằng mấy? :look_down:
 
Anh hikaruu càng nói càng chả hiểu anh ấy muốn tranh luận cái gì, thôi kiến thức hạn hẹp giống tôi thì ngồi xem người ta tranh luận mà học hỏi. Toàn phát biểu xàm :confused:
 
Vậy đề sai rồi bài toán phải có input output chứ, ko có data từ db hoặc các nguồn khác thì bói thông tin từ trong không khí à , tư duy cho gì biết đó dính với cái với oop cả đời là hợp lý rồi , sai đề nên làm lài đi

Input
https://voz.vn/t/oop-hay-fp.137397/page-10#post-4525643

output


#######################################################
[Log]: Begin CustomPrint for customer name = John Smith
Name = ******
Age = 20
$$$$$$$$$$$$$$$$$$$$$$$
[Log]: End CustomPrint for customer name = John Smith

#######################################################
[Log]: Begin CustomPrint for customer name = Tim Batte
Name = ******
Age = 30
VipPoints = 100
@@@@@@@@@@@@@@@@@@@@@@@
[Log]: End CustomPrint for customer name = Tim Batte

#######################################################
[Log]: Begin CustomPrint for customer name = Jenny Lars
Name = Jenny Lars
Age = 40
Department = IT
&&&&&&&&&&&&&&&&&&&&&&&
[Log]: End CustomPrint for customer name = Jenny Lars

Có đọc cái đề cũng ko xong
 
chỉ cần ra kết quả, đầu vào của ai thì người đó tự thêm,data hình dáng thế nào conver qua object thì không có phần quan trọng nhất thì bỏ qua, tôi nghĩ các anh dc thẩy sẵn data nên quen luôn rồi. thêm input data di, dưới dạng text hoặc json , tôi còn vô code nào. Đi giải bài mà tự thêm đầu vào nó kì lắm
 
chỉ cần ra kết quả, đầu vào của ai thì người đó tự thêm,data hình dáng thế nào conver qua object thì không có phần quan trọng nhất thì bỏ qua, tôi nghĩ các anh dc thẩy sẵn data nên quen luôn rồi. thêm input data di, dưới dạng text hoặc json , tôi còn vô code nào. Đi giải bài mà tự thêm đầu vào nó kì lắm

:beat_brick: Haizzz, tôi có đưa rồi, mô tả data trong đề bài phần 1 bằng text, data cụ thể cũng đã show trong code hàm GetCustomers và phần ouput rồi. Tôi thấy a có vấn đề về đọc hiểu đấy, giờ tôi show lại nè:

Có 3 loại khách hàng với tên và các fields tương ứng như sau: Customer (Name, Age), VipCustomer (Name, Age, VipPoints), InternalCustomer (Name, Age, Department). Bên dưới là data json cụ thể để ra ouput:

JSON:
var customer = { Name: "John Smith", Age: 20 };
var vipCcustomer = { Name: "Tim Batte", Age: 30, VipPoints: 100 };
var internalCustomer = { Name: "Jenny Lars", Age: 40, Department: "IT" };

Thoả mãn chưa :sweet_kiss:
 
chỉ cần ra kết quả, đầu vào của ai thì người đó tự thêm,data hình dáng thế nào conver qua object thì không có phần quan trọng nhất thì bỏ qua, tôi nghĩ các anh dc thẩy sẵn data nên quen luôn rồi. thêm input data di, dưới dạng text hoặc json , tôi còn vô code nào. Đi giải bài mà tự thêm đầu vào nó kì lắm
Anh này lì thế nhỉ. Đúng tư duy thợ code, chỉ biết làm theo thiết kế có sẵn.
Giờ anh có thằng khách trả tiền để anh thiết kế hệ thống cho nó thì nó cũng chỉ mô tả theo hướng nghiệp vụ về input và output nó mong muốn, có cứt nó cho anh "file json data input" nhé, vì thằng khách hàng có khi nó đếch biết gì về code cả.
Nếu anh thấy chưa rõ thì anh cũng chỉ có thể hỏi theo hướng nghiệp vụ chứ ko thể đòi "file json data input" được.
:baffle:
 
:beat_brick: Haizzz, tôi có đưa rồi, mô tả data trong đề bài phần 1 bằng text, data cụ thể cũng đã show trong code hàm GetCustomers và phần ouput rồi. Tôi thấy a có vấn đề về đọc hiểu đấy, giờ tôi show lại nè:

Có 3 loại khách hàng với tên và các fields tương ứng như sau: Customer (Name, Age), VipCustomer (Name, Age, VipPoints), InternalCustomer (Name, Age, Department). Bên dưới là data json cụ thể để ra ouput:

JSON:
var customer = { Name: "John Smith", Age: 20 };
var vipCcustomer = { Name: "Tim Batte", Age: 30, VipPoints: 100 };
var internalCustomer = { Name: "Jenny Lars", Age: 40, Department: "IT" };

Thoả mãn chưa :sweet_kiss:
const customs = [ { Name: "John Smith", Age: 20 },{ Name: "Tim Batte", Age: 30, VipPoints: 100 },{ Name: "Jenny Lars", Age: 40, Department: "IT" }]
rồi anh tao object customer từ data trên, rồi in ra tôi xem nào =]]
 
Sửa lần cuối:

Thống kê chủ đề

Ngày tạo
Quynh 123,
Người trả lời cuối
HIL,
Trả lời
593
Lượt xem
97.407
Quay lại
Lên đầu trang