Зачем плодить кучу файлов в Java?

Я недавно поступил в университет, нам преподают программирование на Java. До этого я писал код только на Python, поэтому множество обыденных вещей для меня в новинку. Недавно на практических занятиях мы получили задание реализовать битву покемонов на основе заданного jar-файла внутрь которого заглядывать нельзя, можно пользоваться только документацией к нему. Я всё сделал, подхожу сдавать. Преподаватель спрашивает: "почему у тебя все классы в одном файле?". Меня озадачил данный вопрос. На мой неопытный взгляд гораздо удобнее написать все классы в одном файле и в нём же их и использовать(я понимаю, что есть задачи, в которых гораздо удобнее разграничить классы, но это не одна из них). На такой ответ мне сказали: "хорошо, я приму работу, если расскажешь, какие могут быть ограничения у такого способа написания кода." Я нашёл причины почему нельзя создать несколько файлов с несколькими классами внутри, но конкретно против способа со всеми классами в одном файле ничего не нашёл.

Так почему нельзя написать все классы в том же файле, где я их использую?

Код прилагаю:

import ru.ifmo.se.pokemon.*;

class Castform extends Pokemon{
    public Castform(){
        super();
        setType(Type.NORMAL);
        setStats(70.0, 70.0, 70.0, 70.0, 70.0, 70.0);
        addMove(new Blizzard());
        addMove(new ScaryFace());
        addMove(new IceBeem());
        addMove(new HydroPump());
    }
    public Castform(String name, int lvl){
        super(name, lvl);
        setType(Type.NORMAL);
        setStats(70.0, 70.0, 70.0, 70.0, 70.0, 70.0);
        addMove(new Blizzard());
        addMove(new ScaryFace());
        addMove(new IceBeem());
        addMove(new HydroPump());
    }
}

class Shelmet extends Pokemon{
    public Shelmet(){
        super();
        setType(Type.BUG);
        setStats(50.0, 40.0, 85.0, 40.0, 65.0, 25.0);
        addMove(new Rest());
        addMove(new DoubleTeam());
        addMove(new RockTomb());
    }
    public Shelmet(String name, int lvl){
        super(name, lvl);
        setType(Type.BUG);
        setStats(50.0, 40.0, 85.0, 40.0, 65.0, 25.0);
        addMove(new Rest());
        addMove(new DoubleTeam());
        addMove(new RockTomb());
    }
} 

class Accelgor extends Shelmet{
    public Accelgor(){
        super();
        setStats(80.0, 70.0, 40.0, 100.0, 60.0, 145.0);
        addMove(new X_Scissor());
    }
    public Accelgor(String name, int lvl){
        super(name, lvl);
        setStats(80.0, 70.0, 40.0, 100.0, 60.0, 145.0);
        addMove(new X_Scissor());
    }
}

class Aron extends Pokemon{
    public Aron(){
        super();
        setType(Type.ROCK, Type.STEEL);
        setStats(50.0, 70.0, 100.0, 40.0, 40.0, 30.0);
        addMove(new Rest());
        addMove(new Stomp());
    }
    public Aron(String name, int lvl){
        super(name, lvl);
        setType(Type.ROCK, Type.STEEL);
        setStats(50.0, 70.0, 100.0, 40.0, 40.0, 30.0);
        addMove(new Rest());
        addMove(new Stomp());
    }
}

class Lairon extends Aron{
    public Lairon(){
        super();
        setStats(60.0, 90.0, 140.0, 50.0, 50.0, 40.0);
        addMove(new Agility());
    }
    public Lairon(String name, int lvl){
        super(name, lvl);
        setStats(60.0, 90.0, 140.0, 50.0, 50.0, 40.0);
        addMove(new Agility());
    }
}

class Aggron extends Lairon{
    public Aggron(){
        super();
        setStats(70.0, 110.0, 180.0, 60.0, 60.0, 50.0);
        addMove(new FireBlast());
    }
    public Aggron(String name, int lvl){
        super(name, lvl);
        setStats(70.0, 110.0, 180.0, 60.0, 60.0, 50.0);
        addMove(new FireBlast());
    }
}




class Blizzard extends SpecialMove{
    public Blizzard(){
        super(Type.ICE, 110, 70);
    }
    protected void applyOppEffects(Pokemon p){
        double ch = Math.random() * 10;
        if(ch >= 0 && ch <= 1){
            Effect.freeze(p);
        }
    }
    protected String describe() {
        return "использует атаку Blizzard";
    }
}

class ScaryFace extends StatusMove{
    public ScaryFace(){
        super(Type.NORMAL, 0, 100);
    }
    protected void applyOppEffects(Pokemon p){
        double ch = Math.random() * 10;
        if(ch >= 0 && ch <= 1){
            p.setMod(Stat.SPEED, -2);
        }
    }
    protected String describe() {
        return "использует атаку Scary Face";
    }
}

class IceBeem extends SpecialMove{     
    public IceBeem(){
        super(Type.ICE, 90, 100);
    }
    protected void applyOppEffects(Pokemon p){
        double ch = Math.random() * 10;
        if(ch >= 0 && ch <= 1){
            Effect.freeze(p);
        }
    }
    protected String describe() {
        return "использует атаку Ice Beem";
    }
}

class HydroPump extends SpecialMove{
    public HydroPump(){
        super(Type.WATER, 110, 80);
    }
    protected String describe() {
        return "использует атаку Hydro Pump";
    }
}

