设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

想要写出跟知名开源库一样的代码,首先你需要知道……

2016-12-13 23:33| 发布者: joejoe0332| 查看: 1290| 评论: 0|原作者: oschina|来自: oschina

摘要: 在工作初期,我们可能会有这样的感觉,自己的代码接口设计混乱、代码耦合较为严重、一个类的代码过多等,当自己回过头再看这些代码时可能都会感慨怎么写成那样。再看那些知名的开源库,他们大多有着整齐的代码、清晰 ...

在工作初期,我们可能会有这样的感觉,自己的代码接口设计混乱、代码耦合较为严重、一个类的代码过多等,当自己回过头再看这些代码时可能都会感慨怎么写成那样。再看那些知名的开源库,他们大多有着整齐的代码、清晰简单的接口、职责单一的类,这个时候我们通常会捶胸顿足而感叹:什么时候老夫能写出这样的代码!

其实在做开发这些年中,我渐渐的感觉到,其实国内一些初、中级工程师写的东西不规范或者说不够清晰的原因是缺乏一些指导原则。他们手中挥舞着面向对象的大旗,写出来的东西却充斥着面向过程的气味。也许是他们不知道有这些原则,也许是他们知道但是不能很好运用到实际代码中,亦或是他们没有在实战中体会到这些原则能够带来的优点,以至于他们对这些原则并没有足够的重视。

面向对象六大原则

在此之前,有一点需要大家知道,熟悉这些原则并不是说你写出的程序就一定灵活、清晰,只是为你的优秀代码之路铺上了一层栅栏,在这些原则的指导下你才能避免陷入一些常见的代码泥沼,从而让你专心写出优秀的东西。
下面我们就以Android网络框架SimpleNet为例来学习这六大面向对象的基本原则,体会这些原则在开发过程中带来的强大能量。

1 单一职责原则

单一职责原则的英文名称是Single Responsibility Principle,简称是SRP,简单地说就是一个类只做一件事。这个设计原则备受争议却又极其重要。只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的。因为单一职责的划分界限并不是如马路上的行车道那么清晰,很多时候都是需要靠个人经验来界定。当然,最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。
试想一下,如果你遵守了这个原则,那么你的类就会划分得很细,每个类都有比较单一的职责,这不就是高内聚、低耦合么!当然,如何界定类的职责这需要你的个人经验了。
在SimpleNet中,我觉得很能够体现SRP原则的就是HttpStack这个类族了。HttpStack定义了一个执行网络请求的接口,代码如下:

public interface HttpStack {
  /**
   * 执行Http请求,并且返回一个Response
   */ 
  public Response performRequest(Request<?> request);
}


从上述程序中可以看到,HttpStack只有一个performRequest函数,它的职责就是执行网络请求并且返回一个Response。它的职责很单一,这样在需要修改执行网络请求的相关代码时,只需要修改实现HttpStack接口的类,而不会影响其他的类的代码。如果某个类的职责包含有执行网络请求、解析网络请求、进行gzip压缩、封装请求参数等,那么在你修改某处代码时就必须谨慎,以免修改的代码影响了其他的功能。当你修改的代码能够基本上不影响其他的功能。这就在一定程度上保证了代码的可维护性。注意,单一职责原则并不是说一个类只有一个函数,而是说这个类中的函数所做的工作是高度相关的,也就是高内聚。HttpStack抽象了执行网络请求的具体过程,接口简单清晰,也便于扩展。

优点


(1)类的复杂性降低,实现什么职责都有清晰明确的定义。
(2)可读性提高,复杂性降低,那当然可读性提高了。
(3)可维护性提高,可读性提高,那当然更容易维护了。
(4)变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

 

2 里氏替换原则

面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是所有引用基类、接口的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
还是以HttpStack为例,SimpleNet定义了HttpStack来表示执行网络请求这个抽象概念。在执行网络请求时,只需要定义一个HttpStack对象,然后调用performRequest即可,至于HttpStack的具体实现由更高层的调用者指定。这部分代码在RequestQueue类中,示例如下:

 /**
   * @paramcoreNums线程核心数
   * @paramhttpStack http执行器
   */
  protected RequestQueue(intcoreNums, HttpStackhttpStack) {
    mDispatcherNums = coreNums;
    mHttpStack = httpStack != null ? httpStack : HttpStackFactory.createHttpStack();
  }

 

