1.什么是ContentProvider

 数据库在Android其中是私有的,当然这些数据包含文件数据和数据库数据以及一些其它类型的数据。

 不能将数据库设为WORLD_READABLE,每一个数据库都仅仅能创建它的包訪问,

 这意味着仅仅有由创建数据库的进程可訪问它。假设须要在进程间传递数据,

 则能够使用AIDL/Binder或创建一个ContentProvider,可是不能跨越进程/包边界直接来使用数据库。

 一个Content Provider类实现了一组标准的方法接口,从而可以让其它的应用保存或读取此Content Provider的各种数据类型。

 也就是说,一个程序能够通过实现一个Content Provider的抽象接口将自己的数据暴露出去。

 外界根本看不到,也不用看到这个应用暴露的数据在应用其中是怎样存储的,或者是用数据库存储还是用文件存储,还是通过网上获得,这些一切都不重要,

 重要的是外界能够通过这一套标准及统一的接口和程序里的数据打交道,能够读取程序的数据,也能够删除程序的数据,

 当然,中间也会涉及一些权限的问题。下边列举一些较常见的接口,这些接口例如以下所看到的。

·  query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder):通过Uri进行查询,返回一个Cursor。

·  insert(Uri url, ContentValues values):将一组数据插入到Uri 指定的地方。

·  update(Uri uri, ContentValues values, String where, String[] selectionArgs):更新Uri指定位置的数据。

·  delete(Uri url, String where, String[] selectionArgs):删除指定Uri而且符合一定条件的数据。

2.什么是ContentResolver

外界的程序通过ContentResolver接口能够訪问ContentProvider提供的数据,在Activity其中通过getContentResolver()能够得到当前应用的 ContentResolver实例。

 ContentResolver提供的接口和ContentProvider中须要实现的接口相应,主要有下面几个。

·  query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder):通过Uri进行查询,返回一个Cursor。

·  insert(Uri url, ContentValues values):将一组数据插入到Uri 指定的地方。

·  update(Uri uri, ContentValues values, String where, String[] selectionArgs):更新Uri指定位置的数据。

·  delete(Uri url, String where, String[] selectionArgs):删除指定Uri而且符合一定条件的数据。

3.ContentProvider和ContentResolver中用到的Uri

在ContentProvider和 ContentResolver其中用到了Uri的形式通常有两种,一种是指定所有数据,还有一种是指定某个ID的数据。

我们看以下的样例。

·  content://contacts/people/  这个Uri指定的就是所有的联系人数据。

·  content://contacts/people/1 这个Uri指定的是ID为1的联系人的数据。 

在上边两个类中用到的Uri一般由3部分组成。

·  第一部分是方案:"content://" 这部分永远不变

·  第二部分是授权:"contacts"

·  第二部分是路径:"people/","people/1"(假设没有指定ID,那么表示返回所有)。

因为URI通常比較长,并且有时候easy出错,且难以理解。所以,在Android其中定义了一些辅助类,并且定义了一些常量来取代这些长字符串的使用,比例如以下边的代码:

·  Contacts.People.CONTENT_URI (联系人的URI)。

在我们的实例MyProvider中是例如以下定义的:

public static final String AUTHORITY="com.teleca.PeopleProvider";

public static final String PATH_SINGLE="people/#";

public static final String PATH_MULTIPLE="people";

public static final Uri content_URI=Uri.parse("content://"+AUTHORITY+"/"+PATH_MULTIPLE);

实例1:

文件MyProvider.java

package com.teleca.provider;

import java.util.HashMap;

import android.content.ContentProvider;

import android.content.ContentUris;

import android.content.ContentValues;

import android.content.Context;

import android.content.UriMatcher;

import android.database.Cursor;

import android.database.SQLException;

import android.database.sqlite.SQLiteDatabase;

import android.database.sqlite.SQLiteOpenHelper;

import android.database.sqlite.SQLiteQueryBuilder;

import android.net.Uri;

import android.text.TextUtils;

import android.util.Log;

