S.O.L.I.D. in UnrealScript

Discussions about Coding and Scripting
User avatar
Feralidragon
Godlike
Posts: 5489
Joined: Wed Feb 27, 2008 6:24 pm
Personal rank: Work In Progress
Location: Liandri

S.O.L.I.D. in UnrealScript

Post by Feralidragon »

For a while already, I mentioned and advised some folks here to read and apply SOLID principles to how they build their code to their mods.

I was only meaning to talk about this much later on, probably several months from now, after I got my hands in UnrealScript again for real, so I could present better and proven examples, but then it was mentioned that these should be elaborated now, and I think as long there's interest on this subject, doing it now may as well create an interesting discussion and get other programmers to understand, or even criticize, these principles and how to apply them in UnrealScript.

So, what is S.O.L.I.D. ?

It's an acronym which stands for 5 key principles in OOP (Object-Oriented Programming), and they were coined by Robert C. Martin, and have been proven to actually work.
Generally programmers dealing with OOP languages use these principles to guide them into using the best approach to model their classes after how they want them to work, but always in a way that the code is easily maintainable and extensible, something that does not break at the slightest touch.

These 5 principles are the following:
S - Single Responsibility Principle (SRP)
O - Open/Closed Principle (OCP)
L - Liskov Substitution Principle (LSP)
I - Interface Segregation Principle (ISP)
D - Dependency Inversion Principle (DIP)

The first thing to take in mind even before understanding any of these, these are just principles, and NOT laws, which means that while they ought to be followed, at least in my opinion, it does not mean that they can always be applied or that should be. So while ideally you should have all your code following these principles, it does not strictly mean that it has always to.

Also, keep in mind that these are not the only principles code should abide to, there is a number of other principles to take into account when programming anything: DRY, KISS, and others, which won't be touched in this topic, and there is a set of different things called "design patterns" which are just common practices which tend to follow these principles, but in a more specific way, at the implementation level.

Furthermore these principles are towards OOP languages, therefore while other languages may abide by similar principles, different language paradigms may require a completely different set of principles, and even within OOP some of these principles may be hard to downright impossible to exert, such in the case of UnrealScript itself, at least up until Unreal Engine 2.

From here, what I am going to do is to explain each one in a separate post, so this way each post may be linked to separately. :)
Last edited by Feralidragon on Fri Mar 30, 2018 9:54 pm, edited 1 time in total.
User avatar
Feralidragon
Godlike
Posts: 5489
Joined: Wed Feb 27, 2008 6:24 pm
Personal rank: Work In Progress
Location: Liandri

S.O.L.I.D. in UnrealScript

Post by Feralidragon »

S - Single Responsibility Principle (SRP)

SRP is, for me, the most important principle of them all, and which, unfortunately, is blatantly violated in the Unreal Engine itself (at least the older ones).
It essentially states the following:
A class should have one and only one reason to change, meaning that a class should have only one job.
In other words, this principle states that a class should do just one specific thing.
Rather than being a Swiss Army knife, a class should only be a knife, screwdriver, corkscrew, scissors, etc, but nothing more beyond that.

If you think in terms of testing for example, if you change something like the scissors from a Swiss Army knife, not only you have to test the scissors, now you have to test all the other tools it comes with to check if they still work, if they still open, if you didn't mess up any other mechanism that made all the tools work.
However, if you had split this huge Swiss Army knife into each separate specific tool with just 1 job each, if you changed or improved just the scissors, you don't need to change or test anything on the other tools, because they are independent from each other.

Similarly, when it comes to classes, rather than building everything in one big class, this principle dictates that it's often preferable to split this big class into many smaller ones, each one doing a specific thing.
It makes no sense however to split a big class just for the sake of splitting. While a big class is often a sign of a violation of this principle, it's not always the case, all that there is to it is that a class should be responsible for just one single thing in the entire system, and the simpler the responsibility, the better.


I think the best example I can give you to show exactly this, instead of creating an abstract "foobar" kind of example using this principle, is to give you a clear example of an existing class in the engine which stands as the polar opposite of this principle: Actor.

The Actor class is what's generally called as a God class. While it may sound cool for some, it's the most terrifying way of defining and building a class, and it stands as the most massive and blatant violation of this exact principle.
God classes are classes which alone are responsible for most of the program, if not the entire program, by holding most or even all the functions/methods, most of the properties/members, etc, and which instead of having subclasses to expand with new functionality (simpler to more complex), you have subclasses only using part of the functionality already implemented in such a class (more complex to simpler), while not using the overwhelming majority of the remaining functionality, but still having all its overhead.

