设计模式在开发过程中随处可见,是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。它真的很重要!!!帮助你更快熟悉业务设计。
设计模式的六大原则(SOLID)总结来看都是为了减小耦合,提高可维护性。(类的单依职责,最小接口,依赖抽象接口,依赖最少知道,组合代替继承)
总原则:开闭原则:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。想要达到这样的效果,我们需要使用接口和抽象类等。
1、单一职责:一个类应该只有一个发生变化的原因。不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,否则就应该把类拆分。感觉和数据库的三大范式,都是化大为小。
2、依赖倒置:上层模块不应该依赖底层模块,它们都应该依赖于抽象。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
3,接口隔离原则:接口的方法必须被子类用到。
4.迪米特法则(最少知道原则)一个类对自己依赖的类知道的越少越好。无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
5.、合成复用原则 尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的。rust的trait体现了就是这样一个设计思想。
关于组合和继承。
继承是一种is-a的关系,父类通过继承子类的方法和变量,实现代码重用的目的,这是最大的优点。继承的相关知识点有公共继承,私有继承保护继承区别,多重继承,虚拟继承的概念等等。继承的缺点如下:
1、大千世界,关系很复杂,继承关系也变得复杂,比如想飞行就要继承鸟,想下海就要继承鱼。这就会产生多重继承,会产生二义性。可以用虚基类解决。而且java里面不支持多个基类,所以从这里就可以看出继承的其中一个缺点:无法通过继承的方式,重用多个类中的代码。
2、继承破坏了封装性。因为继承可以访问基类的成员,自然就破坏了封装性,除非你把成员变量设置为私有的,这样子类就不能直接访问。更重要的是,子类必须无条件接收基类的方法,不管是不是完全适用。所以不该用的方法就暴露被污染了。
3、继承无法实现动态继承。继承是编译期就决定下来的,无法在运行时改变。组合加反射就可以。
4、继承是紧密耦合的,如果父类接口改变, 子类必须修改,特别是当不同组人员维护开发,更麻烦。
如何解决这些问题?
《劝学》”假舆马者,非利足也,而致千里;假舟楫者,非能水也,而绝江河。君子生非异也,善假于物也。
君子其实没什么太多特别的地方,只不过善于利用工具而已。这就是所谓的”has-a”。拥有什么,或者使用什么。组合人想上天怎么办呢?可以利用飞机上天。人想下海怎么办呢,可以利用轮船下海。并不要求人要长出翅膀,人要长出鱼尾。把一些特征和行为抽取出来,形成工具类。然后通过聚合/组合成为当前类的属性。再调用其中的属性和行为达到代码重用的目的。
什么时候用继承:父类的方法子类完全适用,子类只需要用一个父类的方法,方法不会在运行时根据需求改变。
整体结构
创建型模式(Creational Pattern):对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。
结构型模式(Structural Pattern):关注于对象的组成以及对象之间的依赖关系,描述如何将类或者对象结合在一起形成更大的结构,就像搭积木,可以通过简单积木的组合形成复杂的、功能更为强大的结构。
行为型模式(Behavioral Pattern):关注于对象的行为问题,是对在不同的对象之间划分责任和算法的抽象化;不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。
单例模式就是保证一个类仅有一个实例,并提供一个访问它的全局访问点。比如线程池就可以用。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
定义一个单例类:
私有化它的构造函数,以防止外界创建单例类的对象;
使用类的私有静态指针变量指向类的唯一实例;
使用一个公有的静态方法获取该实例。
懒汉式和饿汉式
单例实例在第一次被使用时才进行初始化,这叫做延迟初始化。
接下来实现一种无锁的懒汉:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
可以看到把构造函数私有了,公有的静态成员函数可以返回实例,没有的话就创建。还有一个私有的指向实例的指针。
饿汉式:类加载的时候就实例化,并且创建单例对象。
public class Hungry{
private Hungry(){}
// 类加载的时候就实例化,并且创建单例对象
private static final Hungry hungry=new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
线程安全方面:饿汉式线程安全 (在线程还没出现之前就已经实例化了,因此饿汉式线程一定是安全的)。懒汉式线程不安全( 因为懒汉式加载是在使用时才会去new 实例的,那么你去new的时候是一个动态的过程,是放到方法中实现的,比如:public static synchronized Lazy getInstance(){ if(lazy==null){ lazy=new Lazy(); } 如果这个时候有多个线程访问这个实例,这个时候实例还不存在,还在new,就会进入到方法中,有多少线程就会new出多少个实例。一个方法只能return一个实例,那最终return出哪个呢?是不是会覆盖很多new的实例?这种情况当然也可以解决,那就是加同步锁,避免这种情况发生) 。
执行效率上:饿汉式没有加任何的锁,因此执行效率比较高。懒汉式一般使用都会加同步锁,效率比饿汉式差。
性能上:饿汉式在类加载的时候就初始化,不管你是否使用,它都实例化了,所以会占据空间,浪费内存。懒汉式什么时候需要什么时候实例化,相对来说不浪费内存。
工厂模式:包括简单工厂、静态工厂、工厂、抽象工厂。
工厂模式是用工厂方法代替new操作的一种模式。所以以后new时就要多个心眼,是否可以考虑使用工厂模式,虽然这样做,可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量(降低耦合)。在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
简单工厂
public class SimpleCoffeeFactory {
// 根据type判断类型,实例化并返回对应对象
public Coffee createCoffee(String type) {
Coffee coffee = null;
if("americano".equals(type)) {
coffee = new AmericanoCoffee();
} else if("latte".equals(type)) {
coffee = new LatteCoffee();
}
return coffee;
}
}
一旦有了工厂,后期如果需要对象直接从工厂中获取即可。这样也就解除了和实现类的耦合,但同时又产生了新的耦合。后期如果再添加新的类,就必须修改工厂类的代码,违反了开闭原则。如果初始化很复杂会导致这个工厂类很复杂。
在开发中也有一部分人将工厂类中的创建对象的功能定义为静态的,这个就是静态工厂模式。
工厂模式:是一种常用的类创建型设计模式,此模式的核心精神是**封装类中变化的部分,提取其中个性化善变的部分为独立类,通过依赖注入以达到解耦、复用和方便后期维护拓展的目的。**它的核心结构有四个角色,分别是抽象工厂、具体工厂、抽象产品、具体产品。
public interface CoffeeFactory {
Coffee createCoffee();
}
public class LatteCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new LatteCoffee();
}
}
public class AmericanCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new AmericanCoffee();
}
}
在系统增加新的类时只需要添加对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;缺点是每增加一个类就要增加一个对应的具体工厂类,增加了系统的复杂度。
当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。 而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂
工厂方法类中只有一个抽象方法,要想实现多种不同的类对象,只能去创建不同的具体工厂方法的子类来实列化,而抽象工厂则是让一个工厂负责创建多个不同类型的对象 其实就是相当于二维的初始化,首先具体工厂类继承抽象工厂,然后每个具体工厂有这个工厂不同类型的产品创建。
建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。
优点:
(1)将产品本身与产品的创建过程解耦,使相同的创建过程可以创建不同的产品对象;
(2)增加新的具体建造者无效修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便。
缺点:
(1)建造者模式所创建的产品一般具有较多的共同点,产品差异过大,则不适合;
KFC 套餐实例,套餐是一个复杂的对象,一个套餐包含主食和饮料等组成部分,不用的套餐 主食和饮料的类型不同,服务员可以根据顾客的需求,一步步完成组成套餐。
通过以上分析,套餐相当于 产品角色,服务员相当于指挥者,不同种类的套餐制作方法称为建造者。
原型模式:是用于**创建重复的对象,同时又能保证性能。**主要好处是最小化创建实例过程的消耗,通过new来创建一个对象实例的过程的消耗要比通过clone方法创建对象实例的过程的消耗大的多。
用类的拷贝构造函数复制一个对象this指针,用户不需要自己new,自己new需要知道原型的各种参数。很麻烦。
感觉和工厂模式有点像,都是用类实现自己创建对象实例。
---------------------结构型模式---------------------
适配器模式
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能,创建一个新类,继承原有的类,实现新的接口即可。将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。美国电器110V,中国220V,就要有一个变压器将110V转化为220V。
根据合成复用原则,组合大于继承。因此,类的适配器模式应该少用。
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。如果修改代码增加新功能就破坏了开闭原则,所以每增加一个新功能就要新增子类,这样子类继承太多很麻烦。
具体例子就是,首先有一个基类比如手机,然后不同牌子的手机继承这个基类,然后有个抽象装饰类也要继承这个基类(体现了依赖于抽象),然后比如要添加人脸识别这个具体的装饰类,就要在这个装饰类中组合一个手机对象。体现了组合优于继承。
java里的IO包就是利用这个。
代理模式:给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用,提供相同的接口。通俗的来讲代理模式就是我们生活中常见的中介。
为什么要用?:第一中介隔离的作用,在某些情况下,一个客户类不想或者不能直接引用一个委托对象;更重要的是符合开闭原则,比如想要扩展委托类的功能,就不用修改委托类,委托类通常是执行业务的,封装好的。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等,公共服务,比如给项目加缓存层,加日志,就可以用代理类实现。
外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
应用实例:
去医院看病,可能要去挂号、门诊、划价、取药,让患者或患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便。
客户端和子系统解耦,不需要知道子系统内部的实现,隐藏内部细节
桥接模式(Bridge Pattern)是用于把抽象化与实现化解耦,使得二者可以独立变化。它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。从桥接模式的设计上我们可以看出聚合是一种比继承要弱的关联关系,手机类和软件类都可独立的进行变化,不会互相影响。在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。(组合的整体和部分是共存亡的关系,聚合比较松散,生命周期不一致。代码中,组合部分的实例化在整体进行作为整体的成员,聚合在整体外进行然后注入整体)
组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中 声明的公共接口。树枝构件(Composite)角色:是组合中的分支节点对象,它有子节点。它实现了抽象构件角色中声明的接口,它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法
享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。用哈希表保存对象,对象有唯一标识,内部状态对象是通过哈希表保存的,当外部状态相同的时候,不再重复的创建内部状态对象,从而减少要创建对象的数量。
运用共享技术有效地支持大量细粒度的对象。
应用实例:
1、Java中的String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面;
2、数据库的数据池。
策略模式
策略模式定义了一系列的算法,并将每一个算法封装起来,而且使他们可以相互替换,让算法独立于使用它的客户而独立变化。
策略模式的使用场景:
1.针对同一类型问题的多种处理方式,仅仅是具体行为有差别时;购物时,不同会员等级对应的商品价格折扣是不同的,折扣具有不同的算法
2.需要安全地封装多种同一类型的操作时;
3.出现同一抽象类有多个子类,而又需要使用 if-else 或者 switch-case 来选择具体子类时。
这个模式涉及到三个角色:
环境(Context)角色:持有一个Strategy的引用,给客户调用。
抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。
策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。
策略模式要求用户必须知道并且理解所有策略,自行选择策略;而且策略很多的话,那么对象的数目就会很可观。
Environment environment=new Environment(new AddStrategy());
int result=environment.calculate(20, 5);
System.out.println(result);
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
观察者模式
观察者模式又叫做发布-订阅模式、模型-视图模式。定义了一种一对多的依赖关系,一个主题对象可被多个观察者对象同时监听,使得每当主题对象状态变化时,所有依赖它的对象都会得到通知并自动更新,属于行为型设计模式。
观察者的核心是将观察者与被观察者解耦,以类似消息/广播发送的机制联动两者,使被观察者的变动能通知到感兴趣的观察者们,从而做出相应的响应
Subject:抽象被观察者,抽象被观察者类提供一系列接口,可以增加、删除、通知观察者对象。
ConcreteSubject:具体被观察者,内部维护一个观察者集合,并实现抽象被观察者类的抽象方法。
Observer:抽象观察者,定义一个更新接口,使得在得到收到更改通知时更新自己。
Observer:抽象观察者,定义一个更新接口,使得在得到收到更改通知时更新自己。
实例:我开了一个公众号,A,B关注了,当我推送一篇文章时,关注该公众号的人都会收到推送的文章。
public interface Observer{
public void update (String message);
public class User implements Observer {
private String name;
public User(String name) {
this.name = name;
}
@override
public void update (String message){
System.out.println(this.name + "收到通知-" + message);
}
}
public interface Subject{
public void addObserver (Observer Observer);
public void removeObserver (Observer Observer);
public void notify(String message);
}
public class ConcreteSubject implements Subject {
private List<Observer> usersList = new ArrayList<Observer>();
@Override
public void addObserver (Observer observer) {
usersList.add(observer);
}
@Override
public void removeObserver (Observer observer) {
usersList.remove(observer);
}
@Override
public void notify(String message) {
for (Observer observer : usersList) {
observer.update(message);
}
}
=
优点: 观察者增加或删除无需修改主题的代码,只需调用主题对应的增加或者删除的方法即可。增强了程序的可维护性和可拓展性。观察者只需等待主题通知,无需了解主题相关的细节;同时主题只负责通知观察者,无需了解观察者如何处理通知,耦合性低。
缺点:消息的通知顺序执行,如果一个观察者卡顿,会影响整体的执行效率,若遇到这种情况,一般会采用异步实现。主题持有观察者的引用,如果从主题中删除观察者时未正常处理,会导致观察者无法被回收。
迭代器模式(Iterator Pattern)是Java和.Net编程环境中非常常用的设计模式。这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。(比如STL中的迭代器)
责任链模式:如果有多个对象有机会处理请求,责任链可使请求的发送者和接受者解耦,请求沿着责任链传递,直到有一个对象处理了它为止。客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递。判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
命令模式将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将**"行为请求者"与"行为实现者"解耦**?将一组行为抽象为对象,可以实现二者之间的松耦合。(就是在请求者和实现者之间通过命令对象来执行,这样实现变化的话请求不用改,不用if…else来选择)
在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。。状态模式和命令模式一样,也可以用于消除 if…else 等条件选择语句。
忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。比如回滚,打游戏存档操作。其主要缺点是:资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
访问者模式:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离。
中介模式用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。它是迪米特法则(一个类对自己依赖的其他类知道越少越好,把交互依赖封装到方法里面)的典型应用。其主要缺点是:当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。