设计模式的十八般武艺

发布于: 15 小时前

六大原则

单一职责原则

定义:就一个类而言,应该仅有一个引起它变化的原因。

其实字面意思就已经表达的比较明确,单一,也就是干尽量少的事情。在HDU中可以对耦合和内聚程度的评判有一定的了解。

什么叫做少,其实很难有一个标准。但是在Android的MVC框架中,Activity既作为View,又起着Controller的作用的时候是否显得会很臃肿呢?他需要进行页面的刷新,网络处理,消息处理等等,写起来容易,但是在我们进行维护的时候,是不是会很头疼呢,这就是单一职责原则的对应所在了。 

开放封闭原则

定义:类,模块,函数等应该是可以扩展的,但是不可以修改。

在日常的项目开发中,需求一直是处于一个变动的状态,但是这同样也会成为项目开发的壁垒,如果你的Bean今天是一只猫,明天就需要是一只狗呢?重新打补丁吗?显然是一个很不合适的做法。而开放封闭原则,解决的就是这一类问题。不论是猫,还是狗,他们总会有相同的特征,抽象化也就是这个原则实现的基础。

// 定义一个动物抽象类
public abstract class Animal {
abstract void do();
}
// 猫实现抽象方法
class Cat extends Animal {
@Override
void do() {
System.out.println("喵");
}
}
// 狗实现抽象方法
class Dog extends Animal {
@Override
void do() {
System.out.println("汪");
}
}

里氏替换原则

定义:所有引用基类(一般来说都是抽象类或接口)的地方必须能透明的使用其子类的对象。

定义的具体含义就是将一个基类对象替换成其子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。

想来直接看定义可能有点难以理解,接下来就用代码来具体实现里氏替换原则了。

/**
* 基于里氏替换原则实现
*/
class Manage1 {
private Animal animal;
public void setAnimal(Animal animal) {
this.animal = animal;
}
void getSound() {
animal.do();
}
}
/**
* 使用子类实现
*/
class Manage2 {
private Cat cat;
public void setAnimal(Cat cat) {
this.cat = cat;
}
void getSound() {
cat.do();
}
}

以上基于两种写法,给予读者评判,如果使用Manage2,如果我们希望获得Dog的声音,那么就需要重新实现Manager3,然后差异就是只是把Cat置换成Dog。而Manage1很好的解决了这个问题,因为不论是Cat还是Dog都是Animal的子类。

依赖倒置原则

定义:高层模块不应该依赖底层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

两个概念:

  • 抽象/高层模块:接口或者抽象类,一般只定义了有什么操作,但是没有具体实现。

  • 细节/底层模块:实现接口或者继承抽象类而产生的,一般来说就是我们通过new产生出来的对象。

这里的代码与开闭原则、里氏替换原则一致,详细见于上文。

DogCat作为Animal的子类,对于do()函数,父类只是一个抽象的方法,而子类完成了具体的实现。而当类Manage1希望与类Cat发生关联时,就是通过类Animal来完成,好处就是Manage1不再关注了类CatDog的具体实现。

迪米特原则

定义:一个软件实体应当少的与其他实体发生相互作用。

其实和上面的内容差不多,同样都是为了降低耦合程度,直接用Demo证明就清楚明了了。

电话网络形式:

  • 打电话的人 --> 接电话的人

  • 打电话的人 --> 中间服务商转接 --> 接电话的人

第一种已经被时代抛弃了,虽然我们并不知觉,但是第一种电话网络模式,如果用在现代社会,那么带来的后果就是你再也看不见太阳了,头顶密密麻麻的电话线,更何况那是座机互联的时代,能够靠电话线来解决,但是这个时代呢?移动互联的时代呢,不能再靠着电话线来解决问题,而是中间商的介入就改变了这个现状。

(1)第一种电话网络模式

/**
* 打电话的人
*/
class Call {
private Receiver receiver;
public void setReceicer(Receiver receiver) {
this.receiver = receiver;
}
public void call() {
receiver.receive();
}
}
/**
* 接电话的人
*/
class Receiver{
private String number;
Receiver(String number){
this.number = number;
}
public void receive() {
System.out.println("接通电话");
}
}
class Main{
public static void main(String[] args) {
Call call = new Call();
call.setReceicer(new Receiver("电话号码"));
call.call();
}
}

