I have a third option for you: Genomes are data. 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[]
.