Why is Actor a God class?

Let's take a look at what Actor is capable of doing:
http://www.madrixis.de/undox/Source_engine/actor.html

An Actor is capable of: physics, collision, lighting, mesh rendering, texture rendering, sounds, replication, and a LOT more stuff as well.
To not even mention the methods, to do things like execute commands, triggering all sorts of events, tracing, get configuration values, iterators, rendering on the canvas, broadcast messages, give damage, and so on.

In other words, it does too much stuff in the same class, it has too many responsibilities.
And on top of it all, a lot of this stuff is not even used by most of them, such as lighting, and many actors such as Light, BlockAll, Trigger, and many others specialize themselves in using a small part of the functionality already provided by the Actor parent class, rather than being them to implement such a specialized functionality.

How would an Actor look like if it followed SRP?

Well, the best way to understand how it would look like is to look at another engine, such as Unity, which does things right on this regard, where something like lighting, is actually a light component (or class), which can simply be added or attached to another object to give it lighting capabilities.

Therefore, consider the following exemplification of how an Actor could look like if it was better designed by following this principle (along with design by composition):

Code: Select all

abstract class Actor extends Object;

//some basic properties every actor must really have, such as the 3D location in the world

private var Actor nextActor;

final function add(Actor actor)
{
	if (actor == none) {
		return;
	} else if (nextActor != none) {
		actor.nextActor = nextActor;
	}
	nextActor = actor;
}

function tick(float deltaTime)
{
	local Actor a;
	
	for (a = nextActor; a != none; a = a.nextActor) {
		//update stuff like 3D location
	}
}
Then a light could be something just like this:

Code: Select all

class Light extends Actor;

var() enum EType
{
	T_None,
	T_Steady,
	...
	T_TexturePaletteLoop
} Type;

var() enum EEffect
{
	E_None,
	E_TorchWaver,
	...
	E_Rotor,
	E_Unused
} Effect;

var() byte Brightness, Hue, Saturation;
var() byte Radius, Period, Phase, Cone;
var() bool bActorShadows;
var() bool bCorona;
var() bool bLensFlare;
Even the "Light" prefixes are no longer necessary to define the names of the properties, looking far better that way, proving that Light does belong to its own class, with the sole responsibility to lit things up.

While a collision could look like this:

Code: Select all

class Collision extends Actor;

var() const float Radius;
var() const float Height;

native(283) final function bool setSize(float radius, float height);
And now you can see that now you start having actors specialized in doing a single job: Collision can only collide, while Light can only lit things up.
A Collision cannot lit things up, nor a Light is able to collide with anything, since it's not their job to do so.
However, with this approach, should a need arise that a Collision needs generate light, then a Light could just be used by a Collision class, rather than implementing light in its own code, the latter of which would make the collision actor to be responsible for both colliding and lighting, violating this principle.

And from there, you could also create your own light-weight Actor such as:

Code: Select all

class MyActor extends Actor;

function beginPlay()
{
	local Light l;
	local Collision c;
	
	l = Spawn(class'Light');
	l.Radius = 240;
	l.Brightness = 128;
	l.Hue = 72;
	add(l);
	
	c = Spawn(class'Collision');
	c.Height = 8;
	c.Radius = 32;
	add(c);
}
As you may have noticed by now, isolating the responsibility of colliding into a separate class would also allow for a single actor to have multiple collisions.
And of course, in case of Unreal Engine in general, this could be something that you could potentially define in the default properties instead, and all the UnrealScript could be actually native code if Epic did it this way, something I think later engine iterations allow to with a specific syntax but in a similar fashion.

Having that said however, the Actor class cannot become the above, so we are all forever bound to the Swiss Army knife of classes forever in this engine, but that should not prevent you from following this and other principles in what you build for yourselves.

And this finishes the first principle, SRP.
If you have anything to add, correct or even criticize, or if you feel like a better example could be used, please feel free to provide any feedback you deem necessary.
Last edited by Feralidragon on Wed Mar 14, 2018 2:33 am, edited 9 times in total.
User avatar
Feralidragon
Godlike
Posts: 5489
Joined: Wed Feb 27, 2008 6:24 pm
Personal rank: Work In Progress
Location: Liandri

S.O.L.I.D. in UnrealScript

Post by Feralidragon »

O - Open/Closed Principle (OCP)

