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中。