软件工程中的一些思想

Published: 2022年08月30日

In Basic.

今天看了一下午node没理清它的逻辑,里面很多编程思想和平时接触到的区别很大,因此决定每遇到一个就把它记下来(没遇到的先把坑留着)...

Image00002

设计模式

创建型模式

在开发过程中不可避免地接触到了像单例模式等代码,感觉特别精妙,于是抽时间学习了GoF的设计模式,并用python实现以下相关代码,修炼内功吧~

单例模式(Singleton Pattern)

顾名思义就是只要一个实例,或者说共用一个实例,python有四种实现:

利用导入特性

该方法不需要修改原有代码,利用python在import时会首先检测模块是否已经被导入,若是则不再重新导入的特性实现单例,因此单独定义一个模块只实现如下代码:

from something import NeedSingletonClass  # 期待单例的类


singleton_instance = NeedSingletonClass()  # 实例化

之后在其他需要使用的该实例的文件中导入该文件即可,但是有一个很容易出错的地方就是若其他文件使用相对路径导入,当路径不同时将会导入多次,即非单例。

利用构造函数

这是最直观的方法,使用类属性存储实例,并提供统一接口获取该属性,在接口("构造函数")中先判断该属性是否已经被初始化,若未初始化先初始化再返回,否则直接返回实例:

import threading


class Singleton(object):
    _instance = None
    _lock = threading.Lock()  # 使用锁防止竞争条件

    def __init__(self, *args, **kwargs):
        pass

    @classmethod
    def instance(cls, *args, **kwargs):
        if cls._instance is not None:  # 若已经实例化直接返回实例
            return cls._instance
        with cls._lock:
            if cls._instance is None:
                cls._instance = cls(args, kwargs)  # 未实例化则先实例化再返回
            return cls._instance

注:此处强调了同步,之后不再考虑该问题

利用new方法

在python中__init__方法被用来初始化变量,__new__方法才像真正的构造,它真正的创建实例,若创建的实例属于该类才会继续调用__init__进行初始化,利用这个特性写出来的更好看:

class MyLogger(object):
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(MyLogger, cls).__new__(cls)
        return cls._instance

利用元类

class SingletonMeta(type):
    """
    单例元类,用于控制类的实例化过程。
    """
    _instances = {}

    def __call__(cls, *args, **kwargs):
        # 如果类还没有实例化过,则创建一个新实例并保存
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        # 返回已存在的实例
        return cls._instances[cls]

class SingletonClass(metaclass=SingletonMeta):
  ...

原型模式(Prototype Pattern)

通过复制现有对象来创建新对象,而不是通过实例化类。适用于创建成本较高的对象,目前好像就JS中遇到过。

它的优点是避免重复初始化对象的开销,可以动态添加或删除属性,适合创建复杂对象。缺点是如果对象包含引用类型,深拷贝可能复杂,需要暴露对象的内部结构违反了封装原则。

构造器模式(Builder Pattern)

它将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。特别适合构造复杂对象:

class Pizza {
    constructor() {
        this.size = null;
        this.cheese = false;
        this.pepperoni = false;
        this.mushrooms = false;
    }

    toString() {
        return `Pizza: size=${this.size}, cheese=${this.cheese}, pepperoni=${this.pepperoni}, mushrooms=${this.mushrooms}`;
    }
}

class PizzaBuilder {
    constructor() {
        this.pizza = new Pizza();
    }

    setSize(size) {
        this.pizza.size = size;
        return this;
    }

    addCheese() {
        this.pizza.cheese = true;
        return this;
    }

    addPepperoni() {
        this.pizza.pepperoni = true;
        return this;
    }

    addMushrooms() {
        this.pizza.mushrooms = true;
        return this;
    }

    build() {
        return this.pizza;
    }
}

// 使用构造器模式
const builder = new PizzaBuilder();
const pizza = builder.setSize("Large")
                     .addCheese()
                     .addPepperoni()
                     .build();

