谈谈Java接口与实现的分离以及隐藏实现

一. what ?
对于一个框架来说, 用户只需要知道这个框架的关键组件和接口就行了, 不要对外公布太多的细节. 因为用户看到的东西太多反而导致了迷惑. 对于用户来说, 只要调用一个方法就帮我完成我想要的那些复杂功能, 这样最好不过了. 接口和实现分开或者说只对外公布用户要使用的接口, 而其实现则对用户隐藏起来. 这是一个框架应该做的事情, 也是Java的一个重要特性 ------ 封装. 简单的来说接口和实现的分离就是把接口已实现分开, 尽量减少两者之间的依赖, 以方便移植和修改. 那么隐藏实现又怎么说呢? 前面已经说了, 一个框架要做到的是尽量不要公布实现, 只公布接口. 因此就需要对实现进行封装并隐藏. 这样说有些抽象, 你可能有些不知所云. 下面我将说说为什要进行接口和实现的分离对实现方式进行隐藏 以及怎么实现它们.

二. why ?
《在Android上使用SPI机制》一文中已经说过关于接口和实现的分离和动态更换实现的问题, 但是接口的实现并未对外隐藏, 用户可以直接调用接口的实现, 而不使用接口. 这样编写的代码并不是面向接口编程, 而是硬编码. 如果要更换实现, 这将非常麻烦. 为了不让用户看到实现, 只需要将实现类变成包私有的类用反射初始化. 分离接口和实现以及隐藏实现细节好处很多, 下面列举几个:

  1. 面向接口编程, 方便更换实现.
  2. 隐藏实现细节, 减少对外接口和类.
  3. 减少接口和实现直接的相互依赖.
  4. 封装, 高内聚.
  5. ......

三. how ?
下面看看如何实现:

  • (1) 定义接口
public interface ImageLoader {

    /**
     * 初始化ImageLoader
     * @param appContext ApplicatonContext
     */
    void init(@NonNull Context appContext);

    /**
     * 展示图片
     * @param targetView
     * @param uri
     * @param listener
     */
    void displayImage(@NonNull ImageView targetView, @NonNull Uri uri, @Nullable LoadListener listener);

    /**
     * 取消图片展示
     * @param targetView
     */
    void cancelDisplay(ImageView targetView);

    /**
     * 销毁ImageLoader, 回收资源
     */
    void destroy();

}
  • (2) 实现接口 (实现类要定义成包私有的, 即没有修饰符)
    FrescoImageLoader.java文件:
class FrescoImageLoader implements ImageLoader {

    private Context mAppContext;

    @Override
    public void init(@NonNull Context appContext) {

        if(Fresco.hasBeenInitialized()) return;

        // hold appContext
        mAppContext = appContext;

        // init fresco
        OkHttpClient client = new OkHttpClient.Builder()
                .addNetworkInterceptor(chain -> {
                    DevUtil.d("ImageLoader", "request-url: " + chain.request().url().toString());
                    return chain.proceed(chain.request());
                })
                .build();
        ImagePipelineConfig config = OkHttpImagePipelineConfigFactory
                .newBuilder(appContext, client)
                .build();
        Fresco.initialize(appContext, config);
    }

    @Override
    public void displayImage(@NonNull ImageView targetView, @NonNull Uri uri, @Nullable LoadListener listener) {
        // Fresco
        if (targetView instanceof DraweeView) {
            DraweeView realView = (DraweeView) targetView;
            realView.setController(getDraweeController(realView, uri, listener));
            return;
        }

        // Generic ImageView
        targetView.setImageURI(uri);
    }

    private DraweeController getDraweeController(DraweeView targetView, Uri uri, LoadListener listener) {
        DraweeController controller = Fresco.newDraweeControllerBuilder()
                .setOldController(targetView.getController())
                .setUri(uri)
                .setControllerListener(listener == null ? null : new BaseControllerListener<ImageInfo>() {
                    @Override
                    public void onFailure(String id, Throwable throwable) {
                        super.onFailure(id, throwable);
                        if (listener != null) {
                            listener.onFailed(targetView);
                        }
                    }

                    @Override
                    public void onFinalImageSet(String id, ImageInfo imageInfo, Animatable animatable) {
                        super.onFinalImageSet(id, imageInfo, animatable);
                        if (imageInfo instanceof CloseableBitmap) {
                            CloseableBitmap image = (CloseableBitmap) imageInfo;
                            Bitmap resultBitmap = image.getUnderlyingBitmap();
                            if (listener != null) {
                                listener.onSuccess(targetView, resultBitmap);
                            }
                        }
                    }
                })
                .build();
        return controller;
    }