public class MyProvider extends ContentProvider {

 public static final String MIME_DIR_PREFIX="vnd.android.cursor.dir";

 public static final String MIME_ITEM_PREFIX="vnd.android.cursor.item";

 public static final String MIME_ITEM="vnd.msi.people";

 public static final String MIME_TYPE_SINGLE=MIME_ITEM_PREFIX+"/"+MIME_ITEM;

 public static final String MIME_TYPE_MULTIPLE=MIME_DIR_PREFIX+"/"+MIME_ITEM;

 public static final String AUTHORITY="com.teleca.PeopleProvider";

 public static final String PATH_SINGLE="people/#";

 public static final String PATH_MULTIPLE="people";

 public static final Uri content_URI=Uri.parse("content://"+AUTHORITY+"/"+PATH_MULTIPLE);

 public static final String DEFAULT_SORT_ORDER="name DESC";

 public static final String _ID="_id";

 public static final String NAME="name";

 public static final String PHONE="phone";

 public static final String AGE="age";

 public static final int PEOPLE=1;

 public static final int PEOPLES=2;

 private static UriMatcher URI_MATCHER;

 private static HashMap<String,String> PROJECTION_MAP;

 public static String DB_NAME="peopledb";

 public static String DB_TABLE_NAME="people";

 SQLiteDatabase db;

 DBOpenHelper dbOpenHelper;

 static 

 {

  URI_MATCHER=new UriMatcher(UriMatcher.NO_MATCH);

  URI_MATCHER.addURI(AUTHORITY, PATH_MULTIPLE, PEOPLES);

  URI_MATCHER.addURI(AUTHORITY, PATH_SINGLE, PEOPLE);

  PROJECTION_MAP=new HashMap<String,String>();

  PROJECTION_MAP.put(_ID, "_id");

  PROJECTION_MAP.put(NAME, "name");

  PROJECTION_MAP.put(PHONE, "phone");

  PROJECTION_MAP.put(AGE, "age");

 }

 @Override

 public int delete(Uri uri, String selection, String[] selectionArgs) {

  // TODO Auto-generated method stub

  int count=0;

  switch(URI_MATCHER.match(uri))

  {

  case PEOPLES:

   count=db.delete(DB_TABLE_NAME,  selection, selectionArgs);

   break;

  case PEOPLE:

   String segment =uri.getPathSegments().get(1);

   String where="";

   if(!TextUtils.isEmpty(selection))

   {

    where=" AND ("+selection+")";

   }

   count=db.delete(DB_TABLE_NAME, "_id="+segment+where, selectionArgs);

   break;

  default:

   throw new IllegalArgumentException("Unkonw URI"+uri);

  }

  getContext().getContentResolver().notifyChange(uri, null);//@2

  return count;

 }

 @Override

 public String getType(Uri uri) {

  // TODO Auto-generated method stub

  switch(URI_MATCHER.match(uri))

  {

  case PEOPLES:

   return MIME_TYPE_MULTIPLE;

  case PEOPLE:

   return MIME_TYPE_SINGLE;

  default:

   throw new IllegalArgumentException("Unkown URI "+uri);

  }

 }

 @Override

 public Uri insert(Uri uri, ContentValues values) {

  // TODO Auto-generated method stub

  long rowId=0L;

  if(URI_MATCHER.match(uri)!=PEOPLES)

  {

   throw new IllegalArgumentException("Unkown URI"+uri);

  }

  rowId=db.insert(DB_TABLE_NAME, null, values);

  if(rowId>0)

  {

   Uri result=ContentUris.withAppendedId(content_URI, rowId);

   getContext().getContentResolver().notifyChange(result, null);//@2

   return result;

  }

  else

   throw new SQLException("Failed to insert row into "+uri);

 }

 @Override

 public boolean onCreate() {

  // TODO Auto-generated method stub

  dbOpenHelper=new DBOpenHelper(this.getContext(),DB_NAME,1);

  db=dbOpenHelper.getWritableDatabase();

  return false;

 }

 @Override