console.log(pizza.toString()); // Pizza: size=Large, cheese=true, pepperoni=true, mushrooms=false

它的优点是将对象的构建和表示分离,可以逐步构建对象,支持不同的配置, 代码更清晰,易于理解。缺点是增加了类的数量,对于简单对象可能会复杂化。

抽象工厂模式(Abstract Factory)

提供一个接口,用于创建相关或依赖对象的家族,而不需要指定具体类。如:

// 抽象产品
class Button {
    render() {}
}

class Checkbox {
    render() {}
}

// 具体产品
class WindowsButton extends Button {
    render() {
        console.log("Render a button in Windows style");
    }
}

class WindowsCheckbox extends Checkbox {
    render() {
        console.log("Render a checkbox in Windows style");
    }
}

class MacButton extends Button {
    render() {
        console.log("Render a button in Mac style");
    }
}

class MacCheckbox extends Checkbox {
    render() {
        console.log("Render a checkbox in Mac style");
    }
}

// 抽象工厂
class GUIFactory {
    createButton() {}
    createCheckbox() {}
}

// 具体工厂
class WindowsFactory extends GUIFactory {
    createButton() {
        return new WindowsButton();
    }

    createCheckbox() {
        return new WindowsCheckbox();
    }
}

class MacFactory extends GUIFactory {
    createButton() {
        return new MacButton();
    }

    createCheckbox() {
        return new MacCheckbox();
    }
}

// 使用抽象工厂
function clientCode(factory) {
    const button = factory.createButton();
    const checkbox = factory.createCheckbox();
    button.render();
    checkbox.render();
}

// 客户端代码
console.log("Windows UI:");
clientCode(new WindowsFactory());

console.log("\nMac UI:");
clientCode(new MacFactory());

它的优点是确保创建的对象是兼容的,客户端代码与具体类解耦,易于添加新的产品族。缺点是增加了类的数量,添加新产品类型需要修改抽象工厂接口。

工厂方法模式(Factory Method)

定义一个创建对象的接口,但让子类决定实例化哪个类。工厂方法使类的实例化推迟到子类。如:

// 抽象产品
class Product {
    operation() {}
}

// 具体产品
class ConcreteProductA extends Product {
    operation() {
        return "Product A";
    }
}

class ConcreteProductB extends Product {
    operation() {
        return "Product B";
    }
}

// 抽象工厂
class Creator {
    factoryMethod() {}

    someOperation() {
        const product = this.factoryMethod();
        return `Creator: ${product.operation()}`;
    }
}

// 具体工厂
class ConcreteCreatorA extends Creator {
    factoryMethod() {
        return new ConcreteProductA();
    }
}

class ConcreteCreatorB extends Creator {
    factoryMethod() {
        return new ConcreteProductB();
    }
}

// 使用工厂方法
function clientCode(creator) {
    console.log(creator.someOperation());
}

// 客户端代码
console.log("Product A:");
clientCode(new ConcreteCreatorA());

console.log("\nProduct B:");
clientCode(new ConcreteCreatorB());

它的优点是将对象的创建与使用分离, 易于添加新的产品类型,每个工厂只负责一种产品。缺点是每个产品需要一个工厂类,可能增加代码的复杂性。

结构型模式

适配器模式(Adapter Pattern)

将一个类的接口转换成客户端期望的另一个接口,使得原本不兼容的类可以一起工作,如:

// 目标接口
class Target {
    request() {
        return "Target: Default behavior";
    }
}

// 需要适配的类
class Adaptee {
    specificRequest() {
        return "Adaptee: Specific behavior";
    }
}

// 适配器
class Adapter extends Target {
    constructor(adaptee) {
        super();
        this.adaptee = adaptee;
    }

    request() {
        return `Adapter: ${this.adaptee.specificRequest()}`;
    }
}

// 使用适配器
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);

console.log(adapter.request()); // 输出: Adapter: Adaptee: Specific behavior