HttpStackFactory类的createHttpStack函数负责根据API版本创建不同的HttpStack,实现代码如下:

// 根据API版本选择HttpClient或者HttpURLConnection
public final class HttpStackFactory {
  // API 9
  private static final int GINGERBREAD_SDK_NUM = 9;

  /**
   * 根据SDK版本号来创建不同的Http执行器,即SDK 9之前使用HttpClient,之后则使用HttlUrlConnection
   * @return
   */
  public static HttpStackcreateHttpStack() {
     intruntimeSDKApi = Build.VERSION.SDK_INT;
     if (runtimeSDKApi>= GINGERBREAD_SDK_NUM) {
      return new HttpUrlConnStack();
    }
    return new HttpClientStack();
  }
}


上述代码中,RequestQueue类中依赖的是HttpStack接口,而通过HttpStackFactory的createHttpStack函数返回的是HttpStack的实现类HttpClientStack或HttlUrlConnStack。这就是所谓的里氏替换原则,任何父类、父接口出现的地方子类都可以出现,这不就保证了可扩展性吗!
任何实现HttpStack接口的类的对象都可以传递给RequestQueue实现网络请求的功能,这样SimpleNet执行网络请求的方法就有很多种可能性,而不是只有HttpClient和HttpURLConnection。例如,用户想使用OkHttp作为SimpleNet的执行引擎,那么创建一个实现了HttpStack接口的OkHttpStack类,然后在该类的performRequest函数中执行网络请求,最终将OkHttpStack对象注入RequestQueue即可。
细想一下,很多应用框架不就是这样实现吗?框架定义一系列相关的逻辑骨架与抽象,使得用户可以将自己的实现注入到框架中,从而实现变化万千的功能。

优点


(1)代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性。
(2)提高代码的重用性。
(3)提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,很多开源框架的扩展接口都是通过继承父类来完成的。
(4)提高产品或项目的开放性。

 

 

缺点


(1)继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
(2)降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束。
(3)增强了耦合性。当父类的常量、变量和方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大片的代码需要重构。

 

 

3 依赖倒置原则

依赖倒置原则这个名字看着有点不好理解,“依赖”还要“倒置”,这到底是什么意思?依赖倒置原则的几个关键点如下:
(1)高层模块不应该依赖低层模块,两者都应该依赖其抽象。
(2)抽象不应该依赖细节。
(3)细节应该依赖抽象。
在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的。细节就是实现类、实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。软件先驱们总是喜欢将一些理论定义得很抽象,弄得不是那么容易理解,其实就是一句话:面向接口编程,或者说是面向抽象编程,这里的抽象指的是接口或者抽象类。面向接口编程是面向对象精髓之一。
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

优点


(1)可扩展性好。
(2)耦合度低。

 

 

4 开闭原则

开闭原则是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,
可能会给旧代码引入错误。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。
在软件开发过程中,永远不变的就是变化。开闭原则是使我们的软件系统拥抱变化的核心原则之一。对扩展开放,对修改关闭这样的高层次的概括,即在需要对软件进行升级、变化时应该通过扩展的形式来实现,而非修改原有代码。当然这只是一种比较理想的状态,是通过扩展还是通过修改旧代码需要根据代码自身来定。
在SimpleNet中,开闭原则体现得比较好的是Request类族的设计。我们知道,在开发C/S应用时,服务器返回的数据格式多种多样,有字符串类型、xml、Json等。而解析服务器返回的Response的原始数据类型则是通过Request类来实现的,这样就使得Request类对于服务器返回的数据格式有良好的扩展性,即Request的可变性太大。
例如,返回的数据格式是Json,那么,使用JsonRequest请求来获取数据,它会将结果转成JsonObject对象,我们看看JsonRequest的核心实现:

// 返回的数据类型为Json的请求, Json对应的对象类型为JSONObject
public class JsonRequest extends Request<JSONObject> {

  public JsonRequest(HttpMethod method, String url,
     RequestListener<JSONObject> listener) {
     super(method, url, listener);
}

// 将Response的结果转换为JSONObject
@Override
public JSONObjectparseResponse(Response response) {
    String jsonString = new String(response.getRawData());
    try {
      return new JSONObject(jsonString);
    } catch (JSONException e) {
    e.printStackTrace();
    }
    return null;
  }
}


