You are on page 1of 1

N<+6'"6',<< >+$';$,&$+*

!"#$#%$&''()%*$+,-./
'0)12)'(%3$"1$+1)'%0$4/
5'&06$78

!"#$%&'()*+#%, -%))%.
/,&'012'0303'·'4'5"6'&+,*

In part 1, we discussed why organizing the


code in games that will be maintained for many
years, such as Sniper 3D, is so important, and
we saw examples of the two first principles.
They are:

The Single Responsibility Principle

The Open-Closed Principle

Now we will discuss the next three.

The Liskov Substitution Principle

The Interface Segregation Principle

The Dependency Inversion Principle

These five complete the acronym SOLID.


Except that, to make it easier to explain with
examples, I will invert the last two, so we will
discuss the Dependency Inversion before the
Interface Segregation.

!"#$%&'()*$+,-'.&.,.&)/$01&/2&34#5
6#1&*#7$248''#'$9,'.$-#
',-'.&.,.8-4#$:)1$."#&1$-8'#$248''#';
What does “substitutable” in this case mean?
Does it mean that the application behavior
should remain the same? Absolutely not,
because that is the base of polymorphism. You
want to be able to change the behavior of code
just by injecting instances with different types
into classes.

In Unity, that is incredibly common.


GameObject’s components inherit from
MonoBehaviour. They define the object’s
behavior through the changes it imposes
through that inheritance. Although you don’t
declare the methods Awake, Start and Update
as “override,” they are, in fact, a perfect
example of polymorphism, except that Unity
preferred to implement them via reflection, to
make it a bit simpler for developers.

+)<$="8.$=),47$&.$9#8/$.)$-1#8($."&'
31&/2&34#>
To understand how we can end up breaking
this principle, let’s take one more Sniper 3D
example.

We have a class called ItemData, which inherits


from ScriptableObject. Many classes extend
ItemData’s behaviors.

Now there is one particular kind of weapon


that we want to include in the game: a
crossbow, a weapon that can only be used in a
specific mission and is always used and
equipped in that mission; it cannot be fitted
throughout the whole game.

So, to make sure no one will accidentally call


“Equip()” on it, we create a class
CrossbowWeaponData which overrides Equip()
and throws an exception in case it’s called. But
now, the implementations that reference
WeaponData (which could get a
CrossbowWeaponData instance) will be at risk
when calling Equip on it.

They may have to start checking for the “typeof”


the instance or add try-catch clauses. Those are
some significant signs of an LSP violation —
and this case is a violation.

How do you fix it? First, you should consider if


the Equip()/IsEquipped is the only extension
that WeaponData serves for. If that’s the case,
then instead of creating the new class
CrossbowWeaponData, you could just make the
Crossbow be an instance of ItemData. That will
make it impossible to call “Equip” on, and the
compiler would tell you that you cannot do
that.

But, more commonly, WeaponData will also


include other extensions that you want the
Crossbow to inherit. In that case, the thing to
consider is that when you added the “Equip()”
behavior in the WeaponData class, that is a
statement that every WeaponData can be
equipped. If this is no longer true, then that
implementation should change.

A quick solution is to include a new field


“canBeEquipped”. The crossbow instance of
WeaponData would have this field set to false,
and the “Equip()” method could just check for
that and do nothing if this variable is wrong.

You could also have a public property as well


for other places to check that — which is much
better than checking for the typeof. Another
solution is to remove the Equip()/IsEquipped
behavior entirely from WeaponData.

There could be a new class inheriting from


WeaponData called “EquippableWeaponData”
with that behavior (for all weapons except the
Crossbow), or there could be a completely
separate component dedicated to handling the
currently equipped weapon.

The bottom line here is: child classes are meant


to extend the parent’s behavior. If it’s
narrowing it down, restricting methods, then
you probably have the wrong abstraction.
Making decisions about class hierarchy using
the “crossbow purely is a weapon” line of
thinking is a mistake. While it’s essential to
create useful real-world abstractions, they have
to serve your game’s business logic.

?/.#1:82#'$@$8$'9844$7#.),1
Before talking about the next two principles,
let’s make a detour to talk about to something
that we don’t see much on Unity tutorials —
interfaces.

What are interfaces for in Object-Oriented


languages? Well, one metaphor I like is the
power plug one. You don’t see microwaves
soldered to the power plug on the wall too
often, do you? :)

The idea is that electric devices comply with