OCP is a principle which, luckily, is fairly well enforced by the original classes in UnrealScript, given that is what allows us to do mods far more easily in the first place, and it essentially states the following:
Objects or entities should be open for extension, but closed for modification.
In other words, once you've finished a class to serve a specific purpose, if then you need to modify its source code later, namely modifying existing methods for instance, so the class is able to do something more such as accounting for new classes which were created in the meanwhile, meaning that it became impossible to do so as is or by extending it or even by using another class, you may have just failed at following this principle.

At which point you may ask: does this mean that any source code modification violates this principle?

No.
This principle addresses only a very specific kind of source code modification, and forbids it, and which can be translated to the following: do not hard code or otherwise hard limit anything in your class.

Consider the following example:

Code: Select all

class ArmorCounter extends Actor
{
	var private int count;
	
	function beginPlay()
	{
		local Pickup p;
		
		foreach AllActors(class'Pickup', p) {
			if (p.Class == class'Armor' || p.Class == class'Armor2') {
				count++;
			}
		}
	}
}
This actor simply counts the number of armor pickups existent in the map, and this is the exact kind of code which violates OCP.

Why?

For instance, let's say that now you decide to extend Armor2 there to create your own armor pickup, simply because you want it to have a new skin, or a new mesh, or something else entirely, but it's still armor nonetheless.
And ArmorCounter which is meant to count the number of armor pickups in the map should be able to account for this new one as well, right?

However, as you may notice, in order for the new armor to be counted, the original class now has to be directly modified, like this:

Code: Select all

if (p.Class == class'Armor' || p.Class == class'Armor2' || p.Class == class'NewShinyArmor') {
	count++;
}
And as OCP itself states, this cannot happen, given that what was just done above was a modification to the existing source of ArmorCounter just to account for an extension of another class.
This is the exact kind of required modification that this principle aims to prevent.

So, how should the ArmorCounter class be designed to follow OCP?

As you may have probably already guessed by now, it would look like something like this:

Code: Select all

class ArmorCounter extends Actor
{
	var private int count;
	
	function beginPlay()
	{
		local Pickup p;
		
		foreach AllActors(class'Pickup', p) {
			if (Armor(p) != none || Armor2(p) != none) {
				count++;
			}
		}
	}
}
or even:

Code: Select all

class ArmorCounter extends Actor
{
	var private int count;
	
	function beginPlay()
	{
		local Pickup p;
		
		foreach AllActors(class'Pickup', p) {
			if (p.isA('Armor') || p.isA('Armor2')) {
				count++;
			}
		}
	}
}
also works.

Why?

Because now we may safely extend the Armor2 class to our new one with our own shiny new skin and such, but now the ArmorCounter doesn't require any changes whatsoever to account for it, hence being closed for modification, but open for extension (whether this extension is from itself or another class).

And this finishes the second principle, OCP.
If you have anything to add, correct or even criticize, or if you feel like a better example could be used, once again please feel free to provide any feedback you deem necessary.
Last edited by Feralidragon on Wed Mar 14, 2018 2:33 am, edited 2 times in total.
User avatar
Feralidragon
Godlike
Posts: 5489
Joined: Wed Feb 27, 2008 6:24 pm
Personal rank: Work In Progress
Location: Liandri

S.O.L.I.D. in UnrealScript

Post by Feralidragon »

L - Liskov Substitution Principle (LSP)

LSP is a principle which the game itself pretty much follows most of the time, but it's very easy to violate from the moment you create a subclass.
It's a principle introduced by Barbara Liskov (hence the name), and it states the following:
Let q(x) be a property provable about objects x of type T.
Then q(y) should be provable for objects y of type S where S is a subtype of T.
Don't worry if you did not get that definition at all, it took me a few re-reads as well to understand exactly what it meant.

In other words, if you have a class T, and then you extend from it as a new class S (class S extends T), and if you have a function or method somewhere (it does not matter where) defined as q, which may only receive objects x of class T (function q(T x)), then it must be able to safely receive objects y of class S and have S still behave the same as T.

Thus far, this may sound like simple polymorphism, for which languages (such as UnrealScript) generally ensure type safety by enforcing that only T and subtypes or subclasses of T are allowed to be given, whenever you define a function or method parameter of being of type or class T.
However, this principle goes deeper than simple polymorphism, as it addresses a more subtle characteristic of a type or class, for which languages (such as UnrealScript too) generally are not able to enforce whatsoever, and is down to the developers to enforce it instead: behavior.

Behavior is what this principle is all about, and what it really dictates is the following: a subclass must follow the same core behavior as its parent class.

Translating this into a more UnrealScript-oriented view of this principle, consider the following class:

Code: Select all

class Counter extends Actor
{
	var private int count;
	
	function increment()
	{
		count++;
	}
	
