"Remote dictionary"?: an interesting application of the proxy pattern

"Remote dictionary": an interesting application of the proxy pattern

This is the second article in a series of three articles I've written about the proxy pattern (.1).

In the previous article we saw the structure of the proxy pattern, the advantages it offers and we also learned how to apply it in some typical contexts.

In this article, instead, the time has come to get our hands a bit dirty.

So let's take a deep breath and accept this challenge: "We have to create remote dictionary in Android that can be written and read by external applications in a very simple way".

In the search for the solution we will see how the proxy pattern will allow us to make a difference and to obtain an excellent solution with an excellent structure.

Dictionary (like Python) is a data structure that stores key-value pairs.

Basically we would like to have a scenario like the one shown in the picture below.


Non è stato fornito nessun testo alternativo per questa immagine
Figure 1: write and read in a dictionary from external applications.


We have an application "A" which creates the dictionary and publishes the credentials (URL) to access it. Obviously this application can write and read directly on the dictionary, for example it writes a string field on the dictionary with key "Name" and value "Stefano".

Other applications must be able to write and to read from the dictionary created by "A" by connecting to it using the credentials published by "A".

For example, a sequence could look like this:

Application "B" via a proxy object reads the "Name" key from the dictionary and gets the value "Stefano". Then it adds a new field of float data type on the dictionary with key "Weight" and value 75.5.

The "C" application, via the proxy object reads the "Name" key from the dictionary and gets the value "Stefano", reads also the "Weight" key and gets the value 75.5, after it writes a new field on the dictionary, this time of integer type with key "Height" and value 178.

The "D" application via the proxy object reads from the dictionary respectively the string with key "Name", the float with key "Weight", and the integer with key "Height".


This is exactly the same problem I had about a decade ago when I was leading my team to develop an Android AOSP platform and software application for a line of cardiovascular exercise equipment.

We needed remote dictionaries of heterogeneous and thematic data (i.e., one dictionary for?"user data" like weight and age, one for?"exercise data" like burned calories, one for?"equipment data" like speed and incline).

External applications needed?to be able to access these remote dictionaries (created by another process) to read and update data.

We can divide our development plan into two steps.

The first step is to create remote dictionaries with these features and the second step is to apply the proxy pattern in order to make easy the access to the dictionary from external application.

To create remote key-value dictionaries with heterogeneous values we use the HashMap because it is possible to share a HashMap between different processes in Android using a ContentProvider.

In Android, a Content Provider is a component that provides a way to share data between different applications. One way to implement a Content Provider is by using a HashMap to store the data.

A HashMap is a data structure that stores key-value pairs. It provides a way to access values based on their keys. In a HashMap, keys must be unique and values can be duplicated. HashMap are efficient for retrieving data, as they use a hash function to locate the data in constant time.

Let's get started with the easiest way to implement a Content Provider with a HashMap.

We can define a class "HashMapContentProvider" that extends the ContentProvider class and implements the methods required by the Content Provider interface. We can use a HashMap to store the data in memory. Let's see the code below.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 2: HashMap Content Provider.


In the onCreate() method, we create the HashMap.

To query the data, we implement the query() method. In this method, we first extract any query parameters. We then iterate over the HashMap and add any matching items to a MatrixCursor. Finally, we return the MatrixCursor as the result of the query.

We can implement the other methods required by the Content Provider interface in a similar way, using the HashMap to store and retrieve the data. For example, we can implement the insert() method to add new items to the HashMap.

A MatrixCursor is a subclass of the Cursor class that allows yus ou to create a cursor from a two-dimensional array of data. It is useful when we want to create a cursor with a small amount of data that does not require a database. With a MatrixCursor, we can define the column names and data types for the cursor, and then add rows of data to it. The data is stored in memory and can be accessed using the same methods that are available for any other cursor, such as moveToFirst(), getColumnIndex(), and getString().


?Then we have to declare the ContentProvider in the Android manifest file, and specify the android:exported attribute as "true" to allow other applications to access it:

Non è stato fornito nessun testo alternativo per questa immagine
Figure 3: Content Provider declaration in Manifest Application.


To use a content provider, an application must obtain a reference to it through the Android system's ContentResolver. The ContentResolver provides methods for querying, inserting, updating and deleting data managed by a content provider.


