Hiding Unity Library Internals
When writing code for a library, there is invariably some of it you want to hide from the users of the library. You want to keep the public API clean, but Unity makes this tough. Today’s article discusses a strategy for laying out your code so that users of the library aren’t burdened by classes, functions, and properties that they don’t need to know about. Read on to see how!
Say you’re writing a library that gets high scores from a web server. The web call looks like this:
http://server.com/highscores
If the user wants to learn more about a player in the high scores list, they can get their profile with another web call that looks like this:
http://server.com/profile?playerId=123
Your library’s public API might look like this:
public interface IHighScore { string PlayerName { get; } string PlayerId { get; } int Score { get; } } public interface IPlayerProfile { int Level { get; } bool IsOnline { get; } Class Class { get; } // enum with Wizard, Warrior, Cleric, ... } public interface ILibrary { IEnumerator GetHighScores( Action<IHighScore[]> callback ); IEnumerator GetPlayerProfile( int playerId, Action<IPlayerProfile> callback ); }
It’s easy to implement this:
public class HighScore : IHighScore { public string PlayerName { get; private set; } public string PlayerId { get; } public int Score { get; private set; } public HighScore(string playerName, string playerId, int score) { PlayerName = playerName; PlayerId = playerId; Score = score; } } public class PlayerProfile : IPlayerProfile { public int Level { get; private set; } public bool IsOnline { get; private set; } public Class Class { get; private set; } public PlayerProfile(int level, bool isOnline, Class clazz) { Level = level; IsOnline = isOnline; Class = clazz; } } public class Library : ILibrary { public IEnumerator GetHighScores( Action<IHighScore[]> callback ) { var www = new WWW("http://server.com/highscores"); yield return www; var highScores = Parse(www.text); // parse into HighScore[] callback(highScores); } public IEnumerator GetPlayerProfile( int playerId, Action<IPlayerProfile> callback ) { var www = new WWW("http://server.com/profile?playerId=" + playerId); yield return www; var profile = Parse(www.text); // parse into IProfile callback(profile); } }
This has all been very straightforward so far. The library is essentially a thin wrapper around some web calls so that they’re somewhat easier to use. The problem is that it’s presenting a very leaky abstraction. What if the web call to get the profile changes so that it takes the player’s name instead of the player’s ID?
http://server.com/profile?playerName=PopcornBob
The library’s public API will also need to change so that GetPlayerProfile
takes the player’s name instead of the player’s ID. The library isn’t doing it’s job of insulating the user from changes in the web server’s API.
So how can we improve this? For starters, we should recognize that some properties the library exposes aren’t interesting to the user at all. For example, the player’s ID isn’t ever going to be displayed on the GUI. It’s only used internally in the library because that’s how the web call to get a profile is built. The user doesn’t care how to get the profile, only what player’s profile to get.
For the first change, let’s make an IPlayer
to hold the player’s name and ID:
public interface IPlayer { string PlayerName { get; } string PlayerId { get; } }
The player’s ID is still exposed, so let’s use a technique similar to last week’s article to introduce one more level of interface:
public interface IPlayer { string PlayerName { get; } } public interface IInternalPlayer : IPlayer { string PlayerId { get; } }
Now we have an IPlayer
that we can give to the user of the library and an IInternalPlayer
that we can use inside the library. The user doesn’t know about the player’s ID because they don’t need to and it only adds confusion. The library itself does know about the player’s ID because this is essential for it to work.
Let’s see how this changes the API:
public interface IHighScore { IPlayer Player { get; } int Score { get; } } public interface IPlayerProfile { int Level { get; } bool IsOnline { get; } Class Class { get; } // enum with Wizard, Warrior, Cleric, ... } public interface ILibrary { IEnumerator GetHighScores( Action<IHighScore[]> callback ); IEnumerator GetPlayerProfile( IPlayer player, Action<IPlayerProfile> callback ); }
The user now gets an IPlayer
from GetHighScores
and they pass this same IPlayer
to GetPlayerProfile
. Little do they know it’s also an IInternalPlayer
that GetPlayerProfile
can use to get the player’s ID. Here’s how the implementation changes:
public class HighScore : IHighScore { public IInternalPlayer InternalPlayer { get; private set; } public IPlayer Player { get { return InternalPlayer; } } public int Score { get; private set; } public HighScore(IInternalPlayer player, int score) { InternalPlayer = player; Score = score; } } // Note: PlayerProfile is unchanged public class Library : ILibrary { private Dictionary<IPlayer, IInternalPlayer> knownPlayers = new Dictionary<IPlayer, IInternalPlayer>(); public IEnumerator GetHighScores( Action<IHighScore[]> callback ) { var www = new WWW("http://server.com/highscores"); yield return www; var highScores = Parse(www.text); // parse into HighScore[] foreach (var highScore in highScores) { knownPlayers.Add(highScore.Player, highScore.InternalPlayer); } callback(highScores); } public IEnumerator GetPlayerProfile( IPlayer player, Action<IPlayerProfile> callback ) { var internalPlayer = knownPlayers[player]; var www = new WWW("http://server.com/profile?playerId=" + internalPlayer.PlayerId); yield return www; var profile = Parse(www.text); // parse into IProfile callback(profile); } }
The HighScore
class now has an additional InternalPlayer
property for the library to use. Users can still use the Player
property, which just returns the InternalPlayer
.
The library keeps a mapping of players it’s created in knownPlayers
. It fills this in during GetHighScores
and uses it to look up the IInternalPlayer
during GetPlayerProfile
. This allows the function to access the player’s ID.
Now that we have this in place, consider what would happen if the web call to get a profile changed to take the player’s name instead of the player’s ID. All we’d have to do is change a few characters on one line:
var www = new WWW("http://server.com/profile?playerId=" + internalPlayer.PlayerName);
The user of the library wouldn’t have to change anything or even know that there was a change behind the scenes. That’s a huge win since the API can keep backwards-compatibility and its users won’t need to change anything to use new versions.
This is just one strategy for hiding internal aspects of libraries. There are many others, including using C#’s internal
keyword. Unity, unfortunately, makes that one tougher to use. This is because all C# source files included in a project’s Assets
directory are bundled into the same .NET assembly. If this includes the library as well as the user, the user will have access to all of the internal
types, functions, fields, and properties. To work around this, consider moving the library code to a DLL.
How do you hide the internals of your libraries? Let me know in the comments!