 public Cursor query(Uri uri, String[] projection, String selection,

   String[] selectionArgs, String sortOrder) {

  // TODO Auto-generated method stub

  SQLiteQueryBuilder queryBuilder=new SQLiteQueryBuilder();

  queryBuilder.setTables(DBInfo.DB_TABLE_NAME);

  queryBuilder.setProjectionMap(PROJECTION_MAP);

  switch(URI_MATCHER.match(uri))

  {

  case PEOPLES:

   break;

  case  PEOPLE:

   queryBuilder.appendWhere("_id="+uri.getPathSegments().get(1));

   break;

  default:

   throw new IllegalArgumentException("Unkonw URI"+uri);

  }

  String orderBy=null;

  if(TextUtils.isEmpty(sortOrder))

  {

   orderBy=DEFAULT_SORT_ORDER;

  }

  else

   orderBy=sortOrder;

  Cursor c=queryBuilder.query(db, projection, selection, selectionArgs, null, null, orderBy);

  c.setNotificationUri(getContext().getContentResolver(), uri);//@1

  return c;

 }

 @Override

 public int update(Uri uri, ContentValues values, String selection,

   String[] selectionArgs) {

  // TODO Auto-generated method stub

  int count=0;

  switch(URI_MATCHER.match(uri))

  {

  case PEOPLES:

   count=db.update(DB_TABLE_NAME, values, selection, selectionArgs);

   break;

  case PEOPLE:

   String segment =uri.getPathSegments().get(1);

   String where="";

   if(!TextUtils.isEmpty(selection))

   {

    where=" AND ("+selection+")";

   }

   count=db.update(DB_TABLE_NAME, values, "_id="+segment+where, selectionArgs);

   break;

  default:

   throw new IllegalArgumentException("Unkonw URI"+uri);

  }

  getContext().getContentResolver().notifyChange(uri, null);//@2

  return count;

 }

}

class DBOpenHelper extends SQLiteOpenHelper

{

 private static final String DB_CREATE="CREATE TABLE "

  +DBInfo.DB_TABLE_NAME

  +" (_id INTEGER PRIMARY KEY,name TEXT UNIQUE NOT NULL,"

  +"phone TEXT,age INTEGER);";

 final static String tag="hubin";

 public DBOpenHelper(Context context,String dbName,int version)

 {

  super(context,dbName,null,version);

 }

 public void onCreate(SQLiteDatabase db)

 {

  try{

   db.execSQL(DB_CREATE);

  }

  catch(SQLException e )

  {

   Log.e(tag,"",e);

  }

 }

 public void onOpen(SQLiteDatabase db)

 {

  super.onOpen(db);

 }

 public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion)

 {

  db.execSQL("DROP TABLE IF EXISTS "+DBInfo.DB_TABLE_NAME);

  this.onCreate(db);

 }

}

class DBInfo

{

 public static String DB_NAME="peopledb";

 public static String DB_TABLE_NAME="people"; 

}

注意1:c.setNotificationUri(getContext().getContentResolver(), uri);

这里是把Cursor C加入到ContentResolver的监督对象组中去。

一旦有与uri相关的变化,ContentResolver就回通知Cursor C.

可能Cursor有个私有的内部类ContentObserver的实现。ContentResolver是通过该类来通知Cursor的。

public abstract void  setNotificationUri  (ContentResolver  cr, Uri  uri)

Register to watch a content URI for changes. This can be the URI of a specific data row (for example, "content://my_provider_type/23"), 

or a a generic URI for a content type.

Parameters

cr  The content resolver from the caller's context. The listener attached to this resolver will be notified.

uri  The content URI to watch. 

注意2: getContext().getContentResolver().notifyChange(uri, null)

通知数据发生了变化。

  public void  notifyChange  (Uri  uri, ContentObserver  observer)

Notify registered observers that a row was updated. To register, call registerContentObserver(). By default, CursorAdapter objects will get this notification.

Parameters

observer  The observer that originated the change, may be null 