	function int getCount()
	{
		return count;
	}
	
	function reset()
	{
		count = 0;
	}
}
This is a simple class which acts as a counter, and which increments a count by 1 every time the increment method is called.
Then the current count can be retrieved by calling getCount, and it can be reset through reset.

And this defines the behavior of what this Counter class is meant to do.

Now consider the following class:

Code: Select all

class MyActor extends Actor
{
	var private Counter counter;
	
	function setCounter(Counter c)
	{
		c.reset();
		counter = c;
	}
	
	function timer()
	{
		if (counter != none)
		{
			counter.increment();
			if (counter.getCount() == 10) {
				
				//do stuff
				
			}
		}
	}
}
This is just a class for which you can set a Counter instance by using setCounter, and its count is reset and it's then used in the timer method, where it keeps getting incremented until its count reaches 10.

Up until this point everything is fine.

However, now consider the following Counter subclasses:

Code: Select all

class MyCounter1 extends Counter
{
	function increment() {}
}

class MyCounter2 extends Counter
{
	function increment()
	{
		super.increment();
		super.increment();
		super.increment();
	}
}

class MyCounter3 extends Counter
{
	function int getCount()
	{
		return 10;
	}
}
Every single one of the classes above violates LSP.

Why?

The reason is simple: if instead of a Counter instance, I set a MyCounter1, MyCounter2 or MyCounter3 instance through setCounter, MyActor would be broken, given that each one of those 3 subclasses has a very different behavior than the one expected by MyActor:
- MyCounter1 prevents the count to be incremented entirely, therefore timer in MyActor will never finish;
- MyCounter2 increments in steps of 3, therefore timer in MyActor will never finish either, since getCount will never return 10;
- MyCounter3 always returns the same count (10), therefore timer in MyActor will finish right away, instead of going through a full count of 10.

Each one of these 3 behaviors completely contradicts the behavior established in their parent class (Counter), therefore will fail to meet the expectations from any class which uses them and expected not only a Counter type, but a Counter behavior as well, at the very least.
And while it could be argued that the " == 10" could be " >= 10" instead, even because increment can be called from anywhere else in the code, and not exclusively by MyActor, the point here is that increment itself, along the other methods, should not have a different behavior by itself than the one set by the class it was first defined in.

These on the other hand:

Code: Select all

class MyCounter1 extends Counter
{
	function increment()
	{
		super.increment();
	
		//do other stuff
	}
}

class MyCounter2 extends Counter
{
	function incrementBy3()
	{
		increment();
		increment();
		increment();
	}
}
follow LSP perfectly, given that if any of these is given in setCounter, even though MyCounter2 could still break MyActor upon the call of incrementBy3, due to " == " instead of " >= " in the condition, the behavior MyActor expects from increment, getCount and reset is still fully honored, and that's what this principle is all about.

One possible way to ensure LSP is followed in cases like this, is to declare the methods as final, especially the ones which deal directly with private properties (such as count in this case) and are never meant to be overridden or extended in any way by subclasses, however in UnrealScript it is the case that, more often than not, non-final methods and public properties are declared, making this a very important principle to follow whenever possible.

And this finishes the third principle, LSP.
If you have anything to add, correct or even criticize, or if you feel like a better example could be used, once again please feel free to provide any feedback you deem necessary.
Last edited by Feralidragon on Wed Mar 14, 2018 3:51 pm, edited 4 times in total.
User avatar
Feralidragon
Godlike
Posts: 5489
Joined: Wed Feb 27, 2008 6:24 pm
Personal rank: Work In Progress
Location: Liandri

S.O.L.I.D. in UnrealScript

Post by Feralidragon »

I - Interface Segregation Principle (ISP)

ISP is a principle which is pretty much impossible to follow in UnrealScript, at least from the get-go, given that the language itself is missing a critical feature for this principle: interfaces.

However, I believe it's still worth being mentioned and explained, especially considering that while the language itself does not have the needed feature to enable it, it's still possible to emulate the behavior from an interface in UnrealScript to some extent.
And it states the following:
A client should never be forced to implement an interface that it doesn't use or clients shouldn't be forced to depend on methods they do not use.
So, first off: what is an interface?

Many OOP languages, and also UnrealScript in Unreal Engine 3, support something called interfaces.
An interface is something close to a class, but instead it only declares functions or methods to be implemented by other classes, and defines them with no default implementation whatsoever.
Any class which implements an interface, must implement every single method declared in that interface, and a class may implement multiple interfaces.

What is the purpose of an interface?