代码虽然看着很轻松,但是折射到现实情况的时候,每一个拨打电话和接收电话的人之间都等于连接了一条电话线。

(2)第二种电话网络模式

/**
* 转接
*/
public abstract class Manager {
abstract void link();
}
/**
* 打电话的人
*/
class Call {
private Manager manager;
public void setManager(Manager manager) {
this.manager = manager;
}
public void call() {
manager.link();
}
}
/**
* 接电话的人
*/
class Receiver{
private String number;
Receiver(String number){
this.number = number;
}
public void receive() {
System.out.println("接通电话");
}
}
class CMCC extends Manager {
private String number;
CMCC(String number){
this.number = number;
}
public void link() {
System.out.println("连接接电话的人");
Receiver receiver = new Receiver();
receiver.receive();
}
}
class Main{
public static void main(String[] args) {
Call call = new Call();
call.setManager(new CMCC("电话号码"));
call.call();
}
}

这个时候两个实体通过加入中间商的形式降低了耦合度。也就像我们生活中的各个电话厂商,你不会因为厂商不同而担心不能拨通电话,因为中间的处理过程厂商会帮你解决。

接口隔离原则

定义:一个类对另一个类的依赖应该建立在最小的接口上

一个接口内要实现的函数数量可控,有那么一点像数据库里的第一范式。让我们从Demo看看使用原则的与否的不同之处。

正常实现

// 特征
interface Character {
void look();
void nature();
}
class Dog implements Character{
@Override
void look() {
System.out.println("能看");
}
@Override
void nature() {
System.out.println("淘气");
}
}

基于接口隔离原则实现

// 外观
interface Facade {
void look();
}
// 内在
interface Inherent {
void nature();
}
class Dog implements Inherent{
@Override
void nature() {
System.out.println("淘气");
}
}

几个常用的设计模式

单例模式

定义:保证一个类仅有一个实例,并提供用于一个访问它的全局访问点。

四种写法及其优缺点

(1) 饿汉模式

public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}

在类加载时就已经完成了初始化。

优点:1. 保障了线程同步的问题;2. 获取对象的效率高。

缺点:1. 降低了类加载时速度;2. 如果一直不使用,会内存的浪费。

(2) 懒汉模式

  1. 线程不安全

public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null) instance = new Singleton();
return instance;
}
}

缺点:存在线程同步问题

  1. 线程安全

public class Singleton {
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null) instance = new Singleton();
return instance;
}
}

缺点:每一次都需要同步,存在一定的开销问题。

懒汉模式相较于饿汉模式,不会存在不使用的问题。虽然不再在加载时消耗资源,但是实例化时同样会有一定的时间开销。

(3) 双重检查模式/DCL