这里为null的意思可能就是调用在ContentResolver中注冊的ContentObserver,反之则是调用參数指定的

文件People.java

package com.teleca.provider;

public class People {

 public long id;

 public String name;

 public String phone;

 public int age;

}

文件

Hello.java

package com.teleca.provider;

import java.util.ArrayList;

import java.util.List;

import android.app.Activity;

import android.content.ContentUris;

import android.content.ContentValues;

import android.database.Cursor;

import android.database.SQLException;

import android.net.Uri;

import android.os.Bundle;

import android.os.Handler;

import android.util.Log;

import android.view.View;

import android.view.View.OnClickListener;

import android.widget.Button;

public class Hello extends Activity {

    /** Called when the activity is first created. */

 final static String tag="hubin";

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

  Button button = (Button) findViewById(R.id.Button01);

  OnClickListener listener = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_ADD;

    doAction();

   }

  };

  button.setOnClickListener(listener);

  Button button2 = (Button) findViewById(R.id.Button02);

  OnClickListener listener2 = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_UPDATE;

    doAction();

   }

  };

  button2.setOnClickListener(listener2);

  Button button3 = (Button) findViewById(R.id.Button03);

  OnClickListener listener3 = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_QUERY;

    doAction();

   }

  };

  button3.setOnClickListener(listener3);

  Button button4 = (Button) findViewById(R.id.Button04);

  OnClickListener listener4 = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_QUERY_ALL;

    doAction();

   }

  };

  button4.setOnClickListener(listener4);

  Button button5 = (Button) findViewById(R.id.Button05);

  OnClickListener listener5 = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_DELETE;

    doAction();

   }

  };

  button5.setOnClickListener(listener5);

  Button button6 = (Button) findViewById(R.id.Button06);

  OnClickListener listener6 = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_DELETE_ALL;

    doAction();

   }

  };

  button6.setOnClickListener(listener6);

  mHandler = new Handler();

    }

 int cnt = 0;

 private Handler mHandler;

 int cmd = 0;

 final int CMD_ADD = 1;

 final int CMD_UPDATE = 2;

 final int  CMD_QUERY= 3;

 final int CMD_QUERY_ALL = 4;

 final int CMD_DELETE = 5;

 final int CMD_DELETE_ALL = 6;

 People people=new People();

 final static String projection[]=new String[]

                                       {"_id","name","phone","age"};

 class DatabaseThread implements Runnable {

  public void run() {

   if (cmd == CMD_ADD) {

    people.name="robin"+System.currentTimeMillis()%100;

    people.phone=""+System.currentTimeMillis();

    people.age=1;

    ContentValues values=new ContentValues();

    values.put("name", people.name);

    values.put("phone", people.phone);

    values.put("age", people.age);

    Uri uri=getContentResolver().insert(MyProvider.content_URI, values);

    people.id=ContentUris.parseId(uri);

    Log.i("hubin",uri.toString());

   } else if (cmd == CMD_UPDATE) {

    ContentValues values=new ContentValues();

    people.phone=""+System.currentTimeMillis();

    values.put("phone", people.phone);

    Uri uri=ContentUris.withAppendedId(MyProvider.content_URI, people.id);

    getContentResolver().update(uri,values,null,null);

   } else if (cmd == CMD_QUERY) {

    Uri uri=ContentUris.withAppendedId(MyProvider.content_URI, people.id);

    Cursor c=getContentResolver().query(uri, projection, null, null, null);

    People p=get(c);

    printPeople(p);

   } else if (cmd == CMD_QUERY_ALL) {

    Uri uri=MyProvider.content_URI;

    Cursor c=getContentResolver().query(uri, projection, null, null, null);

    List<People> list=getAll(c);

    int total=list.size();

    for(int i=0;i<total;i++)

    {

     printPeople(list.get(i));

    }

   }

   else if (cmd==CMD_DELETE)

   {

    Uri uri=ContentUris.withAppendedId(MyProvider.content_URI, people.id);

    getContentResolver().delete(uri, null, null);

   }

   else if (cmd==CMD_DELETE_ALL)

   {

    Uri uri=MyProvider.content_URI;

    getContentResolver().delete(uri, null, null);

   }

   cnt++;

  }

 }

 void printPeople(People p)

 {

 Log.i(tag, "id:"+p.id);

 Log.i(tag, "name:"+p.name);

 Log.i(tag,"phone:"+p.phone);

 Log.i(tag,"age:"+p.age);

 }

 DatabaseThread dataDealer=new DatabaseThread();

 void doAction() {

  mHandler.post(dataDealer);

 }

 public People get(Cursor c)

 {

  People people=new People();

  try{

   Log.i(tag,"count:"+c.getCount());

   if(c.getCount()>0)

   {

    c.moveToFirst();

    people=new People();

    people.id=c.getLong(0);

    people.name=c.getString(1);

    people.phone=c.getString(2);

    people.age=c.getInt(3);

   }

  }catch(SQLException e)

  {

   Log.i(tag,"",e);

  }

  finally

  {

   if(c!=null&&!c.isClosed())

   {

    c.close();

   }

  }

  return people;

 }

 public List<People> getAll(Cursor c)

 {

  ArrayList<People> ret=new ArrayList<People>();

  try

  {

   int count=c.getCount();

   c.moveToFirst();

   People people;

   for(int i=0;i<count;i++)

   {

    people=new People();

    people.id=c.getLong(0);

    people.name=c.getString(1);

    people.phone=c.getString(2);

    people.age=c.getInt(3);

    ret.add(people);

    c.moveToNext();

   }

   

  }catch(SQLException e)

  {

   Log.i(tag,"",e);

  }

  finally

  {

   if(c!=null&&!c.isClosed())

   {

    c.close();

   }

  }

  return ret;

 }

}