An interface is meant to add more capabilities to a class, and do so in a way which is recognizable from other classes and functions that such a class has those specific capabilities, by making an interface act like a contract which is recognized and strictly followed by every class which implements it.
While a class defines what an object is, an interface defines what an object is able to do.

For anyone who has never heard of interfaces before, this may indeed sound very confusing to grasp at first, but it will get much clearer through some examples.
But since there's no such a thing as interfaces in UnrealScript, the following examples show an "what if" scenario in case UnrealScript did support interfaces at all:

Code: Select all

interface Describable
{
	function string getDescription();
}
This interface defines just a single method to implement: getDescription, and it gives a class which implements it the capability of returning a description.
And as you can see, it has absolutely no code within the body of the method itself, only the declaration, because it cannot and is not meant to have one, this method is only meant to be implemented in classes themselves.

Now let's say that I intend to create a projectile subclass which has a description of its own, so I implement this interface in it, as such:

Code: Select all

class MyAwesomeProjectile extends Projectile implements Describable
{
	function string getDescription()
	{
		return "This awesome projectile is awesome.";
	}
}
By saying that I am implementing the Describable interface (through implements Describable), I am now forced to actually implement the getDescription method the interface declared, and from there on I can retrieve a description from this projectile class.
Easy enough, right?

But we could as easily not implement the interface at all and just declare and implement the same method in this new projectile class, right?
Indeed.

However, the power of an interface is revealed when you want another completely unrelated class to also be able to return a description.
For example:

Code: Select all

class MyPawn extends Pawn implements Describable
{
	function string getDescription()
	{
		return "This is just a normal pawn, meh.";
	}
}
As you can see, I just created a pawn subclass above, and made it also implement the Describable interface.
And thus far this still looks rather useless, right?

So now, the real power is revealed: what if the only thing I care about any class whatsoever, within a function or method, is whether or not it has a description?

Code: Select all

class Broadcaster extends Actor
{
	function string broadcastDescription(Describable obj)
	{
		broadcastMessage(obj.getDescription());
	}
}
In the above example, broadcastDescription is set to accept any object which implements the Describable interface, and as you can see, here Describable is now used as the type of obj, just like a class, in order to allow this:

Code: Select all

function doStuff()
{
	local SomeActor someActor;
	local MyAwesomeProjectile proj;
	local MyPawn p;
	
	...
	
	someActor.broadcastDescription(proj);
	someActor.broadcastDescription(p);
}
Although MyPawn (as p) is completely unrelated to MyAwesomeProjectile (as proj), since they implement the same interface, which is the interface expected to be implemented by whichever object is given to broadcastDescription, the code would compile and work with no issues whatsoever.

And this finishes what an interface is.


Getting back to the principle itself (ISP), it's only about how interfaces should be defined.
In most, if not all, OOP languages which support interfaces, classes may implement multiple interfaces at the same time, they are not limited to just one.

However, an easy mistake to make, and which this principle aims to prevent, is the declaration of too many unrelated methods in a single interface, for example:

Code: Select all

interface TeamDescribable
{
	function string getDescription();
	
	function byte getTeam();
}
This interface adds the capability to a class to both retrieve a description and a team.
While this sounds like an useful interface to apply to classes which are team based and have a description, ultimately a description has nothing to do with a team, therefore classes for which only a team makes sense to be returned would still be required to implement getDescription as well, even if to just return an empty value (empty string), and vice-versa.

However, by segregating this interface into 2 separate ones, like this:

Code: Select all

interface Describable
{
	function string getDescription();
}

interface Teamable
{
	function byte getTeam();
}
allows each class to only implement what they really aim to implement.

This way, a class which only has a team, but no description, would only need to implement the Teamable interface above, like so:

Code: Select all

class SomeTeamActor extends Actor implements Teamable
{
	function byte getTeam()
	{
		return 1;
	}
}
while a class which only has a description, but no team, would only need to implement the Describable interface above, like so:

Code: Select all

class SomeDescriptionActor extends Actor implements Describable
{
	function string getDescription()
	{
		return "This is my description.";
	}
}
while a class which has both a description and a team, can implement both the Describable and Teamable interfaces above at the same time, like so:

Code: Select all

class SomeTeamDescriptionActor extends Actor implements Describable, Teamable
{
	function string getDescription()
	{
		return "This is my team description.";
	}
	
