That being said theories on abstractions do exist. If you learn it you'll be at the top of your game; but it's really really hard to master. If you do master it, you'll be part of a select group of unrecognized elites in a world of programmers that largely turn to "design" while eschewing theory.
Here are two resources to get you started:
The Algebra of Programming: https://themattchan.com/docs/algprog.pdf
Program Design by Calculation: http://www4.di.uminho.pt/~jno/ps/pdbc.pdf
You will note that both of these resources talk about functional programming at its core which should indicate to you that the path to the most optimal abstraction lies with the functional style.
For me this is really good example of how similar looking code is not the same because it has different use case.
So? Just put in some conditionals.
What is the alternative? Duplicate most of the code with minor, non-explicit differences? What's the benefit? You just moved complexity around, you didn't get rid of it.
The drawback is that now anything you have to add, you have to add and maintain it in two places. And since your "add" and "edit" are probably 90% the same, it's going to happen 90% of the time. It's very annoying during development and you're likely to fuck it up at some point.
Obviously an Add/Edit field are operating on the same record in a hypothetical database, so it makes little sense to duplicate the model.
On the other hand, if the conditionals within the abstracted version become too complex or keep referencing some notion of a mode of operation (like, ` if type(self) == EditType && last_name != null` lines of thinking), that is sometimes another type of smell.
But say you make some kind of abstract base class that validates all fields in memory before committing to the database, and then place all of your checking logic in a validate() method. That sounds like pretty clean abstractions to me.
And moreover, this is probably provided by an ORM system and documented by that system anyway--so that's a publicly documented and likely very common abstraction that you see even between different ORMs. That, I think, is the very best kind of abstraction, at least assuming you are already working in such an environment as a high-level language and ORM. Making raw SQL queries from C programs still contain their own levels of abstractions of course without buying whole sale into the many-layered abstraction that is a web framework or something.
This question becomes more important when you aren't just updating a database though. If you're writing some novel method with a very detailed algorithm, over abstraction through OOP can really obscure the algorithm. In such a case, I try to identify logical tangents within the algorithm, and prune/abstract them away into some property or function call, but retain a single function for the main algorithm itself.
The main algorithm gets its definition moved to the base class, and the logical tangents get some kind of stub/virtual method thingy in the base class so that they have to be defined by subclasses. The more nested tangents are frequently where detailed differences between use cases emerge, which makes logical sense. It's not just that it's abstract, but the logic is categorically separated.
It's a very general pattern supported by many languages, so you see it all over the place. That organization and consistency in itself helps you to understand new code. In that way, it also becomes a kind of "idiom" which in a sense is one more layer of abstraction, helping you to manage complexity.
As a counter of that, you see code where `a + x * y - b` becomes self.minus(self.xy_add(a), b). More abstract, but not more logical; not categorically separating; not conforming to common idioms; obscuring the algorithm; and so on...
And then there is performance! Let's not talk about the performance of runtime abstractions.
In the frontend code I decided to use an abstraction and parametrization in the backend code I kept the logic separated.
It really depends on context. Specifically on the layer you are operating on.
The second part is building the intuition for the abstractions themselves. That's tricky as they have to teach themselves. They need to build coherency in their internal mental langauge of abstraction, and the only way to do that is to work directly on real code, and work through the consequences of doing it one way vs. another.
That means you have to let them commit code you don't like. By all means, explain what your concerns are, but then let them see how it evolves and as it becomes more untenable, that's when you go back to rethinking it and trying to state the problem clearly.
Likewise, when they do it well, you can highlight that, especially drawing attention to changes to their code that worked nicely.
The issue is whether sections of similar code implement the same idea or just happen to be accidentally similar. The number of instances does not really matter. If you have 100 lines of code which are almost the same two places in the program, then you should unify sooner rather than later, before they are allowed to diverge.
This is why I like the "Rule of three"[1]. Only once you've done it three times will you truly begin to understand what the abstraction might need to look like.
1. https://wade.be/2019/12/10/rule-of-three.html