The "HashMapClient" class shows how to read and write in the hashMap through the ContentProvider. Let's see the code below.

Non è stato fornito nessun testo alternativo per questa immagine
Figura 4: HashMapCPClient allows the external clients to access to the ContentProvider to read and write into the HashMap.

This code creates a ContentResolver and an Uri to access the ContentProvider. The write methods creates a ContentValues object with the pair key-value to be inserted into the HashMap, and calls the insert() method of the ContentResolver to add the entry to the HashMap.

The read method retrieves the value of a key (named Field) from the HashMap by calling query() method of the ContentResolver.

Below we see a unit test verifying that the code works well.


Non è stato fornito nessun testo alternativo per questa immagine
Figure 5: unit test for HashMapContentProvider e HashMapCPClient.


So summarizing we can say that it is possible to share a HashMap between different processes in Android using a ContentProvider. The process that implements the ContentProvider stores the HashMap in memory and provides an interface to access it. To access the data from another process, we have to use a ContentResolver to query the ContentProvider. The ContentResolver would return a Cursor object that contains the data from the HashMap. We could then use the Cursor to extract the data and use it as needed in the other process.


What is still not good is the way to access these dictionaries, it is the grammar of the "Content Provider" and "Content Resolver". It is a grammar too close to the world of databases and in fact it exposes concepts such as projection and selection. Instead, we are looking for a simpler, more natural and more efficient way to use these dictionaries.

Anyway thanks to the "HashMapCPClient" class we can use the dictionary via simple "read" and "write" methods, no longer seeing content providers grammar.

Well, this is a first result so we could consider the HasMapCPClient class as a first form of the Proxy pattern. By the way it allows us to get the ContentResolver only once in the constructor and this is another optimization.


But we can do a much more to make this proxy smarter. For example we can create a local cache as a dictionary image. In this way all read operations from the proxy object would be optimized because they would read on the local cache and not on the out-of-process content provider

With this aim we can rewrite the dictionary class as follows.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 6: IMCP class, implementation of heterogeneous data dictionaries accessible from external applications


The IMCP class is the acronym of "In Memory Content Provider" (to emphasize that this is a repository implemented with a hashmap and not with a database).

The local cache is an image of the remote dictionary and is kept updated by the notification mechanism we have implemented in the?IMCP class. (the?"notifyChange"?called by the?"contentResolver"?in the?"insert"?function).

Then we also rewrite the proxy to access to the dictionary. Let's see the code below.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 7: IMCPProxy class, the proxy class for InMemoryContentProvider dictionary.


The?"IMCPProxy"?class extends "ContentObserver". ContentObserver is a class in the Android SDK that provides a mechanism for monitoring changes to a particular content provider. A ContentObserver can be used to receive notifications when data managed by a ContentProvider is changed allowing, for example, a local cache map to keep in sync with the data source.

The IMCPProxy class subscribes notifications to the?IMCP class by calling the?"registerContentObserver"?method on?"contentResolver". The notifications arrive at the?"onChange"?method of the?"IMCPProxy"?class.

Within the?"onChange"?method is called the "ContentProviderClient"? "query" method to obtain, through the?"MatrixCursor", the data of the remote dictionary with which to update the local cache. All data read operations called on? "IMCPProxy"?return the value of the local cache.


"PeopleIMCP" is a concrete implementation of IMCP.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 8: Remote Dictionary for "People" (PeopleIMCP)


And below we have the Provider declaration in Manifest file.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 9: PeopleIMCP declaration in Manifest Application.


The following unit test shows that the two classes IMCP and IMCPProxy work well together.


Non è stato fornito nessun testo alternativo per questa immagine
Figure 10: unit test for InMemoryContentProvider e IMCPProxy.


In order to make the code clearer, here is a sequence diagram with two client proxies accessing a "Content Provider with HashMap". The sequence diagram relates to the situation shown in Figure 1.

For simplicity I have limited the scenario to just two client applications ("ApplicationA and ApplicationB").


Non è stato fornito nessun testo alternativo per questa immagine
Figure 11 : Sequence Diagram of scenario shown in figure 1