它的优点是使不兼容的接口能够协同工作,从而复用现有的类,并且适配器可以动态切换。而**缺点是增加了类的数量,可能引入额外的调用。

桥模式(Bridge Pattern)

将抽象部分与实现部分分离,使它们可以独立变化。如:

// 实现部分
class Renderer {
    renderShape(shape) {}
}

class VectorRenderer extends Renderer {
    renderShape(shape) {
        console.log(`Drawing ${shape} as vector`);
    }
}

class RasterRenderer extends Renderer {
    renderShape(shape) {
        console.log(`Drawing ${shape} as pixels`);
    }
}

// 抽象部分
class Shape {
    constructor(renderer) {
        this.renderer = renderer;
    }

    draw() {}
}

class Circle extends Shape {
    constructor(renderer) {
        super(renderer);
    }

    draw() {
        this.renderer.renderShape("Circle");
    }
}

// 使用桥模式
const vectorRenderer = new VectorRenderer();
const rasterRenderer = new RasterRenderer();

const circle1 = new Circle(vectorRenderer);
circle1.draw(); // 输出: Drawing Circle as vector

const circle2 = new Circle(rasterRenderer);
circle2.draw(); // 输出: Drawing Circle as pixels

它的优点是抽象和实现可以独立扩展,可以在运行时切换实现,并且减少了类的数量。缺点是增加了设计的复杂性,对于简单系统可能不必要。

组合模式(Composite Pattern)

将对象组合成树形结构以表示“部分-整体”的层次结构,使得客户端可以统一处理单个对象和组合对象。如:

// 组件接口
class Component {
    operation() {}
}

// 叶子节点
class Leaf extends Component {
    operation() {
        return "Leaf";
    }
}

// 组合节点
class Composite extends Component {
    constructor() {
        super();
        this.children = [];
    }

    add(component) {
        this.children.push(component);
    }

    operation() {
        return this.children.map(child => child.operation()).join(", ");
    }
}

// 使用组合模式
const leaf1 = new Leaf();
const leaf2 = new Leaf();
const composite = new Composite();

composite.add(leaf1);
composite.add(leaf2);

console.log(composite.operation()); // 输出: Leaf, Leaf

它的优点是统一处理单个对象和组合对象,可以动态添加或删除对象,客户端代码更简洁。缺点是增加了类的数量,遍历树形结构可能较慢。

装饰器模式(Decorator Pattern)

学过python的对装饰器都不陌生,装饰器能够在不修改原有函数(对象)的基础上为其添加新功能,即对原代码做封装,它能有效实现代码复用,例如一个装饰器能应用到多个对象上,一个典型的应用是在web开发的url路由上轻松实现权限控制等。在python中装饰器就是一个可回调对象,它的输入是一个函数,调用时参数为被装饰函数的参数:

class MyDecorator(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kargs):
        print('start : {}'.format(self.func.__name__))
        res = self.func(*args, **kargs)
        print('end : {}'.format(self.func.__name__))
        print('the result : {}'.format(res))
        return res

@MyDecorator
def add(a,b):
    return a+b

if __name__ == '__main__':
    add(1,2)

输出为:

start : add
end : add
the result : 3

门面模式(Facade Pattern)

提供一个统一的接口,用来访问子系统中的一群接口,使得子系统更容易使用。如:

// 子系统
class SubsystemA {
    operationA() {
        return "Subsystem A";
    }
}

class SubsystemB {
    operationB() {
        return "Subsystem B";
    }
}

// 门面
class Facade {
    constructor() {
        this.subsystemA = new SubsystemA();
        this.subsystemB = new SubsystemB();
    }

    operation() {
        return `${this.subsystemA.operationA()}, ${this.subsystemB.operationB()}`;
    }
}

// 使用门面模式
const facade = new Facade();
console.log(facade.operation()); // 输出: Subsystem A, Subsystem B