public class Singleton {
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null) {
synchronized (Singleton.class){
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

使用volatile关键词是对正确性的一种保障。

相较于懒汉模式而言,这又是一种升级。因为不在将synchronized套在了函数上,也就不会每次调用都对整个函数同步了,提高了资源的利用率。但是同样存在失效的情况。

存在失效的原因(对使用volatile的解释)

因为JVM的加载顺序是一个无序状态,他可能进行过指令优化的重排操作,那这种情况就是我们不可控制的,而volatile起着不被忽略的作用,保证了我们的instance不被指令重排。这也就是他的优化方法。

(4) 静态内部类单例模式

public class Singleton {
private Singleton(){}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private volatile static Singleton instance = new Singleton();
}
}

这是最常用的方法,也是对DCL的一种升级。

优缺点和前面都差不多,就不再复述了。

模式分析

使用这个模式时说明系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。

缺点:

  • 单例类的扩展有很大的困难

  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。

  • 如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失

工厂模式

工厂模式分为三种:

(1)简单工厂模式

(2)工厂方法模式

(3)抽象工厂模式

所以接下来将从这三种模式中进行分析和比较。

简单工厂模式

写法

/**
* 工厂类
*/
public class Factory {
public static Product createProduct(String type){
Product product = null;
switch (type){
case "鸡翅":
product = new ChickenWing();
break;
case "汉堡":
product = new Hamburger();
break;
}
return product;
}
}
/**
* 抽象产品类
*/
public abstract class Product {
public abstract void use();
}
/**
* 具体产品类
*/
public class Hamburger extends Product {
@Override
public void use() {
System.out.println("汉堡制作完成");
}
}
public class ChickenWing extends Product {
@Override
public void use() {
System.out.println("鸡翅制作完成");
}
}

模式分析

将类的创建细节与使用者隔离,使用者只需要知道对应的参数,将其送入工厂中即可完成创建。就比如我想吃汉堡了,那我就告诉工厂,汉堡这个关键参数,那么工厂就会将汉堡这个好吃的家伙送给我。但是这个工厂要注意,他可以制造鞋子、袜子、零食。。。你所能想到的想要找他做的他都得学会。

缺点:

  • 工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。

  • 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。

工厂方法模式

写法

/**
* 抽象工厂类
*/
public abstract class Factory {
public abstract <T extends Product> T createProduct(Class<T> clazz);
}
/**
* 具体工厂类
*/
public class KFC extends Factory {
@Override
public <T extends Product> T createProduct(Class<T> clazz) {
Product product = null;
try{
product = (Product) Class.forName(clazz.getName()).newInstance();
}catch (Exception e){
e.printStackTrace();
}
return (T) product;
}
}

模式分析

与简单工厂模式不一样的地方在于我们创建了专门的工厂,也就是说比如我们今天想吃汉堡,但是同时有肯德基、麦当劳、汉堡王等好几家公司可以生产,就轮到了我们选择谁来进行制作的问题了。但是这样的模式依旧存在一个问题,那就是我们需要专门跑到肯德基、麦当劳又或者是汉堡王的门店去,我们才能点餐。

缺点:

在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。

抽象工厂模式

写法

public class FactoryProducer {
public static AbstractFactory getFactory(String factory){
if(factory.equalsIgnoreCase("KFC")){
return new KFC();
}
return null;
}
}

模式分析

再次与上述的工厂方法模式进行比较,这次我们不需要再到肯德基门店去就可以买汉堡了,为什么呢?因为我们现在手上有饿了么,有美团了。我在搜索栏中输入了肯德基,他就告诉我了有这样的一个工厂,这样我们就能远程遥控获得这样的一个我们想要的油炸食品了。

缺点:

在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改

观察者模式

写法

/**
* 抽象观察者类
*/
public interface IObserver {
void update(String message);
}
/**
* 抽象主题类
*/
public interface ISubject {
void add(IObserver observer);
void remove(IObserver observer);
void notify(String message);
}
/**
* 具体观察者类
*/
public class Observer implements IObserver {
@Override
public void update(String message) {
System.out.println(message);
}
}
/**
* 具体主题类
*/
public class Subject implements ISubject {
List<IObserver> list = new ArrayList<>();
@Override
public void add(IObserver observer) {
list.add(observer);
}
@Override
public void remove(IObserver observer) {
list.remove(observer);
}
@Override
public void notify(String message) {
for(IObserver observer: list){
observer.update(message);
}
}
}

模式分析

抛去模式中的接口类,就剩下了主题和观察者,这个模式的发生就是基于主题的变更与对观察者的通知。

这个模式在我的 helper 工具包中也有使用,就是基于对系统服务的监听,发现变化后,对订阅此变化的观察者们发出通知,并由观察者自己作出相应的动作。

缺点:

  1. 一个主题存在多个观察者,而通知的方式是通过轮询,这样的通知会有一定的时间消耗。

  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。

代理模式

写法

静态代理

/**
* 抽象主题类
* 中心主题,买东西。
*/
public interface IShop {
void buy();
}
/**
* 真实主题类
* 也就是我们购买者
*/
public class Person implements IShop {
@Override
public void buy() {
System.out.println("购买");
}
}
/**
* 代理类
* 持有被代理者
*/
public class StaticPurchase implements IShop {
private IShop shop;
WhoBuy(IShop shop){
this.shop = shop;
}
@Override
public void buy() {
shop.buy();
}
}

动态代理

/**
* 动态代理类
*/
public class DynamicPurchase implements InvocationHandler {
private Object object;
DynamicPurchase(Object object){
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(object, args);
if(method.getName().equals("buy")){
System.out.println("买上");
}
return result;
}
}
/**
* 客户端类
*/
public class User {
public static void main(String[] args) {
Shop person = new Person();
DynamicPurchase dynamicPurchase = new DynamicPurchase(person);
ClassLoader loader = person.getClass().getClassLoader();
Shop purchase = (Shop) Proxy.newProxyInstance(loader, new Class[]{Shop.class}, dynamicPurchase);
purchase.buy();
}
}

模式分析

你可以看到上述文中和之前出现了不一样的地方,就是分为了两份代码,也就是动态代理和静态代理。那么就出现了这样的一个问题,何为静,何为动?

我们拿身边的代购举例,分为两种:

  1. 静就相当于我们需要找到专门的代理商,就是你微信里加的好友,你专门找到了他,然后跟他说想买Dior745,这样等他出国时就会帮你去买了。

  1. 动就是你只需要知道自己想要买Dior745,但是你这个时候手头没有静中的代理商,那你就找了淘宝,你只用知道自己想要的,不用再去思考代理商的问题了。也就是我们上文中所使用到的Proxy的代理技术。

缺点:

1.由于在客户端和真实主题之间增加了代理对象,因此 有些类型的代理模式可能会造成请求的处理速度变慢。

  1. 实现代理模式需要额外的工作,有些代理模式的实现 非常复杂。

适配器模式

类适配器:

对象适配器:

写法

类适配器

public interface MP4{
void play();
}
public class MP4Player implements MP4{
public void play(){
// doSomething
}
}
public interface Player{
void action();
}
public class Adapter extends MP4Player implements Player{
public void action(){
play();
}
}

对象适配器

public class PlayerAdapter implements Player{
public MP4 mp4;
public PlayerAdapter (MP4 mp4){
this.mp4 = mp4;
}
public void action(){
if(mp4!= null){
mp4.play();
}
}
}

模式分析

适配器模式简单来说就是为两个互不兼容的两者提供了合作的桥梁。

想一想我们在Android中使用的RecyclerView中为了进行数据的适配是不是都会加上一个Adapter,因为我们从网络获取的数据Bean是无法直接和XML文件中的每个View需要填充的数据项进行对应的。而适配器就是将两者进行了沟通协作。

类适配器模式和对象适配器模式的区别是什么?

类适配器使用了继承的方式来完成、对象适配器使用了依赖的关系来完成任务。拿代码来说的话就是类适配器继承了MP4Player,而对象适配器依赖就是MP4这个接口类的使用。

缺点

  • 类适配器模式

对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。

  • 对象适配器模式

与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

策略模式

写法

/**
* 上下文
* 根据传入策略给出解决方法
*/
public class Context {
private Strategy strategy;
Context(Strategy strategy){
this.strategy = strategy;
}
void solve(){
strategy.solve();
}
}
/**
* 两种策略方法
* 1. 普通用户
* 2. Vip用户
*/
public class CustomStrategy implements Strategy {
@Override
public void solve() {
System.out.println("普通用户");
}
}
public class VipCustomStrategy implements Strategy {
@Override
public void solve() {
System.out.println("Vip用户");
}
}
/**
* 抽象策略角色
*/
public interface Strategy {
void solve();
}

模式分析

在掘金平台上看到最多的模式,没有之一。那我们是如何通过策略模式来干掉我们日常开发中的if-else的呢?

就拿上面的代码来说好了,如果使用if-else来完成任务。

if (level == "普通用户") {
// ...
}else if (level == "Vip用户") {
// ...
}

显然的很麻烦,而且会随着逻辑的复杂化而繁琐起来,就比如我今天Vip用户要分等级了,白银应该9折,黄金8折。。。。还ok,在Vip用户里继续加入if-else,第二天产品经理说要白银有一、二、三段了。你是不是想砍死你的产品经理了??

而策略模式去完成的时候只是多了一个或者多个类,虽然类增多了,但是从至少让我们的代码从屎山变成了一堆堆屎。

缺点:

  1. 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。

  2. 策略模式将造成产生很多策略类。

参考文献

发布于: 15 小时前 阅读数: 11
用户头像

ClericYi

关注

公众号:DevGW 2019.05.14 加入

还未添加个人简介

评论 (1 条评论)

发布
没有更多了
设计模式的十八般武艺