JsonRequest通过实现Request抽象类的parseResponse解析服务器返回的结果,这里将结果转换为JSONObject,并且封装到Response类中。
例如,SimpleNet添加对图片请求的支持,即要实现类似ImageLoader的功能。这个时候我的请求返回的数据是Bitmap图片。因此,我需要在该类型的Request中得到的结果是Request,但支持一种新的数据格式不能通过修改源码的形式,这样可能会为旧代码引入错误,但是,你又必须实现功能扩展。这就是开闭原则的定义:对扩展开放,对修改关闭。我们看看SimpleNet是如何做的:

public class ImageRequest extends Request<Bitmap> {

  public ImageRequest(HttpMethod method, String url,
    RequestListener<Bitmap> listener) {
    super(method, url, listener);
  }

  // 将数据解析为Bitmap
  @Override
  public Bitmap parseResponse(Response response) {
      return BitmapFactory.decodeByteArray(response.rawData,
       0, response.rawData.length);
  }

}


ImageRequest类的parseResponse函数中将Response中的原始数据转换为Bitmap即可。当我们需要添加其他数据格式时,只需要继承自Request类,并且在parseResponse方法中将数据转换为具体的形式即可。这样通过扩展的形式来应对软件的变化或者说用户需求的多样性,既避免了破坏原有系统,又保证了软件系统的可扩展性。依赖于抽象,而不依赖于具体,使得对扩展开放,对修改关闭。开闭原则与依赖倒置原则、里氏替换原则一样,实际上最终都遵循一句话:面向接口编程。

优点


(1)增加稳定性。
(2)可扩展性高。

 

 

5 接口隔离原则

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
可能描述起来不是很好理解,我们还是以示例来加强理解吧。
我们知道,在SimpleNet的网络队列中是会对请求进行排序的。SimpleNet内部使用PriorityBlockingQueue来维护网络请求队列,PriorityBlockingQueue需要调用Request类的compareTo函数来进行排序。试想一下,PriorityBlockingQueue其实只需要调用Request类的排序方法就可以了,其他的接口它根本不需要,即PriorityBlockingQueue只需要compareTo这个接口,而这个compareTo方法就是我们上述所说的最小接口。当然,compareTo这个方法并不是SimpleNet本身定义的接口方法,而是Java中的Comparable接口,但我们这里只是为了学习,至于哪里定义的无关紧要:

public abstract class Request<T> implements Comparable<Request<T>> {

  /**
   * 排序方法,PriorityBlockingQueue只需要调用元素的compareTo即可进行排序
   */
  @Override
  public intcompareTo(Request<T> another) {
    Priority myPriority = this.getPriority();
    Priority anotherPriority = another.getPriority();
    // 如果优先级相等,那么按照添加到队列的序列号顺序来执行
    return myPriority.equals(anotherPriority) ?this.getSerialNumber()
        - another.getSerialNumber()
        : myPriority.ordinal() - anotherPriority.ordinal();
} 
// 代码省略
}

 

PriorityBlockingQueue类相关代码 :
 

public class PriorityBlockingQueue<E> extends AbstractQueue<E>
  implements BlockingQueue<E>, java.io.Serializable {

  // 代码省略

  // 添加元素时进行排序
  public boolean offer(E e) {
    if (e == null)
      throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
        try {
           Comparator<? super E>cmp = comparator;
          // 没有设置Comparator,则使用元素本身的compareTo方法进行排序
          if (cmp == null)
             siftUpComparable(n, e, array);
          else
             siftUpUsingComparator(n, e, array, cmp);
          size = n + 1;
          notEmpty.signal();
    } finally {
      lock.unlock();
    }
    return true;
  }

  private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
    int parent = (k - 1) >>> 1;
      Object e = array[parent];
      // 调用元素的compareTo方法进行排序
      if (key.compareTo((T) e) >= 0)
        break;
      array[k] = e;
      k = parent;
    }
    array[k] = key;
  }
 }


从PriorityBlockingQueue的代码可知,在元素排序时,PriorityBlockingQueue只需要知道元素是个Comparable对象即可,不需要知道这个对象是不是Request类以及这个类的其他接口。它只需要排序,因此,只要知道它是实现了Comparable接口的对象即可,Comparable就是它的最小接口,也是通过Comparable隔离了PriorityBlockingQueue类对Request类的其他方法的可见性。