它的优点是提供一个简单的接口,客户端与子系统解耦,减少客户端代码的复杂性。缺点是可能隐藏子系统的灵活性,可能引入额外的调用。

享元模式(Flyweight Pattern)

通过共享大量细粒度对象来减少内存使用,提高性能。如:

// 享元工厂
class FlyweightFactory {
    constructor() {
        this.flyweights = {};
    }

    getFlyweight(key) {
        if (!this.flyweights[key]) {
            this.flyweights[key] = new Flyweight(key);
        }
        return this.flyweights[key];
    }
}

// 享元
class Flyweight {
    constructor(key) {
        this.key = key;
    }

    operation(uniqueState) {
        return `Flyweight: ${this.key}, Unique: ${uniqueState}`;
    }
}

// 使用享元模式
const factory = new FlyweightFactory();
const flyweight1 = factory.getFlyweight("shared");
const flyweight2 = factory.getFlyweight("shared");

console.log(flyweight1.operation("state1")); // 输出: Flyweight: shared, Unique: state1
console.log(flyweight2.operation("state2")); // 输出: Flyweight: shared, Unique: state2

它的优点是共享相同状态的对象,减少对象的创建和销毁,客户端代码更简洁。缺点是增加了设计的复杂性,需要小心处理共享状态。

代理模式(Proxy Pattern)

代理模式对实际对象做了一层抽象(代理),用户不直接访问对象而是访问它的代理,这种抽象为下层提供了更多的灵活性,比如代理可以实现访问控制,代理可以忽略底层实现的具体差异(底层接口发生变化,底层实现发生变化,如远程资源映射为本地资源),提供统一且固定的接口,不同的语言代理实现方式不一样,此处由具有代表性的Java为例,python作为解释型语言当然也可以很简单的实现同样的代理:

静态代理

静态代理是比较直观的代理,它为委托(被代理对象)编写了代理类代码,它的缺点是不能自适应,代理类需要实现所有需要被代理的接口,当委托类接口发生增删时代理类需要同样的修改才能应用更改:

package MyProxy;

public class StaticProxy {
    static public void main(String[] args) {
        // 创建委托对象
        Delegate d = new Delegate();
        // 创建代理对象,为其传入委托对象实例
        Proxy p = new Proxy(d);
        // 使用代理调用委托对象的方法
        p.eat();
    }
}

class Proxy {
    Delegate intance = null;

    Proxy(Delegate intance) {
        this.intance = intance;
    }

    void eat() {
        System.out.println("use proxy...");
        this.intance.eat();
    }

}

class Delegate {
    String name = "Mao";

    void eat() {
        System.out.println(name + " eat...");
    }
}

输出:

use proxy...
Mao eat...

动态代理

动态代理利用语言的动态特性,如Java的反射实现了更灵活的代理,此时代理类是在运行时生成的,它依靠接口,只需要实现InvocationHandler接口,在其内部写入代理逻辑,所有对接口的访问都会经过其invoke方法,利用反射就可以实现自适应:

package MyProxy;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;

public class DynamicProxy {
    public static void main(String[] args) {
        // 实例化委托对象
        Cat cat = new Cat("Beta");  
        // 使用反射,为委托对象创建InvocationHandler,用于处理代理逻辑
        MyInvocationHandler handler = new InvocationHandler(cat);  
        // 创建代理类并实例化
        Pet p = (Pet) Proxy.newProxyInstance(Cat.class.getClassLoader(), Cat.class.getInterfaces(), handler);  
        // 使用代理调用cat的方法
        p.eat();
    }
}

class MyInvocationHandler<T> implements InvocationHandler {
    private T target;

    MyInvocationHandler(T target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Dynamic Proxy....");
        return method.invoke(target, args);
    }
}

interface Pet {
    void eat();

    void play();
}

class Cat implements Pet {
    private String name = null;

    Cat(String name) {
        this.name = name;
    }