    @Override
    public void cancelDisplay(ImageView targetView) {

    }

    @Override
    public void destroy() {
        Fresco.shutDown();
    }
}

UILImageLoader.java文件

class UILImageLoader implements ImageLoader {

    private com.nostra13.universalimageloader.core.ImageLoader mImpl;

    @Override
    public void init(@NonNull Context appContext) {
        mImpl = com.nostra13.universalimageloader.core.ImageLoader.getInstance();
    }

    @Override
    public void displayImage(@NonNull ImageView targetView, @NonNull Uri uri, @Nullable LoadListener listener) {
        com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(uri.toString(), targetView);
    }

    @Override
    public void cancelDisplay(ImageView targetView) {
        mImpl.cancelDisplayTask(targetView);
    }

    @Override
    public void destroy() {
        mImpl.destroy();
    }

}

其余省略................

  • (3) 为实现类定义初始化工厂类
class ImageLoaderFactory {

    private static ImageLoader mImageLoader;

    private ImageLoaderFactory() {
        //no instance
    }

    /**
     * @return
     */
    public static ImageLoader createImageLoader(String implClass) {
        if (mImageLoader == null) {
            mImageLoader = createImageLoaderWithClassName(implClass);
        }
        return mImageLoader;
    }

    /**
     * 此处使用类反射, 所有implClass都不能混淆, 类名必须keep:  {@code -keep class a.b.c.ImplClass}
     * @param implClass 实现类有: FrescoImageLoader, GlideImageLoader, PicassoImageLoader, UILImageLoader
     * @return
     */
    private static ImageLoader createImageLoaderWithClassName(String implClass) {
        try {
            Class klass = Class.forName(implClass);
            Constructor constructor = klass.getDeclaredConstructor();
            if (constructor == null) {
                throw new RuntimeException(implClass + " 的实现类必须有一个无参构造方法 !");
            }
            boolean isAccessible = constructor.isAccessible();
            constructor.setAccessible(true);
            Object obj = constructor.newInstance();
            constructor.setAccessible(isAccessible);
            if ( !(obj instanceof ImageLoader) ) {
                throw new RuntimeException(implClass + "必须实现" + ImageLoader.class.getName() + "接口");
            }

            ImageLoader imageLoader = (ImageLoader) obj;
            return imageLoader;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
}
  • (4) 要使用ImageLoader, 直接用工厂类创建即可, 我们只需要知道实现类的名称, 其它细节都不需要知道. 实现类都是高度内聚, 外部完全不知道其内部的逻辑. 外部只需要调用接口的方法实现业务逻辑即可. 下面是ImageLoader及其相关实现的整体结构:
接口与实现分离, 隐藏实现.png
  • (5) 下面是使用工厂类创建ImageLoader实现的代码
import android.content.Context;

public final class ImageManager {

    private static String TAG = "ImageManager";
    private static ImageLoader sImageLoader;

    private ImageManager() {
        //no instance
    }

    public static void init(Context appContext) {
        if (sImageLoader != null) {
            throw new IllegalStateException(TAG + " already initalized");
        }

        sImageLoader = ImageLoaderFactory.createImageLoader("com.stone.app.manager.imageloader.internal.FrescoImageLoader");
        sImageLoader.init(appContext);
    }

    public static ImageLoader getImageLoader() {
        return sImageLoader;
    }
}

总结:

  1. 将接口和实现类分离, 接口和实现分别放在单独的包中, 并且实现类定义为包私有的 (即类没有修饰符).
  2. 定义工厂类, 使用反射初始化实现类.
  3. 注意混淆的时候不能混淆实现类的类名, 因为其初始化使用了反射.

有一个设计模式也是分离接口和实现并使其各自独立变化, 那就是桥接模式. 具体可以参考Android中的Window和Context的设计. 关于动态扩展分离接口和实现, 可以参考《在Android上使用SPI机制》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,649评论 18 139
  • 本文包括:1、Listener简介2、Servlet监听器3、监听三个域对象创建和销毁的事件监听器4、监听三个域对...
    廖少少阅读 6,059评论 6 28
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,035评论 25 707
  • 同样是使用Java语言,为什么做MobileAPI的开发人员写不了Android程序,反之亦然。我想大概是各行有各...
    lookid阅读 809评论 1 2
  • 2017年9月16日8点半,当很多人还在享受慵懒周末时,一群热爱阅读的新教育萤火虫青岛分站的小小阅读领跑者...
    yayama2001阅读 247评论 0 0