优点


(1)降低耦合性。
(2)提升代码的可读性。
(3)隐藏实现细节。

 

 

6 迪米特原则

迪米特法则也称为最少知识原则(Least Knowledge Principle),虽然名字不同,但描述的是同一个原则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,这有点类似接口隔离原则中的最小接口的概念。类的内部如何实现、如何复杂都与调用者或者依赖者没有关系,调用者或者依赖者只需要知道它需要的方法即可,其他的一概不关心。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
迪米特法则还有一个英文解释是:Only talk to your immedate friends(只与直接的朋友通信)。什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。
例如,SimpleNet中的Response缓存接口的设计。

/**
 * 请求缓存接口
* @param<K> key的类型
 * @param<V> value类型
 */
public interface Cache<K, V> {
  public V get(K key);
  public void put(K key, V value);
  public void remove(K key);
}


Cache接口定义了缓存类需要实现的最小接口,依赖缓存类的对象只需要知道这些接口即可。例如,需要将Http Response缓存到内存中,并且按照LRU的规则进行存储。我们需要LruCache类实现这个功能,代码如下: 

// 将请求结果缓存到内存中
public class LruMemCache implements Cache<String, Response> {

  /**
   * Reponse LRU缓存
   */
  private LruCache<String, Response>mResponseCache;

  public LruMemCache() {
    // 计算可使用的最大内存
    final intmaxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // 取八分之一的可用内存作为缓存
    final intcacheSize = maxMemory / 8;
    mResponseCache = new LruCache<String, Response>(cacheSize) {
      @Override
      protected intsizeOf(String key, Response response) {
        return response.rawData.length / 1024;
      }
    };

  }

  @Override
  public Response get(String key) {
    return mResponseCache.get(key);
  }

  @Override
  public void put(String key, Response response) {
    mResponseCache.put(key, response);
  }

  @Override
  public void remove(String key) {
    mResponseCache.remove(key);
  }
}


在这里,SimpleNet的直接朋友就是Cache或者LruMemCahce,间接朋友就是LruCache类。SimpleNet只需要直接和Cache类交互即可,并不需要知道LruCache的对象的存在,即真正实现缓存功能的对象是LruCache。这就是迪米特原则,尽量少地知道对象的信息,只与直接的朋友交互。

优点


(1)降低复杂度。
(2)降低耦合度。
(3)增加稳定性。
面向对象六大原则在开发过程中极为重要,它们给灵活、可扩展的软件系统提供了更细粒度的指导原则。如果能够很好地将这些原则运用到项目中,再在一些合适的场景运用一些经过验证过的设计模式,那么开发出来的软件在一定程度上能够得到质量保证。其实这六大原则最终可以简化为几个关键词:抽象、单一职责、最小化。那么在实际开发过程中如何权衡、实践这些原则,也是需要大家在工作中不断地思考、摸索。

3 依赖倒置原则

依赖倒置原则这个名字看着有点不好理解,“依赖”还要“倒置”,这到底是什么意思?依赖倒置原则的几个关键点如下:
(1)高层模块不应该依赖低层模块,两者都应该依赖其抽象。
(2)抽象不应该依赖细节。
(3)细节应该依赖抽象。
在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的。细节就是实现类、实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。软件先驱们总是喜欢将一些理论定义得很抽象,弄得不是那么容易理解,其实就是一句话:面向接口编程,或者说是面向抽象编程,这里的抽象指的是接口或者抽象类。面向接口编程是面向对象精髓之一。
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

优点


(1)可扩展性好。
(2)耦合度低。

 

 

4 开闭原则

开闭原则是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,
可能会给旧代码引入错误。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。
在软件开发过程中,永远不变的就是变化。开闭原则是使我们的软件系统拥抱变化的核心原则之一。对扩展开放,对修改关闭这样的高层次的概括,即在需要对软件进行升级、变化时应该通过扩展的形式来实现,而非修改原有代码。当然这只是一种比较理想的状态,是通过扩展还是通过修改旧代码需要根据代码自身来定。
在SimpleNet中,开闭原则体现得比较好的是Request类族的设计。我们知道,在开发C/S应用时,服务器返回的数据格式多种多样,有字符串类型、xml、Json等。而解析服务器返回的Response的原始数据类型则是通过Request类来实现的,这样就使得Request类对于服务器返回的数据格式有良好的扩展性,即Request的可变性太大。
例如,返回的数据格式是Json,那么,使用JsonRequest请求来获取数据,它会将结果转成JsonObject对象,我们看看JsonRequest的核心实现:

