面向对象的六大原则(二)
- 昨天看书学习了六大原则的前两个,单一职责原则和开闭原则,假若不太了解的同僚可以去这里参观一下
- 今天我们接着往下看,先来瞧瞧里氏替换原则
里氏替换原则
- 定义:如果对每一个类型为S的对象O1,都有类型为T的对象O2,使得以T定义的所有程序P在所有的对象O1都替换成O2时,程序P的行为没有发生变化,那么类型S就是类型T的子类型;
- 阿西吧,定义太冠冕堂皇了,用白话文说就是所有用到父类 的地方,都可以将父类换成子类,而保证程序的功能不会发生变化
- 我们知道,面向对象的语言三大特点是继承,封装,多态,里氏替换原则就是依赖于继承和多态这两大特性,也就是说子类能干父类干的所有事,但是父类就不一定能干之类的所有事
- 这里我们用Window和View的关系举个例子
了解
- 先看看代码吧
- 注:这里全都是自己定义的类,而非是用的系统给的类
- 先写个Window类
//先写个Window类
public class Window {
public void show(View child){
child.draw();
}
}
//再写个View抽象类
public abstract class View {
public abstract void draw();
public void measure(int width,int height){
//测量视图大小
}
}
//然后实现两个View的子类
public class TextView extends View {
@Override
public void draw() {
//绘制文本
}
}
public class ImageView extends View {
@Override
public void draw() {
//绘制图片
}
}
- 可以看到,在Window里面,我们通过传入View父类的类型来做draw这个方法,这样一来,我们就可以任意去写继承父View的子View了,而这些子View又都分别能干Window想干的事情,通过里式替换,就可以自定义各种各样的View,然后传给Window,Window负责组织View,并且将View显示到屏幕上
- 其实这个样子就是里氏替换原则
- 里式替换原则的核心原理是抽象,抽象又依赖于继承这个特性,在OOP当中,继承的优缺点相当明显
优点 | 缺点 |
---|---|
代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性 | 继承是可侵入性的,只要继承就必须拥有父类的所有属性和方法 |
子类与父类基本相似,但又与父类有所区别 | 可能造成子类代码冗余,灵活度降低,因为子类必须拥有子类的属性和方法 |
提高代码的可扩展性 |
- 那么由此可以看出,我们在昨天分析单一职责原则和开闭原则的时候写的图片缓存那里也是满足了里氏替换原则
依赖倒置原则
- 依赖倒置原则指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了
这里面有几个关键点
- 高层模块不应该依赖于低层次模块,两者都应该依赖其抽象
- 抽象不应该依赖细节
- 细节应该依赖于抽象
在java语音中,抽象指的是抽象类或者接口,两者都不可被直接实例化,细节就是实现类,实现接口或者实现抽象类而产生的类就是细节,其特点就是,可以直接被实例化
- 高层模块指的就是调用端,底层模块就是具体实现类,依赖倒置原则在java中的表现是:模块间的依赖通过抽象产生,实现类之间不发生直接依赖关系
- 这里我直接贴一下昨天的代码,有兴趣的小伙伴可以去文章顶部看看昨天的博客
- 假设我们需要图片加载与缓存,于是写一个图片缓存的接口
public interface IImageCache {
public Bitmap get(String url);
public void put(String url,Bitmap bitmap);
}
- 然后依赖于这个抽象接口书写我们的加载类
public class ImageLoader {
private final static String TAG = "ImageLoader";
//默认为内存缓存
IImageCache mImageCache = new MemeryCache();
//线程池,线程数量为CPU的数量
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
//UI Handler
Handler mUIHandler = new Handler(Looper.getMainLooper());
//外部注入缓存
public void setImageCache(IImageCache mImageCache){
this.mImageCache = mImageCache;
}
public void displayImage(final String url, final ImageView imageView){
//直接来获取缓存
Bitmap bitmap = mImageCache .get(url);
if(bitmap != null){
Log.d(TAG, "displayImage: 获取到缓存");
imageView.setImageBitmap(bitmap);
return;
}
//如果没有缓存,就去线程池中请求下载网络图片
submitLoadRequest(url,imageView);
}
private void submitLoadRequest(final String url, final ImageView imageView) {
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if(bitmap == null){
Log.d(TAG, "run: 网络图片下载失败");
return;
}
if (imageView.getTag().equals(url)){
updataImageView(imageView,bitmap);
}
//设置缓存
mImageCache.put(url,bitmap);
}
});
}
//更新UI
private void updataImageView(final ImageView imageView,final Bitmap bmp){
mUIHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bmp);
}
});
}
//下载网络图片
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;
}
}
- 然后比方说我们自己写一个缓存的实现类
public class MemeryCache implements IImageCache{
//图片缓存
LruCache<String,Bitmap> mMemeryCache;
public MemeryCache() {
initImageCache();
}
private void initImageCache(){
//计算可使用的最大内存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory()/1024);
//取四分之一的可用内存作为缓存
final int cacheSize = maxMemory / 4;
mMemeryCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
@Override
public void put(String url , Bitmap bitmap){
mMemeryCache.put(url,bitmap);
}
@Override
public Bitmap get(String url){
return mMemeryCache.get(url);
}
}
- 在使用的时候这个样子
String url = "http://img2.imgtn.bdimg.com/it/u=4060216298,1329589408&fm=27&gp=0.jpg";
mImageView = findViewById(R.id.main_IV);
ImageLoader loader = new ImageLoader();
DoubleCache doubleCache = new DoubleCache();
loader.setImageCache(doubleCache);
loader.displayImage(url,mImageView);
- 可见,我们完全是依赖于抽象,而具体实现细节之间并没有联系
- 这个也就是里氏替换原则的用法了
接口隔离原则
- 定义:客户端不应该依赖他不需要的接口,接口隔离的原则是将非常庞大的,臃肿的接口拆分成更小的更具体的接口,这样客户端只需要知道他们感兴趣的方法,从而使系统解开耦合,更容易重构
- 先来看一个简单地例子吧
- 我们可能都写过这样的代码
try{
String s = cacheDir + creatFileName(url);
fileOutputStream = new FileOutputStream(s);
bmp.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
if (fileOutputStream != null){
try{
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 本来只是一两句的代码,却因为流文件要用完关闭,需要异常处理等等,使得我们的代码可读性变得很差,我们看看怎么去解决这个问题
- 在java中有一个Closeable接口,该接口标识了一个可关闭的对象,它只有一个close方法
- 基本上所有的流文件相关的类都实现了这个接口,我们上面代码中的fileOutputStream类也是,这就表示,实现了这个接口的类在使用 的时候必须对关闭是否进行处理,况且一般我们见到的好些个类都是需要处理异常,或者这样那样的操作,我们看看能有什么便捷的方法来解决
- 先写一个工具类CloseUtils
public final class CloseUtils{
private CloseUtils(){
//关闭closeable对象
}
private static void closeQuietly(Closeable closeable){
if(nulll != closeable){
try{
closeable.close();
}catch(IOException e){
e.printStackTrance();
}
}
}
}
- 可以看到,我们将这个接口包装成了一个方法,这个方法专门用来关闭实现了这个接口的对象
- 那么接下来,我们上面的方法就可以写成
try{
String s = cacheDir + creatFileName(url);
fileOutputStream = new FileOutputStream(s);
bmp.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
CloseUtils.closeQuietly(fileOutputStream);
}
- 不知道大家看明白了没?很简单的使用它们实现的closeable接口就将原来的必须要写的三四句代码变成一句
- 而且因为这个接口只是表征这些类是可关闭的,并没有体现出这些类其他的特性,而在这里,我们也不用去关心别的
- 这就是接口隔离原则
迪米特原则
- 也称最少知识原则:他的定义是:一个对象应该对其他对象有最少的了解,。粗俗的讲,一个类应对自己需要耦合或者需要调用的类知道的越少越好,类的内部如何实现与调用者或者依赖这没关系,调用者或者依赖者只需要知道他所需要的方法即可,其他的一概不管,
- 下面我们就举个例子来说明一下这个原则:
- 假设我们在外头租房的时候,如果人生地不熟,更快的方式是通过中介找房,我们设定的情况为:我只要求房间的面积和租金,其他的一概不管,
- 那么先来写一个房子类,这个房子我们需要关注的信息是面积和房租
public class Room {
public float arae;
public float price;
public Room(float arae, float price) {
this.arae = arae;
this.price = price;
}
@Override
public String toString() {
return " 房子 [ 价格 :" + price + ",面积: " + arae +" ] ";
}
}
- 接下来跟我们交互的就是中介了,看看他怎么给我们推荐房子吧
public class Mediator {
List<Room> mRoomList = new ArrayList<>();
public Mediator(){
for (int i = 0 ; i < 5 ; i ++){
mRoomList.add(new Room(14 + i,(14 +i) * 40));
}
}
public List<Room> getRoomList() {
return mRoomList;
}
}
- 代码简单,我们接着写我们自己与中介交互的类
public class Tenant {
private final static String TAG = "Tenant";
public void rentRoom(float roomArea,float roomPrice,Mediator mediator){
List<Room> roomList = mediator.getRoomList();
for (int i = 0 ; i < roomList.size(); i++){
if(isSuitable(roomArea,roomPrice,roomList.get(i))){
Log.d(TAG, "rentRoom: 嗯,找到适合我的房子了 == " + roomList.get(i));
}
}
}
private boolean isSuitable(float area , float price,Room room){
return room.arae >= area && room.price <= price;
}
}
- 虽然代码简单,但是我们仔细分析一下就不难发现,耦合度简直太高了,作为客户的我们竟然需要知道这么多的信息才能租到称心如意的房子,而在顾客就是上帝的现今社会,这种服务态度的中介怎么混的下去?
- 那么便改吧,首先我们不想知道Room的具体东西,房子是否适合我们应该是中介帮我们来选的,作为上帝的我们,只需要提供想要的房子的价格和面积即可
- 看看重构后的用户类吧
public class Tenant {
private final static String TAG = "Tenant";
public void rentRoom(float area,float price,Mediator mediator){
Room room = mediator.rentRoomOut(area,price);
if(room != null){
Log.d(TAG, "rentRoom: 租到房子啦 == " + room.toString());
}else {
Log.d(TAG, "rentRoom: 哎呀,你的条件太苛刻了呢");
}
}
}
- 是不是简洁许多,我们再看一下中介怎么做的
public class Mediator {
private final static String TAG = "Mediator";
List<Room> mRoomList = new ArrayList<>();
public Mediator(){
for (int i = 0 ; i < 5 ; i ++){
mRoomList.add(new Room(14 + i,(14 +i) * 40));
}
}
public Room rentRoomOut(float roomArea,float roomPrice){
for (int i = 0 ; i < mRoomList.size(); i++){
Room room = mRoomList.get(i);
if(isSuitable(roomArea,roomPrice,room)){
Log.d(TAG, "rentRoom: 嗯,找到适合你的房子了 == " + room.toString());
return room;
}
}
return null;
}
private boolean isSuitable(float area , float price,Room room){
return room.arae >= area && room.price <= price;
}
}
- 对嘛,像挑选房子这种事,就应该交给中介去做
- 以上就是这个原则的基本方式了,是不是看起来一点也不难呢?其实就是这样,只不过代码多了之后我们需要慧眼识金就好啦,适当的运用这些六大原则会使我们的代码越来越漂亮
总结
- 到这里六大原则就说完了,其实也不难,关键在于适当合理的运用