注意:Cursor c不用时要关掉。

文件AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

      package="com.teleca.provider"

      android:versionCode="1"

      android:versionName="1.0">

    <application android:icon="@drawable/icon" android:label="@string/app_name">

        <activity android:name=".Hello"

                  android:label="@string/app_name">

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>

    <provider android:syncable="true" android:name="MyProvider" android:authorities="com.teleca.PeopleProvider"></provider>

</application>

    <uses-sdk android:minSdkVersion="7" />

</manifest>

list_row.xml文件 
<?xml version="1.0" encoding="utf-8"?> 
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" android:id="@+id/list_row"
/>
main.xml文件
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > 
<ListView android:id="@id/android:list" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#00FF00" android:layout_weight="1" android:drawSelectorOnTop="false"/> 
<Button android:text="@+string/Add" android:id="@+id/Button01" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
<Button android:text="@+string/DeleteAll" android:id="@+id/Button02" android:layout_width="wrap_content"></Button>
</LinearLayout>
须要的权限:
<uses-permission android:name="android.permission.READ_CONTACTS" />
 <uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_CONTACTS"></uses-permission>

真的须要这些权限?为什么须要呢?或许是我曾经写错了吧

ContentProvider简单介绍的更多相关文章

  1. Intent的作用和表现形式简单介绍

    Intent的作用和表现形式简单介绍 1.描写叙述:Intent负责相应用中一次操作的动作,动作涉及的数据,附加数据进行描写叙述.系统或者应用依据此Intent的描写叙述,负责找到相应的组件,将Int ...

  2. ANDROID培训准备资料之四大组件的简单介绍

    Android四大组件是一个android app 最基本的组成部分,这篇博客主要给大家简单的介绍一下四种组件 (1)Activities (2)Services (3)BroadcastReceiv ...

  3. [原创]关于mybatis中一级缓存和二级缓存的简单介绍

    关于mybatis中一级缓存和二级缓存的简单介绍 mybatis的一级缓存: MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候 ...

  4. 利用Python进行数据分析(7) pandas基础: Series和DataFrame的简单介绍

    一.pandas 是什么 pandas 是基于 NumPy 的一个 Python 数据分析包,主要目的是为了数据分析.它提供了大量高级的数据结构和对数据处理的方法. pandas 有两个主要的数据结构 ...

  5. 利用Python进行数据分析(4) NumPy基础: ndarray简单介绍

    一.NumPy 是什么 NumPy 是 Python 科学计算的基础包,它专为进行严格的数字处理而产生.在之前的随笔里已有更加详细的介绍,这里不再赘述. 利用 Python 进行数据分析(一)简单介绍 ...

  6. yii2的权限管理系统RBAC简单介绍

    这里有几个概念 权限: 指用户是否可以执行哪些操作,如:编辑.发布.查看回帖 角色 比如:VIP用户组, 高级会员组,中级会员组,初级会员组 VIP用户组:发帖.回帖.删帖.浏览权限 高级会员组:发帖 ...

  7. angular1.x的简单介绍(二)

    首先还是要强调一下DI,DI(Denpendency Injection)伸手获得,主要解决模块间的耦合关系.那么模块是又什么组成的呢?在我看来,模块的最小单位是类,多个类的组合就是模块.关于在根模块 ...

  8. Linux的简单介绍和常用命令的介绍

    Linux的简单介绍和常用命令的介绍 本说明以Ubuntu系统为例 Ubuntu系统的安装自行百度,或者参考http://www.cnblogs.com/CoderJYF/p/6091068.html ...

  9. iOS-iOS开发简单介绍

    概览 终于到了真正接触IOS应用程序的时刻了,之前我们花了很多时间去讨论C语言.ObjC等知识,对于很多朋友而言开发IOS第一天就想直接看到成果,看到可以运行的IOS程序.但是这里我想强调一下,前面的 ...