specific protocols. Some countries might have
different protocols, the signal coming from
your power plug might be any combination
between 110V, 127V, or 220V and 50Hz or
60Hz. Some regions also have different power
plugs standards for 10A devices or 20A devices.
We see adapters everywhere, but it’s essential
to create that difference and prevent us from
plugging in a device that doesn’t conform to
that power plug’s protocol.

Interfaces (or what some languages call


protocols) have the purpose of allowing the
developers to create specific rules that if a class
obeys than it becomes “pluggable” to other
classes. The runtime can call the correct class
accordingly through polymorphism, even
without having a direct reference to that class.
Awesome, isn’t it?

!"#$6#3#/7#/2A$?/*#1'&)/$01&/2&34#5
6#3#/7$)/$8-'.182.&)/'<$/).$)/
2)/21#.&)/';
I’ll invert things a bit and start talking about
the Dependency Inversion Principle before the
Interface Segregation one.

Say you want to implement an Achievements


system in your game. You may want to start by
only using the off-the-shelf Google Play and
Game Center systems by implementing Unity’s
social interface. But then you want to
implement a similar solution for Steam. And
then, you may decide to replace both for your
answer.

Knowing about all those future changes, you


can use interfaces to isolate each of those
implementations so that you won’t even have to
touch your core game code to do any of those
changes. Your game’s code contains the
achievements logic and UI, and it knows high-
level interfaces that will then be implemented
by classes with solution details. The game has
the power plug, and each achievements
solution is a pluggable electric device.

So naively you could start mixing


implementation details and adding a bunch of
dependencies in your core project like this:

public class AchievementSystem


{
public void
UnlockAchievement(string id,

double progressPercentage,

double maxProgress)
{
#if UNITY_IOS || UNITY_ANDROID

UnityEngine.Social.ReportProgress(i
d,
progressPercentage,
b => { });
#else

SteamWorks.IndicateAchievementProgr
ess(id,
(uint)progressPercentage,
(uint)maxProgress);
#endif
}
}

This principle states that your code should


depend on abstractions, not concretions. So
instead, you make the implementation details
pluggable:

public class AchievementSystem


{
readonly IAchievementPlatform
platform;
public
AchievementSystem(IAchievementPlatf
orm platform)
{
this.platform = platform;
}

public void
UnlockAchievement(string id,

double progressPercentage,

double maxProgress)
{
platform.ReportProgress(id,
progressPercentage, maxProgress);
}
}

public interface
IAchievementPlatform
{
void ReportProgress(string id,
double progressPercentage,
double maxProgress);
}

// each of the classes below could


be in a separate asmdef
public class
SteamAchievementPlatform :
IAchievementPlatform
{
public void ReportProgress(string
id,
double
progressPercentage,
double
maxProgress)
{

SteamWorks.IndicateAchievementProgr
ess(id,
(uint)progressPercentage,
(uint)maxProgress);
}
}

public class
UnityAchievementPlatform :
IAchievementPlatform
{
public void ReportProgress(string
id,
double progressPercentage,
double maxProgress)
{

UnityEngine.Social.ReportProgress(i
d,
progressPercentage,
(b) => { });
}
}

Is that a lot more verbose? Indeed, it is. That’s


why you have to select very well the things you
want to make pluggable. But imagine that
when you want to stop supporting Steam, you
can just delete ONE folder, and you’re done.
This is what enabled this dream to come true.

So, why is this called “inverting the


dependency”? To make that a bit easier to
understand, we can think of them being on
separate Assemblies because then you would
have to include the dependencies explicitly in
the Assembly Definition Files (asmdefs).

Imagine that the AchievementSystem is inside


your Core assembly, the Steam API is in its
assembly and you already isolated
SteamAchievementPlatform in the
SteamWrapper assembly, which depends on
the Steam API.

Now, if AchievementSystem had a direct


reference to SteamAchievementPlatform, the
Core would have to depend on
SteamWrapper, and therefore rely indirectly
on the Steam API. That would make things
harder when you want to stop supporting
Steam, right?

But because the AchievementSystem knows


only the IAchievementPlatform, which is also
on the Core assembly, then the Core does not
have to depend on anything else. Instead,
SteamWrapper depends on Core through
knowing and implementing
IAchievementPlatform.

