2

Context

I am having an argument with my friend on how to start implementing a Genetic Algorithm called NEAT (NeuroEvolution of Augmenting Topologies).

Method 1

I am arguing in favor of having two classes, one for Genome and one for Mutation. This Mutation class will provide methods for mutating genomes by taking a genome as input and returning a mutated genome. Furthermore, this implementation will have classes for each process like Selection, Crossover etc.

Pros

  • This way we will have the option to derive from Mutation class later on to implement various kinds of mutations.
  • I think it will also help with better unit testing.

Cons

  • More classes
  • Does not model real life (for example, a human can walk like a genome can mutate)

Method 2

My friend wants to have only one class for this, the Genome class. In this implementation mutate will be a method of Genome class which will return a mutated genome as output.

Pros

  • This will help reduce complexity.
  • It will also model that a genome can mutate, just like a human can walk.
  • It will also follow convention (seeing code from already implemented solutions).

Cons

  • Less separation of concern, therefore, harder unit testing.
  • Harder to extend later when different kinds of mutations need to be added.
  • 3
    Do genomes mutate, or are they mutated by some other outside force? Is "mutating" something genomes do, or is it something that happens to a genome? Your argument that option 1 does not reflect real life may not actually be true. Furthermore, classes are not required to reflect real life. – Greg Burghardt Jul 25 at 17:07
  • do you have any suggestions on which design should be preferable? – Mohsin Bukhari Jul 25 at 17:11
1

I have a third option for you: Genomes are data. I mean, have value semantics. However, let me go over your pros and cons first.


On Method one

This way we will have the option to derive from Mutation class later on to implement various kinds of mutations.

The idea that you are thinking about deriving from the Mutation class suggests to me that you want the Genome object to take a Mutation object (forcing you to pass an object of the Mutation class or a derived one).

Then inside what? the Genome class could pass data to the Mutation class, so that it creates the data for the new instance, and then the Genome class creates it. Well, if you are doing that, Genome and Mutation are tightly coupled.

Would an interface IMutation serve better here? Perhaps. However, the root of the problem is not creating different mutations, but that the mutations need to work on the data that is in Genome. It could be hard to change implementation details of genome without chaging Mutation (or IMutation for that matter).

I think it will also help with better unit testing.

An interface would be better for unit testing.

More classes

Do not worry about the number of classes. Worry about the Single responsibility principle and coupling.

If you need to have more classes, then so be it.

Does not model real life (for example, a human can walk like a genome can mutate)

I would argue that is not that important.

That is not to say that moderling reality is not useful. It is useful, it makes the code easier to understand and thus easier to maintain. However, when modeling reality becomes counterproductive for maintenance, then please stop worrying abour reality.


On method two

This will help reduce complexity.

Arguebly, yes. Less moving parts means less complexity. It can also mean harder to test and maintain. I'd still count it as a pro in this case.

It will also model that a genome can mutate, just like a human can walk.

This is more of a philosophical argument: You are assigning agency on genomes. Are they alive?

Alright, let us not go there. From a computer science perspective, a genome is a list of attributes. When we use genetic algorithms to evolve - for example - the design of a car, that is not modeling reality.

Besides... modeling reality is not that all important.

It will also follow convention (seeing code from already implemented solutions).

There is a reason for that. The mutation needs to access the data of the genome. If you put the mutation code in the genome class, you do not have to break encapsulation or have tightly coupled classes.

Having the behaviour with the data is the OOP way. If your goal is to stick with OOP, follow this method.


My suggested third method

Have the genome a type with value samantics. If you can, implement the genetic algorithm as a template/generic type. It takes a TGenome and a IMutation<TGenome> or something like that...

Have a class that runs the algorithm. It does not need to know how a Genome is represented. You initialize it with a Mutation. Think of it as an strategy pattern. You can implement mutations that work on different kinds of Genomes.

It is easy to test. It has the code fully decoupled.

In fact, If you can enforce it, make it so genome is not mutable! (it is immutable). Mutation would actually create a copy. It would be "ImperfectClonation". Aside from using an interface, using an immutable type will help you with testing.

Encapsulation? Look: objects are a way of thinking. They are great to model reality. However, sometimes that is not the best tool for the job. If there weren't true, other programming paradigms could not thrive.

Value types have other advantages that objects in general do not have. One of them is the ease of moving them across systems. They are easy to serialize to send over the network or to commit to permanent storage.

If you are making a library, you do not know what kind of atributes the developer will need to run the genetic algorithm on. This option provides that versatility.

However, thinking about it. Perhaps not TGenome but TGene[].

5

In approach 1 you can apply a strategy pattern to support different kinds of mutation.

[Con's] More classes

This is not really a downside.  Classes organize instance members.  You probably have the same number of instance members in either approach.  We should not seek to minimize classes at the expense of conflating concepts.

[Cons] Does not model real life (for example, a human can walk like a genome can mutate)

We don't generally attempt to "model real life" directly using classes.  We seek to model desired concepts, their relationships & behaviors in the overall system, but not to model real life 1:1 with classes — consider that some of our programming languages do not support multiple inheritance, while "real life" biology does (i.e. 2 parents), yet we can still model anything we need to using these programming languages.


In approach 2 you can apply subclassing to override mutation behavior.  This suggests that there is an "is-a" relationship between instances of genome and their mutation, which I would say is false.  Since in most languages we cannot change the class of an instance, you'd have to clone the genome into another class to apply a different mutation.  (This is doable, but it looks like unnecessary complexity.)

[Pro] This will help reduce complexity.

Everything in software can be done in only one class, but this does not reduce complexity.

[Pro] It will also model that a genome can mutate, just like a human can walk.

You need to be able to mutate genomes but that does not necessarily require self-mutating genomes.


Let's also consider lifetimes.  The usefulness/utility of a genome class is likely independent of mutation.  Mutations can be temporarily applied, but genomes are likely used in many other scenarios in your software without being associated with a mutation capability.  Here, I'm referring to the other consumers and whether they also need mutation.

If there is any state associated with mutation, it would appear doubly true that two different lifetimes are being conflated.  Whenever we have different lifetimes among instance fields, this indicates teasing apart multiple concepts.


Naming things is hard.  In approach 1, I'd suggest that another name may be better for the mutation.  It is a mutation kind, approach, cause, or strategy, rather than a mutation itself.  The term mutation colloquially refers to the result of applying a mutation-cause (to a genome).

1

Method 2 is the default design to have based on the most basic object-oriented principle that behavior should live where the data is. It is also the most maintainable, the simplest one.

So I would go with that, until Method 1 is absolutely needed. You are allowed to refactor things later. So go with the simplest design first and introduce indirections/separation only if you get immediate benefit.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.