OO way of adding base field definitions
Without using any hook, it's not possible, since to change the class used for an entity, it's necessary to implement hook_entity_type_alter(), which is problematic to implement when more modules need to alter the entity definition, for example changing the same entity handler, or the entity class, for the same entity type.
Imagine two different modules that try to add two different fields to the User entity by replacing the entity class used for it. For example, the first module could use the following code.
/**
 * Implements hook_entity_type_alter().
 */
function login_id_entity_type_alter(array &amp;$entity_types) {
  $entity_types['user']-&gt;setClass(LoginIdUser::class);
}

namespace Drupal\login_id\Entity;

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\user\Entity\User;

class LoginIdUser extends User {

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
    $fields = parent::baseFieldDefinitions();

    $fields['login_id'] = BaseFieldDefinition::create('string')
      -&gt;setLabel(t('Login ID'))
      -&gt;setDescription(t('The ID used for the login credentials.'))
      -&gt;setRequired(TRUE)
      -&gt;setConstraints([
        'LoginId' =&gt; [],
        'LoginIdUnique' =&gt; [],
    ]);

    $fields['name']-&gt;getItemDefinition()
      -&gt;setClass('\\Drupal\\login_id\\LoginIdItem');

    return $fields;
  }

}

The second module could use the following code.
/**
 * Implements hook_entity_type_alter().
 */
function admin_email_entity_type_alter(array &amp;$entity_types) {
  $entity_types['user']-&gt;setClass(AdminEmailUser::class);
}

namespace Drupal\admin_email\Entity;

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\user\Entity\User;

class AdminEmailUser extends User {

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
    $fields = parent::baseFieldDefinitions();

    $fields['admin_mail'] = BaseFieldDefinition::create('email')
      -&gt;setLabel(t('Admin email'))
      -&gt;setDescription(t('The email used from administrator users to contact the user.'))
      -&gt;setDefaultValue('')
      -&gt;addConstraint('UserMailUnique')
      -&gt;addConstraint('UserMailRequired')
      -&gt;addConstraint('ProtectedUserField');

    return $fields;
  }

}

With two modules implementing that code, only a field would be added to the entity. To get both the entity fields, LoginIdUser should extend AdminEmailUser, which would mean that a module should have the other one as dependency.
This assumes that admin_email_entity_type_alter() is executed before login_id_entity_type_alter(). This is what normally happens when the hook execution order isn't altered by a hook_module_implements_alter() implementation, or when the weight associated to module isn't altered by a call to module_set_weight(). In the case login_id_entity_type_alter() is invoked before admin_email_entity_type_alter(), for example because a module implements hook_module_implements_alter() and changes the order of those hook implementations, AdminEmailUser should extend LoginIdUser.
Implementing hook_entity_base_field_info() allows to modules to add different fields without interfering with each other.
As side note, hooks can be placed in a file different from a .module file, which is automatically loaded from Drupal. Which files are loaded from Drupal when looking for hooks is influenced by hook_hook_info(). With the implementations of that hook done from Drupal core modules, the files Drupal core looks for are the following.

&lt;module_name&gt;.tokens.inc
&lt;module_name&gt;.views.inc

If you are interested in reducing the size of the .module file, implementing hook_hook_info() to tell Drupal where your module hooks are could be a way to achieve it.