概述

Posted on 2024-07-12 16:29  Aderversa  阅读(2)  评论(0编辑  收藏  举报

设计模式概述

设计模式是解决特定问题的一系列套路,其本质是面向对象原则的实际运用。

分类

(1)创建者模式

用于描述怎样“创建对象”,它的主要作用在于“将对象的创建与使用”分离。有单例、原型、工厂方法、抽象工厂、建造者共5种设计模式。

使用这种设计模式的好处,我猜测是:如果对象的创建方法发生了变动,那么只需要修改创建者对象,而引用创建者对象来获取并使用特定对象的对象不需要进行修改,这样就将“对象的创建与使用”解耦。

(2)结构型模式

用于描述如何将类或对象按某种布局组成更大的结构,有代理、适配器、桥接、装饰、外观、享元、组合共7种结构型模式。

(3)行为型模式

用于描述类或对象之间怎么相互协作,共同完成单个对象无法单独完成的任务,以及怎样分配职责。有模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、备忘录、解释器等11种行为型模式

软件设计原则

开闭原则

“对扩展开放,对修改关闭”意思是:在程序需要进行修改时,不能修改原有的代码,实现热插拔的效果。

想要达到这样的效果,我们需要使用“接口”和“抽象类”

因为抽象灵活性好,适用性广,使用合理能保证软件架构稳定。

当细节发生变化时,只需要从抽象类中派生出新的实现类即可。

例如:搜狗输入法有很多的皮肤,“皮肤”就可以定义为一个抽象类,而“哆啦A梦皮肤”,“火影忍者皮肤”等等就是“皮肤”的实现类。

而搜狗输入法只需要管理一个“皮肤”对象,具体的实现类是什么,就看用户定义使用什么皮肤。

用C++实现上述例子:

#include <iostream>
#include <memory>
#include <type_traits>

class AbstractSkin { // 在java中定义为:implement,在C++中能使用纯虚函数模拟implement
public:
	virtual ~AbstractSkin() {};
	virtual void display() = 0;
};

class DefaultSkin : public AbstractSkin {
public:
	void display() override {
		std::cout << "默认皮肤" << std::endl;
	}
};

class NinjaSkin : public AbstractSkin {
public:
	void display() override {
		std::cout << "火影忍者皮肤" << std::endl;
	}
};

class DuoraSkin : public AbstractSkin {
public:
	void display() override {
		std::cout << "哆啦A梦皮肤" << std::endl;
	}
};

class SouGouInputer {
public:
	SouGouInputer() : skin_(std::make_unique<DefaultSkin>()){}
	void display() {
		skin_->display();
	}

	void setSkin(std::unique_ptr<AbstractSkin>&& pSkin) {
		skin_ = std::move(pSkin); // unique_ptr转换控制权必须使用move转发
	}

private:
	std::unique_ptr<AbstractSkin> skin_;
};

int main(int argc, char** argv) {
	SouGouInputer inputer;
	inputer.display();
	inputer.setSkin(std::make_unique<NinjaSkin>());
	inputer.display();
	inputer.setSkin(std::make_unique<DuoraSkin>());
	inputer.display();
	return 0;
}

运行结果如下:

这说明,我们在没有改变SouGouInputer这个类的前提下,完成了“皮肤”实现的切换。

上述代码的UML类图如下:

classDiagram class AbstractSkin <<interface>> AbstractSkin SouGouInputer <-- AbstractSkin : assosiaction AbstractSkin <|.. NinjaSkin : implements AbstractSkin <|.. DefaultSkin : implements AbstractSkin <|.. DuoraSkin : implements class SouGouInputer { +skin: AbstractSkin +display() +setSkin() } class AbstractSkin { +display() } class NinjaSkin { +display() } class DefaultSkin { +display() } class DuoraSkin { +display() }

里氏代换原则

任何基类可以出现,子类一定可以出现。

也就是说:子类可以扩展父类没有的功能,但不能改变父类原有的功能。

例如:正方形不是长方形。

一开始我们有这样子的长方形类Rectangle

classDiagram RectangleDemo <.. Rectangle class Rectangle { +getWidth() : int +setWidth(int) : void +getLength() : int +setLength(int) : void } class RectangleDemo { +resize(Rectangle) : void +printLengthAndWidth(Rectangle) : void }

RectangleDemo类的resize()方法,在正方形的长小于宽时,将正方形的长调整到大于宽。

按照数学定义,我们认为:正方形也是长方形的一种,于是我们从Rectangle中派生出一个Square正方形类。

classDiagram RectangleDemo <.. Rectangle Rectangle <|-- Square class Rectangle { +getWidth() : int +setWidth(int) : void +getLength() : int +setLength(int) : void } class RectangleDemo { +resize(Rectangle) : void +printLengthAndWidth(Rectangle) : void } class Square { +setWidth(int) : void +setLength(int) : void }

但是,由于正方形的长永远等于宽,所以长和宽永远相等,所以正方形的设置长和宽的例子需要进行修改。

于是就出现了这样的情况:在将RectangleDemo中的Rectangle传入Square的resize()方法时,程序先入死循环,到最后程序因为int溢出而崩溃。这是因为在resize()方法中想要将长调整到比宽长,但正方形的长和宽一直都是相等的,不可能出现长大于宽的情况,所以程序就一直调整。

这里想说明的问题是,如果子类对基类的方法做出的修改,那把基类替换成子类后,程序很可能会出现错误,所以子类尽量避免修改基类的方法。

但是,实际上resize()方法本身就有问题,长方形的长不一定要比宽长,也可能长和宽相等,当将长和宽调整到相同的长度时,完全就可以停下了,这才是符合数学逻辑的

改进类的结构

正方形并非是长方形,所以两个类独立出来,然后由于正方形和长方形都可以认为有长和宽,就抽象出来一个获取长和宽的接口。

注意,上面的图是讲课老师乱画的,具体关系还是看自己理解,然后查UML的标准画法。并且这里只是为了说明:子类继承父类的正确方法。至于长方形和正方形问题,完全就是讲课老师把数学定义都没有搞清楚导致的。

依赖倒转原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。

简单来说就是,面向抽象来编程。

例子:现在要组装一台电脑,可以用来组装电脑的硬件有不同的品牌,比如:

  1. CPU:Intel、AMD
  2. 内存条:芝奇、威刚、海盗船
  3. 硬盘:三星、西数
  4. 主板:华硕、微星、技嘉

如果你组装的电脑需要适配不同的CPU、内存条、硬盘、主板等,就不能组合一些特定的硬件(抽象成类的关系就是,电脑这个类,不应该和具体的硬件类形成依赖),必须组合一个统一的概念(抽象类)。也就是说,电脑类需要组合CPU的抽象类、内存条的抽象类等,而非某个具体的厂商的实现类。


这样做的话,如果你为电脑更换硬件,就不需要改动电脑类中原有的代码。

并且,我们可以将更改硬件的方法暴露出来,让用户可以更换硬件实现热插拔的效果,这就是依赖倒转。

或许这就是Spring IOC容器的设计的来源?

接口隔离原则

客户端不应该被迫依赖它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。

比如:

classDiagram classA <.. classB class classA { +method1() +method2() } class classB { +myMethod() }

这里的classB需要使用到classA的method1()方法(method2()完全不使用),如果我们classB通过继承的方式获得classA的method1()方法,那就无端地为classB引入了对method2()的依赖,这不是我们希望看到的。

所以我们可以这样设计,将classB需要用到的所有classA中的方法全都抽象成一个接口,然后classA实现它,classB通过获取该接口访问method1()方法。

classDiagram abstractAPI <|.. classA abstractAPI <.. classB class classA { +method1() +method2() } class classB { +myMethod() } class abstractAPI { +method1() }

B类就可以通过引用接口的方法,只获取自己想要的方法,不需要依赖实现类中其他它用不到的方法。

迪米特法则

迪米特法则又叫最少知识原则。只和你的直接朋友交谈,不跟陌生人说话

其含义是:如果两个软件实体无须直接通信,那么就不应发生相互调用。可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的独立性。

朋友:当前对象本身,当前对象的成员对象,当前对象所创建的对象,当前对象的方法参数。

总结为:尽最大可能避免“依赖循环”

合成复用原则

尽量先使用组合或聚合等关联关系来实现,其次才考虑继承。

  • 继承复用:

    • 优点:简单,容易实现

    • 缺点:

      • 破坏类的封装性,破坏开闭原则
      • 限制复用的灵活性
  • 组合或聚合:

    • 优点:

      • 维持封装
      • 耦合度低
      • 复用灵活

Copyright © 2024 Aderversa
Powered by .NET 8.0 on Kubernetes