// 返回的数据类型为Json的请求, Json对应的对象类型为JSONObject
public class JsonRequest extends Request<JSONObject> {

  public JsonRequest(HttpMethod method, String url,
     RequestListener<JSONObject> listener) {
     super(method, url, listener);
}

// 将Response的结果转换为JSONObject
@Override
public JSONObjectparseResponse(Response response) {
    String jsonString = new String(response.getRawData());
    try {
      return new JSONObject(jsonString);
    } catch (JSONException e) {
    e.printStackTrace();
    }
    return null;
  }
}


JsonRequest通过实现Request抽象类的parseResponse解析服务器返回的结果,这里将结果转换为JSONObject,并且封装到Response类中。
例如,SimpleNet添加对图片请求的支持,即要实现类似ImageLoader的功能。这个时候我的请求返回的数据是Bitmap图片。因此,我需要在该类型的Request中得到的结果是Request,但支持一种新的数据格式不能通过修改源码的形式,这样可能会为旧代码引入错误,但是,你又必须实现功能扩展。这就是开闭原则的定义:对扩展开放,对修改关闭。我们看看SimpleNet是如何做的:

public class ImageRequest extends Request<Bitmap> {

  public ImageRequest(HttpMethod method, String url,
    RequestListener<Bitmap> listener) {
    super(method, url, listener);
  }

  // 将数据解析为Bitmap
  @Override
  public Bitmap parseResponse(Response response) {
      return BitmapFactory.decodeByteArray(response.rawData,
       0, response.rawData.length);
  }

}


ImageRequest类的parseResponse函数中将Response中的原始数据转换为Bitmap即可。当我们需要添加其他数据格式时,只需要继承自Request类,并且在parseResponse方法中将数据转换为具体的形式即可。这样通过扩展的形式来应对软件的变化或者说用户需求的多样性,既避免了破坏原有系统,又保证了软件系统的可扩展性。依赖于抽象,而不依赖于具体,使得对扩展开放,对修改关闭。开闭原则与依赖倒置原则、里氏替换原则一样,实际上最终都遵循一句话:面向接口编程。

优点


(1)增加稳定性。
(2)可扩展性高。

 

 

5 接口隔离原则

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
可能描述起来不是很好理解,我们还是以示例来加强理解吧。
我们知道,在SimpleNet的网络队列中是会对请求进行排序的。SimpleNet内部使用PriorityBlockingQueue来维护网络请求队列,PriorityBlockingQueue需要调用Request类的compareTo函数来进行排序。试想一下,PriorityBlockingQueue其实只需要调用Request类的排序方法就可以了,其他的接口它根本不需要,即PriorityBlockingQueue只需要compareTo这个接口,而这个compareTo方法就是我们上述所说的最小接口。当然,compareTo这个方法并不是SimpleNet本身定义的接口方法,而是Java中的Comparable接口,但我们这里只是为了学习,至于哪里定义的无关紧要:

public abstract class Request<T> implements Comparable<Request<T>> {

  /**
   * 排序方法,PriorityBlockingQueue只需要调用元素的compareTo即可进行排序
   */
  @Override
  public intcompareTo(Request<T> another) {
    Priority myPriority = this.getPriority();
    Priority anotherPriority = another.getPriority();
    // 如果优先级相等,那么按照添加到队列的序列号顺序来执行
    return myPriority.equals(anotherPriority) ?this.getSerialNumber()
        - another.getSerialNumber()
        : myPriority.ordinal() - anotherPriority.ordinal();
} 
// 代码省略
}

 

PriorityBlockingQueue类相关代码 :
 