    @Override
    public void eat() {
        System.out.println(name + " eat...");
    }

    @Override
    public void play() {
        System.out.println(name + " play...");
    }
}

输出:

Dynamic Proxy....
Beta eat...

行为型模式

责任链模式 (Chain of Responsibility Pattern)

将请求沿着处理链传递,直到有对象处理它。每个处理者决定是否处理请求或传递给下一个处理者。

它的优点是请求的发送者和接收者之间没有直接的依赖关系,可以动态地改变处理请求的对象链, 每个处理者只负责处理自己职责范围内的请求。缺点是如果链中没有合适的处理者,请求可能会被忽略,如果链过长,可能会影响性能。

命令模式 (Command Pattern)

将请求封装为对象,使得可以用不同的请求对客户进行参数化,并支持请求的排队、记录和撤销。

它的优点是将请求的发送者和执行者解耦,可以很容易地添加新的命令,可以实现命令的撤销和重做。缺点是每个命令都需要一个具体的类,可能会导致类的数量增加,可能会增加系统的复杂性。

解释器模式 (Interpreter Pattern)

定义语言的语法规则,并用解释器来解释和执行这些规则。通常用于实现特定领域语言(DSL)。

它的优点是可以很容易地扩展语法规则,可以灵活地定义语法规则。缺点是对于复杂的语法,解释器模式可能会变得复杂,解释器模式通常效率较低,特别是在处理复杂语法时。

迭代器模式 (Iterator Pattern)

提供一种方法顺序访问一个聚合对象中的各个元素,而不暴露其内部表示。

它的优点是提供了一种统一的方式来遍历集合,可以定义不同的迭代器来实现不同的遍历方式。缺点是对于简单的集合,使用迭代器模式可能会增加不必要的复杂性,迭代器模式可能会引入一些性能开销。

仲裁者模式 (Mediator Pattern)

用一个中介对象来封装一系列对象之间的交互,使得对象之间不需要显式地相互引用,从而降低耦合。

它的优点是减少了对象之间的直接依赖,将对象之间的交互逻辑集中在一个地方。缺点是仲裁者可能会变得复杂,特别是当对象之间的交互逻辑很多时,仲裁者可能会成为系统的单点故障。

备忘录模式 (Memento Pattern)

在不破坏封装的前提下,捕获并外部化一个对象的内部状态,以便以后可以将该对象恢复到原先保存的状态。

它的优点是可以保存对象的状态,以便以后恢复,备忘录模式封装了对象的状态,外部无法直接访问。缺点是如果保存的状态很多,可能会消耗大量内存,可能会增加系统的复杂性。

观察者模式 (Observer Pattern)

定义对象间的一对多依赖关系,当一个对象改变状态时,所有依赖它的对象都会收到通知并自动更新。

它的优点是观察者和被观察者之间是松耦合的,可以动态地添加或删除观察者。缺点是如果观察者很多,通知可能会影响性能,可能会增加系统的复杂性。

状态模式 (State Pattern)

允许对象在其内部状态改变时改变其行为,看起来像是对象的类发生了改变。

它的优点是将状态相关的逻辑分散到不同的状态类中,简化了代码,可以很容易地添加新的状态。缺点是每个状态都需要一个具体的类,可能会导致类的数量增加,可能会增加系统的复杂性。

策略模式 (Strategy Pattern)

定义一系列算法,将它们封装起来,并且使它们可以互相替换。策略模式使得算法可以独立于使用它的客户端而变化。

它的优点是可以在运行时切换算法或策略,将算法的实现与使用它的类解耦。缺点是每个策略都需要一个具体的类,可能会导致类的数量增加,可能会增加系统的复杂性。

模板方法模式 (Template Method Pattern)

定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变算法的结构即可重新定义算法的某些特定步骤。

它的优点是将公共的代码放在父类中,子类只需实现特定的部分,可以很容易地扩展模板方法。缺点是子类的行为受到父类的限制,可能会增加系统的复杂性。