	function byte getTeam()
	{
		return 1;
	}
}
and in the example given above with the Broadcaster class, which receives an object which implements the Describable interface in the broadcastDescription method, it would accept both SomeDescriptionActor and SomeTeamDescriptionActor objects since they implement that interface, but would deny (in compile time) SomeTeamActor objects since they do not implement the expected interface.
Similarly, a function which only accepts Teamable objects, would accept both SomeTeamActor and SomeTeamDescriptionActor, and deny SomeDescriptionActor.

And this finishes the forth principle, ISP.
If you have anything to add, correct or even criticize, or if you feel like a better example could be used, once again please feel free to provide any feedback you deem necessary.



In a last note, as first mentioned above, while UnrealScript does not have the ability to create interfaces, which would be extremely useful on many levels, hence being the cornerstone of many OOP languages nowadays, they may effectively be emulated to some extent by using classes and some mechanisms to dictate type safety and "isA" relationships.

While it would never be comparable to the real thing, both in performance and powerfulness, it would still open the doors to a more powerful style of programming, and could potentially solve many issues concerning dependency management, package mismatching (in a way) and enable classes to have the same set of capabilities without being necessarily inherited from the same parent class, although the way to do it approximates more to "design by composition" than actual interfaces, but a similar principle would still apply.

However, given that such a subject falls outside of the scope addressed here, and the fact that this very post is already a quite lengthy read as it is, I may address it in another separate topic someday, depending on the overall level of interest on this specific subject.
Last edited by Feralidragon on Thu Mar 29, 2018 12:06 pm, edited 2 times in total.
User avatar
Feralidragon
Godlike
Posts: 5489
Joined: Wed Feb 27, 2008 6:24 pm
Personal rank: Work In Progress
Location: Liandri

S.O.L.I.D. in UnrealScript

Post by Feralidragon »

D - Dependency Inversion Principle (DIP)

DIP is a principle which is somewhat followed in UnrealScript, in some cases more, and in others less, and it states the following:
Entities must depend on abstractions not on concretions.
In other words, a class should not depend on another specific concrete class, and rather it should depend on either an abstract class or an interface.
While interfaces are often preferable for this principle, and while I already explained what an interface is in the previous principle, I will focus this explanation on abstract classes instead given that's what can already be done in UnrealScript.

So, first off, abstract vs concrete classes, what is the difference?

An abstract class is a class which is not meant and cannot be instantiated (spawned) given that it's a class which is meant to represent a base, representing a broader but specific kind or set of classes.
And while it may have a base implementation of some methods and some properties, it generally has methods which are meant to be implemented by subclasses, as the real implementation and usage of such a class is done by extending it into something more concrete: a concrete class.
For example: Decoration, Projectile, Pawn, these are all abstract classes, which by themselves have no substance and aren't usable, as they only represent decoration classes, projectile classes and pawn classes respectively, but no decoration, projectile or pawn in specific.

While a concrete class is a class which is meant to be instantiated (spawned) and it has a specific implementation, as it represents a specific thing which is meant to have an active role in the code or program.
They generally implement what an abstract class has set to be implemented by their subclasses, but are not necessarily required to extend from an abstract class.
For example: the Barrel class is a concrete implementation of the Decoration class, as it represents a barrel, which is something very specific, but it belongs to a bigger and more abstract group of things called decoration, hence being an implementation of Decoration.

What this principle addresses is what the code should always depend on, and it states that the code should depend on abstract, and not on concrete.

Why?

Let's take the following example:

Code: Select all

class RedTeamChecker extends Actor
{
	function bool isSameTeam(byte team)
	{
		return team == 0;
	}
}
This class does a very concrete thing: it has a method isSameTeam to check if a given team corresponds to the red team.

Now, let's say we have the following:

Code: Select all

class Door extends Actor
{
	var private RedTeamChecker teamChecker;

	function setRedTeamChecker(RedTeamChecker team_checker)
	{
		teamChecker = team_checker;
	}
	
	function bool canOpen(byte team)
	{
		return teamChecker.isSameTeam(team);
	}
}
This class represents a door, which has a method canOpen to check if a given team can open it or not, and that decision is retrieved from a team checker actor, which this class depends on, which in this case is the RedTeamChecker class, which is a concrete class which only checks for the red team specifically.

So, what's the problem with this?

This is answered with another question: what if I want the door to be able to be opened by the blue team instead?
Or any other team, like green or yellow/gold, or even any other at all that doesn't exist yet?

Well, the obvious answer would be "just extend the RedTeamChecker class into a BlueTeamChecker class and override the isSameTeam method", right?
However, the problem with that approach is that it violates the Liskov Substitution Principle (LSP, the third principle), since a RedTeamChecker should clearly always be a check concerning the red team, and never any other team since the setRedTeamChecker clearly does not expect a class to validate any other team other than red.