public class PriorityBlockingQueue<E> extends AbstractQueue<E>
  implements BlockingQueue<E>, java.io.Serializable {

  // 代码省略

  // 添加元素时进行排序
  public boolean offer(E e) {
    if (e == null)
      throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
        try {
           Comparator<? super E>cmp = comparator;
          // 没有设置Comparator,则使用元素本身的compareTo方法进行排序
          if (cmp == null)
             siftUpComparable(n, e, array);
          else
             siftUpUsingComparator(n, e, array, cmp);
          size = n + 1;
          notEmpty.signal();
    } finally {
      lock.unlock();
    }
    return true;
  }

  private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
    int parent = (k - 1) >>> 1;
      Object e = array[parent];
      // 调用元素的compareTo方法进行排序
      if (key.compareTo((T) e) >= 0)
        break;
      array[k] = e;
      k = parent;
    }
    array[k] = key;
  }
 }


从PriorityBlockingQueue的代码可知,在元素排序时,PriorityBlockingQueue只需要知道元素是个Comparable对象即可,不需要知道这个对象是不是Request类以及这个类的其他接口。它只需要排序,因此,只要知道它是实现了Comparable接口的对象即可,Comparable就是它的最小接口,也是通过Comparable隔离了PriorityBlockingQueue类对Request类的其他方法的可见性。

优点


(1)降低耦合性。
(2)提升代码的可读性。
(3)隐藏实现细节。

 

 

6 迪米特原则

迪米特法则也称为最少知识原则(Least Knowledge Principle),虽然名字不同,但描述的是同一个原则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,这有点类似接口隔离原则中的最小接口的概念。类的内部如何实现、如何复杂都与调用者或者依赖者没有关系,调用者或者依赖者只需要知道它需要的方法即可,其他的一概不关心。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
迪米特法则还有一个英文解释是:Only talk to your immedate friends(只与直接的朋友通信)。什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。
例如,SimpleNet中的Response缓存接口的设计。

/**
 * 请求缓存接口
* @param<K> key的类型
 * @param<V> value类型
 */
public interface Cache<K, V> {
  public V get(K key);
  public void put(K key, V value);
  public void remove(K key);
}


Cache接口定义了缓存类需要实现的最小接口,依赖缓存类的对象只需要知道这些接口即可。例如,需要将Http Response缓存到内存中,并且按照LRU的规则进行存储。我们需要LruCache类实现这个功能,代码如下: 

// 将请求结果缓存到内存中
public class LruMemCache implements Cache<String, Response> {

  /**
   * Reponse LRU缓存
   */
  private LruCache<String, Response>mResponseCache;

  public LruMemCache() {
    // 计算可使用的最大内存
    final intmaxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // 取八分之一的可用内存作为缓存
    final intcacheSize = maxMemory / 8;
    mResponseCache = new LruCache<String, Response>(cacheSize) {
      @Override
      protected intsizeOf(String key, Response response) {
        return response.rawData.length / 1024;
      }
    };

  }

  @Override
  public Response get(String key) {
    return mResponseCache.get(key);
  }

  @Override
  public void put(String key, Response response) {
    mResponseCache.put(key, response);
  }

  @Override
  public void remove(String key) {
    mResponseCache.remove(key);
  }
}


在这里,SimpleNet的直接朋友就是Cache或者LruMemCahce,间接朋友就是LruCache类。SimpleNet只需要直接和Cache类交互即可,并不需要知道LruCache的对象的存在,即真正实现缓存功能的对象是LruCache。这就是迪米特原则,尽量少地知道对象的信息,只与直接的朋友交互。

优点


(1)降低复杂度。
(2)降低耦合度。
(3)增加稳定性。
面向对象六大原则在开发过程中极为重要,它们给灵活、可扩展的软件系统提供了更细粒度的指导原则。如果能够很好地将这些原则运用到项目中,再在一些合适的场景运用一些经过验证过的设计模式,那么开发出来的软件在一定程度上能够得到质量保证。其实这六大原则最终可以简化为几个关键词:抽象、单一职责、最小化。那么在实际开发过程中如何权衡、实践这些原则,也是需要大家在工作中不断地思考、摸索。

设计模式

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。这个术语是由Erich Gamma等人在1990年从建筑设计领域引入到软件工程领域,从此设计模式在面向对象设计领域逐渐被重视起来。

设计模式并不直接用来完成代码的编写,而是描述在各种情况下要如何解决软件设计问题。面向对象设计模式通常以类或对象来描述其中的关系和相互作用,它们的相互作用能够使软件系统具有高内聚、低耦合的特性,并且使软件能够应对变化。


酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部