访问者模式 (Visitor Pattern)

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

它的优点是可以很容易地添加新的操作,而不需要修改现有的类,将数据结构与操作分离。缺点是可能会增加系统的复杂性,访问者模式可能会违反封装原则,因为它需要访问对象的内部状态。

编程范式

编程范式(Programming Paradigm)是指编程的基本风格或方式,它决定了程序员如何组织代码、解决问题以及表达逻辑。以下是常见的编程范式及其含义、优缺点和例子:


命令式编程 (Imperative Programming)

通过明确的指令告诉计算机如何完成任务,关注“怎么做”,如C、Pascal、汇编语言通常使用该范式:

int sum = 0;
for (int i = 1; i <= 10; i++) {
    sum += i;
}

它的优点是直观易于理解,适合处理具体的、步骤明确的任务。缺点是会容易使代码变得冗长和复杂,难以复用和维护。

面向对象编程 (Object-Oriented Programming, OOP)

将程序组织为对象的集合,对象包含数据(属性)和行为(方法),通过封装、继承和多态实现代码复用和扩展,如Java、C++、Python经常使用:

class Dog:
    def __init__(self, name):
        self.name = name
    def bark(self):
        print(f"{self.name} says woof!")
dog = Dog("Buddy")
dog.bark()

它的优点是代码复用性强,易于维护和扩展,代码贴近直观感受,适合大型复杂系统。而缺点,可能引入不必要的复杂性(耦合度会很大),性能开销较大。

函数式编程 (Functional Programming, FP)

将计算视为数学函数的求值,避免状态和可变数据,需要满足如下三点:

1.声明式(Declarative):它是相对于命令式(Imperative)而言的,命令式或叫指令式就是告知怎么做(如C的过程编程),而声明式或叫说明式是要什么而不管怎么做,如python的map/filter等函数用法就是声明式,声明式效率相对会低但接口更清晰(解偶)且能减少重复代码。

2.纯函数(Pure Function):纯函数是执行过程仅由参数决定,且不改变任何外部状态的函数,最简单的若一个函数参数固定时可用常数代替则可认为是纯函数。

3.数据不可变性(Immutability):即指数据一旦产生它的值就不再变化,后续运算过程会(可在原值基础上)产生新值。

例如,Python,Haskell、Scala、JavaScript(部分支持)等会使用(函数是一类公民):

const numbers = [1, 2, 3, 4];
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8]

它的优点是代码简洁、易测试,适合并发编程,避免副作用,提高可靠性。缺点是学习曲线较高,不适合所有问题领域。

声明式编程 (Declarative Programming)

描述“做什么”而不是“怎么做”,关注结果而非过程,如SQL、HTML、正则表达式:

SELECT name FROM users WHERE age > 18;

它的优点是代码简洁、易读,适合处理复杂逻辑。缺点是可能隐藏底层细节,导致性能问题。

逻辑编程 (Logic Programming)

通过逻辑规则和事实描述问题,程序通过推理找到解决方案,如Prolog:

parent(john, jim).
parent(jim, ann).
grandparent(X, Y) :- parent(X, Z), parent(Z, Y).

优点是适合解决逻辑推理问题,代码简洁。缺点学习曲线高,性能较差。

事件驱动编程 (Event-Driven Programming)

程序的执行流程由事件(如用户输入、消息)驱动,通过事件监听和回调机制响应事件,在JavaScript(Node.js)、GUI 框架(如 Qt、Electron)中大量使用:

document.getElementById("myButton").addEventListener("click", () => {
    console.log("Button clicked!");
});

它的优点是适合交互式应用(如 GUI、游戏),异步处理能力强。缺点是代码结构复杂,难以调试,容易陷入“回调地狱”。

并发编程 (Concurrent Programming)

同时执行多个任务,通过线程、进程或协程实现并行处理,如Go、Erlang、Java(多线程)。