Ok... then how about "create a new BlueTeamChecker on the side extending from Actor instead, with similar code, and add a new method setBlueTeamChecker to Door"?
While LSP wouldn't be violated this way, it would violate another principle instead: the Open/Closed Principle (OCP, the second principle), since this means that the Door code would require to be changed to account for every new variation of a team checker class.

Either way, by making Door depend on a concrete class, in order to account for any new changes and extensions, other principles would be violated, and the only way to not violate them is to not change the code at all and keep it concrete and immutable. Which means you're screwed. :lol2:

So, let's see now an example using an abstraction instead:

Code: Select all

abstract class TeamChecker extends Actor
{
	function bool isSameTeam(byte team);
}
Here an abstract TeamChecker class was declared, and, as you can see, the isSameTeam method has no implementation whatsoever.
This is because this class represents a team checker in an abstract way, and for this class it doesn't matter what specific team it needs to check, that's the responsibility of a concrete class to extend it and implement such a concrete detail.

From there, now if we structure the Door class this way instead:

Code: Select all

class Door extends Actor
{
	var private TeamChecker teamChecker;

	function setTeamChecker(TeamChecker team_checker)
	{
		teamChecker = team_checker;
	}
	
	function bool canOpen(byte team)
	{
		return teamChecker.isSameTeam(team);
	}
}
While it will never actually receive an instance from the TeamChecker class itself, it can receive and accept anything which is a concrete implementation of it.
And this is what depending on an abstraction is.

From here, concrete classes could be created to extend TeamChecker in order to be used with the Door, like so:

Code: Select all

class RedTeamChecker extends TeamChecker
{
	function bool isSameTeam(byte team)
	{
		return team == 0;
	}
}

class BlueTeamChecker extends TeamChecker
{
	function bool isSameTeam(byte team)
	{
		return team == 1;
	}
}
Any of them could be given as team checkers to a Door, without violating any of the other principles.
And it would be easy to create more team checkers for other teams, even for completely new teams which do not yet exist in the game, and this is why classes should always depend on abstractions, and never on concretions.

And this finishes the fifth principle, DIP.
If you have anything to add, correct or even criticize, or if you feel like a better example could be used, once again please feel free to provide any feedback you deem necessary.
Last edited by Feralidragon on Fri Mar 30, 2018 9:52 pm, edited 2 times in total.
ShaiHulud
Adept
Posts: 459
Joined: Sat Dec 22, 2012 6:37 am

Re: S.O.L.I.D. in UnrealScript

Post by ShaiHulud »

I don't want to clutter the thread with with anything that doesn't build constructively on what you're sharing here, so I'm glad you planned for that by adding those place-holder messages!

This is very interesting, thank you for adding it. It's been a long time since I updated my OOP ecosystem knowledge and this isn't a system I've heard about before. I was instructed in the use of UML at a time when it was first being introduced, and haven't moved on from there - I was never very proficient anyway. A well partitioned class system is really something to behold. I do find, though, that it makes coming to a new system much more challenging up front. You (or I, anyhow) often end up hunting through half a dozen classes to find the start point of some operation which finds its conclusion in an entirely different location.
User avatar
Feralidragon
Godlike
Posts: 5489
Joined: Wed Feb 27, 2008 6:24 pm
Personal rank: Work In Progress
Location: Liandri

Re: S.O.L.I.D. in UnrealScript

Post by Feralidragon »

ShaiHulud wrote:I don't want to clutter the thread with with anything that doesn't build constructively on what you're sharing here, so I'm glad you planned for that by adding those place-holder messages!
:tu:
ShaiHulud wrote: This is very interesting, thank you for adding it. It's been a long time since I updated my OOP ecosystem knowledge and this isn't a system I've heard about before. I was instructed in the use of UML at a time when it was first being introduced, and haven't moved on from there
UML defines how the program will look like in the end, establishing use cases and the classes to implement, but it's outdated nowadays, especially due to the implementation of Agile processes, which promote quick development and adaptation in smaller iterations, generally spanning just a couple of weeks, rather than planning everything up-front.
ShaiHulud wrote: A well partitioned class system is really something to behold. I do find, though, that it makes coming to a new system much more challenging up front. You (or I, anyhow) often end up hunting through half a dozen classes to find the start point of some operation which finds its conclusion in an entirely different location.
More often than not what really happens is that developers tend to abide by these and other principles so strictly, that they end up violating some other core principles such as KISS (Keep It Simple Stupid), and end up over-engineering a system.
This is one of the reasons why I have been creating my own PHP framework for example, since the existing ones suffer from that exact problem, and they require so much documentation to be read to be properly understood, it's not even funny, and in the end are not suitable to my needs and they commit just too many atrocious mistakes (some of which I made myself in the past, that's how I know they're mistakes).

