Typescript下的面向对象(OOP) 与 函数式(FP)编程

什么是编程范式(programming paradigm)?
编程范式是依据编程语言的特征对其分类的方式。
Programming paradigms are a way to classify programming languages based on their features.

申明式与指令式(declarative programming & imperative programming)编程
申明式:表达了计算逻辑,不包含控制流程。
指令式:使用表达式语句控制程序的状态。
Declarative: expresses the logic of a computation without describing its control flow.
Imperative: uses statements that change a program’s state.

面向对象与函数式编程
通过上述申明式与指令式编程的定义,我们可以简单地归纳为(为了理解简单,并不准确):有状态控制的是指令式,反之为申明式。
而面向对象编程(OOP),最常用到的Class,其内部包含状态逻辑,从而属于指令式;而函数式编程(FP),由于通常用到纯函数,不包含逻辑,因此属于申明式。
实际上,一些OOP的语言,如C#,也能支持函数式编程,而其它的语言,如TypeScript,也是既可以做OOP也可以做FP。

OOP的基本原则:
SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion)

FP的基本原则:
pure function
shared state
currying
function composition
immutability

下面通过一组代码简单描述OOP与FP的区别
先说OOP,假设我们实现一个产品页面,该页面中有两个组件:ImageGallery和ReviewGallery,同时该页面需要适配Desktop和Phone两个版本,通常我们会这么做:
base.ts

export abstract class ImgGallery {
    protected thumbnails: HTMLSpanElement[];
    protected bigImg: HTMLImageElement;
    protected isLoading: boolean = false;
    constructor()
    {
        this.thumbnails = $(".js-thumbnail").toArray() as HTMLSpanElement[];
        this.bigImg = $(".js-img")[0] as HTMLImageElement;
    }

    abstract loadBigImage(url: string): void;
}

export abstract class ReviewGallery {
}

class ProductDetailPage {
    private imgGallery: ImgGallery;
    private reviewGallery: ReviewGallery

    constructor(_imgGallery: ImgGallery, _reviewGallery: ReviewGallery){
        this.imgGallery = _imgGallery;
        this.reviewGallery = _reviewGallery;
    }

}

export default ProductDetailPage;

PDP-Desktop.ts

import ProductDetailPage, { ImgGallery, ReviewGallery } from './Base';

export class ImgGalleryDesktop extends ImgGallery {
    constructor() {
        super();

        this.loadBigImage(this.bigImg.src);
    }
    loadBigImage(url: string): void {
        this.isLoading = true;
        console.log(`load big image from ${url}`);
        this.isLoading = false;
    }
}

export class ReviewGalleryDesktop extends ReviewGallery {
    
}

PDP-Phone.ts

import ProductDetailPage, { ImgGallery, ReviewGallery } from './Base';

export class ImgGalleryPhone extends ImgGallery {
    constructor() {
        super();

        this.loadBigImage(this.bigImg.src);
    }
    loadBigImage(url: string): void {
        console.log(`load big image from ${url}`);
    }
}

export class ReviewGalleryPhone extends ReviewGallery {
    
}

App.ts

import { ImgGalleryDesktop, ReviewGalleryDesktop } from './PDP-Desktop';
import { ImgGalleryPhone, ReviewGalleryPhone } from './PDP-Phone';
import ProductDetailPage from './Base';

const device = "desktop";

const imgGallery = device == "desktop"? new ImgGalleryDesktop() : new ImgGalleryPhone();
const reviewGallery = device == "desktop"? new ReviewGalleryDesktop() : new ReviewGalleryPhone();

const pdp = new ProductDetailPage(imgGallery, reviewGallery);

上述示例可以看出,OOP的特点是Class继承,组合,以及内部状态的维护。

再给个FP的例子,场景是通过接口读取产品数据并显示。 初级程序员通常会这么做:

function FetchProduct() {
    fetch("product api endpoint").then((response) => {
        response.json().then((data)=>{
            if(data as Array<string>) {
                //do something
                // data.forEach(d => {
                //     //...
                // });
            }
            else {

            }
        })
    });
}

上述代码既不是OOP,违反了OOP原则中的单一职责(一个方法既有获取数据的逻辑,又有处理数据的逻辑),又没有接口分离(根本就没用到接口)等等。
这段代码显然也不是FP,没有实现FP的设计原则。 那么设想一下如何对其做改造:
改造尝试一

const getProduct: ()=> Promise<Product> = ()=>{
   return new Promise(resolve => {
        setTimeout(function() {
            let data: Product = { name:"foo", quantity:100};
          resolve(
            data
          );          
        }, 2000)
    });
};

const updateProd = (prod: Product) => {
    console.log("update product");
}

async function FPSampleI() {
    let data = await getProduct();
    console.log(JSON.stringify(data));

    updateProd(data);
}

FPSampleI();

独立出出获取数据和显示数据的逻辑。感觉这样还是不够FP,于是再继续改造,实现chainable function

class FPSampleII<T> {
    private _value?: T;
    public async fetchData(getFun: ()=> Promise<T>) {
        console.log("fetching...");
        this._value = await getFun();
        return this;
    } 

    public updateUI(updateUIFun: (data: T)=>void) {
        this._value && updateUIFun(this._value);
        return this;
    }
}

new FPSampleII<Product>().fetchData(getProduct).then((instance)=> instance.updateUI(updateProd));

此时我们注意到函数内部有共享变量,如何对其优化呢? (共享变量往往是会有竟用的问题,也就是多处方法同时调用,造成脏数据。 解决方式是使用深拷贝)。
以下是一个优化的简单例子

type Customer = {
    name: string;
    age: number;
    balance?: number;
}

function useState<T>(defaultValue: T) : [T, (arg: T)=>void] {
    let state: T = {...defaultValue};

    const setState = (newState: T) => {
        state = {...newState};
    }

    return [state, setState]
}

function CustomerFun(cus: Customer) {
    const [customer, setCustomer] = useState<Customer>(cus);

    setCustomer( { ...cus, balance: cus.balance?? 200 })
}

此处代码的思路是模仿React中的useState Hook,把对于state的操作提取到一个独立的function中。

posted @ 2021-07-26 23:37  老胡Andy  阅读(567)  评论(0编辑  收藏  举报