In un mondo orientato agli oggetti dominato da linguaggi come Java e C #, l'opzione 1 è la strada da percorrere. Esistono diversi modi per modellare i dati, ad esempio un altro modo è con un albero che può essere percorso. Ma tenendo fede allo spirito della tua domanda, ci sono due approcci da considerare. In entrambi i casi è importante utilizzare le interfacce. La scrittura di programmi in cima alle astrazioni è lontana più flessibile rispetto alla loro scrittura in cima alle implementazioni concrete.
Il primo approccio consiste nell'estrarre codice comune in super classi astratte. Questo è davvero facile da fare con linguaggi come Java e C # e, a prima vista, sembra la scelta più ovvia. Il problema principale di questo approccio è che è troppo facile confondere l'ereditarietà con le astrazioni che si sta tentando di creare. Io sostengo che queste classi astratte create interamente per condividere elementi comuni, ma altrimenti non importanti per l'ereditarietà, il codice fa parte dell'implementazione e quindi non dovrebbero essere invocate nel codice client. Questo può o non può essere un problema, ma in entrambi i casi non c'è nulla che possa essere fatto per evitare che ciò accada.
Per rimediare a questo, il secondo approccio sarebbe quello di favorire la composizione rispetto alle classi astratte. Sebbene questo non sia facile da implementare come il primo, risolve il problema lasciato dalle classi astratte. In particolare, non è possibile fare affidamento sugli artefatti lasciati dai dettagli di implementazione delle classi astratte, a patto che vengano incapsulati correttamente gli oggetti composti. Se, tuttavia, le classi astratte (se hanno anche bisogno di essere astratte) sono importanti per la gerarchia dell'ereditarietà e sono quindi più che semplici "bucket di codice comuni", quindi favorire la composizione ridurrà l'utilità delle classi.
Alla fine penso che dipenda se le classi astratte sono importanti per costruire un'astrazione appropriata basata sull'eredità, o se il codice comune è lì solo per alleggerire l'onere di mantenerlo. Potrebbe essere fastidioso implementare se ci sono molti metodi che delegano solo a un oggetto incapsulato, ma il trade-off potrebbe valerne la pena per qualcosa di più puro. Tuttavia, alcuni linguaggi come Ruby facilitano il supporto della composizione con i suoi moduli, che sono fondamentalmente mixins .