随机推荐

  1. Wpf ListBox数据绑定实例1--绑定字典集合

    1.使用ListBox绑定Dictionary字典数据 ListBox常用事件SelectionChanged private void bindListBox() { Dictionary<s ...

  2. MVC3 展示数据含有html代码处理,配合上篇发布的StringHelper

    @Html.Raw(@StringHelper.SubstringToHTML(Content,30)) StringHelper 地址:http://www.cnblogs.com/Jiawt/p/ ...

  3. Linux系统中,main函数的执行过程

    http://blog.csdn.net/rrerre/article/details/6728431

  4. underscorejs-each学习

    2.1 each 2.1.1 语法: _.each(list, iteratee, [context]) 2.1.2 说明: 依次对集合的所有元素进行某种操作,原样返回list.接收3个参数,list ...

  5. [xfire]使用xfire开发webservice的简单示例

    目前项目上有用到xfire,所以临时看了些xfire的资料和示例,自己照着写了一个简单示例. xfire在2007年后已经停止更新,正式更名为apache cxf,也可以说是xfire2.0. xfi ...

  6. thinkphp框架之模型(数据库查询)

    1. 模型定义 文件名称必须是 表名+Model.class.php 例如:UserModel.class.php namespace Home\Model; //该模型类的命名空间 use Thin ...

  7. Day4 内置函数补充、装饰器

    li = [11,22,33,44]def f1(arg): arg.append(55)#函数默认返回值None,函数参数传递的是引用li = f1(li) print(li)   内置函数补充: ...

  8. Java 保留两位小数

    在实际项目开发中,经常会存在浮点数四舍五入保留几位小数的问题,故收集了几种常用方法: 直接上代码(保留两位小数). Format.java: import java.math.BigDecimal; ...

  9. [转]为什么移动Web 应用程序很慢

    原文出处: Herb Sutter   译文出处: tangzhnju 我写过不少文章来讨论为什么移动Web应用程序很慢,这也引起了不少的讨论.但是不幸的是,这些讨论没有像我喜欢的那样的基于事实. 所 ...

  10. 前端工程之模块化(来自百度FEX)

    模块化 是一种处理复杂系统分解成为更好的可管理模块的方式,它可以把系统代码划分为一系列职责单一,高度解耦且可替换的模块,系统中某一部分的变化将如何影响其它部分就会变得显而易见,系统的可维护性更加简单易 ...