!"#$?/.#1:82#$+#B1#B8.&)/$01&/2&34#5
C8(#$:&/#$B18&/#7$&/.#1:82#'$."8.$81#
24&#/.D'3#2&:&2;
Let’s say hypothetically we want to support
checking a friend’s progress, and only Steam
supports that. The most straightforward way to
do that is, of course, to include a new method
in the IAchievementPlatform interface

public interface
IAchievementPlatform
{
void ReportProgress(string id,
double progressPercentage,
double maxProgress);
double GetFriendProgress(string
userId, string achievementId);
}

And then implement “GetFriendProgress” on


SteamAchievementPlatform class. But then,
you’ll have to implement GetFriendProgress on
UnityAchievementPlatform as well, which does
not support it. So what do you do?

Well, if your game just shows friends progress


when it’s higher than 0f, then you could just
hard-code it to return 0f. Simple and effective!
It’s great when it’s possible to use the game
logic to simplify the code.

But maybe that’s not the case. So, going down


this rabbit hole, you would have to implement
one more method: bool
IsFriendProgressSupported. This not only starts
complicating the solution but also breaks the
Interface Segregation Principle in two ways:

1. Parts of the code that care only for


reporting the player’s progress will depend
on the two methods they don’t need

2. UnityAchievementPlatform will rely on a


method that it doesn’t know how to
implement — yes, implementing an
interface (or inheriting from other classes)
is also a form of dependency!

So the best solution is also the not-so-


straightforward one. Create a new interface,
say “IFriendAchievementsRepository” with one
method: “GetFriendProgress.” This might have
only one implementation for now: the
“SteamFriendAchievementsRepository” and
that’s okay.

Again, IFriendAchievementsRepository might


be in the Core and
SteamFriendAchievementsRepository in the
SteamWrapper assembly. No asmdef
dependencies have to change.

Another thing that this principle states is that


“Many client-specific interfaces are better than
one general-purpose interface.” That’s why at
Sniper 3D, we avoid the use of the word
“Manager” in classes.

This word makes the intent of the class very


misleading, and these classes usually get big
very fast — after all, the manager can handle
this one more method, right? There are no
limits on what a “manager” can do.

A similar example at Sniper 3D was a class


called MetagameClient, which provided direct
access to our backend services. Its interface was
general-purpose because all of its public
methods were accessible to every class that had
a direct reference to it.

It was refactored by separating it into


IMetagameStatus (methods providing
connection status), IMetagameCommunication
(methods enabling sending data), and
IMetagameConnection (methods to connect
and disconnect). This way, the classes can have
access only to the methods that they need
through each one of these interfaces.

E)/24,'&)/
This is a very complex and vital subject. Each of
the principles was created after years of
research and experience. I hope this series of
articles could give you a taste of it and motivate
you to search more about it. You can find
excellent resources in Uncle Bob’s blog and in
his book Clean Architecture.

Special thanks to Luciano “Lut” Puhl, who


helped me reviewing the article and providing
examples.

7%)"* 8")*)"9+'7$:*"%; 76"<+&'=*

>,5+'?+@+)%<5+6$ A6B"6++&"6B

14 0

8CDEEAF'GH
F&2.)1$G47#2)8 -%))%.

H&474&:#$+.,7&)'$!#2"$I4)B -%))%.

8")*)"9+'7$:*"%;'";'I:")*"6B'6+J$KB+6+&,$"%6'5%I")+
B,5+;2',6*'"$'$,L+;',')%$'%9'*,$,2'"66%@,$"%62',6*
L6%.)+*B+M'N:&'$+#O'<+%<)+',&+'O+&+'$%';O,&+'O%.
$O+P',&+'I:")*"6B'$O+'I+;$'"6'#),;;'$+#O6%)%BP'$%
"5<&%@+'<+%<)+Q;')"9+'."$O'9:6',6*'"66%@,$"%6M

C)1#$J1)9$C#7&,9

G+0;/#.$5$H#-:)19'$F+$CFE
!"<"6'RO+&"P,6@++$")'"6'$"6P.,@+

K$0&#2#'$):$G7*&2#$B&*#'$E)93,.#1
+2&#/2#$J1#'"98/
8AFKS7DF>'ST(F>'"6'N%<;UV3V

L8/,81A$MN$OPMQ$8.$PNRSNGC
S,5&,;'5%O,5+*

T)=$.)$3,.$?/:);34&'.$&/.)$8$A#44)=
:)47#1
F"#L'FB:P+6

U048AB1),/7V$GW,1#$EX?$4)BY'),12#
2)7#$8/84A'&'
7O:,6BW:

+.8.&)/$E8'&/)'$%8'$F#B8'$E4)'#'.
!)$T817$Z)2($E8'&/)

You might also like