As we can see, each write operation ("set" method) on the proxy translates into an "update" operation on the Content Provider, then into an "insert" or "update" operation into the dictionary (the HashMap) and then into an "update notification" from the content provider towards all connected proxies. As a consequence of this notification the proxies perform a "query" operation on the content provider with a null projection value in order to obtain the whole content of the "hasmap" with which they update their local cache. So all read operations ("get" method) on proxies become simple get on local cache.


At this point we can finally say that we have won the initial challenge.

We have create remote dictionaries in android and thanks to the proxy pattern we obtained a performing solution and an excellent software structure.

Let's summarize what we did:

  • We have created an IMCP class to share a hashmap from external applications.
  • We have created a proxy class IMCPProxy to allow external applications (clients) to access the hashmap.

The advantages of using the proxy class IMCPProxy are the following:

  • The first advantage is that we need to create the channel with the remote dictionary process only once. This channel is the instance of the? "ContentProvider Client" ?that we obtain from the?"ContentResolver"?and the authentication Uri declared on the dictionary application manifest. This channel must be released by calling the?"tearDown"?method before the object is finalized through garbage collection.
  • The second advantage is the local cache. The local cache is an image of the remote dictionary and is kept updated by the notification mechanism we have implemented on the?IMCP ("notifyChange"?called on the "contentResolver"? in the?"insert"?function). The?"IMCPProxy"?class subscribes notifications to the?IMCP?class by calling the? "registerContentObserver"?method on? "contentResolver". The notifications arrive at the?"onChange"?method of the?"IMCPProxy"?class. Within the "onChange"? method is called the "query" method on? "ContentProviderClient"?to obtain, via the?"MatrixCursor", the remote dictionary data with which to update the local cache. All read operations called on?"IMCPProxy"?return the value of the local cache.
  • The third advantage is the simple access via the set and get methods. IMCPProxy hides all the grammar of the "ContentProvider"? and? "ContentResolvers"? like "Projection",?"Selection",?"SelectionArgs", etc.


One last interesting element, although not very evident but very useful, is the data-member of the IMCP which is called fullProjection. As we can see this string array is used whenever the query() method is called with a null projection. The fullProjection data member is invalidated at each insertion of a new key-value pair on the HashMap.

This little trick greatly improves the performance of the IMCP as far as the garbage collector is concerned. In fact, with each modification of the data in content provider, all the connected proxies invoke the refresh() method to keep the internal cache updated. But the refresh() method queries the Content Provider with a null projection value so that we have the whole dictionary to update its cache. So it's important inside the query method of IMCP that we don't have to recreate the full projection every time because that would cause a lot of garbage when the HashMap has a lot of keys.


I share the?"IMCP Framework"?repository with you. It is a mini framework to manage remote dictionaries in android. I made this project with Android Studio and consists of three modules:

-)?"imcplib"?is a module which contains classes for performing write / read operations and listeners registration on remote dictionaries (I didn't explain this listening feature in the sources that I showed you in this article because it will be part of another article on the pattern observer).

-)?"app"?is the application that creates the dictionary.

-)?"app_client"?is the application that, through the proxy class of the?"imcplib"?module, performs operations of reading, writing and listeners recording on remote dictionaries. Contains two unit-test to perform operations on the user data dictionary and the exercise data dictionary respectively.



That's it for the "Content Provider with a HashMap".

Before concluding I want to anticipate the topic of the next article

In the next article I will show you an unconventional use of the Proxy pattern in Android environment. We will see an extension of the Android SDKs to easily access to the shared memory and then we will see how the application of the proxy pattern make it easier the usage of shared memory because it creates a wrapper to cover the object’s complexity to the client.



I remind you my newsletter?"Sw Design & Clean Architecture":??https://lnkd.in/eUzYBuEX?where you can find my previous articles and where you can register, if you have not already done, so you will be notified when I publish new articles.



Thanks for reading my article, and I hope you have found the topic useful,

Feel free to leave any feedback.

Your feedback is much appreciated.

?Thanks again.

Stefano

?

?

References:

1.?S. Santilli: "https://www.dhirubhai.net/pulse/proxy-design-pattern-right-those-people-who-act-behalf-santilli/".?

要查看或添加评论,请登录

Stefano Santilli的更多文章

社区洞察

其他会员也浏览了