However, there are systems which become fairly complex and rightfully so, and which may be hard to track down the logic as you put it.
This is where "design patterns" come in: a set of well established common ways for doing something, and which make it easier for other developers to understand the logic of your code.

If you ever heard of things like Factory Pattern, Strategy Pattern, Adapter, Bridge, Proxy, etc, those are all design patterns.
From there, if you check a new system that you don't know anything about, but if get to know at least which design patterns it uses (if it uses any at all that is), it becomes substantially easier to know where to expect and find the rest of the stuff, and how the rest of the classes look like.
SC]-[WARTZ_{HoF}
Adept
Posts: 426
Joined: Tue Feb 21, 2012 7:29 pm

Re: S.O.L.I.D. in UnrealScript

Post by SC]-[WARTZ_{HoF} »

Great read so far Ferali. You definitely explain what you are talking about here so even a newb coder can follow what you are saying. I look forward to the next paragraph. :gj:
Image
Image
Image
User avatar
ANUBITEK
Adept
Posts: 261
Joined: Sun Dec 28, 2014 1:10 am
Location: Anubitek

Re: S.O.L.I.D. in UnrealScript

Post by ANUBITEK »

Definitely appreciated. Higor sent me the spriteanimation manager actor and spriteanimation object I use now and it definitely opened me up to the idea of generally using 1 actor for individual tasks. My spriteparser (reads an ,INT for texture names) does this and is destroyed upon usage to prevent clutter and potential parsing issues.
<<| http://uncodex.ut-files.com/ |>>

Code reference for UGold, UT99, Unreal2, UT2k3, UT3
Additional Beyond Unreal Wiki Links
wiki.beyondunreal.com/Legacy:Console_Bar
wiki.beyondunreal.com/Exec_commands#Load
wiki.beyondunreal.com/Legacy:Exec_Directive#Loading_Other_Packages
wiki.beyondunreal.com/Legacy:Config_Vars_And_.Ini_Files
wiki.beyondunreal.com/Legacy:INT_File
User avatar
Feralidragon
Godlike
Posts: 5489
Joined: Wed Feb 27, 2008 6:24 pm
Personal rank: Work In Progress
Location: Liandri

Re: S.O.L.I.D. in UnrealScript

Post by Feralidragon »

The second principle (OCP) is done and up: viewtopic.php?f=15&t=12753&p=105373#p105373
User avatar
Feralidragon
Godlike
Posts: 5489
Joined: Wed Feb 27, 2008 6:24 pm
Personal rank: Work In Progress
Location: Liandri

Re: S.O.L.I.D. in UnrealScript

Post by Feralidragon »

The third principle (LSP) is done and up: viewtopic.php?f=15&t=12753&p=105372#p105374
User avatar
papercoffee
Godlike
Posts: 10443
Joined: Wed Jul 15, 2009 11:36 am
Personal rank: coffee addicted !!!
Location: Cologne, the city with the big cathedral.
Contact:

Re: S.O.L.I.D. in UnrealScript

Post by papercoffee »

'made this thread sticky.
User avatar
Feralidragon
Godlike
Posts: 5489
Joined: Wed Feb 27, 2008 6:24 pm
Personal rank: Work In Progress
Location: Liandri

Re: S.O.L.I.D. in UnrealScript

Post by Feralidragon »

papercoffee wrote:'made this thread sticky.
Thanks. :highfive:

I am not sure if what I had written thus far is clear enough for everyone, and if it's proving to be useful or not, but at the very least whenever I get to mention SOLID again, I have this topic to point to.
Well, 2 principles left to go, and they're probably the easiest ones to explain, so this will be completed very soon. :)
User avatar
Feralidragon
Godlike
Posts: 5489
Joined: Wed Feb 27, 2008 6:24 pm
Personal rank: Work In Progress
Location: Liandri

Re: S.O.L.I.D. in UnrealScript

Post by Feralidragon »

The forth principle (ISP) is done and up: viewtopic.php?f=15&t=12753&p=105375#p105375

This one ended up being quite a challenge to explain in the context of UnrealScript (since there's currently none), unlike my initial estimate, and it ended a bit more lengthy than the others, which is also why it took a lot longer to post.
Having that said, only one principle is left to explain, and is probably the easiest one to explain, so it will come soon. :)
Post Reply