go func() {
    fmt.Println("Hello from a goroutine!")
}()

它的优点是提高资源利用率,适合处理高并发任务。而缺点是容易引入竞态条件和死锁,调试和测试复杂。

面向切面编程 (Aspect-Oriented Programming, AOP)

将横切关注点(如日志、安全)从核心业务逻辑中分离出来,通过切面(Aspect)实现模块化。在Java中特别常见,尤其Spring AOP(Java)、AspectJ:

@Aspect
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before method: " + joinPoint.getSignature().getName());
    }
}

它的优点是提高代码模块化,减少重复代码。缺点是可能引入运行时开销,学习曲线较高。

元编程 (Metaprogramming)

编写能够生成或操作其他程序的程序,通常在运行时动态修改代码行为,如Ruby、Python(装饰器、元类)、Lisp:

def decorator(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@decorator
def say_hello():
    print("Hello!")

它的优点是提高代码灵活性,减少重复代码。缺点是可读性差,调试困难。

响应式编程 (Reactive Programming)

通过数据流和变化传播来处理异步数据流,强调对数据变化的响应,如 RxJS(JavaScript)、ReactiveX:

const { from } = rxjs;
const { map, filter } = rxjs.operators;

from([1, 2, 3, 4])
  .pipe(
    filter(x => x % 2 === 0),
    map(x => x * 2)
  )
  .subscribe(console.log); // 4, 8

它的优点是适合处理实时数据流,代码简洁。缺点是学习曲线高(第一次看RxJS代码时,一脸懵逼,学了半天也不知所云),调试复杂。响应式系统

img

把函数式和响应式结合起来叫做函数响应式编程(Functional Reactive Programming/FRP)。

背压:由于消费速度慢于生成速度,缓冲区被填满溢出的现象,此时只能通过丢弃数据来解决。

泛型编程 (Generic Programming)

编写与数据类型无关的通用代码,通过模板或泛型实现代码复用,在C++(模板)、Java(泛型)、Rust中大量使用:

public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

它的优点是提高代码复用性,减少重复代码。而缺点是可能增加编译时间,可读性较差。

数据驱动编程 (Data-Driven Programming)

程序的行为由数据决定,通过配置文件或数据源动态调整逻辑。在游戏开发(行为树)、规则引擎(Drools)中大量使用:

{
    "actions": [
        { "type": "move", "direction": "left" },
        { "type": "attack", "target": "enemy" }
    ]
}

它的优点是灵活,易于扩展,适合处理动态规则。而缺点是数据错误可能导致程序异常,调试比较困难。

其他

柯里化(Currying)与偏函数(partial application)

偏函数用于冻结参数,它讲函数与部分(或所有函数绑定),之后只需要传入剩余的参数即可调用该函数,python中可用functools.partial实现,js中可用bind实现,用偏函数可以去除一些重复代码。

柯里化是将一个多参数的函数转换成单参数的函数,其返回为函数,链式调用直到返回最终结果,如:

const add = a => b => c => a + b + c
add(1)(2)(3)  // 结果为6

柯里化可以看做一种特殊的偏函数,它限制了有且只有一个参数,这个特性相对于减少重复代码,它更可以用于函数嵌套,因为很多语言里函数只允许有一个返回值!

异步与非阻塞

阻塞非阻塞:是做某件事时,因为条件不满足而等待,例如文件操作/网络操作因为外设未准备好资源,程序陷入等待状态(用户态表现为syscall阻塞),而非阻塞是执行操作时立即返回而不管条件是否满足。

同步异步:对于非阻塞,由于条件可能并不满足,因此即使此时操作返回可能也无法继续依赖于条件的任务,于是程序可能先自己轮询等待条件满足再执行,这是同步,也可以先去做其他事,等条件满足时再回来继续之前的任务,这是异步。

参考

《软件设计模式》-中国科学技术大学

太好了!总算有人把动态代理、CGlib、AOP都说清楚了!