Specification pattern implementation - how to avoid cyclic dependencies

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By lvl7

First of all, I’m quite new in GDscript and godot itself so even unrelated hints are welcome.

I’m trying to implement specification pattern in GDScript. Simplified, not-working implementation:

SpecificationInterface.gd:

extends Resource
class_name SpecificationInterface

# abstract
func is_satisfied(candidate) -> bool:
	push_error("need to be implemented")
	return false

BaseSpecification.gd:

extends SpecificationInterface
class_name BaseSpecification


class AndSpecification:
	extends BaseSpecification  # <-- cyclic dependency

	var first: SpecificationInterface
	var second: SpecificationInterface

	func is_satisfied(candidate) -> bool:
		return self.first.is_satisfied(candidate) and self.second.is_satisfied(candidate)


func and_(other: SpecificationInterface):
	var and_specification := AndSpecification.new()
	and_specification.first = self
	and_specification.second = other

	return and_specification

The problem is: AndSpecification can’t extend BaseSpecification from script where it’s defined. It can’t be defined in separated script either because BaseSpecification use AndSpecification.

I tired some dirty hack where BaseSpecification call FuncRef of AndSpecification.is_satisfied but it’s also don’t work:

extends SpecificationInterface
class_name BaseSpecification

var _validation_method: FuncRef # (candidate) -> bool

func is_safisfied(candidate) -> bool:
	if self._validation_method != null:
		return self._validation_method.call_func(candidate)

	push_error("need to be implemented")
	return false


class AndSpecification:
	var first: SpecificationInterface
	var second: SpecificationInterface

	func is_satisfied(candidate) -> bool:
		return self.first.is_satisfied(candidate) and self.second.is_satisfied(candidate)


func and_(other: SpecificationInterface): # -> LogicSpecificationInterface:
	var and_specification := AndSpecification.new()
	and_specification.first = self
	and_specification.second = other

    # using own name is not allowed - cyclic dependency again 
	# var new_sepc: = BaseSpecification.new()
	
    # this, surprisingly, is allowed, however it duplicates 
    # my extension of  `BaseSpecification` and not pure 
    # `BaseSpecification` itself. So `is_satisfied` is already overwritten
    # and my dirty hack doesn't work.
    var new_spec: = (self as BaseSpecification).duplicate()   
	new_spec._validation_method = funcref(new_spec, "is_satisfied")
	return and_specification

This is a very interesting design pattern I’ve never heard of until now. Can I ask what some high-level goals you have for the pattern are?
Also, this isn’t really an answer, but godot supports mingling of GDScript, C#, and GDNative scripts among others. It might not be ideal, but have you considered lifting the C# example from the Wiki page you linked for your implementation? I haven’t tried using C# in my projects so I’m not sure if there’d still be odd linking issues or not.

DownloadFLD | 2022-07-26 22:20

I’m working on a game where a player can build some machine from parts. There’re couple kinds of parts like engine, manipulator, life-support. Each kind has it’s own general rules, for example a manipulator can’t be placed directly near an engine. Besides that, each particular part can have special rules, like require at least 5 energy or be connected with life-support.

On top of that, I decided that parts (and their rules) are defined as json file and loaded during gameplay. It’d make game extension/modification trivial, even for non-developers.

I can define atomic rules in code and mention them with logic operators, in json:

{
    "rule": {
        "or": [
            {"and": [{"connected_with": "ENGINE"}, {"min_energy": 5}]},
            {"connected_with": "LIFE_SUPPORT"}
        ]
    }
}

Just writing this example, it made me thinking that I… probably don’t need implementation of specification pattern after all… I’ve just wrote them with normal Polish notation so it should be feasible to create combined rules in the code.
With specification pattern I could write in code:

rule = (
    ConnectedWith("ENGINE").and(MinEngery(5))
).or(Connected_with("LIFE_SUPPORT")

Which is more readable but I have no idea how wanted to parse it to json…

Oh my! Thank you! I should mention my goals in initial post to avoid xy problem.

Also, for some reason, it didn’t even cross my mind that I can use multiple languages at the same time (thanks for that also!). As you said, it isn’t answer I was looking for, however it should work. With small drawback: cross language inheritance is forbidden.

lvl7 | 2022-07-28 18:49

Ah nice, glad you figured it out! I’ve had lots of moments like that where, in the middle of asking for help or describing my problem again, a solution will strike me. Sounds like you know exactly what you need and how to get it.

I was not aware that cross-language inheritance was forbidden, that would definitely throw a wrench into things here or there. Makes sense, though.

DownloadFLD | 2022-07-28 21:50