设计模式:如何面向对象分析设计编程

一、面向对象分析

如果针对框架,类库,组件等非业务系统的开发,就有一个比较大的难点:需求一般比较抽象,模糊,需要你自己去挖掘,做合理取舍,权衡,假设,把抽象的问题具体化,最终产生清晰的,可落地的需求定义。

需求定义是否清晰合理,直接影响后续的设计,编码实现是否顺畅。

需求分析的过程实际上是一个不断迭代优化的过程,不要试图一下就能给出完美的解决方案,而是先给出一个粗糙,基础的方案,有一个迭代的基础,然后再慢慢优化,这样一个思考过程能让我们摆脱无法下手的窘境。

二、面向对象设计

1、划分职责而识别出有哪些类

根据需求描述,我们把其中涉及的功能点,以一个个罗列出来,然后再去看哪些功能点职责相近,操作同样的属性,可否归为同一个类。

2、定义类及其属性和方法

我们识别出需求描述中的动词,作为候选的方法,再进一步过滤筛选,把功能点中涉及的名词,作为候选属性,然后同样再进行过滤筛选。

3、定义类与类之间的交互关系

UML统一建模语言中定义了六种类之间的关系。它们分别是:泛化,实现,关联,聚合,组合,依赖。

4、将类组装起来并提供执行入口

我们将所有的类组装在一起,提供一个执行入口。这个入口可能是main()函数,也可能是一个组给外部用的API接口。通过这个入口,我们能触发整个代码跑起来。

三、面向对象开发案例:接口鉴权功能

1、面向对象分析

需求分析是不断迭代优化的过程。

第一轮基础分析

通过用户名和密码做接口权限认证:

(1)给允许访问服务的调用方,派发一个AppId和密码

(2)调用方每次接口请求时候,携带这个AppId和密码

(3)服务方接收到请求后,对比AppId和密码

第二轮分析优化

AppId和密码用明文传输,被截获不安全,可以借助加密算法(比如SHA)进行加密。

不过,未认证系统截获加密密码,还可以利用加密密码伪装成已认证系统访问接口。

我们可以利用OAuth的验证思路来解决:

第三轮分析优化

这种token方式可以解决截获方修改id参数获取更多接口数据的问题。但是,截获方还是可以不改动id参数,重复不断调用接口。

我们可以引入随机变量时间戳,只能在一定时间窗口内(比如一分钟)才能请求成功,避免截获方重复攻击的风险。

2、面向对象设计

(1)划分职责而识别出有哪些类

根据需求描述,罗列功能点:

【1】把 URL、AppID、密码、时间戳拼接为一个字符串;

【2】对字符串通过加密算法加密生成 token;

【3】将 token、AppID、时间戳拼接到 URL 中,形成新的URL;

【4】解析 URL,得到 token、AppID、时间戳等信息;

【5】从存储中取出 AppID 和对应的密码;

【6】根据时间戳判断 token 是否过期失效;

【7】验证两个 token 是否匹配;

职责相近,操作同样属性的归为同一个类:

【1】【2】【6】【7】负责token的生成和验证,归为AuthToken类;
【3】【4】负责URL生成和解析,归为URL类;
【5】负责从存储读取AppID和密码,归为CredentialStorage类;
(2)定义类及其属性和方法

AuthToken类:

从业务模型角度看,URL,AppID,密码,时间戳并不属于AuthToken类的属性,就没有放在这个类里,而是在外部拼接成token参数传进构造函数里。

还从业务模型角度看,理应还具有哪些属性,增加了createTime,expiredTimeInterval属性。

类的属性和方法设计要从业务模型角度思考,保证类定义的完整性,甚至需要为未来需求做些准备。

ApiRequest类:
接口请求并不一定以URL形式来表达,还可能是Dubbo,RPC等其他形式,为了让类更加通用,所以类命名为ApiRequest。

CredentialStorage接口:
为了做到抽象封装具体的存储方式,将CredentialStorage设计成接口,基于接口而非具体的实现编程。

(3)定义类与类之间的交互关系

UML统一建模语言定义了六种类之间的关系:泛化,实现,关联,聚合,组合,依赖。

泛化:继承关系,比如B子类继承A父类;

public class A { ... }
public class B extends A { ... }

实现:接口和实现类关系,比如B实现类实现A接口类;

public interface A {...}
public class B implements A { ... }

聚合:包含关系,比如A类对象包含B类对象,A类对象销毁不影响B类对象,就像课程与学生之间的关系;

public class A {
  private B b;
  public A(B b) {
    this.b = b;
  }
}

组合:包含关系,比如A类对象包含B类对象,B类对象不能单独存在,就像鸟与翅膀的关系;

public class A {
  private B b;
  public A() {
    this.b = new B();
  }
}

关联:弱包含关系,包括聚合和组合,比如B类对象是A类对象的成员变量;

public class A {
  private B b;
  public A(B b) {
    this.b = b;
  }
}
或者
public class A {
  private B b;
  public A() {
    this.b = new B();
  }
}

依赖:比关联关系更弱的关系,包含关联关系,只要B类对象和A类对象有任何使用关系就行,比如A类的方法使用B类对象作为参数或者返回值,局部变量;

public class A {
  private B b;
  public A(B b) {
    this.b = b;
  }
}
或者
public class A {
  private B b;
  public A() {
    this.b = new B();
  }
}
或者
public class A {
  public void func(B b) { ... }
}

(4)将类组装起来并提供执行入口

设计一个最顶层的ApiAuthenticator接口类,暴露一组给外部调用者使用的API接口,作为触发执行鉴权逻辑的入口。

三、面向对象编程

面向对象编程的工作,就是将这些设计思路翻译成代码实现。

ApiAuthenticator的实现:

public interface ApiAuthenticator {
  void auth(String url);
  void auth(ApiRequest apiRequest);
}
public class DefaultApiAuthenticatorImpl implements ApiAuthenticator {
  private CredentialStorage credentialStorage;
  
  public DefaultApiAuthenticatorImpl() {
    this.credentialStorage = new MysqlCredentialStorage();
  }
  
  public DefaultApiAuthenticatorImpl(CredentialStorage credentialStorage) {
    this.credentialStorage = credentialStorage;
  }
  @Override
  public void auth(String url) {
    ApiRequest apiRequest = ApiRequest.buildFromUrl(url);
    auth(apiRequest);
  }
  @Override
  public void auth(ApiRequest apiRequest) {
    String appId = apiRequest.getAppId();
    String token = apiRequest.getToken();
    long timestamp = apiRequest.getTimestamp();
    String originalUrl = apiRequest.getOriginalUrl();
    AuthToken clientAuthToken = new AuthToken(token, timestamp);
    if (clientAuthToken.isExpired()) {
      throw new RuntimeException("Token is expired.");
    }
    String password = credentialStorage.getPasswordByAppId(appId);
    AuthToken serverAuthToken = AuthToken.generate(originalUrl, appId, password, timestamp);
    if (!serverAuthToken.match(clientAuthToken)) {
      throw new RuntimeException("Token verfication failed.");
    }
  }
}

你可能感兴趣的:(设计模式,设计模式,uml)