精华内容
下载资源
问答
  • 面向对象六大原则

    万次阅读 多人点赞 2015-11-30 00:10:44
    小民的主管是个工作经验丰富的技术专家,对于小民的工作并不是很满意,尤其小民最薄弱的面向对象设计,而Android开发又是使用Java语言,什么抽象、接口、六大原则、23种设计模式等名词把小民弄得晕头转向。...

    1、优化代码的第一步——单一职责原则

    单一职责原则的英文名称是Single Responsibility Principle,简称SRP。它的定义是:就一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类中应该是一组相关性很高的函数、数据的封装。就像秦小波老师在《设计模式之禅》中说的:“这是一个备受争议却又及其重要的原则。只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的”。因为单一职责的划分界限并不是总是那么清晰,很多时候都是需要靠个人经验来界定。当然,最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。
    对于计算机技术,通常只单纯地学习理论知识并不能很好地领会其深意,只有自己动手实践,并在实际运用中发现问题、解决问题、思考问题,才能够将知识吸收到自己的脑海中。下面以我的朋友小民的事迹说起。

    自从Android系统发布以来,小民就是Android的铁杆粉丝,于是在大学期间一直保持着对Android的关注,并且利用课余时间做些小项目,锻炼自己的实战能力。毕业后,小民如愿地加入了心仪的公司,并且投入到了他热爱的Android应用开发行业中。将爱好、生活、事业融为一体,小民的第一份工作也算是顺风顺水,一切尽在掌握中。
    在经历过一周的适应期以及熟悉公司的产品、开发规范之后,小民的开发工作就正式开始了。小民的主管是个工作经验丰富的技术专家,对于小民的工作并不是很满意,尤其小民最薄弱的面向对象设计,而Android开发又是使用Java语言,什么抽象、接口、六大原则、23种设计模式等名词把小民弄得晕头转向。小民自己也察觉到了自己的问题所在,于是,小民的主管决定先让小民做一个小项目来锻炼锻炼这方面的能力。正所谓养兵千日用兵一时,磨刀不误砍柴工,小民的开发之路才刚刚开始。

    在经过一番思考之后,主管挑选了使用范围广、难度也适中的ImageLoader(图片加载)作为小民的训练项目。既然要训练小民的面向对象设计,那么就必须考虑到可扩展性、灵活性,而检测这一切是否符合需求的最好途径就是开源。用户不断地提出需求、反馈问题,小民的项目需要不断升级以满足用户需求,并且要保证系统的稳定性、灵活性。在主管跟小民说了这一特殊任务之后,小民第一次感到了压力,“生活不容易呐!”年仅22岁至今未婚的小民发出了如此深刻的感叹!

    挑战总是要面对的,何况是从来不服输的小民。主管的要求很简单,要小民实现图片加载,并且要将图片缓存起来。在分析了需求之后,小民一下就放心下来了,“这么简单,原来我还以为很难呢……”小民胸有成足的喃喃自语。在经历了十分钟的编码之后,小民写下了如下代码:

    /**
     * 图片加载类
     */
    public class ImageLoader {
        // 图片缓存
        LruCache<String, Bitmap> mImageCache;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
        public ImageLoader() {
            initImageCache();
        }
    
        private void initImageCache() {
                // 计算可使用的最大内存
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
                // 取四分之一的可用内存作为缓存
            final int cacheSize = maxMemory / 4;
            mImageCache = new LruCache<String, Bitmap>(cacheSize) {
    
                @Override
                protected int sizeOf(String key, Bitmap bitmap) {
                    return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
                }
            };
        }                   
    
        public  void displayImage(final String url, final ImageView imageView) {
            imageView.setTag(url);
            mExecutorService.submit(new Runnable() {
    
               @Override
                public  void run() {
                  Bitmap bitmap = downloadImage(url);
                    if (bitmap == null) {
                        return;
                    }
                    if (imageView.getTag().equals(url)) {
                        imageView.setImageBitmap(bitmap);
                    }
                    mImageCache.put(url, bitmap);
              }
           });
        }
    
        public  Bitmap downloadImage(String imageUrl) {
            Bitmap bitmap = null;
            try {
                URL url = newURL(imageUrl);
                final HttpURLConnection conn =         
                    (HttpURLConnection)url.openConnection();
                bitmap = BitmapFactory.decodeStream(
                      conn.getInputStream());
                conn.disconnect();
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return bitmap;
        }
    }

    并且使用git软件进行版本控制,将工程托管到github上,伴随着git push命令的完成,小民的ImageLoader 0.1版本就正式发布了!如此短的时间内就完成了这个任务,而且还是一个开源项目,小民暗暗自喜,幻想着待会儿主管的称赞。

    在小民给主管报告了ImageLoader的发布消息的几分钟之后,主管就把小民叫到了会议室。这下小民纳闷了,怎么夸人还需要到会议室。“小民,你的ImageLoader耦合太严重啦!简直就没有设计可言,更不要说扩展性、灵活性了。所有的功能都写在一个类里怎么行呢,这样随着功能的增多,ImageLoader类会越来越大,代码也越来越复杂,图片加载系统就越来越脆弱……”Duang,这简直就是当头棒喝,小民的脑海里已经听不清主管下面说的内容了,只是觉得自己之前没有考虑清楚就匆匆忙忙完成任务,而且把任务想得太简单了。

    “你还是把ImageLoader拆分一下,把各个功能独立出来,让它们满足单一职责原则。”主管最后说道。小民是个聪明人,敏锐地捕捉到了单一职责原则这个关键词。用Google搜索了一些优秀资料之后总算是对单一职责原则有了一些认识。于是打算对ImageLoader进行一次重构。这次小民不敢过于草率,也是先画了一幅UML图,如图1-1所示。

    图1-1

    ImageLoader代码修改如下所示:

    /**
     * 图片加载类
     */
    public  class ImageLoader {
        // 图片缓存
        ImageCache mImageCache = new ImageCache() ;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
        // 加载图片
        public  void displayImage(final String url, final ImageView imageView) {
            Bitmap bitmap = mImageCache.get(url);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }
            imageView.setTag(url);
            mExecutorService.submit(new Runnable() {
    
                @Override
                public void run() {
                Bitmap bitmap = downloadImage(url);
                    if (bitmap == null) {
                        return;
                    }
                    if (imageView.getTag().equals(url)) {
                        imageView.setImageBitmap(bitmap);
                    }
                    mImageCache.put(url, bitmap);
                }
            });
         }
    
        public  Bitmap downloadImage(String imageUrl) {
            Bitmap bitmap = null;
            try {
                URL url = new URL(imageUrl);
                final HttpURLConnection conn = 
                (HttpURLConnection) 
                            url.openConnection();
                bitmap = BitmapFactory.decodeStream(conn.getInputStream());
                conn.disconnect();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return bitmap;
        }
    }   

    并且添加了一个ImageCache类用于处理图片缓存,具体代码如下:

    public class ImageCache {
        // 图片LRU缓存
        LruCache<String, Bitmap> mImageCache;
    
        public ImageCache() {
            initImageCache();
        }
    
        private void initImageCache() {
             // 计算可使用的最大内存
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
            // 取四分之一的可用内存作为缓存
            final int cacheSize = maxMemory / 4;
            mImageCache = new LruCache<String, Bitmap>(cacheSize) {
    
                @Override
                protected int sizeOf(String key, Bitmap bitmap) {
                    return bitmap.getRowBytes() *  
                        bitmap.getHeight() / 1024;
               }
            };
         }
    
        public void put(String url, Bitmap bitmap) {
            mImageCache.put(url, bitmap) ;
        }
    
        public Bitmap get(String url) {
            return mImageCache.get(url) ;
        }
    }

    如图1-1和上述代码所示,小民将ImageLoader一拆为二,ImageLoader只负责图片加载的逻辑,而ImageCache只负责处理图片缓存的逻辑,这样ImageLoader的代码量变少了,职责也清晰了,当与缓存相关的逻辑需要改变时,不需要修改ImageLoader类,而图片加载的逻辑需要修改时也不会影响到缓存处理逻辑。主管在审核了小民的第一次重构之后,对小民的工作给予了表扬,大致意思是结构变得清晰了许多,但是可扩展性还是比较欠缺,虽然没有得到主管的完全肯定,但也是颇有进步,再考虑到自己确实有所收获,小民原本沮丧的心里也略微地好转起来。

    从上述的例子中我们能够体会到,单一职责所表达出的用意就是“单一”二字。正如上文所说,如何划分一个类、一个函数的职责,每个人都有自己的看法,这需要根据个人经验、具体的业务逻辑而定。但是,它也有一些基本的指导原则,例如,两个完全不一样的功能就不应该放在一个类中。一个类中应该是一组相关性很高的函数、数据的封装。工程师可以不断地审视自己的代码,根据具体的业务、功能对类进行相应的拆分,我想这会是你优化代码迈出的第一步。

    2、让程序更稳定、更灵活——开闭原则

    开闭原则的英文全称是Open Close Principle,简称OCP,它是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本已经经过测试的旧代码中,破坏原有系统。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。当然,在现实开发中,只通过继承的方式来升级、维护原有系统只是一个理想化的愿景,因此,在实际的开发过程中,修改原有代码、扩展代码往往是同时存在的。

    软件开发过程中,最不会变化的就是变化本身。产品需要不断地升级、维护,没有一个产品从第一版本开发完就再没有变化了,除非在下个版本诞生之前它已经被终止。而产品需要升级,修改原来的代码就可能会引发其他的问题。那么如何确保原有软件模块的正确性,以及尽量少地影响原有模块,答案就是尽量遵守本章要讲述的开闭原则。

    勃兰特·梅耶在1988年出版的《面向对象软件构造》一书中提出这一原则。这一想法认为,一旦完成,一个类的实现只应该因错误而被修改,新的或者改变的特性应该通过新建不同的类实现。新建的类可以通过继承的方式来重用原类的代码。显然,梅耶的定义提倡实现继承,已存在的实现对于修改是封闭的,但是新的实现类可以通过覆写父类的接口应对变化。
    说了这么多,想必大家还是半懂不懂,还是让我们以一个简单示例说明一下吧。

    在对ImageLoader进行了一次重构之后,小民的这个开源库获得了一些用户。小民第一次感受到自己发明“轮子”的快感,对开源的热情也越发高涨起来!通过动手实现一些开源库来深入学习相关技术,不仅能够提升自我,也能更好地将这些技术运用到工作中,从而开发出更稳定、优秀的应用,这就是小民的真实想法。

    小民第一轮重构之后的ImageLoader职责单一、结构清晰,不仅获得了主管的一点肯定,还得到了用户的夸奖,算是个不错的开始。随着用户的增多,有些问题也暴露出来了,小民的缓存系统就是大家“吐槽”最多的地方。通过内存缓存解决了每次从网络加载图片的问题,但是,Android应用的内存很有限,且具有易失性,即当应用重新启动之后,原来已经加载过的图片将会丢失,这样重启之后就需要重新下载!这又会导致加载缓慢、耗费用户流量的问题。小民考虑引入SD卡缓存,这样下载过的图片就会缓存到本地,即使重启应用也不需要重新下载了!小民在和主管讨论了该问题之后就投入了编程中,下面就是小民的代码。
    DiskCache.java类,将图片缓存到SD卡中:

    public class DiskCache {
        // 为了简单起见临时写个路径,在开发中请避免这种写法 !
        static String cacheDir = "sdcard/cache/";
         // 从缓存中获取图片
        public Bitmap get(String url) {
            return BitmapFactory.decodeFile(cacheDir + url);
        }
    
        // 将图片缓存到内存中
        public  void  put(String url, Bitmap bmp) {
           FileOutputStream fileOutputStream = null;
            try {
                fileOutputStream = new 
                     FileOutputStream(cacheDir + url);
                bmp.compress(CompressFormat.PNG, 
                     100, fileOutputStream);
          } catch (FileNotFoundException e) {
                e.printStackTrace();
          } final ly {
                if (fileOutputStream != null) {
                    try {
                        fileOutputStream.close();
                  } catch (IOException e) {
                        e.printStackTrace();
                 }
              }
          }
        }
    }

    因为需要将图片缓存到SD卡中,所以,ImageLoader代码有所更新,具体代码如下:

    public class ImageLoader {
        // 内存缓存
        ImageCache mImageCache = new ImageCache();
        // SD卡缓存
        DiskCache mDiskCache = new DiskCache();
        // 是否使用SD卡缓存
        boolean isUseDiskCache = false;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
    
        public  void displayImage(final String url, final ImageView imageView) {
            // 判断使用哪种缓存
           Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) 
                    : mImageCache.get (url);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
           }
            // 没有缓存,则提交给线程池进行下载
        }
    
        public void useDiskCache(boolean useDiskCache) {
            isUseDiskCache = useDiskCache ;
        }
    }

    从上述的代码中可以看到,仅仅新增了一个DiskCache类和往ImageLoader类中加入了少量代码就添加了SD卡缓存的功能,用户可以通过useDiskCache方法来对使用哪种缓存进行设置,例如:

    ImageLoader imageLoader = new ImageLoader() ;
     // 使用SD卡缓存
    imageLoader.useDiskCache(true);
    // 使用内存缓存
    imageLoader.useDiskCache(false);

    通过useDiskCache方法可以让用户设置不同的缓存,非常方便啊!小民对此很满意,于是提交给主管做代码审核。“小民,你思路是对的,但是有些明显的问题,就是使用内存缓存时用户就不能使用SD卡缓存,类似的,使用SD卡缓存时用户就不能使用内存缓存。用户需要这两种策略的综合,首先缓存优先使用内存缓存,如果内存缓存没有图片再使用SD卡缓存,如果SD卡中也没有图片最后才从网络上获取,这才是最好的缓存策略。”主管真是一针见血,小民这时才如梦初醒,刚才还得意洋洋的脸上突然有些泛红……
    于是小民按照主管的指点新建了一个双缓存类DoudleCache,具体代码如下:

    /**
     * 双缓存。获取图片时先从内存缓存中获取,如果内存中没有缓存该图片,再从SD卡中获取。
     *  缓存图片也是在内存和SD卡中都缓存一份
     */
    public class DoubleCache {
        ImageCache mMemoryCache = new ImageCache();
        DiskCache mDiskCache = new DiskCache();
    
        // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
        public   Bitmap get(String url) {
           Bitmap bitmap = mMemoryCache.get(url);
            if (bitmap == null) {
                bitmap = mDiskCache.get(url);
            }
            return  bitmap;
        }
    
        // 将图片缓存到内存和SD卡中
        public void put(String url, Bitmap bmp) {
            mMemoryCache.put(url, bmp);
            mDiskCache.put(url, bmp);
       }
    }

    我们再看看最新的ImageLoader类吧,代码更新也不多:

    public class ImageLoader {
        // 内存缓存
        ImageCache mImageCache = new ImageCache();
        // SD卡缓存
        DiskCache mDiskCache = new DiskCache();
        // 双缓存
        DoubleCache mDoubleCache = new DoubleCache() ;
        // 使用SD卡缓存
        boolean isUseDiskCache = false;
        // 使用双缓存
        boolean isUseDoubleCache = false;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
    
        public void displayImage(final String url, final ImageView imageView) {
            Bitmap bmp = null;
             if (isUseDoubleCache) {
                bmp = mDoubleCache.get(url);
            } else if (isUseDiskCache) {
                bmp = mDiskCache.get(url);
            } else {
                bmp = mImageCache.get(url);
            }
    
             if ( bmp != null ) {
                imageView.setImageBitmap(bmp);
            }
            // 没有缓存,则提交给线程池进行异步下载图片
        }
    
        public void useDiskCache(boolean useDiskCache) {
            isUseDiskCache = useDiskCache ;
        }
    
        public void useDoubleCache(boolean useDoubleCache) {
            isUseDoubleCache = useDoubleCache ;
        }
    }

    通过增加短短几句代码和几处修改就完成了如此重要的功能。小民已越发觉得自己Android开发已经到了的得心应手的境地,不仅感觉一阵春风袭来,他那飘逸的头发一下从他的眼前拂过,小民感觉今天天空比往常敞亮许多。

    “小民,你每次加新的缓存方法时都要修改原来的代码,这样很可能会引入Bug,而且会使原来的代码逻辑变得越来越复杂,按照你这样的方法实现,用户也不能自定义缓存实现呀!”到底是主管水平高,一语道出了小民这缓存设计上的问题。

    我们还是来分析一下小民的程序,小民每次在程序中加入新的缓存实现时都需要修改ImageLoader类,然后通过一个布尔变量来让用户使用哪种缓存,因此,就使得在ImageLoader中存在各种if-else判断,通过这些判断来确定使用哪种缓存。随着这些逻辑的引入,代码变得越来越复杂、脆弱,如果小民一不小心写错了某个if条件(条件太多,这是很容易出现的),那就需要更多的时间来排除。整个ImageLoader类也会变得越来越臃肿。最重要的是用户不能自己实现缓存注入到ImageLoader中,可扩展性可是框架的最重要特性之一。

    “软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的,这就是开放-关闭原则。也就是说,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。”小民的主管补充到,小民听得云里雾里的。主管看小民这等反应,于是亲自“操刀”,为他画下了如图1-2的UML图。


    图1-2

    小民看到图1-2似乎明白些什么,但是又不是太明确如何修改程序。主管看到小民这般模样只好亲自上阵,带着小民把ImageLoader程序按照图1-2进行了一次重构。具体代码如下:

    public class ImageLoader {
        // 图片缓存
        ImageCache mImageCache = new MemoryCache();
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
        // 注入缓存实现
        public void setImageCache(ImageCache cache) {
            mImageCache = cache;
        }
    
        public void displayImage(String imageUrl, ImageView imageView) {
            Bitmap bitmap = mImageCache.get(imageUrl);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }
            // 图片没缓存,提交到线程池中下载图片
            submitLoadRequest(imageUrl, imageView);
        }
    
        private void submitLoadRequest(final String imageUrl,
                 final ImageView imageView) {
            imageView.setTag(imageUrl);
            mExecutorService.submit(new Runnable() {
    
                @Override
                public  void run() {
                  Bitmap bitmap = downloadImage(imageUrl);
                    if (bitmap == null) {
                        return;
                 }
                   if (imageView.getTag().equals(imageUrl)) {
                        imageView.setImageBitmap(bitmap);
                 }
                    mImageCache.put(imageUrl, bitmap);
             }
          });
        }
    
        public  Bitmap downloadImage(String imageUrl) {
           Bitmap bitmap = null;
            try {
               URL url = new URL(imageUrl);
                final HttpURLConnection conn = (HttpURLConnection) 
                            url.openConnection();
                bitmap = BitmapFactory.decodeStream(conn.getInputStream());
                conn.disconnect();
            } catch (Exception e) {
                  e.printStackTrace();
            }
    
            return bitmap;
        }
    }

    经过这次重构,没有了那么多的if-else语句,没有了各种各样的缓存实现对象、布尔变量,代码确实清晰、简单了很多,小民对主管的崇敬之情又“泛滥”了起来。需要注意的是,这里的ImageCache类并不是小民原来的那个ImageCache,这次程序重构主管把它提取成一个图片缓存的接口,用来抽象图片缓存的功能。我们看看该接口的声明:

    public interface ImageCache {
        public Bitmap get(String url);
        public void put(String url, Bitmap bmp);
    }

    ImageCache接口简单定义了获取、缓存图片两个函数,缓存的key是图片的url,值是图片本身。内存缓存、SD卡缓存、双缓存都实现了该接口,我们看看这几个缓存实现:

    // 内存缓存MemoryCache类
    public class MemoryCache implements ImageCache {
        private LruCache<String, Bitmap> mMemeryCache;
    
        public MemoryCache() {
            // 初始化LRU缓存
        }
    
         @Override
        public Bitmap get(String url) {
            return mMemeryCache.get(url);
        }
    
        @Override
        public void put(String url, Bitmap bmp) {
            mMemeryCache.put(url, bmp);
        }
    }
    
    // SD卡缓存DiskCache类
    public  class  DiskCache implements ImageCache {
        @Override
        public Bitmap get(String url) {
            return null/* 从本地文件中获取该图片 */;
        }
    
        @Override
        public void put(String url, Bitmap bmp) {
            // 将Bitmap写入文件中
        }
    }
    
    // 双缓存DoubleCache类
    public class DoubleCache implements ImageCache{
        ImageCache mMemoryCache = new MemoryCache();
        ImageCache mDiskCache = new DiskCache();
    
        // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
        public Bitmap get(String url) {
           Bitmap bitmap = mMemoryCache.get(url);
            if (bitmap == null) {
                bitmap = mDiskCache.get(url);
           }
            return bitmap;
         }
    
        // 将图片缓存到内存和SD卡中
        public void put(String url, Bitmap bmp) {
            mMemoryCache.put(url, bmp);
            mDiskCache.put(url, bmp);
        }
    }

    细心的朋友可能注意到了,ImageLoader类中增加了一个setImageCache(ImageCache cache)函数,用户可以通过该函数设置缓存实现,也就是通常说的依赖注入。下面就看看用户是如何设置缓存实现的:

    ImageLoader imageLoader = new ImageLoader() ;
            // 使用内存缓存
    imageLoader.setImageCache(new MemoryCache());
            // 使用SD卡缓存
    imageLoader.setImageCache(new DiskCache());
            // 使用双缓存
    imageLoader.setImageCache(new DoubleCache());
            // 使用自定义的图片缓存实现
    imageLoader.setImageCache(new ImageCache() {
    
                @Override
            public void put(String url, Bitmap bmp) {
                // 缓存图片
           }
    
                @Override
            public Bitmap get(String url) {
                return null/*从缓存中获取图片*/;
           }
        });

    在上述代码中,通过setImageCache(ImageCache cache)方法注入不同的缓存实现,这样不仅能够使ImageLoader更简单、健壮,也使得ImageLoader的可扩展性、灵活性更高。MemoryCache、DiskCache、DoubleCache缓存图片的具体实现完全不一样,但是,它们的一个特点是都实现了ImageCache接口。当用户需要自定义实现缓存策略时,只需要新建一个实现ImageCache接口的类,然后构造该类的对象,并且通过setImageCache(ImageCache cache)注入到ImageLoader中,这样ImageLoader就实现了变化万千的缓存策略,而扩展这些缓存策略并不会导致ImageLoader类的修改。经过这次重构,小民的ImageLoader已经基本算合格了。咦!这不就是主管说的开闭原则么!“软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的。而遵循开闭原则的重要手段应该是通过抽象……”小民细声细语的念叨中,陷入了思索中……

    开闭原则指导我们,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。这里的“应该尽量”4个字说明OCP原则并不是说绝对不可以修改原始类的,当我们嗅到原来的代码“腐化气味”时,应该尽早地重构,以使得代码恢复到正常的“进化”轨道,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。我们的开发过程中也没有那么理想化的状况,完全地不用修改原来的代码,因此,在开发过程中需要自己结合具体情况进行考量,是通过修改旧代码还是通过继承使得软件系统更稳定、更灵活,在保证去除“代码腐化”的同时,也保证原有模块的正确性。

    3、构建扩展性更好的系统——里氏替换原则

    里氏替换原则英文全称是Liskov Substitution Principle,简称LSP。它的第一种定义是:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。上面这种描述确实不太好理解,理论家有时候容易把问题抽象化,本来挺容易理解的事让他们一概括就弄得拗口了。我们再看看另一个直截了当的定义。里氏替换原则第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。

    我们知道,面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是,所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。说了那么多,其实最终总结就两个字:抽象。
    小民为了深入地了解Android中的Window与View的关系特意写了一个简单示例,为了便于理解,我们先看如图1-3所示。

    ▲图1-3

    我们看看具体的代码:

    // 窗口类
    public class Window {
        public void show(View child){
            child.draw();
        }
    }
    
    // 建立视图抽象,测量视图的宽高为公用代码,绘制交给具体的子类
    public abstract class  View {
        public abstract void  draw() ;
        public void  measure(int width, int height){
            // 测量视图大小
        }
    }
    
    // 按钮类的具体实现
    public class Button extends View {
        public void draw(){
            // 绘制按钮
        }
    }
    // TextView的具体实现
    public class TextView extends View {
        public void draw(){
            // 绘制文本
        }
    }

    上述示例中,Window依赖于View,而View定义了一个视图抽象,measure是各个子类共享的方法,子类通过覆写View的draw方法实现具有各自特色的功能,在这里,这个功能就是绘制自身的内容。任何继承自View类的子类都可以设置给show方法,也就我们所说的里氏替换。通过里氏替换,就可以自定义各式各样、千变万化的View,然后传递给Window,Window负责组织View,并且将View显示到屏幕上。
    里氏替换原则的核心原理是抽象,抽象又依赖于继承这个特性,在OOP当中,继承的优缺点都相当明显。
    优点如下:

    • (1)代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性;
    • (2)子类与父类基本相似,但又与父类有所区别;
    • (3)提高代码的可扩展性。

    继承的缺点:

    • (1)继承是侵入性的,只要继承就必须拥有父类的所有属性和方法;
    • (2)可能造成子类代码冗余、灵活性降低,因为子类必须拥有父类的属性和方法。

    事物总是具有两面性,如何权衡利与弊都是需要根据具体场景来做出选择并加以处理。里氏替换原则指导我们构建扩展性更好的软件系统,我们还是接着上面的ImageLoader来做说明。
    上文的图1-2也很好地反应了里氏替换原则,即MemoryCache、DiskCache、DoubleCache都可以替换ImageCache的工作,并且能够保证行为的正确性。ImageCache建立了获取缓存图片、保存缓存图片的接口规范,MemoryCache等根据接口规范实现了相应的功能,用户只需要在使用时指定具体的缓存对象就可以动态地替换ImageLoader中的缓存策略。这就使得ImageLoader的缓存系统具有了无线的可能性,也就是保证了可扩展性。

    想象一个场景,当ImageLoader中的setImageCache(ImageCache cache)中的cache对象不能够被子类所替换,那么用户如何设置不同的缓存对象以及用户如何自定义自己的缓存实现,通过1.3节中的useDiskCache方法吗?显然不是的,里氏替换原则就为这类问题提供了指导原则,也就是建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的高扩展性、灵活性。开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换来达到对扩展开放,对修改关闭的效果。然而,这两个原则都同时强调了一个OOP的重要特性——抽象,因此,在开发过程中运用抽象是走向代码优化的重要一步。

    4、 让项目拥有变化的能力——依赖倒置原则

    依赖倒置原则英文全称是Dependence Inversion Principle,简称DIP。依赖反转原则指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。这个概念有点不好理解,这到底是什么意思呢?
    依赖倒置原则的几个关键点:

    • (1)高层模块不应该依赖低层模块,两者都应该依赖其抽象;
    • (2)抽象不应该依赖细节;
    • (3)细节应该依赖抽象。

    在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是,可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。高层模块就是调用端,低层模块就是具体实现类。依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。这又是一个将理论抽象化的实例,其实一句话就可以概括:面向接口编程,或者说是面向抽象编程,这里的抽象指的是接口或者抽象类。面向接口编程是面向对象精髓之一,也就是上面两节强调的抽象。

    如果在类与类直接依赖于细节,那么它们之间就有直接的耦合,当具体实现需要变化时,意味着在这要同时修改依赖者的代码,并且限制了系统的可扩展性。我们看1.3节的图1-3中,ImageLoader直接依赖于MemoryCache,这个MemoryCache是一个具体实现,而不是一个抽象类或者接口。这导致了ImageLoader直接依赖了具体细节,当MemoryCache不能满足ImageLoader而需要被其他缓存实现替换时,此时就必须修改ImageLoader的代码,例如:

    public class ImageLoader {
        // 内存缓存 ( 直接依赖于细节 )
        MemoryCache mMemoryCache = new MemoryCache();
         // 加载图片到ImageView中
        public void displayImage(String url, ImageView imageView) {
           Bitmap bmp = mMemoryCache.get(url);
            if (bmp == null) {
                downloadImage(url, imageView);
            } else {
                imageView.setImageBitmap(bmp);
            }
        }
    
        public void setImageCache(MemoryCache cache) {
            mCache = cache ;
        }
        // 代码省略
    }

    随着产品的升级,用户发现MemoryCache已经不能满足需求,用户需要小民的ImageLoader可以将图片同时缓存到内存和SD卡中,或者可以让用户自定义实现缓存。此时,我们的MemoryCache这个类名不仅不能够表达内存缓存和SD卡缓存的意义,也不能够满足功能。另外,用户需要自定义缓存实现时还必须继承自MemoryCache,而用户的缓存实现可不一定与内存缓存有关,这在命名上的限制也让用户体验不好。重构的时候到了!小民的第一种方案是将MemoryCache修改为DoubleCache,然后在DoubleCache中实现具体的缓存功能。我们需要将ImageLoader修改如下:

    public class ImageLoader {
        // 双缓存 ( 直接依赖于细节 )
        DoubleCache mCache = new DoubleCache();
        // 加载图片到ImageView中
        public void displayImage(String url, ImageView imageView) {
           Bitmap bmp = mCache.get(url);
            if (bmp == null) {
              // 异步下载图片
                downloadImageAsync(url, imageView);
           } else {
                imageView.setImageBitmap(bmp);
           }
        }
    
        public void setImageCache(DoubleCache cache) {
             mCache = cache ;
        }
        // 代码省略
    }

    我们将MemoryCache修改成DoubleCache,然后修改了ImageLoader中缓存类的具体实现,轻轻松松就满足了用户需求。等等!这不还是依赖于具体的实现类(DoubleCache)吗?当用户的需求再次变化时,我们又要通过修改缓存实现类和ImageLoader代码来实现?修改原有代码不是违反了1.3节中的开闭原则吗?小民突然醒悟了过来,低下头思索着如何才能让缓存系统更灵活、拥抱变化……

    当然,这些都是在主管给出图1-2(1.3节)以及相应的代码之前,小民体验的煎熬过程。既然是这样,那显然主管给出的解决方案就能够让缓存系统更加灵活。一句话概括起来就是:依赖抽象,而不依赖具体实现。针对于图片缓存,主管建立的ImageCache抽象,该抽象中增加了get和put方法用以实现图片的存取。每种缓存实现都必须实现这个接口,并且实现自己的存取方法。当用户需要使用不同的缓存实现时,直接通过依赖注入即可,保证了系统的灵活性。我们再来简单回顾一下相关代码:

    ImageCache缓存抽象:

    public interface ImageCache {
        public Bitmap get(String url);
        public void put(String url, Bitmap bmp);
    }

    ImageLoader类:

    public class ImageLoader {
        // 图片缓存类,依赖于抽象,并且有一个默认的实现
        ImageCache mCache = new MemoryCache();
    
        // 加载图片
        public void displayImage(String url, ImageView imageView) {
           Bitmap bmp = mCache.get(url);
            if (bmp == null) {
            // 异步加载图片
                downloadImageAsync(url, imageView);
           } else {
                imageView.setImageBitmap(bmp);
           }
        }
    
        /**
         * 设置缓存策略,依赖于抽象
         */
        public void setImageCache(ImageCache cache) {
            mCache = cache;
        }
        // 代码省略
    }

    在这里,我们建立了ImageCache抽象,并且让ImageLoader依赖于抽象而不是具体细节。当需求发生变更时,小民只需要实现ImageCahce类或者继承其他已有的ImageCache子类完成相应的缓存功能,然后将具体的实现注入到ImageLoader即可实现缓存功能的替换,这就保证了缓存系统的高可扩展性,拥有了拥抱变化的能力,而这一切的基本指导原则就是我们的依赖倒置原则。从上述几节中我们发现,要想让我们的系统更为灵活,抽象似乎成了我们唯一的手段。

    5、系统有更高的灵活性——接口隔离原则

    接口隔离原则英文全称是InterfaceSegregation Principles,简称ISP。它的定义是:客户端不应该依赖它不需要的接口。另一种定义是:类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署。

    接口隔离原则说白了就是,让客户端依赖的接口尽可能地小,这样说可能还是有点抽象,我们还是以一个示例来说明一下。在此之前我们来说一个场景,在Java 6以及之前的JDK版本,有一个非常讨厌的问题,那就是在使用了OutputStream或者其他可关闭的对象之后,我们必须保证它们最终被关闭了,我们的SD卡缓存类中就有这样的代码:

    // 将图片缓存到内存中
    public void put(String url, Bitmap bmp) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
              } catch (IOException e) {
                    e.printStackTrace();
              }
           } // end if
        } // end if finally
    }

    我们看到的这段代码可读性非常差,各种try…catch嵌套,都是些简单的代码,但是会严重影响代码的可读性,并且多层级的大括号很容易将代码写到错误的层级中。大家应该对这类代码也非常反感,那我们看看如何解决这类问题。
    我们可能知道Java中有一个Closeable接口,该接口标识了一个可关闭的对象,它只有一个close方法,如图1-4所示。
    我们要讲的FileOutputStream类就实现了这个接口,我们从图1-4中可以看到,还有一百多个类实现了Closeable这个接口,这意味着,在关闭这一百多个类型的对象时,都需要写出像put方法中finally代码段那样的代码。这还了得!你能忍,反正小民是忍不了的!于是小民打算要发挥他的聪明才智解决这个问题,既然都是实现了Closeable接口,那只要我建一个方法统一来关闭这些对象不就可以了么?说干就干,于是小民写下来如下的工具类:

    ▲图1-4

    public final class CloseUtils {
    
        Private CloseUtils() { }
    
        /**
         * 关闭Closeable对象
         * @param closeable
         */
        public static void closeQuietly(Closeable closeable) {
            if (null != closeable) {
                try {
                    closeable.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
           }
        }
    }

    我们再看看把这段代码运用到上述的put方法中的效果如何:

    public void put(String url, Bitmap bmp) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
       } catch (FileNotFoundException e) {
            e.printStackTrace();
       } final ly {
            CloseUtils.closeQuietly(fileOutputStream);
       }
    }

    代码简洁了很多!而且这个closeQuietly方法可以运用到各类可关闭的对象中,保证了代码的重用性。CloseUtils的closeQuietly方法的基本原理就是依赖于Closeable抽象而不是具体实现(这不是1.4节中的依赖倒置原则么),并且建立在最小化依赖原则的基础,它只需要知道这个对象是可关闭,其他的一概不关心,也就是这里的接口隔离原则。

    试想一下,如果在只是需要关闭一个对象时,它却暴露出了其他的接口函数,比如OutputStream的write方法,这就使得更多的细节暴露在客户端代码面前,不仅没有很好地隐藏实现,还增加了接口的使用难度。而通过Closeable接口将可关闭的对象抽象起来,这样只需要客户端依赖于Closeable就可以对客户端隐藏其他的接口信息,客户端代码只需要知道这个对象可关闭(只可调用close方法)即可。小民ImageLoader中的ImageCache就是接口隔离原则的运用,ImageLoader只需要知道该缓存对象有存、取缓存图片的接口即可,其他的一概不管,这就使得缓存功能的具体实现对ImageLoader具体的隐藏。这就是用最小化接口隔离了实现类的细节,也促使我们将庞大的接口拆分到更细粒度的接口当中,这使得我们的系统具有更低的耦合性,更高的灵活性。

    Bob大叔(Robert C Martin)在21世纪早期将单一职责、开闭原则、里氏替换、接口隔离以及依赖倒置(也称为依赖反转)5个原则定义为SOLID原则,指代了面向对象编程的5个基本原则。当这些原则被一起应用时,它们使得一个软件系统更清晰、简单、最大程度地拥抱变化。SOLID被典型地应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发基本原则的重要组成部分。在经过第1.1~1.5节的学习之后,我们发现这几大原则最终就可以化为这几个关键词:抽象、单一职责、最小化。那么在实际开发过程中如何权衡、实践这些原则,是大家需要在实践中多思考与领悟,正所谓”学而不思则罔,思而不学则殆”,只有不断地学习、实践、思考,才能够在积累的过程有一个质的飞越。

    6、更好的可扩展性——迪米特原则

    迪米特原则英文全称为Law of Demeter,简称LOD,也称为最少知识原则(Least Knowledge Principle)。虽然名字不同,但描述的是同一个原则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,类的内部如何实现、如何复杂都与调用者或者依赖者没关系,调用者或者依赖者只需要知道他需要的方法即可,其他的我一概不关心。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

    迪米特法则还有一个英文解释是:Only talk to your immedate friends,翻译过来就是:只与直接的朋友通信。什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。

    光说不练很抽象呐,下面我们就以租房为例来讲讲迪米特原则。
    “北漂”的同学比较了解,在北京租房绝大多数都是通过中介找房。我们设定的情境为:我只要求房间的面积和租金,其他的一概不管,中介将符合我要求的房子提供给我就可以。下面我们看看这个示例:

    /**
     * 房间
     */
    public class Room {
        public float area;
        public float price;
    
        public Room(float  area, float  price) {
            this.area = area;
            this.price = price;
        }
    
        @Override
        public String toString() {
            return "Room [area=" + area + ", price=" + price + "]";
        }
    
    }
    
    /**
     * 中介
     */
    public class Mediator {
        List<Room> mRooms = new ArrayList<Room>();
    
        public Mediator() {
            for (inti = 0; i < 5; i++) {
                mRooms.add(new Room(14 + i, (14 + i) * 150));
           }
       }
    
        public List<Room>getAllRooms() {
            return mRooms;
       }
    }
    
    
    /**
     * 租户
     */
    public class Tenant {
        public float roomArea;
        public float roomPrice;
        public static final float diffPrice = 100.0001f;
        public static final float diffArea = 0.00001f;
    
        public void rentRoom(Mediator mediator) {
            List<Room>rooms = mediator.getAllRooms();
            for (Room room : rooms) {
                if (isSuitable(room)) {
                 System.out.println("租到房间啦! " + room);
                    break;
              }
           }
        }
    
        private boolean isSuitable(Room room) {
            return Math.abs(room.price - roomPrice) < diffPrice
                    &&Math.abs(room.area - roomArea) < diffArea;
       }
    }

    从上面的代码中可以看到,Tenant不仅依赖了Mediator类,还需要频繁地与Room类打交道。租户类的要求只是通过中介找到一间适合自己的房间罢了,如果把这些检测条件都放在Tenant类中,那么中介类的功能就被弱化,而且导致Tenant与Room的耦合较高,因为Tenant必须知道许多关于Room的细节。当Room变化时Tenant也必须跟着变化。Tenant又与Mediator耦合,就导致了纠缠不清的关系。这个时候就需要我们分清谁才是我们真正的“朋友”,在我们所设定的情况下,显然是Mediator(虽然现实生活中不是这样的)。上述代码的结构如图1-5所示。

    ▲图1-5

    既然是耦合太严重,那我们就只能解耦了,首先要明确地是,我们只和我们的朋友通信,这里就是指Mediator对象。必须将Room相关的操作从Tenant中移除,而这些操作案例应该属于Mediator,我们进行如下重构:

    /**
     * 中介
     */
    public class Mediator {
        List<Room> mRooms = new ArrayList<Room>();
    
        public Mediator() {
            for (inti = 0; i < 5; i++) {
                mRooms.add(new Room(14 + i, (14 + i) * 150));
           }
        }
    
        public Room rentOut(float  area, float  price) {
            for (Room room : mRooms) {
                if (isSuitable(area, price, room)) {
                    return  room;
              }
           }
            return null;
        }
    
        private boolean isSuitable(float area, float price, Room room) {
            return Math.abs(room.price - price) < Tenant.diffPrice
                && Math.abs(room.area - area) < Tenant.diffPrice;
        }
    }
    
    /**
     * 租户
     */
    public class Tenant {
    
        public float roomArea;
        public float roomPrice;
        public static final float diffPrice = 100.0001f;
        public static final float diffArea = 0.00001f;
    
        public void rentRoom(Mediator mediator) {
            System.out.println("租到房啦 " + mediator.rentOut(roomArea, roomPrice));
         }
    }

    重构后的结构图如图1-6所示。

    ▲图1-6

    只是将对于Room的判定操作移到了Mediator类中,这本应该是Mediator的职责,他们根据租户设定的条件查找符合要求的房子,并且将结果交给租户就可以了。租户并不需要知道太多关于Room的细节,比如与房东签合同、房东的房产证是不是真的、房内的设施坏了之后我要找谁维修等,当我们通过我们的“朋友”中介租了房之后,所有的事情我们都通过与中介沟通就好了,房东、维修师傅等这些角色并不是我们直接的“朋友”。“只与直接的朋友通信”这简单的几个字就能够将我们从乱七八糟的关系网中抽离出来,使我们的耦合度更低、稳定性更好。
    通过上述示例以及小民的后续思考,迪米特原则这把利剑在小民的手中已经舞得风生水起。就拿sd卡缓存来说吧,ImageCache就是用户的直接朋友,而SD卡缓存内部却是使用了jake wharton的DiskLruCache实现,这个DiskLruCache就不属于用户的直接朋友了,因此,用户完全不需要知道它的存在,用户只需要与ImageCache对象打交道即可。例如将图片存到SD卡中的代码如下。

    public void put(String url, Bitmap value) {
        DiskLruCache.Editor editor = null;
        try {
           // 如果没有找到对应的缓存,则准备从网络上请求数据,并写入缓存
            editor = mDiskLruCache.edit(url);
            if (editor != null) {
                    OutputStream outputStream = editor.newOutputStream(0);
                if (writeBitmapToDisk(value, outputStream)) {
                  // 写入disk缓存
                    editor.commit();
              } else {
                    editor.abort();
              }
                CloseUtils.closeQuietly(outputStream);
           }
        } catch (IOException e) {
             e.printStackTrace();
        }
    }

    用户在使用SD卡缓存时,根本不知晓DiskLruCache的实现,这就很好地对用户隐藏了具体实现。当小民已经“牛”到可以自己完成SD卡的rul实现时,他就可以随心所欲的替换掉jake wharton的DiskLruCache。小民的代码大体如下:

    @Override
    public  void put(String url, Bitmap bmp) {
        // 将Bitmap写入文件中
        FileOutputStream fos = null;
        try {
           // 构建图片的存储路径 ( 省略了对url取md5)
            fos = new FileOutputStream("sdcard/cache/" + imageUrl2MD5(url));
            bmp.compress(CompressFormat.JPEG, 100, fos);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if ( fos != null ) {
                try {
                    fos.close();
              } catch (IOException e) {
                    e.printStackTrace();
              }
           }
        } // end if finally
    }

    SD卡缓存的具体实现虽然被替换了,但用户根本不会感知到。因为用户根本不知道DiskLruCache的存在,他们没有与DiskLruCache进行通信,他们只认识直接“朋友”ImageCache,ImageCache将一切细节隐藏在了直接“朋友”的外衣之下,使得系统具有更低的耦合性和更好的可扩展性。

    7、总结

    在应用开发过程中,最难的不是完成应用的开发工作,而是在后续的升级、维护过程中让应用系统能够拥抱变化。拥抱变化也就意味着在满足需求且不破坏系统稳定性的前提下保持高可扩展性、高内聚、低耦合,在经历了各版本的变更之后依然保持清晰、灵活、稳定的系统架构。当然,这是一个比较理想的情况,但我们必须要朝着这个方向去努力,那么遵循面向对象六大原则就是我们走向灵活软件之路所迈出的第一步。

    展开全文
  • 我在很多演讲的会议上遇到了很多知名的处理双十一和618的讲师,普遍反馈当前的Java应用基本上4核8G是标配,如果遇见容量不足的情况,少部分通过纵向扩容的方式进行,部分采用横向扩容的方式进行。 如果4核8G是...

    文章转自网易云架构师刘超的个人微信公众号:刘超的通俗云计算
    做容器的研究和容器化几年了,从最初对于容器的初步认识,到积攒了大量的容器迁移经验,并和客户解释了容器技术之后,发现原来对于容器的理解有大量的误解,而且容器并非虚拟机的替代,而是有十分具体的应用场景的。


    #第一部分:容器的理解误区
    ##误区一:容器启动速度快,秒级启动
    这是很多人布道容器的时候经常说的一句话,往往人们会启动一个nginx之类的应用,的确很快就能够启动起来了。

    容器为啥启动快,一是没有内核,二是镜像比较小。

    然而容器是有主进程的,也即Entrypoint,只有主进程完全启动起来了,容器才算真正的启动起来,一个比喻是容器更像人的衣服,人站起来了,衣服才站起来,人躺下了,衣服也躺下了。衣服有一定的隔离性,但是隔离性没那么好。衣服没有根(内核),但是衣服可以随着人到处走。
    所以按照一个nginx来评判一个容器的启动速度有意义么?对于Java应用,里面安装的是tomcat,而tomcat的启动,加载war,并且真正的应用启动起来,如果你盯着tomcat的日志看的话,还是需要一些时间的,根本不是秒级。如果应用启动起来要一两分钟,仅仅谈容器的秒级启动是没有意义的。

    现在OpenStack中的VM的启动速度也优化的越来越快了,启动一个VM的时候
    ,原来需要从Glance下载虚拟机镜像,后来有了一个技术,是的Glance和系统盘共享Ceph存储的情况下,虚拟机镜像无需下载,启动速度就快很多。

    而且容器之所以启动速度快,往往建议使用一个非常小的镜像,例如alpine,里面很多东西都裁剪掉了,启动的速度就更快了。

    OpenStack的虚拟机镜像也可以经过大量的裁剪,实现快速的启动

    这里写图片描述
    我们可以精细的衡量虚拟机启动的每一个步骤,裁剪掉相应的模块和启动的过程,大大降低虚拟机的启动时间。

    例如在UnitedStack的一篇博客里面https://www.ustack.com/blog/build-block-storage-service,我们可以看到这样的实现和描述
    这里写图片描述

    使用原生的OpenStack创建虚拟机需要1~3分钟,而使用改造后的OpenStack仅需要不到10秒钟时间。这是因为nova-compute不再需要通过HTTP下载整个镜像,虚拟机可以通过直接读取Ceph中的镜像数据进行启动。”

    所以对于虚拟机的整体启动时间,现在优化的不错的情况下,一般能够做到十几秒到半分钟以内。这个时间和Tomcat的启动时间相比较,其实不算是负担,和容器的启动速度相比,没有质的差别,可能有人会说启动速度快一点也是快,尤其是对于在线环境的挂掉自修复来讲,不是分秒必争么?关于自修复的问题,我们下面另外说。

    然而虚拟机有一个好处,就是隔离性好,如果容器是衣服,虚拟机就是房子,房子立在那里,里面的人无论站着还是躺着,房子总是站着的,房子也不会跟着人走。使用虚拟机就像人们住在公寓里面一样,每人一间,互补干扰,使用容器像大家穿着衣服挤在公交车里面,看似隔离,谁把公交弄坏了,谁都走不了。
    综上所述,容器的启动速度不足以构成对OpenStack虚拟机的明显优势,然而虚拟机的隔离性,则秒杀容器。

    ##误区二:容器轻量级,每个主机会运行成百上千个容器。
    很多人会做实验,甚至会跟客户说,容器平台多么多么牛,你看我们一台机器上可以运行成百上千个容器,虚拟机根本做不到这一点。

    但是一个机器运行成百上千个容器,有这种真实的应用场景么?对于容器来讲,重要的是里面的应用,应用的核心在于稳定性和高并发支撑,而不在于密度。

    我在很多演讲的会议上遇到了很多知名的处理双十一和618的讲师,普遍反馈当前的Java应用基本上4核8G是标配,如果遇见容量不足的情况,少部分通过纵向扩容的方式进行,大部分采用横向扩容的方式进行。

    如果4核8G是标配,不到20个服务就可以占满一台物理服务器,一台机器跑成百上千个nginx有意思么? 这不是一个严肃的使用场景。

    当然现在有一个很火的Serverless无服务架构,在无服务器架构中,所有自定义代码作为孤立的、独立的、常常细粒度的函数来编写和执行,这些函数在例如AWS Lambda之类的无状态计算服务中运行。这些计算服务可以是虚拟机,也可以是容器。对于无状态的函数来讲,需要快速的创建可删除,而且很可能执行一个函数的时间本身就非常短,在这种情况下容器相比于虚拟机还是有一定优势的。
    这里写图片描述
    目前无服务架构比较适用于运行一些任务型批量操作,利用进程级别的横向弹性能力来抵消进程创建和销毁带来的较大的代价。

    在spark和mesos的集成中,有一个Fine-Grained模式,同通常大数据的执行的时候,任务的执行进程早就申请好了资源,等在那里分配资源不同,这种模式是当任务分配到的时候才分配资源,好处就是对于资源的弹性申请和释放的能力,坏处是进程的创建和销毁还是粒度太大,所以这种模式下spark运行的性能会差一些。
    这里写图片描述
    spark的这种做法思想类似无服务架构,你会发现我们原来学操作系统的时候,说进程粒度太大,每次都创建和销毁进程会速度太慢,为了高并发,后来有了线程,线程的创建和销毁轻量级的多,当然还是觉得慢,于是有了线程池,事先创建在了那里,用的时候不用现创建,不用的时候交回去就行,后来还是觉得慢,因为线程的创建也需要在内核中完成,所以后来有了协程,全部在用户态进行线程切换,例如AKKA,Go都使用了协程,你会发现趋势是为了高并发,粒度是越来越细的,现在很多情况又需要进程级别的,有种风水轮流转的感觉。
    ##误区三:容器有镜像,可以保持版本号,可以升级和回滚
    容器有两个特性,一个是封装,一个是标准。有了容器镜像,就可以将应用的各种配置,文件路径,权限封装起来,然后像孙悟空说“定”,就定在了封装好的那一刻。镜像是标准的,无论在哪个容器运行环境,将同样的镜像运行起来,都能还原当时的那一刻。

    容器的镜像还有版本号,我们可以根据容器的版本号进行升级,一旦升级有错,可以根据版本号进行回滚,回滚完毕则能够保证容器内部还是原来的状态。
    这里写图片描述
    但是OpenStack虚拟机也是有镜像的,虚拟机镜像也是可以打snapshot的,打snapshot的时候,也会保存当时的那一刻所有的状态,而且snapshot也可以有版本号,也可以升级和回滚。
    这里写图片描述

    似乎容器有的这些特性OpenStack虚拟机都有,二者有什么不同呢?

    虚拟机镜像大,而容器镜像小。虚拟机镜像动不动就几十个G甚至上百G,而容器镜像多几百M。

    虚拟机镜像不适合跨环境迁移。例如开发环境在本地,测试环境在一个OpenStack上,开发环境在另一个OpenStack上,虚拟机的镜像的迁移非常困难,需要拷贝非常大的文件。而容器就好的多,因为镜像小,可以很快的从不同的环境之间迁移。

    虚拟机镜像不适合跨云迁移。当前没有一个公有云平台支持虚拟机镜像的下载和上传(安全的原因,盗版的原因),因而一个镜像在不同的云之间,或者同一个云不同的region直接,无法进行迁移,只能重新做一个镜像,这样环境的一致性就得不到保障。而容器的镜像中心是独立于云之外的,只要能够连上镜像中心,到哪个云上都可以下载,并且因为镜像小,下载速度快,并且镜像是分层的,每次只需要下载差异的部分。

    OpenStack对于镜像方面的优化,基本上还是在一个云里面起作用,一旦跨多个环境,镜像方便的多。
    ##误区四:容器可以使用容器平台管理自动重启实现自修复
    容器的自修复功能是经常被吹嘘的。因为容器是衣服,人躺下了,衣服也躺下了,容器平台能够马上发现人躺下了,于是可以迅速将人重新唤醒工作。而虚拟机是房子,人躺下了,房子还站着,因而虚拟机管理平台不知道里面的人能不能工作,所以容器挂了会被自动重启,而虚拟机里面的应用挂了,只要虚拟机不挂,很可能没人知道。

    这些说法都没错,但是人们慢慢发现了另外的场景,就是容器里面的应用没有挂,所以容器看起来还启动着,但是应用以及不工作没有反应了。当启动容器的时候,虽然容器的状态起来了,但是里面的应用还需要一段时间才能提供服务。所以针对这种场景,容器平台会提供对于容器里面应用的health check,不光看容器在不在,还要看里面的应用能不能用,如果不能,可自动重启。

    一旦引入了health check,和虚拟机的差别也不大了,因为有了health check,虚拟机也能看里面的应用是否工作了,不工作也可以重启应用。

    还要就是容器的启动速度快,秒级启动,如果能够自动重启修复,那就是秒级修复,所以应用更加高可用。

    这个观点当然不正确,应用的高可用性和重启的速度没有直接关系。高可用性一定要通过多个副本来实现,在任何一个挂掉之后,不能通过这一个应用快速重启来解决,而是应该靠挂掉的期间,其他的副本马上把任务接过来进行解决。虚拟机和容器都可以有多副本,在有多个副本的情况下,重启是一秒还是20秒,就没那么重要了,重要的是挂掉的这段时间内,程序做了什么,如果程序做的是无关紧要的操作,那么挂了20秒,也没啥关系,如果程序正在进行一个交易和支付,那挂掉一秒也不行,也必须能够修复回来。所以应用的高可用性要靠应用层的重试,幂等去解决,而不应该靠基础设施层重启的快不快来解决。

    对于无状态服务,在做好重试的机制的情况下,通过自动重启修复是没有问题的,因为无状态的服务不会保存非常重要的操作。
    这里写图片描述

    对于有状态服务,容器的重启不但不是推荐的,而且可能是灾难的开始。一个服务有状态,例如数据库,在高并发场景下,一旦挂了,哪怕只有一秒,我们必须要弄清楚这一秒都发生了什么,哪些数据保存了,哪些数据丢了,而不能盲目的重启,否则会很可能造成数据的不一致性,后期修都没法修。例如高频交易下的数据库挂了,按说DBA应该严格审核丢了哪些数据,而不是在DBA不知情的情况下,盲目的重启了,DBA还觉得没什么事情发生,最终很久才能发现问题。

    所以容器比较适合部署无状态服务的,随便重启都可以。
    这里写图片描述
    而容器部署有状态容器不是不能,而是要非常小心,甚至都是不推荐的。虽然很多的容器平台都支持有状态容器,然而平台往往解决不了数据问题,除非你对容器里面的应用非常非常非常熟悉,当容器挂了,你能够准确的知道丢了哪些,哪些要紧,哪些不要紧,而且要写代码处理这些情况,然后才能支持重启。网易这面的数据库主备同步的情况下,是通过修改mysql源代码,保证主备之间数据完全同步,才敢在主挂了的情况下,备自动切换主。

    而宣传有状态容器的自动重启,对于服务客户来讲是很不经济的行为,因为客户往往没有那么清楚应用的逻辑,甚至应用都是买的,如果使用有状态容器,任凭自动重启,最终客户发现数据丢失的时候,还是会怪到你的头上。

    所以有状态的服务自动重启不是不可用,需要足够专业才行。
    ##误区五:容器可以使用容器平台进行服务发现

    容器平台swarm, kubernetes,mesos都是支持服务发现的,当一个服务访问另一个服务,都会有服务名转化为VIP,然后访问具体的容器。
    这里写图片描述

    然而人们会发现,基于Java写的应用,服务之间的调用多不会用容器平台的服务发现,而是用Dubbo或者spring cloud的服务发现。因为容器平台层的服务发现,还是做的比较基础,基本是一个域名映射的过程,对于熔断,限流,降级都没有很好的支持,然而既然使用服务发现,还是希望服务发现中间件能够做到这一点,因而服务之间的服务发现之间使用容器平台的少,越是需要高并发的应用,越是如此。
    这里写图片描述
    那容器平台的服务发现没有用了么?不是,慢慢你会发现,内部的服务发现是一方面,这些Dubbo和spring cloud能够搞定,而外部的服务发现就不同了,比如访问数据库,缓存等,到底是应该配置一个数据库服务的名称,还是IP地址呢?如果使用IP地址,会造成配置十分复杂,因为很多应用配置之所以复杂,就是依赖了太多的外部应用,也是最难管理的一方面。如果有了外部的服务发现,配置就会简单很多,也只需要配置外部服务的名称就可以了,如果外部服务地址变了,可以很灵活的改变外部的服务发现。

    误区六:容器可以基于镜像进行弹性伸缩

    在容器平台上,容器有副本数的,只要将副本数从5改到10,容器就基于镜像进行了弹性伸缩。其实这一点虚拟机也能做到,AWS的Autoscaling就是基于虚拟机镜像的,如果在同一个云里面,就没有区别。
    这里写图片描述
    当然如果跨云无状态容器的弹性伸缩,容器方便很多,可以实现混合云模式,当高并发场景下,将无状态容器扩容到公有云,这一点虚拟机是做不到的。
    这里写图片描述
    ##容器理解误区总结
    这里写图片描述
    如图,左面是经常挂在嘴边的所谓容器的优势,但是虚拟机都能一一怼回去。
    如果部署的是一个传统的应用,这个应用启动速度慢,进程数量少,基本不更新,那么虚拟机完全能够满足需求。

    • 应用启动慢:应用启动15分钟,容器本身秒级,虚拟机很多平台能优化到十几秒,两者几乎看不出差别
    • 内存占用大:动不动32G,64G内存,一台机器跑不了几个。
    • 基本不更新:半年更新一次,虚拟机镜像照样能够升级和回滚
    • 应用有状态:停机会丢数据,如果不知道丢了啥,就算秒级启动有啥用,照样恢复不了,而且还有可能因为丢数据,在没有修复的情况下,盲目重启带来数据混乱。
    • 进程数量少:两三个进程相互配置一下,不用服务发现,配置不麻烦

    如果是一个传统应用,根本没有必要花费精去容器化,因为白花了力气,享受不到好处。
    #第二部分:容器化,微服务,DevOps三位一体
    这里写图片描述
    什么情况下,才应该考虑做一些改变呢?

    传统业务突然被互联网业务冲击了,应用老是变,三天两头要更新,而且流量增大了,原来支付系统是取钱刷卡的,现在要互联网支付了,流量扩大了N倍。

    没办法,一个字:拆

    拆开了,每个子模块独自变化,少相互影响。
    拆开了,原来一个进程扛流量,现在多个进程一起扛。

    所以称为微服务。
    这里写图片描述
    微服务场景下,进程多,更新快,于是出现100个进程,每天一个镜像。

    容器乐了,每个容器镜像小,没啥问题,虚拟机哭了,因为虚拟机每个镜像太大了。

    所以微服务场景下,可以开始考虑用容器了。
    这里写图片描述

    虚拟机怒了,老子不用容器了,微服务拆分之后,用Ansible自动部署是一样的。

    这样说从技术角度来讲没有任何问题。

    然而问题是从组织角度出现的。

    一般的公司,开发会比运维多的多,开发写完代码就不用管了,环境的部署完全是运维负责,运维为了自动化,写Ansible脚本来解决问题。

    然而这么多进程,又拆又合并的,更新这么快,配置总是变,Ansible脚本也要常改,每天都上线,不得累死运维。

    所以这如此大的工作量情况下,运维很容易出错,哪怕通过自动化脚本。
    这个时候,容器就可以作为一个非常好的工具运用起来。

    除了容器从技术角度,能够使得大部分的内部配置可以放在镜像里面之外,更重要的是从流程角度,将环境配置这件事情,往前推了,推到了开发这里,要求开发完毕之后,就需要考虑环境部署的问题,而不能当甩手掌柜。

    这样做的好处就是,虽然进程多,配置变化多,更新频繁,但是对于某个模块的开发团队来讲,这个量是很小的,因为5-10个人专门维护这个模块的配置和更新,不容易出错。

    如果这些工作量全交给少数的运维团队,不但信息传递会使得环境配置不一致,部署量会大非常多。

    容器是一个非常好的工具,就是让每个开发仅仅多做5%的工作,就能够节约运维200%的工作,并且不容易出错。

    然而本来原来运维该做的事情开发做了,开发的老大愿意么?开发的老大会投诉运维的老大么?

    这就不是技术问题了,其实这就是DevOps,DevOps不是不区分开发和运维,而是公司从组织到流程,能够打通,看如何合作,边界如何划分,对系统的稳定性更有好处。
    这里写图片描述
    所以微服务,DevOps,容器是相辅相成,不可分割的。

    不是微服务,根本不需要容器,虚拟机就能搞定,不需要DevOps,一年部署一次,开发和运维沟通再慢都能搞定。

    所以,容器的本质是基于镜像的跨环境迁移。

    镜像是容器的根本性发明,是封装和运行的标准,其他什么namespace,cgroup,早就有了。这是技术方面。

    在流程方面,镜像是DevOps的良好工具。

    容器是为了跨环境迁移的,第一种迁移的场景是开发,测试,生产环境之间的迁移。如果不需要迁移,或者迁移不频繁,虚拟机镜像也行,但是总是要迁移,带着几百G的虚拟机镜像,太大了。

    第二种迁移的场景是跨云迁移,跨公有云,跨Region,跨两个OpenStack的虚拟机迁移都是非常麻烦,甚至不可能的,因为公有云不提供虚拟机镜像的下载和上传功能,而且虚拟机镜像太大了,一传传一天。

    所以跨云场景下,混合云场景下,容器也是很好的使用场景。这也同时解决了仅仅私有云资源不足,扛不住流量的问题。
    #第三部分:容器的正确使用场景

    根据以上的分析,我们发现容器推荐使用在下面的场景下。

    1. 部署无状态服务,同虚拟机互补使用,实现隔离性

    2. 如果要部署有状态服务,需要对里面的应用十分的了解

    3. 作为持续集成的重要工具,可以顺利在开发,测试,生产之间迁移

    4. 适合部署跨云,跨Region,跨数据中心,混合云场景下的应用部署和弹性伸缩

    5. 以容器作为应用的交付物,保持环境一致性,树立不可变更基础设施的理念

    6. 运行进程基本的任务类型的程序

    7. 用于管理变更,变更频繁的应用使用容器镜像和版本号,轻量级方便的多

    8. 使用容器一定要管理好应用,进行health check和容错的设计

    展开全文
  •  Swift语言的 常量类型比C 语言的constants类型更强大,语义更加明确。  常量类型使用let 关键字进行声明,变量类型使用var 关键字进行声明。如 let maximumNumberOfLoginAttempts = 10 var ...

      

           一  、   常量和变量

                       Swift语言 对常量和变量的声明进行了明确的区分

             Swift语言的常量类型比C 语言的constants类型更加强大,语义更加明确。

             常量和变量的区别是常量在设置或初始化后其值不允许改变,而变量允许,除此之外,Swift语言中变量类型能使用的地方,常量类型也同样能使用,如作为输入参数等

             Swift语言中常量类型使用let 关键字进行声明,变量类型使用var 关键字进行声明,如

                                    let maximumNumberOfLoginAttempts =10
                   var currentLoginAttempt =0

             以上语句声明了一个名字为maximumNumberOfLoginAttempts的常量,其值为10;接着又声明了一个名字为currentLoginAttempt的变量,其初始值为0。

             如果一行中只有一条语句,语句最后就不需要带分号,如果一行中带有多条语句,语句之间就需要加分号,而最后一条不需要,这也体现和反应了Swift语言语法设计简洁和明确的思想。如以下语句所示:

     let cat = ""; println(cat)


             在Swift中常量和变量可以是任何类型。当声明一个常量或一个变量时,你可以为其提供一个类型标识,来清楚表示该常量或变量的类型。            声明语法是在常量和变量名字后面跟着一个冒号,接着跟着类型的名字。如下所示声明了一个类型为字符串类型(String)名字为welcomeMessage的变量。

     var welcomeMessage: String


     在作了以上声明后,welcomeMessage变量就可以设置为任何字符串值,如:

      welcomeMessage = “Hello"


       实际上你很少需要为一个变量声明或常量声明指定类型。如上面的对maximumNumberOfLoginAttempts常量和currentLoginAttempt变量的声明,Swift编译器能够根据声明中提供的初始值自动推断其为Int类型。

      如下语句Swift推断你想创建一个Double类型的常量。

      let pi =3.14159

      因次当你定义一个常量或变量时为其提供了一个初始值,就不再需要另外为其指定类型,Swift编译器可以从中推断出其类型。

       二、多样化的数字表示。

      Swift支持以8位、16位、32, 和64 位形式来表达一个有符号和无符号形式的整数类型。整数类型的命名遵从和C语言相似的约定,如UInt8代表一个8位无符号整数,Int32代表一个32位有符号整数,Swift建议你通常使用Swift定义的一个位数和和当前平台的本地字大小相同的Int整数类型,这样可以保持代码的一致性和互操作性,避免了不同整数类型之间的相互转换。

      Swift中整型常量数字的表达可以使用十进制、二进制、八进制、十六进制等多种形式: 

    let decimalInteger = 17        //十进制表达(没有前缀)

    let binaryInteger = 0b10001       // 二进制表达(0b开头)

    let octalInteger = 0o21           // 八进制表达(0o开头)

    let hexadecimalInteger = 0x11     // 十六进制表达(0x开头)

    Swift也提供Double(64位)和Float(32位)两种形式的浮点数支持。

    Swift中,浮点数常量数字的表达支持十进制(没有前缀)或十六进制(0x开头)两种形式,并支持不同的指数形式。如下所示:

    1.25e2 means 1.25 × 102, or125.0十进制表达形式(不带前缀,指数用大小写的e来指示)

     0xFp2 means 15 × 22, or60.0.    六进制表达形式(以0x开头,指数用大小写的p来指示)

    Swift中,为了使数字表达更加易读和自然,数字表达格式还能包含额外的信息,如整数和浮点数都能在前面添加额外的0以及在数字之间包含下划线。如下所示:

    let paddedDouble = 000123.456

    let oneMillion = 1_000_000

    let justOverOneMillion = 1_000_000.000_000_1

            

        三、 多元组


         多元组(Tuples)是Swift语言提供的一种新的数据类型,是一种多个数值的组合。一个多元组可以是任意类型甚至是不同类型数值的组合。
         如下所示:

      let http404Error = (404,"Not Found")

    该例子定义了一个描述HTTP状态码的多元组常量http404Error,该常量的类型为(Int, String)的多元组类型,其值为(404, "Not Found")。该多元组表示了一个整数和一个字符串的组合。

    当使用时,你可以从多元组中分解出每个分离的值。如下语句所示:

    let (statusCode,statusMessage) =http404Error

    println("The status code is\(statusCode)")

    // prints "The status code is 404"

    println("The status message is\(statusMessage)")

    // prints "The status message is Not Found"


    当你仅需要多元组的部分值时,可以使用符合‘_’来指示忽略的其它项:

    let (justTheStatusCode,_) =http404Error

    println("The status code is\(justTheStatusCode)")

    // prints "The status code is 404"

    另外还可以使用索引来存取一个多元组中的每个独立的项:

    println("The status code is\(http404Error.0)")

    // prints "The status code is 404"

    println("The status message is\(http404Error.1)")

    // prints "The status message is Not Found"

    当多元组定义时你还可以为多元组中的每一个独立项命名:

     let http200Status = (statusCode:200,description:“OK")

    然后你可以使用命名的元素名来存取这些元素的值: 

    println("The status code is\(http200Status.statusCode)")

    // prints "The status code is 200"

    println("The status message is\(http200Status.description)")

    // prints "The status message is OK”


    多元组主要用于函数的返回值,如果一个函数需要返回多个值,如一个函数需要返回上面描述的HTTP状态码,就可以定义和返回一个上面描述的的多元组。

    多元组类型提供了一种简洁的方式使一个函数可以返回多个不同类型的返回值。

    多元组适合于创建相关值的临时组合,而不适合于创建复杂的数据结构。


    四 、选项类型:


     选项类型是Swift语言提供的又一种强大的新的数据类型,用来表达一个可能存在也可能不存在的值类型。

          与选项类型能力最接近的是Objective-C语言中的一个可能返回一个对象,也可能返回一个nil的方法的使用。在Objective-C语言中nil意味着一个有效对象的不存在,但在Objective-C语言中nil仅能够工作于对象,而不能工作于结构以及其它基本C类型或者枚举类型。 对于这些类型值的不存在Objective-C语言用一个NSNotFound特殊值表示。

          而Swift的选项类型可以指示任何类型值的不存在,而不需要另外定义任何其它特殊值。

              如一个字符串类型可以使用其方法toInt来转换一个字符串的值为一个整数,可是,不是每一个字符串都能够转换为一个整数。如含有数字的字符串如"123" 可以进行转换,而不含数字的字符串如"hello, world" 则不能转换。这种情况如果使用选项类型就非常有用。

     如例子:

      let possibleNumber ="123"

     let convertedNumber =possibleNumber.toInt()

          由于toInt方法可能失败,因此编译器推断possibleNumber.toInt()返回的是一个optional Int类型。一个 optional Int 类型在语法上写作:Int?。选项类型的变量或常量指示该常量或变量允许没有值。

           ‘?’标识符指示该值是一个选项类型,意味着该类型值可以包含有效的整数值,也可能什么值也没有包含。

            Swift中你可以使用if语句来判断一个选项是否包含一个值。如果一个选项包含一个值,它被评估为true,否则评估为false。如果使用if语句评估一个选项包含了一个值,然后就能够在选项名字后面添加一个!来获取该选项的值。这在Swift语言中称作选项值的强制展开。

       如下例子展示了如何强制展开一个选项值:

     ifconvertedNumber{

       println("\(possibleNumber) has an integer value of \(convertedNumber!)")

    }else {

       println("\(possibleNumber) could not be converted to an integer")

    }

       需要强调的是,如果试图使用!来存取一个不存在的选项值会触发一个运行时错误,因此在使用!来存取选项值之前应该总是确保该选项值包含一个有效值,或者采用上面的方式进行判断。


       Swift中你还能够使用选项绑定来判断一个选项是否包含一个有效值。选项绑定作为if 或while语句的判断语句使用,在判断语句中先取出选项中的值并赋值给一个常量或变量,然后使用该常量或变量来检查和使用该选项中的值。

            如下例子展示了如何使用选项绑定来使用一个选项中的值。

          

       ifletactualNumber = possibleNumber.toInt() {

       println("\(possibleNumber) has an integer value of \(actualNumber)")

    }else {

       println("\(possibleNumber) could not be converted to an integer")

    }

       你可以通过为一个选项变量赋值为nil,来设置该选项变量为一个无值的状态:

          var serverResponseCode: Int? = 404

         选项变量serverResponseCode 包含一个实际的整数值404.

         serverResponseCode = nil

         现在serverResponseCode不包含值。

        如果你定义一个选项常量或变量,但没有为其提供默认值,该常量或变量自动被设置为nil。

        nil在Objective-C是一个代表不存在对象的指针,而在Swift中它代表一个确定类型值的不存在,并且在Swift中任意类型 的选项都可以设置为nil,而不仅仅是对象类型。
          

        一个选项类型在首次设置时,如果为其设置了一个有效值,由于这时选项类型的值程序是清楚的,并且如果在此后使用该选项的每个地方也能够确信该选项的值存在,这种类型的选项就可以定义为一个称为隐含的已展开的选项。

             一个隐含的已展开的选项在使用时不再需要使用条件语句来检查以及使用强制展开或选项绑定来取出选项中的值。

             你在选项类型的常量或变量后面放置一个‘!’符号标识来指示该选项是一个隐含的已展开的选项。

             隐含的已展开的选项通常在类的初始化期间使用。

        如下展示了如何定义一个隐含的已展开的选项及如何使用它:

    letassumedString:String! ="An implicitly unwrapped optional string."

    println(assumedString)  

       // no exclamation mark is needed to access its value

        当然需要注意的是如果试图存取一个不包含值的隐含的已展开的选项,仍然会触发一个运行时错误。


                                            版权所有,请转载时注明链接和出处,谢谢!

    展开全文
  • Redis 是 C 语言开发的一个开源的(遵从 BSD 协议)高性能**键值对**(key-value)的内存数据库,可以用作`数据库`、`缓存`、`消息中间件`等。...3.丰富的数据类型,支持字符串(strings)、散列(hashes)、列

    一、Redis简介

    Redis 是 C 语言开发的一个开源的(遵从 BSD 协议)高性能键值对(key-value)的内存数据库,可以用作数据库缓存消息中间件等。Redis是一种 NoSQL(not-only sql,泛指非关系型数据库)的数据库。

    Redis 作为一个内存数据库:
    1.性能优秀,数据在内存中,读写速度非常快,支持并发 10W QPS。
    2.单进程单线程,是线程安全的,采用 IO 多路复用机制。
    3.丰富的数据类型,支持字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
    4.支持数据持久化。
    5.可以将内存中数据保存在磁盘中,重启时加载。
    6.主从复制,哨兵,高可用。
    7.可以用作分布式锁。
    8.可以作为消息中间件使用,支持发布订阅。

    二、Redis的五种基本数据类型

    String

    String 是 Redis 最基本的类型,一个 Key 对应一个 Value。Value 不仅是 String,也可以是数字。String 类型是二进制安全的,意思是 Redis 的 String 类型可以包含任何数据,比如 jpg 图片或者序列化的对象。String 类型的值最大能存储 512M。
    String应用场景 :
    常规key-value缓存应用; 常规计数:微博数,粉丝数等。

    Hash

    Hash是一个键值(key-value)的集合。Redis 的 Hash 是一个 String 的 Key 和 Value 的映射表,Hash 特别适合存储对象。常用命令:hget,hset,hgetall 等。
    Hash应用场景 : 存储用户信息,商品信息等等

    List

    List 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边) 常用命令:lpush、rpush、lpop、rpop、lrange(获取列表片段)等。

    外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功 能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
    应用场景:List 应用场景非常多,也是 Redis 最重要的数据结构之一,比如 微博的关注列表,粉丝列表都可以用 List 结构来实现。
    数据结构:List 就是链表,可以用来当消息队列用。Redis 提供了 List 的 Push 和 Pop 操作,还提供了操作某一段的 API,可以直接查询或者删除某一段的元素。
    实现方式:Redis List 的是实现是一个双向链表,既可以支持反向查找和遍历,更方便操作,不过带来了额外的内存开销。

    Set

    Set 是 String 类型的无序集合。集合是通过 hashtable 实现的。Set 中的元素是没有顺序的,而且是没有重复的。常用命令:sdd、spop、smembers、sunion 等。
    Redis Set 对外提供的功能和 List 一样是一个列表,特殊之处在于 Set 是自动去重的。当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在 一 个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
    应用场景
    比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常 方 便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程

    Zset

    Zset 和 Set 一样是 String 类型元素的集合,且不允许重复的元素。常用命令:zadd、zrange、zrem、zcard 等。
    Sorted Set 可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择 Sorted Set 结构。
    和 Set 相比,Sorted Set关联了一个 Double 类型权重的参数 Score,使得集合中的元素能够按照 Score 进行有序排列,Redis 正是通过分数来为集合中的成员进行从小到大的排序。
    使用场景
    在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度 的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。

    实现方式:Redis Sorted Set 的内部使用 HashMap 和跳跃表(skipList)来保证数据的存储和有序,HashMap 里放的是成员到 Score 的映射。而跳跃表里存放的是所有的成员,排序依据是 HashMap 里存的 Score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

    在这里插入图片描述

    SpringBoot使用RedisTemplate简单操作Redis的五种数据类型

    三、Redis pom依赖、yml配置

      <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.2.RELEASE</version>
        </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    

    spring-boot-starter-data-redis:在 Spring Boot 2.x 以后底层不再使用 Jedis,而是换成了 Lettuce。
    commons-pool2:用作 Redis 连接池,如不引入启动会报错。
    spring-session-data-redis:Spring Session 引入,用作共享 Session。

    配置文件 application.yml 的配置:

    server:
      port: 8082
      servlet:
        session:
          timeout: 30ms
    spring:
      cache:
        type: redis
      redis:
        host: 127.0.0.1
        port: 6379
        password:
        # redis默认情况下有16个分片,这里配置具体使用的分片,默认为0
        database: 0
        lettuce:
          pool:
            # 连接池最大连接数(使用负数表示没有限制),默认8
            max-active: 100
    

    四、RedisTemplate 的使用方式

    默认情况下的模板只能支持 RedisTemplate<String, String>,也就是只能存入字符串,所以自定义模板很有必要。

    添加配置类 RedisCacheConfig.java

    package com.lsh.config;
    
    import org.springframework.boot.autoconfigure.AutoConfigureAfter;
    import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * @author :LiuShihao
     * @date :Created in 2021/2/18 9:52 上午
     * @desc :配置类 RedisCacheConfig
     * 默认情况下的模板只能支持 RedisTemplate<String, String>,也就是只能存入字符串,所以自定义模板很有必要。
     */
    @Configuration
    @AutoConfigureAfter(RedisAutoConfiguration.class)
    public class RedisCacheConfig {
    
        @Bean
        public RedisTemplate<String, String> redisCacheTemplate(LettuceConnectionFactory connectionFactory) {
            System.out.println("RedisTemplate加载...");
            RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
    //        template.setKeySerializer(new StringRedisSerializer());
    //        Caused by: java.lang.NoClassDefFoundError: com/fasterxml/jackson/core/JsonProcessingException
    //        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            RedisSerializer redisSerializer = new StringRedisSerializer();
            redisTemplate.setKeySerializer(redisSerializer);
            redisTemplate.setValueSerializer(redisSerializer);
            redisTemplate.setHashKeySerializer(redisSerializer);
            redisTemplate.setHashValueSerializer(redisSerializer);
            redisTemplate.setConnectionFactory(connectionFactory);
            return redisTemplate;
        }
    }
    
    

    五、使用 Spring Cache 集成 Redis

    Spring Cache 具备很好的灵活性,不仅能够使用 SPEL(spring expression language)来定义缓存的 Key 和各种 Condition,还提供了开箱即用的缓存临时存储方案,也支持和主流的专业缓存如 EhCache、Redis、Guava 的集成。

    缓存注解

    核心是三个注解:
    @Cachable
    @CachePut
    @CacheEvict

    @Cacheable

    示例:

    @Cacheable(value = "user", key = "#id")
    

    根据方法的请求参数对其结果进行缓存:
    Key:缓存的 Key,可以为空,如果指定要按照 SPEL 表达式编写,如果不指定,则按照方法的所有参数进行组合。
    Value:缓存的名称,必须指定至少一个(如 @Cacheable (value=‘user’)或者 @Cacheable(value={‘user1’,‘user2’}))
    Condition:缓存的条件,可以为空,使用 SPEL 编写,返回 true 或者 false,只有为 true 才进行缓存。

    @CachePut

    示例:

    @CachePut(value ="user", key = "#user.id")
    

    根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用。参数描述见上。

    @CacheEvict

    示例:

    @CacheEvict(value="user", key = "#id")
    

    根据条件对缓存进行清空:
    Key:同上。
    Value:同上。
    Condition:同上。
    allEntries:是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存。
    beforeInvocation:是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存。缺省情况下,如果方法执行抛出异常,则不会清空缓存。

    用缓存要注意,启动类要加上一个注解开启缓存:@EnableCaching

    package com.lsh;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cache.annotation.EnableCaching;
    
    /**
     * @author :LiuShihao
     * @date :Created in 2021/2/1 8:41 下午
     * @desc :
     */
    @EnableCaching
    @SpringBootApplication
    public class SpringDataApplication {
        public static void main(String[] args) {
            SpringApplication.run(SpringDataApplication.class);
        }
    }
    
    

    User

    @Data
    public class User  {
    
        private int id;
    
        private String name;
        
        private Integer age;
    
        public User(){
        }
        public User(int id,String name,int age){
            this.id = id;
            this.name = name;
            this.age = age;
        }
    }
    
    

    UserService

    public interface UserService {
        User save(User user);
    
        void delete(int id);
    
        User get(Integer id);
    
    }
    
    

    UserServiceImpl

    package com.lsh.service.impl;
    
    import com.lsh.entity.User;
    import com.lsh.service.UserService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.CachePut;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author :LiuShihao
     * @date :Created in 2021/2/1 9:08 下午
     * @desc :
     */
    @Slf4j
    @Service
    public class UserServiceImpl implements UserService {
        private static Map<Integer, User> userMap = new HashMap<>();
        static {
            userMap.put(1, new User(1, "刘德华", 25));
            userMap.put(2, new User(2, "李焕英", 26));
            userMap.put(3, new User(3, "唐人街探案", 24));
        }
    
        /**
         * 存入缓存
         * @param user
         * @return
         */
        @CachePut(value ="user", key = "#user.id")
        @Override
        public User save(User user) {
            userMap.put(user.getId(), user);
            log.info("进入save方法,当前存储对象:{}", user.toString());
            return user;
        }
    
        /**
         * 删除缓存
         * @param id
         */
        @CacheEvict(value="user", key = "#id")
        @Override
        public void delete(int id) {
            userMap.remove(id);
            log.info("进入delete方法,删除成功");
        }
    
        /**
         * 获得缓存
         * @param id
         * @return
         */
        @Cacheable(value = "user", key = "#id")
        @Override
        public User get(Integer id) {
            log.info("进入get方法,当前获取对象:{}", userMap.get(id)==null?null:userMap.get(id).toString());
            return userMap.get(id);
        }
    }
    
    

    测试类

    package com.lsh.repository;
    
    import com.lsh.entity.User;
    import com.lsh.service.UserService;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.test.context.junit4.SpringRunner;
    
    /**
     * @author :LiuShihao
     * @date :Created in 2021/2/18 9:52 上午
     * @desc :
     */
    @Slf4j
    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class RedisTest {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @Autowired
        private RedisTemplate<String, String> redisCacheTemplate;
    
        @Autowired
        private UserService userService;
    
        @Test
        public void test() {
            User user = new User();
            user.setId(1001);
            user.setName("张三");
            user.setBirthday("2021-02-18");
            redisCacheTemplate.opsForValue().set("userkey", user.toString() );
            String user1 = redisCacheTemplate.opsForValue().get("userkey");
            log.info("当前获取对象:{}", user1);
        }
    
        @Test
        public void testCaCheAdd() {
            User user = new User(4, "唐仁", 30);
            userService.save(user);
        }
        @Test
        public void testCaCheGet() {
    	    User user1 = userService.get(1);
    	    User user2 = userService.get(2);
    	    User user3 = userService.get(3);
            User user4 = userService.get(4);
            System.out.println(user1);
            System.out.println(user2);
            System.out.println(user3);
            System.out.println(user4);
        }
        @Test
        public void testCaCheDel() {
            userService.delete(4);
    
        }
    
    }
    
    

    六、缓存和数据库数据一致性问题

    分布式环境下非常容易出现缓存和数据库间数据一致性问题,针对这一点,如果项目对缓存的要求是强一致性的,那么就不要使用缓存。

    我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。

    合适的策略包括合适的缓存更新策略,更新数据库后及时更新缓存、缓存失败时增加重试机制。

    七、Redis 缓存雪崩

    一般缓存都是定时任务去刷新,或者查不到之后去更新缓存的,定时任务刷新就有一个问题。
    举个栗子:如果首页所有 Key 的失效时间都是 12 小时,中午 12 点刷新的,我零点有个大促活动大量用户涌入,假设每秒 6000 个请求,本来缓存可以抗住每秒 5000 个请求,但是缓存中所有 Key 都失效了。

    此时 6000 个/秒的请求全部落在了数据库上,数据库必然扛不住,真实情况可能 DBA(数据库管理员) 都没反应过来直接挂了。
    解决方法:
    处理缓存雪崩简单,在批量往 Redis 存数据的时候,把每个 Key 的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效。

    setRedis(key, value, time+Math.random()*10000;
    

    如果 Redis 是集群部署,将热点数据均匀分布在不同的 Redis 库中也能避免全部失效。
    或者设置热点数据永不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就好了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。

    缓存穿透

    缓存穿透是指缓存和数据库中都没有的数据,而用户(黑客)不断发起请求。
    解决方案:

    1. 查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短;
    2. 布隆过滤器(Bloom Filter):将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据 会被这个 bitmap 拦截掉,从而避免了对 DB 的查询。

    缓存击穿

    缓存击穿,跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了 DB。
    而缓存击穿不同的是缓存击穿是指一个 Key 非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存。
    解决方法:

    1. 设置热点数据永不过期,物理不过期,但逻辑过期(后台异步线程去刷新)。
    2. 加上互斥锁:当缓存失效时,不立即去load db,先使用如Redis的setnx去设置一个互斥锁,当操作成功返回时再进行load db的操作并回设缓存,否则重试get缓存的方法。
    public static String getData(String key) throws InterruptedException {
            //从Redis查询数据
            String result = getDataByKV(key);
            //参数校验
            if (StringUtils.isBlank(result)) {
                try {
                    //获得锁
                    if (reenLock.tryLock()) {
                        //去数据库查询
                        result = getDataByDB(key);
                        //校验
                        if (StringUtils.isNotBlank(result)) {
                            //插进缓存
                            setDataToKV(key, result);
                        }
                    } else {
                        //睡一会再拿
                        Thread.sleep(100L);
                        result = getData(key);
                    }
                } finally {
                    //释放锁
                    reenLock.unlock();
                }
            }
            return result;
        }
    

    下一章

    深入学习Redis_(二)淘汰策略、持久化机制、主从复制、哨兵模式等

    展开全文
  • MFC六大关键技术

    千次阅读 2007-03-24 22:33:00
    MFC六大关键技术之初始化过程我并不认为MFC减轻了程序员们的负担,MFC出现的目的虽然似乎是为了让程序员不用懂得太多就可以进行视窗编程,但本人在MFC里徘徊了很久很久(因为那时没有书本详细介绍MFC的原理),毫无...
  • 数据分析必会的六大实用模型

    万次阅读 2019-09-03 21:55:29
    细分:细分来源或不同的客户类型在转化率上的表现,发现一些高质量的来源或客户,通常用于分析网站的广告或推广的效果及ROI。 PEST模型 所谓的PEST,其实是指政治、经济、社会、技术这四个方面,本质上是通过对...
  • 用户研究是用户中心的设计流程中的第一步。它是一种理解用户,将他们的目标、需求与商业宗旨相匹配的理想方法,能够帮助企业定义产品的...六大用户分析方法论 1、行为事件分析 2、点击分析模型 3、用户行为路...
  • 之前我们对设计模式的六大原则做了简单归纳,这篇博客是对开放封闭原则进行的举例说明。 开放封闭原则的意义软件实体应该对扩展开放,对修改关闭,其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已...
  • 国内Android应用推广的六大主流方式

    千次阅读 2011-12-23 10:28:27
    国内Android应用推广的六大主流方式 http://mobi.baike.com/article-19433.html 随着Android市场份额的飞速增长,越来越多的国内开发团队和公司开始投入Android应用的开发;由于Android平台的开放性, 也决定了...
  • 六大设计原则--开闭原则

    千次阅读 多人点赞 2015-09-08 08:44:52
    可见视图是提供给客户使用的界面,如 jsp 程序,swing 界面等,该部分的变化一般会引起连锁反应(特别是在国内做项目,做欧美的外包项目一般不会影响太) ,如果仅仅是界面上按钮、文字的重新排布倒是简单,最...
  • PLC常见的六大应用 可乐的工控技术 摘要 : 似乎无所不能啊,都来看看PLC在控制系统中都扮演了什么!!! 这是一张典型的PLC控制系统的框图 1、用于开关量控制,PLC控制开关量的能力是很强的。所控制的入出点数,少的...
  • 设计模式六大原则之--开闭原则(OCP)

    万次阅读 多人点赞 2014-04-18 15:21:03
    一般的设计原则之所以强调方法参数尽量避免基本类型,原因正在于此。比较如下两个方法定义: 1. //定义1 2. bool Connect(string userName, string password, string ftpAddress, int port); 3. //定义2 4....
  • 可见视图是提供给客户使用的界面,该部分的变化一般会引起连锁反应(特别是在国内做项目,做欧美的外包项目一般不会影响太),如果仅仅是界面上按钮、文字的重新排布倒是简单,最司空见惯的是业务耦合变化,什么...
  • 嵌入式基本知识必备

    万次阅读 多人点赞 2016-08-18 10:24:16
    电路的阈值电平,基本上是二分之一的电源电压值,但要保证稳定的输出,则必须要求输入高电平> Vih,输入低电平对于一般的逻辑电平,以上参数的关系如下:  Voh > Vih > Vt > Vil > Vol  6:Ioh:...
  • 目前apm监控一般都遵循Google公司发布的Dapper规范,特转载一篇,供广大网友交流概述当代的互联网的服务,通常都是用复杂的、规模分布式集群来实现的。互联网应用构建在不同的软件模块集上,这些软件模块,有可能...
  • 一些基本的测试方法

    千次阅读 2018-08-09 16:55:22
    图描绘的质量属性的六大类和测试类型之间的关系,并没有深入到各个质量子属性和各个子属性对应的测试类型中去(大家不妨自己动手绘制一下“质量子属性”的车轮图)。 从“车轮图”中能够分析出产品测试的两个关键...
  • 第二章 5G三应用场景(外在服务) 2.1 eMBB 增强移动宽带 2.2 URLLC 超可靠低时延通信 2.3 mMTC 海量机器类通信 第三章 5G的四大特征 (内在) 3.1 泛在(网络自身的存在) 3.2 低功耗 3.3 网络虚拟化 3.4 ...
  • 遥感基本知识

    千次阅读 多人点赞 2018-05-10 09:44:45
     遥感的基本概念1. 遥感的基本知识“遥感”一词来自英语Remote Sensing,从字面上理解就是“遥远的感知”之意。顾名思义,遥感就是不直接接触物体,从远处通过探测仪器接受来自目标物体的电磁波信息,经过对信息...
  • javaScript需要掌握的基本知识

    千次阅读 2018-09-06 17:56:37
    (1)5中基本数据类型:number、string、boolean、undefined、null (2)1种复合类型:object 3.变量、函数、属性、函数参数命名规范 (1)、区分小写 (2)、第一个字符必须是字母、下划线(_)或者美元...
  • 算法交易的主要类型与策略分析

    千次阅读 2019-07-02 11:06:06
    该算法所具有的适应性特点主要体现在对市场价格的适应或反应。价格适应性落差算法实际上是一种更加倾向于机会导向的算法。 一个 主动实值策略(AIM) 是指当价格有利时交易更加主动,而当价格变得不利时交易变得...
  • 流处理基本介绍

    万次阅读 多人点赞 2017-01-23 10:14:56
    然而,用流和批次来定义数据集的时候就有问题了,因为如前所述,这就意味着用处理数据的引擎的类型来定义数据的类型。现实中,这两类数据的本质区别在于是否有限,因此用能体现出这个区别的词汇
  • 第二十章、MVP应用构架模式1.MVP介绍 MVP模式是MVC模式的一个演化版本,MVP全称Model-View-Presenter。目前MVP在Android应用开发中越来越重要了。 在Android中,业务逻辑和数据存取是紧紧耦合的,很多缺乏经验的...
  • 然后,我们考察了Linux内核如何实现系统调用,以及执行系统调用的连锁反应:陷入内核,传递系统调用号和参数,执行正确的系统调用函数,并把返回值带回用户空间。最后讨论了如何增加系统调用,并提供了从用户空间...
  • 机器学习基本概念

    万次阅读 多人点赞 2018-03-04 20:10:48
    假设我把时间作为自变量,譬如我发现小Y所有迟到的日子基本都是星期五,而在非星期五情况下他基本不迟到。于是我可以建立一个模型,来模拟小Y迟到与否跟日子是否是星期五的概率。见下图: 图3 决策树模型   这样的...
  • UML模型的基本概念

    千次阅读 2006-10-23 17:52:00
    第一章 UML模型的基本概念1 UML的建筑块 组成UML有三种基本的建筑块:1、事物(Things)2、关系(Relationships)3、图(Diagrams)事物是UML中重要的组成部分。关系把事物紧密联系在一起。图是很多有相互相关的...
  • 这一点是不会改变的,但是2015年9月创始人 Guido van Rossum 在 Python 3.5 引入了一个类型系统,允许开发者指定变量类型。它的主要作用是方便开发,供IDE 和各种开发工具使用,对代码运行不产生影响,运行时会过滤...
  • 硬件基本知识

    万次阅读 2005-10-24 11:14:00
    所以先了解一些主板的基本知识对大家攒机是大有裨益的。下面, 我就把主板常用的一些术语简单的给大家解释一下。 大家喜欢将CPU比作电脑的大脑或心脏,那么电脑主板就可称为电脑的神经系统。主板是一种高科技、高...
  • 实验项目 图结构基本操作的实现 课程名称:数据结构 实验项目名称:图结构基本操作的实现 实验目的: 1.掌握图的基本操作—遍历。 实验要求: 1、分别用DFS和BFS的方法实现一个无向图的遍历。 实验...
  • 深度学习基本概念

    万次阅读 2016-04-13 13:59:36
    、浅层学习(Shallow Learning)和深度学习(Deep Learning)  浅层学习是 机器学习 的第一次浪潮。  20世纪80年代末期,用于人工神经网络的反向传播算法(也叫Back Propagation算法或者BP算法)的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 27,283
精华内容 10,913
关键字:

六大基本反应类型