class RockTomb extends PhysicalMove{
    public RockTomb(){
        super(Type.ROCK, 60, 95);
    }
    protected void applyOppEffects(Pokemon p){
        double ch = Math.random() * 10;
        if(ch >= 0 && ch <= 1){
            p.setMod(Stat.SPEED, -1);
        }
    }
    protected String describe() {
        return "использует атаку Rock Tomb";
    }
}

class Rest extends StatusMove{
    public Rest(){
        super(Type.PSYCHIC, 0, 0);
    }
    protected void applySelfEffects(Pokemon p){
        p.setMod(Stat.HP, (int)(p.getHP() - p.getStat(Stat.HP)));
        p.setCondition((new Effect()).condition(Status.SLEEP).turns(2).attack(0));
    }
    protected String describe() {
        return "отдыхает (использует Rest)";
    }
}

class DoubleTeam extends StatusMove {
    public DoubleTeam(){
        super(Type.NORMAL, 0, 0);
    }
    protected void applySelfEffects(Pokemon p){
        p.setMod(Stat.EVASION,(int) p.getStat(Stat.EVASION) + 1);
    }
    protected String describe() {
        return "использует атаку Double Team";
    }
}

class X_Scissor extends PhysicalMove{
    public X_Scissor(){
        super(Type.BUG, 80, 100);
    }
    protected String describe(){
        return "использует атаку X-Scissor";
    }
}

class Stomp extends PhysicalMove{
    public Stomp(){
        super(Type.NORMAL, 65, 100);
    }
    protected void applyOppEffects(Pokemon p){
        double ch = Math.random() * 10;
        if(ch >= 0 && ch <= 3){
            Effect.flinch(p);
        }
    }
    protected String describe(){
        return "использует атаку Stomp";
    }
}

class Agility extends StatusMove{
    public Agility(){
        super(Type.PSYCHIC, 0, 0);
    }
    protected void applySelfEffects(Pokemon p){
        p.setMod(Stat.SPEED,(int) p.getStat(Stat.SPEED) + 2);
    }
    protected String describe(){
        return "ускоряется (использует Agility)";
    }
}

class FireBlast extends SpecialMove{
    public FireBlast(){
        super(Type.FIRE, 110, 85);
    }
    protected void applyOppEffects(Pokemon p){
        double ch = Math.random() * 10;
        if(ch >= 0 && ch <= 1){
            Effect.burn(p);
        }
    }
    protected String describe(){
        return "использует атаку Fire Blast";
    }
}




public class Poke {
    public static void main(String[] args){
        Battle b = new Battle();
        Pokemon p1 = new Shelmet("Shelmet", 1);
        Pokemon p2 = new Castform("Castform", 1);
        Pokemon p3 = new Accelgor("Accelgor", 1);
        Pokemon p4 = new Aron("Aron", 1);
        Pokemon p5 = new Lairon("Lairon", 1);
        Pokemon p6 = new Aggron("Aggorn", 1);
        b.addAlly(p1);
        b.addAlly(p2);
        b.addAlly(p3);
        b.addFoe(p4);
        b.addFoe(p5);
        b.addFoe(p6);
        b.go();
    }
}

Ответы (2 шт):

Автор решения: Komdosh

Дело в контексте. Предполагается, что один класс должен выполнять только одну задачу, причём делать он это должен очень хорошо.

Когда вы смотрите в файл на Java ваш мозг в один момент времени должен держать только маленькую часть контекста всей задачи. Реализуя покемона Aron, вам не требуется думать о Shelmet или о том, как реализуется его SpecialMove.

Кстати в Java в одном файле может быть только 1 публичный класс.

На мой взгляд, самая главная проблема появляется при добавлении контроля версий (возьмём самую популярную - Git). В случае разработки в одном файле у вас всегда будет изменён только 1 файл, коммиты станут не информативными, так как если вы поменяете логику StatusMove, то чтобы это понять, придётся прочитать весь файл, когда в раздельном виде было бы сразу понятно, что поменялась именно эта часть кода. Также будет большая проблема с маржами, если вы добавите в середине файла или поменяете местоположение класса, то скорее всего вам почти всегда придётся решать мёрж конфликты. Ну и pull request на 10к строк тоже читать очень неудобно.

→ Ссылка
Автор решения: Wlad

На такой ответ мне сказали: "хорошо, я приму работу, если расскажешь, какие могут быть ограничения у такого способа написания кода."

В Java в 1 файле может быть только 1 public класс.
В Джаве принято (на уровне этикета) правило "для каждого класса - отдельный файл".

А то, что вы описываете, реализовано в Kotlin'е. И там это правило "этикета" смягчается до "можно объединять классы с общим смыслом в один файл".

Проблема вашего кода в том, что в нем легко разберетесь только ВЫ.
Разбитие классов на файлы (и объединения этих файлов в папки / package) еще нужно и для того, чтобы другой программист смог разобраться.

Я в покемонах не силен, но у вас уже есть логические разбития:

  • виды покемонов - наследники Pokemon
  • скиллы атаки и передвежения (если я правильно понял) - наследники StatusMove
  • атаки-наследники PhysicalMove
  • ультимативные способности - наследники от SpecialMove

И объединять 4 группы таких разных классов в 1 файл не есть хорошо.

→ Ссылка