Java EE6標準の範囲でフルスタックのWebアプリケーションが簡単に作成できることを確かめてみました。
Java EE6でさらに開発は容易になった?
以前JavaEE標準の進化から最近の業務アプリケーション開発手法の変遷について考える - 達人プログラマーを目指してにてJava EE標準の開発モデルの進化について説明しました。10年前の相当面倒だったJ2EEの開発モデルと比べて、最新のJava EE6では、様々なOSSの良い特徴を取り入れて、簡単にプログラミングできるように大幅に改良されています。また、Glassfish 3.1やJBoss AS7などは起動時間が非常に短縮されており*1、よほど遅いPCでなければわずか数秒で再起動することができます。さらに、Java EEサーバーが重くてテスト不能というイメージはもう過去の話かもしれない - 達人プログラマーを目指してで紹介したように、Java EE6では従来困難であった単体試験の自動化も容易になっています。
個々の技術は優れているのだけれど適切なフルスタックのサンプルがない
いまこそ、Java EEの機能をフルに活用してエンタープライズJavaアプリケーションを楽に開発しましょう。
と、自信を持って言いたいところだったのですが、いざ、Java EE6の機能を組み合わせてアプリケーションを開発しようとしても、実はなかなか良いサンプルというかお手本が見つからないという問題があります。Java EE6のドキュメントは少なくとも英語では様々なチュートリアルがWeb上や書籍で見つかるのですが、JSFやJPAなど特定の技術要素をターゲットにした説明がほとんどで、画面からデータアクセスまでを組み合わせた実案件で使えそうなよいお手本がなかなか現時点では見つからないようです。
そこで、NetBeansで自動生成可能なJSF2のサンプルアプリケーションである、JsfScrumToysをリファクタリングし、EJB、CDI、JPAと組み合わせるように修正してみました。最初は簡単にできると予想していたのですが、単純なCRUD処理のアプリケーションを作成するだけでも、想定外の試行錯誤がいろいろと必要で、満足な設計に到達するまでに結構時間がかかってしまいました。それでも、最終的には標準技術のみで実際にかなり簡単に書けることがわかりましたのでここで紹介させていただきます。
リファクタリング結果は以下にアップしてあります。例外処理など、実案件への適用に対してはまだまだ考慮が足りていない部分がありますが、Java EE6開発のベースとして使っていただけると思います。
GitHub - ryoasai/jsf-scrumtoys-refactored: A sample web application using Java EE6 stack.
本エントリに関連して以下のまとめもご参照ください。
JavaEE6を使ったアプリケーション開発について - Togetter
オリジナルのScrumToysの問題点
オリジナルのScrumToysアプリケーションは、NetBeansを用いて自動的に生成することができます。
このアプリケーションは以下のようなドメインモデルのエンティティに対して、各エンティティのCRUD処理を実現する簡単なアプリケーションとなっています。
特に特殊なところはなく、単にお互いに入れ子の関係にあるエンティティを管理するアプリケーションになっています。独立した単テーブルのCRUD処理に比べて関連を適切に処理しなくてはならないところが多少難しいところです。
オリジナルの実装は基本的にJSF2の新機能のデモという位置づけのため、特にデータアクセス層やHTTPセッションの管理などは簡易化できるポイントがたくさん残っています。
データアクセス時のトランザクション管理が面倒
まず、トランザクションの管理はEJBを使わず、以下のように独自にコールバックパターンを使って実装されています。
@PersistenceUnit private EntityManagerFactory emf; @Resource private UserTransaction userTransaction; protected final <T> T doInTransaction(PersistenceAction<T> action) throws ManagerException { EntityManager em = emf.createEntityManager(); try { userTransaction.begin(); T result = action.execute(em); userTransaction.commit(); return result; } catch (Exception e) { try { userTransaction.rollback(); } catch (Exception ex) { Logger.getLogger(AbstractManager.class.getName()).log(Level.SEVERE, null, ex); } throw new ManagerException(e); } finally { em.close(); } }
そして、個別のManagerクラスで以下のようにデータアクセスを実行します。
public String remove() { final Project project = projects.getRowData(); if (project != null) { try { doInTransaction(new PersistenceActionWithoutResult() { public void execute(EntityManager em) { if (em.contains(project)) { em.remove(project); } else { em.remove(em.merge(project)); } } }); getProjectList().remove(project); } catch (Exception e) { getLogger(getClass()).log(Level.SEVERE, "Error on try to remove Project: " + getCurrentProject(), e); addMessage("Error on try to remove Project", FacesMessage.SEVERITY_ERROR); return null; } } init(); // Using implicity navigation, this request come from /projects/show.xhtml and directs to /project/show.xhtml // could be null instead return "show"; }
コールバックパターンを使ってある程度処理を共通化しているものの、それでも相当の鋳型コード(boilerplate code)の記述が必要になっています。Java EE5まではEJBを利用するのがそれなりに面倒だった(earファイルを使う必要があるなど)のですが、EJBを使わない限り標準の範囲内ではコンテナ管理の永続コンテキストが適切に利用できないため*2、このような冗長なコードはやむを得ないところがありました。
セッションの肥大化
オリジナルのScrumToysではほとんどのBeanをSessionスコープに保持しています。
@ManagedBean(name = "sprintManager") @SessionScoped public class SprintManager extends AbstractManager implements Serializable { private static final long serialVersionUID = 1L; private Sprint currentSprint; private DataModel<Sprint> sprints; private List<Sprint> sprintList; @ManagedProperty("#{projectManager}") private ProjectManager projectManager; private Project currentProject; ... }
DataTableなどJSFの多くのコンポーネントは画面表示中データがメモリ上に保持されていることを前提としていることもあり、これも仕方がないところがあります。ただし、上記の例を見てもわかるように検索結果もすべてメモリ上に保持して、ログアウトまでクリアされない状態になってしまっています。検索するデータ量や同時ログインユーザー数が増えればこれは性能上の問題となる可能性が高いですし、特にクラスタ環境では肥大したセッションのレプリケーションは性能上大きなオーバーヘッドになってしまいます。
それから、意外に知られていない事実ですが、上記のコードはスレッドセーフではありません。SessionScopeやApplicationScopeの管理Beanは並行アクセスに対してデフォルトでは保護されないからです。これも、サンプルアプリケーションでは問題にならなくても、実際の案件に適用する際には問題です。
手動の状態同期
ScrumToyのアプリケーションのほとんどはCRUD処理なので本来業務ロジックはほとんどないのですが、メモリ上に保持しているエンティティの状態を手動で同期するコードがかなりの分量記述されています。
public String save() { if (currentSprint != null) { try { Sprint merged = doInTransaction(new PersistenceAction<Sprint>() { public Sprint execute(EntityManager em) { if (currentSprint.isNew()) { em.persist(currentSprint); } else if (!em.contains(currentSprint)) { return em.merge(currentSprint); } return currentSprint; } }); if (!currentSprint.equals(merged)) { setCurrentSprint(merged); int idx = sprintList.indexOf(currentSprint); if (idx != -1) { sprintList.set(idx, merged); } } getProjectManager().getCurrentProject().addSprint(merged); if (!sprintList.contains(merged)) { sprintList.add(merged); } } catch (Exception e) { getLogger(getClass()).log(Level.SEVERE, "Error on try to save Sprint: " + currentSprint, e); addMessage("Error on try to save Sprint", FacesMessage.SEVERITY_ERROR); return null; } } return "show"; }
メモリ上にエンティティの状態を保持しているため、どこかで更新処理を行った場合に正しく状態を同期してやらないと不整合になってしまうわけです。これは一般にこのようなステートフルのアプリケーションを設計する際のもっとも難しいポイントになるのですが、本来同じIDのエンティティであっても複数のインスタンスが存在した場合、お互いの状態を同期してやる必要があります。手動でこのような同期を毎回行うのは面倒ですしバグの原因になります。
EJBの導入によるトランザクション管理の簡易化
改良版では、EJB3.1を組み合わせることで、まず、最初の問題であるトランザクション管理が面倒な点を解消しています。EJB3.1では、
- earファイルを作成する必要がなくwarファイル中に格納できる。
- インターフェースはオプション
ということがあり、以前のEJB3.0の頃と比較して導入の敷居は実際にかなり下がっています。
まず、通常よく行われているGenericDaoパターンのように以下のような親クラスを定義しておきます。
public abstract class JpaRepository<K extends Serializable, E extends PersistentEntity<K>> implements Repository<K, E> { private Class<E> entityClass; @PersistenceContext protected EntityManager em; public JpaRepository(Class<E> entityClass) { this.entityClass = entityClass; } @Override public E findById(K id) { return em.find(entityClass, id); } @Override @SuppressWarnings("unchecked") public List<E> findByNamedQuery(String queryName) { return (List<E>) em.createNamedQuery(queryName).getResultList(); } @Override public E persist(E entity) { if (entity.isNew()) { em.persist(entity); return entity; } else { return entity; } } @Override public void remove(K id) { E managed = findById(id); em.remove(managed); } @Override public void remove(E entity) { remove(entity.getId()); } }
そして、これを継承した各エンティティ用のレポジトリクラスをステートレスEJBとして以下のように作成するようにしました。
@Stateless public class ProjectRepository extends JpaRepository<Long, Project> { public ProjectRepository() { super(Project.class); } public long countOtherProjectsWithName(Project project, String newName) { Query query = em.createNamedQuery(project.isNew() ? "project.new.countByName" : "project.countByName"); query.setParameter("name", newName); if (!project.isNew()) { query.setParameter("currentProject", project); } return (Long) query.getSingleResult(); } }
通常のGenericDaoパターンと同様に、正しくインターフェースを定義して実装させても良いのですが、ここではEJB3.1のノーインターフェースビューの機能を使い、インターフェース定義を省略しています。
JSFの管理BeanをCDIの管理Beanに修正
オリジナルではJSFの管理Beanとして定義されていたのですが、これをCDIの管理Beanとして定義しなおしました。修正点としては、
import javax.faces.bean.ManagedBean; import javax.faces.bean.SessionScoped; ... @ManagedBean(name = "skinManager") @SessionScoped public class SkinManager extends AbstractManager implements Serializable {
のような宣言を、以下のように修正します。
import javax.enterprise.context.SessionScoped; import javax.inject.Named; ... @Named @SessionScoped public class SkinManager extends AbstractManager implements Serializable {
ここで、単純名が同じなので間違えやすいのですが、@javax.faces.bean.SessionScopedを@javax.enterprise.context.SessionScopedに修正しています。(大混乱に陥っているJavaEE 6のアノテーションに関する使い分けについて - 達人プログラマーを目指して)
CDIを利用することで、JSFの管理Beanを使う場合と比べて以下のメリットがあります。
@SessionScopedを@ConversationScopedに修正
@SessionScopedのBeanはログイン中ずっと状態が保持されますが、@ConversationScopedのBeanはプログラム中で会話の開始と終了を指示することでメモリの状態を効率的に管理することができます。実際、以下のようなベースクラスを作成し、Conversationを@Injectによりインジェクションすることで、会話の開始と終了をコントロールするようにしてみました。
public abstract class BaseCrudAction<K extends Serializable, E extends PersistentEntity<K>> extends AbstractAction implements Serializable { ... @Inject protected Conversation conversation; // 会話をbiginしている状態中のみ複数リクエストにまたがって状態が保持される。 public void beginConversation() { if (conversation.isTransient()) { conversationNested = false; conversation.begin(); } else { conversationNested = true; } } // 会話をbiginしていない、あるいはendを呼び出した後は各リクエスト終了後に解放される。 public void endConversation() { if (!isConversationNested() && !conversation.isTransient()) { conversation.end(); } this.currentEntity = null; } ...
独自の@ViewScopedを作成して検索結果を保持させる
この時点で問題となったことがあります。会話スコープによりメモリの解放を管理できるようになったのですが、会話をいつbeginし、endすればよいのかということを適切に設計する必要があるという点です。もちろん、最初から会話をbeginすることで、もともとのSessionスコープの時と同様の動作をさせることができるのですが、それでは、セッションの肥大化というもともとの問題が解決されません。
通常考えられるあるべき設計としては、
- 一覧表示は会話の外で行う。
- 特定の行を画面で選択して入力フォーム画面を表示させるタイミングで会話を開始する。
- 更新、作成、キャンセルなどの処理実行時に会話を終了する。
- 行削除は会話を開始させずに実行する。
という方法です。
しかし、この方法をそのまま利用するのではうまくいかないことがわかりました。なぜなら、JSFのDataTableは表示時とポストバック時でデータが保持されていることが前提のためです。一覧表示を会話の外で行った場合、ポストバック時にデータが消えてしまうためテーブル中の行選択が正しく動作しません。
一覧データを長期間保持したくないのに、少なくとも多くのJSFのコンポーネントは同一画面へのポストバック時にデータが残っていることを期待しています。
JSFの場合@javax.faces.bean.ViewScopedで指定されるビュースコープというものがあります。このスコープを使うとセッションや会話スコープにデータを保存しなくても、一つのビューを表示している最中のみデータを保持させることができ便利です。問題なのは、JSFとCDIを組み合わせる上で便利なビュースコープがCDIの標準では定義されていないことです。それで、いろいろやり方を探していたのですが、以下のサイトに方法が書かれていました。
http://www.verborgh.be/articles/2010/01/06/porting-the-viewscoped-jsf-annotation-to-cdi/
この方法に従うと、意外に簡単にCDIで独自のビュースコープを定義することが可能です。まず、以下のようにViewScopedのアノテーションを定義します。
@Inherited @NormalScope @Retention(RUNTIME) @Target({METHOD, FIELD, TYPE}) public @interface ViewScoped { }
次に、以下のようにContextとSystemEventListenerを実装するViewScopedContextを作成し、JSFのビューに関するイベントをハンドリングしてデータをJSFのUIツリー上で正しく管理するロジックを実装します。
public class ViewScopedContext implements Context, SystemEventListener { ... }
そして、以下のようなCDIのExtentionを作成して上記のクラスを登録します。
public class ViewScopedExtension implements Extension { public void addScope(@Observes final BeforeBeanDiscovery event) { event.addScope(ViewScoped.class, true, true); } public void registerContext(@Observes final AfterBeanDiscovery event) { event.addContext(new ViewScopedContext()); } }
最後に、以上のExtentionをMETA-INF/services配下のjavax.enterprise.inject.spi.Extensionという名前のファイル中で登録します。
以上の拡張を行うことで、検索結果のリストをビュースコープに保持できるようになります。例えば、以下のようなCDIの生成メソッドを記述することで、検索結果のリストをビュースコープに保持できます。
@Produces @Named @ViewScoped public List<Project> getProjects() { return projectRepository.findByNamedQuery("project.getAll"); }
したがって、JSFのテーブルで以下のようにして正しく表示することができます。
<h:dataTable value="#{projects}" var="project" rendered="#{not empty projects}" title="#{i18n['project.show.table.title']}" summary="#{i18n['project.show.table.title']}" border="0" headerClass="datatableHeader" rowClasses="datatableRow,datatableRow2" columnClasses="dataTableFirstColumn" styleClass="datatable" id="dtProjects"> <h:column> <f:facet name="header">#{i18n['project.show.table.header.name']}</f:facet> #{project.name} </h:column> <h:column> <f:facet name="header">#{i18n['project.show.table.header.startDate']}</f:facet> <h:outputText value="#{project.startDate}"> <f:convertDateTime pattern="#{i18n['project.show.table.header.startDate.pattern']}" /> </h:outputText> </h:column> ....
ステートフルセッションBeanを利用してエンティティの一意性を自動的に保障させる
最後に、状態の同期の問題について考えます。JPAでは永続コンテキストがあり、これが持続している期間中は自動的にエンティティの一意性を保証してくれるようになっています。(Identity Mapパターン、O/Rマッピングで緩和されるインピーダンスミスマッチには静的と動的の側面がある - 達人プログラマーを目指して)
したがって、基本的な発想として、CDIの会話スコープが継続している最中ずっとJPAの永続コンテキストを持続させることで、エンティティの状態管理とキャッシュをJPAに任せてしまうことができればアプリケーション側の状態管理が大幅に簡単になるはずです。
複数のDBトランザクションをまたがって長期間JPAの永続コンテキストを持続させるには、
- ステートフルセッションBeanを使う
- ステートフルセッションBeanを会話スコープに保持する
- @PersistenceContext(type= PersistenceContextType.EXTENDED)というアノテーションを使ってEntityManagerをインジェクションする
- EJBのデフォルトのトランザクション属性をTransactionAttributeType.NOT_SUPPORTEDにして勝手にDBに更新が行われないようにする。
- 本当にDBに対して書き換えを行いたいメソッドに対してTransactionAttributeType.REQUIREDを付ける。
といった規則に従う必要があります。
実際に試してみて分かったのですが、この方式で最も問題となるのはEXTENDED指定された永続コンテキストの伝搬の制約です。理想的には同一の会話スコープ内であれば、複数のBeanから単一の永続コンテキストのインスタンスにアクセスしたいのですが、現状のJava EEの仕様では単一のステートフルBeanからステートレスBeanに対してのみ正しく伝搬されるようです。特に、複数のステートフルBeanに分割した場合、同一の永続コンテキストを簡単に共有させることができません。*3
とりあえず、今回はこのアプリケーションの会話で管理するすべての状態をScrumManagerImplという一つのステートフルBeanに集約することで対応することにしました。
@Stateful @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) @ConversationScoped public class ScrumManagerImpl implements ScrumManager, Serializable { private static final long serialVersionUID = 1L; //========================================================================= // Fields. //========================================================================= private Project currentProject; private Sprint currentSprint; private Story currentStory; private Task currentTask; @Inject ProjectRepository projectRepository; @Inject SprintRepository sprintRepository; @Inject StoryRepository storyRepository; @Inject TaskRepository taskRepository; @Inject @SuppressWarnings("NonConstantLogger") transient Logger logger; @PersistenceContext(type= PersistenceContextType.EXTENDED) protected EntityManager em; //========================================================================= // Bean lifecycle callbacks //========================================================================= @PostConstruct public void construct() { logger.log(Level.INFO, "new intance of {0} in conversation", getClass().getName()); } @PreDestroy public void destroy() { logger.log(Level.INFO, "destroy intance of {0} in conversation", getClass().getName()); } //========================================================================= // Project management //========================================================================= @Produces @Current @Named @Override public Project getCurrentProject() { return currentProject; } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void setCurrentProject(Project currentProject) { this.currentProject = projectRepository.toManaged(currentProject); currentSprint = null; currentStory = null; currentTask = null; } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void saveCurrentProject() { assertThatEntityIsNotNull(currentProject); if (!currentProject.isNew()) return; projectRepository.persist(currentProject); } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void removeProject(Project project) { projectRepository.remove(projectRepository.findById(project.getId())); } //========================================================================= // Sprint management //========================================================================= @Produces @Current @Named @Override public Sprint getCurrentSprint() { return currentSprint; } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void setCurrentSprint(Sprint currentSprint) { this.currentSprint = sprintRepository.toManaged(currentSprint); currentStory = null; currentTask = null; } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void saveCurrentSprint() { assertThatEntityIsNotNull(currentProject); assertThatEntityIsNotNull(currentSprint); if (!currentSprint.isNew()) return; currentProject.addSprint(currentSprint); } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void removeSprint(Sprint sprint) { assertThatEntityIsNotNull(currentProject); currentProject.removeSprint(sprint); } //========================================================================= // Story management //========================================================================= @Produces @Current @Named @Override public Story getCurrentStory() { return currentStory; } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void setCurrentStory(Story currentStory) { this.currentStory = storyRepository.toManaged(currentStory); currentTask = null; } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void saveCurrentStory() { assertThatEntityIsNotNull(currentSprint); assertThatEntityIsNotNull(currentStory); if (!currentStory.isNew()) return; currentSprint.addStory(currentStory); } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void removeStory(Story story) { assertThatEntityIsNotNull(currentSprint); currentSprint.removeStory(story); } //========================================================================= // Task Management //========================================================================= @Produces @Current @Named @Override public Task getCurrentTask() { return currentTask; } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void saveCurrentTask() { assertThatEntityIsNotNull(currentStory); assertThatEntityIsNotNull(currentTask); if (!currentTask.isNew()) return; currentStory.addTask(currentTask); } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void removeTask(Task task) { assertThatEntityIsNotNull(currentStory); currentStory.removeTask(task); } @TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void setCurrentTask(Task currentTask) { this.currentTask = taskRepository.toManaged(currentTask); } }
なお、Seam3ではこの問題に対処するため、EXTENDED指定された永続コンテキストを利用する代わりに、アプリケーション管理の永続コンテキストを会話スコープに生成して、任意のBeanにインジェクションして共有するしくみが存在します。
http://seamframework.org/Seam3/PersistenceModule
ただし、残念ながら現時点では互換性上の問題からか、Glassfish上では動作しませんでした。
まとめ
ライブラリーの助けを借りず、Java EE6の標準機能のみを利用して簡単にWebアプリケーションが作成できることを確かめるため、JsfScrumToysというサンプルアプリケーションをリファクタリングする実験を試みました。
今後、
などの課題が残っていますが、いくつかの注意点を克服すれば、Java EE6標準の範囲内で実際にかなり簡単にWebアプリケーションを開発できることがわかりました。
ただし、このような簡単なアプリケーションですら実際にいくつかの落とし穴があるわけですので、実際のアプリケーションを開発する際にはプログラミングの方式を十分に研究してどの技術をどのように組み合わせるのかを考えることが大切であると思いました。今回はなるべく広範囲の技術の組み合わせを確認してみたのですが、場合によってはJPAだけ使う、JSFだけ使うといったことが適切な場合もあるかもしれません。また、今回はオリジナルの設計を踏襲してステートフルな設計にしましたが、得失を見極めて採用するかどうか検討する必要があります。
staticおじさんとオブジェクトおじさんはお互いに分かり合えるようになるかもしれません。
先日書いたstaticおじさん達に伝えたい、手続き指向とオブジェクト指向の再利用の考え方の違いについて - 達人プログラマーを目指してのエントリに、なんと、みながわけんじ氏ご本人よりコメントを頂きました。もともとは一般のstaticおじさん達(英語ではstatic ojisansという感じ)に向けて書いたのですが、思いがけず、元祖staticおじさん(The static ojisanあるいはMister staticといった感じ)ご本人からのご意見をいただき、本当に嬉しく思います。
オブジェクト指向の再利用性と非オブジェクト指向の関数やサブルーチンとの違いを明確に示していないから
いろいろ理屈を込めても無駄ではないでしょうか?
誰かが作ったクラスを継承して再利用したところで、バグが少なくて、メンテナンス性がいいものができるでしょうか?
そんなものをあてにするより、天才が作ったクラスライブラリやフレームワークを利用して、自分はstaticで作ってしまったほうが、
よっぽど開発効率がよい!というのが今の考え方です。これが今時点で勝つための方法です。
今時、再利用云々いっているのは、十年古い考え方で、私はユーザー企業なので、そういう古臭い会社とは、おつきあいしません。
私が仕事をぜひとも依頼したい人はマイクロソフトのクラスライブラリをよく知っている技術者です。
私の考え方は古くない、むしろコンテンポラリーだと認めていただけないと、たぶん、あなたはビジネスチャンスを潰すでしょう。
いつも通り、なかなかに手厳しいご意見なのですが、ご本人のブログに以下のように書かれていました。
レイヤーつまりソフトウェアアーキテクチャについて正しい認識、センスを持つということで彼に同意です。
いいクラスライブラリ、いいライブラリ関数というレイヤーの上で、いい業務アプリケーションが開発できるというのが素直な考え方です。多くのかたがレイヤーという概念というかセンスを持っていないことに日本のプログラム開発の悲劇がありそうです。レイヤーという概念を把握せずに、クラス分け、つまりプログラムをクラスというサブプログラムに分割してしまうことにより、趣味の悪いメンテナンス性の悪いプログラムができあがってしまう。
私の自論としては、最上位のレイヤーは関数やstatic関数でかなりいけてしまう、その下のレイヤーは現代の開発ツールですとクラスライブラリ化、コンポーネント化されているのでオブジェクト指向となります。だからと言って、業務アプリケーション開発者はオブジェクト指向の勉強をしなくていいということにはなりません。クラスライブラリを使いこなすには、かなりの努力が必要です。
なるほど、以上を読むとオブジェクトおじさんの私としても、かなり共感できるところがありますね。つまり、アプリケーション開発(特に業務アプリケーション開発)の世界において、オブジェクトは部品として利用するけれども、最上位のレイヤーでは手続き的な記述で十分であるということです。staticにするかどうかはともかく、多くの業務システムは上位のアプリケーション層やプレゼンテーション層はステートレスで手続き的な処理を記述することが一般的ですし、並行処理の観点やスケーラビリティの観点からも、それが好ましい場合が実際に少なくないのです。実際、Springなどを使ったJavaのサーバーサイドの開発ではコントローラーやサービスといったクラスはステートレスで、かつシングルトン(インスタンスが一つ)として作成することが一般的であり、実質的にはstaticメソッドで手続きを記述するといったことと大差はありません。また、AccessのVBAなどでもデータアクセスやボタン、入力項目などの画面部品はオブジェクトとして再利用しますが、処理は標準モジュールと言語の構文を使って手続き的に記述すればよいことが多いです。
実際、以前にJava EEや.NETはCOBOLやVB6よりも本当に生産性が高いか? - 達人プログラマーを目指してでも書いたのですが、たいしたロジックが不要なデータベースのCRUD処理を中心とした業務システムで、無意味なオブジェクトを多用すればかえって生産性が大きく下がるといったことも事実なのです。
残念なことに、単純にUPDATE文を一つ発行すれば済む処理なのに、SIerの不適切なフレームワークの規約に従う必要から、大量のクラスを作成して何度も値の詰め替えのみ繰り返しながら、ビジネスロジックであるSQL文の実行を行い、結果も逆順に詰め替えてようやく画面に表示するといったようなケースをいろいろな現場で目撃してきました。(侵略的なフレームワーク - 達人プログラマーを目指して)ひどいケースでは本質的なロジックが占める割合が分量からいって10%以下というケースも珍しいことではありません。このような設計は開発工数や保守費用を水増しして売上を増大させるというSIerのメリットにはなっても、決してユーザ企業のメリットになることはありません。もしかしたら、みながわさんもそのようなSIerのダメダメな自称オブジェクト指向フレームワークの被害者の一人なのかもしれないと思いました。
私自身本当にそのような無意味なオブジェクト(お邪魔妖怪アンチパターン)が世の中に氾濫することを心から憎みますし、そういうフレームワークが広く使われているということは本当に問題であると考えています。そうであれば、みながわさんの主張されるようにマイクロソフトなどが提供する使い勝手のよいフレームワークや言語ツールを活用して、生産性を上げるということは(ベンダロックインなどの問題を考えないのであれば)有効な手段であると考えます。
CRUD処理を行うような簡単なアプリケーションに対して、画面を作成するのであれば、
- フォームを生成する
- フォームに入力フィールドを追加する
- フォームに検索グリッドを追加する
- 検索グリッドにDBのXXXテーブルをバインドする
- フォームにボタンを配置する
といった非常に簡単な記述(場合によってはGUIツール上のプロパティ設定)により、アプリケーションが完成します。この場合は、フォームやDBグリッドといったようなオブジェクトがプログラム記述の言葉としても使われているのですが、VBAのような上位レベルのプログラムをこのような特化した「言葉」を用いたある種のDSL(ドメイン特化言語)であると考えることができます。*1GUI部品のクラスライブラリがDSLのモデルを提供し、それを利用する上位層は手続き的なスクリプトやグラフィカルなエディタ上の設定をDSLとして利用すればよいのです。この場合のDSLは一般にデータベースアプリ構築や画面構築に特化した言語であり、汎用のオブジェクト指向言語のように継承したりデザインパターンを使ったりすることは必要ありません。このようなDSLという考え方については、最近以下のような書籍が出版されています。
Domain-Specific Languages (Addison-Wesley Signature Series (Fowler))
- 作者: Martin Fowler
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 2010/09/23
- メディア: ハードカバー
- 購入: 1人 クリック: 55回
- この商品を含むブログ (10件) を見る
- 作者: Debasish Ghosh
- 出版社/メーカー: Manning Publications
- 発売日: 2011/01/07
- メディア: ペーパーバック
- クリック: 16回
- この商品を含むブログ (2件) を見る
ただし、ここで見落としてはならない重大な落とし穴があるという点に注意が必要なのです。マイクロソフトが提供する便利な部品はあくまでもCRUD処理のような汎用的なデータベースアプリケーションを開発することを念頭においたしかけのみを提供しているということです。したがって、処理が純粋なデータアクセス処理の範囲であれば非常に有効に機能するのですが、業務処理そのものが複雑なドメインではまったく力不足ということがあるのです。多種類の注文の銘柄を扱ったり、顧客の種類に応じてチェックロジックが微妙に異なったりするといったことを汎用部品はカプセル化してくれません。そのような複雑な業務ロジックを画面部品とDBアクセス部品と手続的なif文やループ文のみで表現した場合、きわめて複雑でメンテナンス不可能なスパゲッティコードになったり、ほんの一部しか異なる部分が無いような大量の関数がいたるところにコピーされてしまうといった状況に陥ってしまうのです。
画面部品はオブジェクトとして実際には複雑な描画ロジックをカプセル化してくれています。だから、わざわざ昔のBASICのように線を引く、色を塗るといった低レベルのルーチンの組み合わせを呼び出さなくても簡単に画面に配置することで再利用できます。同様に、本来は複雑な注文や顧客といったオブジェクトも同様に部品として再利用可能なオブジェクト指向のドメインモデルを作って上位のアプリケーション層やプレゼンテーション層から再利用するという発想だって可能なのです。それが、本ブログで既に何度も紹介している
エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)
- 作者: エリック・エヴァンス,今関剛,和智右桂,牧野祐子
- 出版社/メーカー: 翔泳社
- 発売日: 2011/04/09
- メディア: 大型本
- 購入: 19人 クリック: 1,360回
- この商品を含むブログ (131件) を見る
結局、staticおじさんとオブジェクトおじさん(DDDおじさん)の違いは、常に与えられたオブジェクトのみを利用してアプリケーションを作ると考えるか、必要に応じて独自の部品を作ろうと考えるかの違いに過ぎないのではないでしょうか。いずれにしても、SIerの変なフレームワークが嫌いということでは共通していますし、レイヤーという考え方を重視するという点、部品の利用を重視する点においても共通しています。(少なくも元祖の)staticおじさんとオブジェクトおじさんは、本来はお互いに分かり合えるようになるかもしれないと思いました。
JavaのFileクラスは不変(immutable)クラスという点に関する注意点
長年Javaを書いてきた人間としてはちょっと情けないことに、先日、会社で自分の書いたコードが原因でちょっとしたバグを出してしまいました。きちんとテストファーストで単体試験は書いていたのですがテストが不十分でしたね。
バグの原因は、Fileクラスの仕様をちょっと勘違いして使っていたことが原因でした。FileクラスにはrenameTo()というメソッドがあって、このメソッドの呼び出しにより、操作が成功すればもともとFileクラスのオブジェクトに対応していたファイルの名前がファイルシステム上で変更されます。ここで、うっかり、Fileクラスが可変なクラスだと勘違いしてしまっていたのですが、実は、Java Docにも明記されている通り、Fileクラスは不変(immutable)なクラスであり、一度生成したら状態が決して変更されることがない設計となっています。これは、以下のテストケースを見ると確認できます。
@Test public void legacyAPI() throws IOException { File srcFile = new File("temp.txt"); File targetFile = new File("temp2.txt"); if (!srcFile.createNewFile()) { throw new IOException(); } if (!srcFile.renameTo(targetFile)) { throw new IOException(); }; assertThat(srcFile.getName(), is("temp.txt")); //srcFileの状態はrename前のまま。 assertThat(targetFile.getName(), is("temp2.txt")); if (!targetFile.delete()) { throw new IOException(); } }
ただ、言い訳ではありませんが、FileクラスのrenameTo()というメソッドは
boolean renameTo(File dest)
というシグネチャで定義されており、いかにも状態が変更されそうな雰囲気なため、Fileが不変ということを忘れていると、私のようにうっかりミスしてしまいそうです。実際、EclipseなどのIDEの入力補完に頼っていると間違いそうですね。おまけに、戻り値で成功か失敗かがbooleanで返ってくるのですが、どうしてIOExceptionを送出するようになっていないのでしょうか。以下のようになっていたら、間違いにくいと思うのですれどね。
File renameTo(File dest) throws IOException
このように例外を使わず、戻り値で成功失敗を判断させるというのはFileクラスの他のメソッドにも見られますが、必ず戻り値をチェックするようにする必要があります。このようになっている原因はC言語の雰囲気の影響を受けていることと、FileクラスがJDK1.0の時代からある相当古いクラスであるということもあるかもしれませんが注意が必要だと思います。
なお、Java7からはファイルシステムを扱う新しいAPIが導入されています。
http://download.java.net/jdk7/docs/api/java/nio/file/package-summary.html
2011-07-07
Fileクラスの代わりにPathクラスを使って特定のディレクトリやファイルが表現され、Filesクラスを使って実際のファイルの操作ができます。
@Test public void java7API() throws IOException { Path srcPath = FileSystems.getDefault().getPath("temp.txt"); Path targetPath = FileSystems.getDefault().getPath("temp2.txt"); assertThat(srcPath.toString(), is("temp.txt")); Path createdPath = Files.createFile(srcPath); assertThat(srcPath, is(createdPath)); Files.move(srcPath, targetPath); Files.delete(targetPath); }
Fileクラスは依然としてDeprecatedなわけではありませんが、Java7からはなるべくこちらのAPIを使うようにすべきでしょう。また、Java6までの環境では必要に応じてcommons-ioなどのライブラリーを使うとよいと思います。
なお、この問題は話題のGroovyを使うときにも同様に出くわしますので注意が必要です。
Javaエンジニア必携の「プログラミングGroovy」を献本していただきました
昨日、ポストを確認したところ、執筆陣の皆さんより、献本として送っていただきましたプログラミングGroovyが届いていました。
- 作者: 関谷和愛,上原潤二,須江信洋,中野靖治
- 出版社/メーカー: 技術評論社
- 発売日: 2011/07/06
- メディア: 単行本(ソフトカバー)
- 購入: 6人 クリック: 392回
- この商品を含むブログ (155件) を見る
そして、この本*1に対して、最初に感じた印象は「想像していたよりもずっとコンパクトな本だな」というものでした。一般的にプログラミング言語を解説する書籍は、入門書であってもそれなりのサイズとページ数があるのが普通なのですが、この本はいわゆる単行本サイズで、電車の中でも本当に気軽に鞄にいれて持ち運べるサイズです。しかし、内容が薄いかといえば全然そんなことはなく、今までそれなりにGroovyを勉強して使ってきた私が読んでも十分に読み応えのある内容が詰まっていました。
この時に出た感想は以下の通りです。
そうしたら、さっそく関谷さん(id:ksky、@kazuchika)より、
というコメントが返ってきました。なるほど、そうですね。確かにまったく未知の言語を勉強するというわくわく感は感じられないかもしれませんが、GroovyというのはJavaプログラマーにとっては、自分の「母国語」がいつの間にか最強のツールに生まれ変わっていたといった感じのする言語です。一から文法を暗記し直さなくても、差分を効率よく復習していきなり便利なプログラムを書けるようになるといったところがあります。
この本はそのようなGroovyの特性を最大限に活用し、Javaプログラマーならだれでも知っているような内容はあえてバッサリと省略されています。そのかわり、
- Groovyのインストールと開発環境の構築
- Java言語との違いに重点を置いたプログラミング言語Groovyの解説
- Groovyの主要なライブラリ
- 進んだ話題
- Grapeによるモジュール管理
- メタプログラミング
- AST変換
- Groovyエコシステム(JGGUGの勉強会(G*ワークショップ)に初めて参加してきました - 達人プログラマーを目指してでも一部紹介しています。)
- Groovy1.8の新機能(第16回 G*ワークショップ(Groovy勉強会)に参加してきました。 - 達人プログラマーを目指しての上原さんの資料も参照)
など、JGGUGの勉強会などで紹介されるGroovyのエッセンスがわずか320ページの中に凝集されています。基本的にはどこの章から読んでも良い感じですし、時間がなければ、まず、3章までを読み、あとは興味のあるところから読むというスタイルでも役に立つと思います。
今回、献本いただいたからというわけではありませんが(普段お世辞を言うことは苦手なので)表紙に書いてある通りJavaエンジニア必携の良書だと思います。「Pragmatic Groovy for Java Programmers」「Groovy distilled」的な感じで英訳したら世界中で売れるのではないかと思われる出来です。
逆に、まったくJavaのコードを書いたことのない人だと、いきなり本書を学習するのは難しいかもしれません。あと、一つ難点をあえて挙げるとすれば、多少フォントが小さ目(特にサンプルプログラム)なところはあるかもしれません。
普段業務でJavaを使っている人であれば、本書を学習することによって最短で、実用的に、仕事でGroovyを活用できるGroovy使いになることが可能だと思います。つまり、業務ロジックの記述を簡易化したり、ビルドツールとして活用したり、単体試験のツールとして利用するといったことがすぐにできるようになるでしょう。値段の価値が十分にある本で、すべてのJavaプログラマーにお勧めします。
staticおじさん達に伝えたい、手続き指向とオブジェクト指向の再利用の考え方の違いについて
何が良いプログラムかという点はもちろん人やコンテキストによって異なりますが、少なくともプログラマーとしての私の信念としては、
- 機能拡張や変更が容易なプログラム
- 単体試験によって正しく動作することの検証が容易なプログラム
- どういった内容が記述されているか理解しやすいプログラム
といったものこそ、「品質の高い」プログラムが持つべき性質として、まず真っ先に挙げるべき事項であると考えています。もちろん、前提として顧客の要件に従うということは大切なことです。しかし、一般に要件は長期にわたって変更されるものですし、使い捨てのプログラムを除けば、プログラムを長期にわたって保守するコストという点も見過ごすべきではありません。したがって、ユーザーの目には触れない上記の性質をもっと重視すべきだと思うのです。
DRYの原理
上記のような性質を満たすプログラムを作る上で大切になってくる原理として、DRYの原理という原理が知られています。これは、Don't Repeat Yourselfということで、同じ作業を2度と繰り返すなという考え方です。同じようなロジックのコードが現れたら、メソッドとして一か所にくくり出すなどの共通化を図れということですね。この原理は
- 作者: アンドリューハント,デビッドトーマス,Andrew Hunt,David Thomas,村上雅章
- 出版社/メーカー: ピアソンエデュケーション
- 発売日: 2000/11
- メディア: 単行本
- 購入: 42人 クリック: 1,099回
- この商品を含むブログ (347件) を見る
- そもそも記述するコードを少なくできる。
- ロジックの単体試験が一か所ですむ。
- バグの修正や機能変更があった場合でも一か所の修正ですむ。
- プログラムのサイズが小さくなるからコードが理解しやすくなる。
- なによりも、プログラマーにとってきれいなコードを書いているという満足感がある。
など、さまざまなメリットが得られます。
もちろん、さまざまな制約から、原理主義的に完璧にこの原理を守るということは不可能ですし、また、特に大規模な構造の設計になれば、意図的にDRYの原理を破った方がメリットが高いということも考えられます。達人プログラマーの本でも理想的なプログラムは決して作れないということが書かれています。したがって、実践的な職業プログラマーに対する教えとして、あくまでもチャンスがあれば少しでも目指すべき方向という意味でこの原理を紹介しているのだと理解しています。
staticおじさんの頭の中における再利用のイメージ
だから、私は「できれば、DRYなコードを目指したい。それができないときは、罪の意識を持って実行し、チャンスがあればリファクタリングしたい。」というような考え方でプログラムを書きたいと考えてきました。
しかし、SI業界の多くの現場においては、共有すべき目標という意味においてすら、このDRYの原理が守られていないということがあるのだということを知りました。業界の情シス部門やSIerで何十年前にCOBOLやアセンブラなどで開発を担当し、現在は現役でコードを読むことも書くこともないが、開発基準やアーキテクチャを決める上で発言権のある、いわゆる上級エンジニアという立場の方々が多数いらっしゃいます。ここではそのような方々をちょっと親しみを込めて総称的にstaticおじさんと呼ぶことにしましょう。つまり、まったくIT技術と無関係の方々ではなく、長年専門の技術者として業務システムの開発や運用に関する仕事を経験され、組織内で技術面での意思決定者として、かなり高い地位を得ているような方々です。
もともとのモデルはこの方ですが、ここではこの業界ではどこにでもいそうな一般的な技術者を指すものとします。
実はオブジェクト指向ってしっくりこないんです!:気分はstatic!:エンジニアライフ
あるいは、
高慢と偏見(1)隣は何をする人ぞ:Press Enter■:エンジニアライフ
に登場する三浦マネージャのような人をイメージしています。
むしろ、プログラミングをまったく経験したことのない人であれば、できる限り無駄を省くDRYの法則というのは合理的であり、直感的にメリットを理解しやすいと思います。しかし、staticおじさんの場合は、
- コードを共有化すると、共有しているプログラムを修正した場合の修正の影響範囲が広がってしまう。
- 機能ごとに似たようなコードをコピーし、独立したプログラムとして開発すれば、それぞれ独立して変更できるからメンテナンスが楽。
- コピペを中心とした開発であれば開発担当のPGのスキルも低くてすみ、外注コストも削減できる。
- ホストからのダウンサイジングもある程度進んでおり、今時フルスクラッチで新規開発する案件は少なく、2次開発案件では部分的なコピペで機能を追加できれば十分。
- だから、小難しい理屈を使いこなすような達人プログラマーなどは不要であり、若いうちにSEやPMになることを考えた方がよい。
というような考え方をされる場合が多いということが、最近私もそのような方々と何回か接するにつれて、ようやくわかってきました。もちろん、そのようなstaticおじさん達を「老害」などと呼んで最初から相手にしないでおくということもできるかもしれません。しかし、正しくコミュニケーションするためには、冷静に相手の立場に立って考える必要があります。そう思って、staticおじさんの気持ちを考えると、彼らの考え方も一理あるのではと思うところが出てきます。
まず、アセンブリやCOBOLのような言語では、オブジェクト指向言語で一般的なカプセル化という考え方がきわめて弱いということがあります。変数は静的なグローバル変数が中心であり、処理をくくりだして共通化しても、それは見かけ上コードサイズが削減されたということでしかなく、結局各処理は密に結合したものになってしまいます。また、言語自体のサポートとしてはポリモーフィズムという考え方がなく*1、共通処理の呼び出し元と共通処理とは結局コンパイル時に結合されて、一体の目的ファイルにコンパイルされます。これも、共通処理とその呼び出し側が密に結合する原因となります。
つまり、staticおじさんの世界観におけるコードの共通化は、単にコードのサイズを少し削減するという手段でしかないのです。逆に、下手にコードを共通化したことによって、スパゲッティコードになったり、影響範囲が理解しにくくなったりするというデメリットの大きさを考えればあまりにも費用対効果の小さなものに見えるのも当然です。だから、結局文字列の編集といったごく基本的な処理は除いて、業務ロジックにかかわるような処理は画面ごと、機能ごとに独立してコピーを作成するという考え方も冷静になって考えればまったく理解できないものではありません。
オブジェクト指向のアーキテクチャではパッケージの安定性を考えることが大切になる
では、次に、Javaのようなオブジェクト指向言語のアーキテクチャではどうして再利用が可能なのか、そして、それがどうして望ましいものにできるのか、その理由を考えていくことにしましょう。
カプセル化と依存関係
まず、重要なこととして、カプセル化という考え方の存在するオブジェクト指向の世界では、手続き型言語におけるグローバル変数というものが、少なくとも見かけ上は存在しないということがあります。*2だから、プログラムの状態というものは各オブジェクトの中身(インスタンス変数)、あるいは各メソッド内(ローカル変数)に限定されます。この事実だけでも、共通処理をくくりだした場合の結合度というのは低くなります。
つまり、手続き指向のプログラムではくくりだした各関数がグローバル変数を通して暗黙に結合していたのに、オブジェクト指向ではお互いの依存関係がより明確に可視化しやすいということが言えます。多くのケースでは、あるクラスが別のクラスをimportして呼び出していたら依存関係があり、そうでなければ独立していると考えることができるのです。
安定依存の原則
クラスやパッケージを再利用するということは、必然的に再利用する側とされる側の間に依存関係が生じるということになります。したがって、staticおじさんが心配するように、変更の影響による再利用のデメリットを少なくするには
- 頻繁に変更される不安定なモジュールはなるべく依存されることを避ける(再利用される側でなく再利用する側に回る)
- 逆に、変更が少ない安定したモジュールを再利用する
という方向になるように、全体的なアーキテクチャを工夫すればかなり前進できることになります。つまり、安定する方向に依存せよということですね。依存関係とパッケージ(モジュール)の安定性との関連に関するこの規則は安定依存の原則(SDP、Stable Dependencies Principle)と呼ばれています。
実際に、モジュールの安定度は以下のように定量的なメトリックとして定義することも可能です。
- (求心結合度):あるパッケージの中のクラスに対して、外部の別のパッケージ中から依存しているクラスの個数。
- (遠心結合度):あるパッケージの中のクラスが依存している外部のパッケージのクラスの個数。
- (不安定度、instability):パッケージの不安定性。0から1の範囲の数値で1に近いほど不安定。
この場合、不安定度Iがパッケージの安定性の目安となる指標です。結局、外部のパッケージから依存されているだけで、逆に自分は外部に依存していないというパッケージは(一般的にフレームワークやutilなど)I=0という安定なパッケージとなり、逆に他からまったく依存されていないパッケージはI=1という不安定なパッケージということができます。*3
以下の図はUMLの書き方にしたがって、破線の矢印の元が矢印の先のパッケージに依存していることを示しています。(ここでは、依存される安定側を下に描いています。)まず、安定したパッケージでは(I=0)、以下のように他から依存されることはあっても、逆に他に依存することがありません。
逆に、不安定なパッケージ(I=1)では、以下のように他から依存される(共有される)ことがないということになります。この場合、不安定なパッケージ内のクラスに手が加わっても、外部に影響を与えることがないことが依存関係から理解できます。
したがって、まず再利用性を高めるためには、全体のアーキテクチャ設計の観点からパッケージ分けを適切に行って、安定した再利用が可能なパッケージとそうでないパッケージの色分けを明確にできるようにするということが大切です。Javaのような言語においてパッケージ分けとは単に巨大なプログラムを小さく分類する入れ物の分割ということだけでなくて、このような安定性の分類という重要な観点があるということですね。
ちなみに、言葉の印象から誤解しそうですが、必ずしも不安定なパッケージが悪で安定したパッケージが善というわけではありませんので注意が必要です。以上の定義による不安定なパッケージとは他から使われていないということですから、自由に変更ができるということでもあります。画面など変更が頻繁に発生するホットスポットを不安定なパッケージに分離しておくことで、修正の影響を最小限にすることができます。
安定性とアーキテクチャパターン
このようにパッケージ間の依存性と安定性の関連を念頭に入れて考えると、一般的なアプリケーションにおけるMVCやレイヤーなどのアーキテクチャパターンは実にうまく考えられているということがわかります。
まず、MVCパターンでは
- 画面の表示やユーザーの操作を受け付けるビュー
- ユーザーの入力をもとにモデルを操作するコントローラー
- 本質的なロジックやデータをカプセル化するモデル
に分割して考えますが、このパターンに従った設計では、モデルの部分は他の要素には依存しません。
これは、一般的にはユーザーインターフェースの変更頻度の方が本質的な部分よりも多いという傾向を考えれば納得のいく設計です。
また、一般的な業務システムでは
- プレゼンテーション層
- アプリケーション層
- ドメイン層
- インフラ層
などのレイヤーに分割して設計します。(DDDの読書記録(第4章、ドメインを隔離する) - 達人プログラマーを目指して)レイヤーパターンでは上位レイヤーから下位レイヤーの方向で依存性を持たせるということになるため、上位層に行くにしたがって不安定であり再利用性に乏しいと考えているということになります。
このように、オブジェクト指向的なアーキテクチャ*4を適切に設計することによって、再利用が可能な安定した部分と、逆に、修正を頻繁に行える不安定な部分を切り分けることができます。これによって、修正の影響範囲が共通化により大きくなるという問題を軽減しながら、再利用のメリットを享受するということが可能になるのです。
オブジェクト指向における再利用性をけた違いにアップさせるポリモーフィズム
このように、変更頻度などの安定性を考えて正しいパッケージにクラスを格納するようなアーキテクチャにするだけでも、従来のstaticおじさん的な手続き指向の世界とはまったく違う次元での再利用が達成できます。
ポリモーフィズムについて再び復習
しかし、オブジェクト指向設計の再利用における本当の切り札は、インターフェースを中心としたポリモーフィズムの活用というところにあります。
いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について
で継承とポリモーフィズムについて紹介しましたが、結局、この記事で言いたかったもっとも大切なことは、あるクラスがインターフェースをimplementsしていたり、(抽象)クラスを継承して、メソッドをオーバーライドしている場合、インターフェースや親クラス型の変数にサブクラスのインスタンスを代入して利用できるということでした。つまり、メソッドを呼び出す側は、抽象的なインターフェースや親クラスのみに依存するという形になっているにも関わらず、ポリモーフィズムにより実際にはオーバーライドしているサブクラスのメソッドが呼び出されるということです。
依存関係逆転の法則
このポリモーフィズムが再利用性を促進する上でどうして重要なのかというと、インターフェースを固定することができれば呼び出し側のロジックをインターフェースとともに安定性の高いパッケージに格納して再利用する対象にできるという事実があるからです。再利用が可能な安定性の高いパッケージは文字通り変更頻度が少なくなくてはなりません。そうするとポリモーフィズムが存在しない世界では、結局、数学ライブラリーや文字列計算、カレンダー計算のように本当にロジックが一つに固定できるようなものしか再利用できないということになってしまいます。つまり、設計上まったく柔軟性や拡張性がないものしか安全に再利用の対象にできないということですね。だから、不変の原理として数学ライブラリを共有できても、どんどん仕様の変化する業務処理を共有するということは極めて難しかったのです。
しかし、インターフェースを使ってポリモーフィックにさまざまな処理が呼び出すことが可能なら、別のパッケージ内でそのインターフェースを実装したさまざまなクラスを作成することで、柔軟に機能を拡張することができます。これは、StrutsやSpringなどのフレームワークで必ずと言っていいほど利用されている発想であり、依存関係逆転の原則(Dependency Inversion Principle、DIP)と呼ばれています。
この原則を使ってフレームワークをうまく設計することで、フレームワーク自身を変更の影響を受けにくい安定したパッケージにおいて再利用しながらも、インターフェースを実装する個別のクラスを後付けすることで柔軟な拡張を行うことができるのです。なお、このことはなるべく抽象度の高いパッケージに依存せよという考え方にもつながってきます。(安定度・抽象度等価の原則、SAP)
もちろん、staticおじさんが特に意識していなくてもWebブラウザや.NETなどのフレームワークを使って開発する以上、水面下でDIPによるロジックの再利用は活用されています。実際、私がこうして文章を打ち込んでいる環境でも水面下では表示やプロセス管理、ネットワーク通信などOSの基本的な処理の中でDIP的な発想が活用されています。これと同じ発想を少しでも業務ドメインやアプリケーションの領域に取り込むことで、アプリケーション開発の再利用性を向上させることができたら素晴らしいことではないでしょうか。
ポリモーフィズムはレガシーシステムとの連携にも有効活用できる
以上紹介したポリモーフィズムは、まったく新規にアプリケーションを開発する際のみに活躍するわけではありません。たとえば、ほとんどソースを読みたくなくなるようなスパゲッティーコードでできたプログラムに対して、安定したインターフェースからなるパッケージを定義し、システムのその他の部分はこのインターフェースを経由してレガシーシステムにアクセスするといったような設計が可能です。そのようにレガシーシステム(あるいはモジュール)と新システムとの間にレイヤーを設けることで、新システムがレガシーシステムの悪い影響を受けることを防止することができます。(腐敗防止層)このようにしておき、レガシーシステムをあるべき設計に徐々に置き換えていくなど、全体的なアーキテクチャを段階的に改善するようなことが可能になります。
この考え方は、特定の製品への依存やデータベースなどオブジェクト指向でないレイヤーとのインターフェースにも活用できます。
まとめ
ここでは、従来型の手続き指向のプログラム設計に対する再利用の限界と、オブジェクト指向的なアーキテクチャではその限界をどのように克服できるのかという点について説明しました。そして、再利用性の高い設計を実現するうえでは、パッケージ間の依存関係や安定性といったことが大切であることを説明しました。特に、オブジェクト指向設計における
という考え方について紹介しました。もちろん、これらの法則以外にも設計上考慮すべきことはたくさんありますし、また、あるべき正しいアーキテクチャを構築することは簡単なことではないということは確かです。しかし、努力して正しいアーキテクチャ設計を採用するメリットは長期的には保守性や拡張性の向上において無視できないレベルのメリットを生み出すことができます。それゆえ、長期にわたって保守拡張していくようなエンタープライズの基幹システムにおいてこそ、正しいアーキテクチャ設計を頭を使って実施するということが大切になってくるものと私は信じます。
なお、ここで紹介した原則については、以下の書籍を参考にしました。
アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技
- 作者: ロバート・C・マーチン,Robert C. Martin,瀬谷啓介
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2008/07/01
- メディア: 単行本
- 購入: 18人 クリック: 586回
- この商品を含むブログ (64件) を見る
http://d.hatena.ne.jp/asakichy/
なお、以下のtogetterのまとめでも、今回のテーマについて議論しています。
staticおじさんに再利用の有効性をわかってもらうには? - Togetter
ちなみに、staticおじさんに支配された世界での開発がどのようなものなのかについては以下のまとめが参考になります。
派遣PG時代の思い出 - Togetter
これは誇張ではなく、現場によっては今でも普通にみられる光景です。2011年現在、このような開発が行われているのは世界でも類を見ないのではないでしょうか。
汎用のフレームワークがあれば業務アプリ実装にオブジェクト指向は不要という考え方は適切でないと思う
前回のエントリいまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味についてのブクマのコメントで、
すごく今さら感がw 最近の開発はフレームワーク使うことが多いようだから知らなくても作れちゃうと思ってたけど違うのかなあ。
という感想をいただきました。実際に、SI業界で多くの方々、特に、アプリケーション開発の下流工程を担当しない層の方でこのように考えている方はほんとうに多いのではないかと思います。確かに最近ではSalesforceなどの製品もありますし、CRUD処理を行うような見栄えの良い業務アプリケーションは非常に簡単に開発できるようになっているということはあります。また、Visual BasicやMS Accessなど気軽にアプリケーションを開発できるツール類は昔からありました。そして、業界構造などの理由からやむを得ない側面があるとはいえ、SIerの提供する多くのフレームワークでは、アプリケーション開発を行うPGができるかぎり頭を使わず単純作業でアプリケーションを開発できるようなツールやフレームワークを必要以上に尊重する傾向があります。(Java EEや.NETはCOBOLやVB6よりも本当に生産性が高いか? - 達人プログラマーを目指して)
そして、大規模案件ではSOAやデータ統合などの名目で上流の工程に莫大な予算をつぎ込んで長期間「分析」「設計」をし、Excel方眼紙などの成果物を山のように作成する一方で、肝心のプログラムは自動生成だったり、コピペだらけだったり、まったく目も当てられないようなひどいコードを大量に作成するようなことが今でも日常行われているようです。
業務アプリケーション開発といっても、マスタデータの管理など単純な案件もあり、そういった業務では確かにツールで自動生成してしまえば、あえてオブジェクト指向設計する必要のない場合もあるでしょう。しかし、一般的に何百億円をかけて開発するような、大規模な基幹業務システムの業務ロジックは、想像を絶するほどきわめて複雑なものとなっているという事実を忘れてはならないと思います。実際、金融のシステムで一つの注文取引を投入する際には、残高の確認やインサイダー取引のチェックなど様々なチェックロジックが何万ステップにもわたってスパゲッティーのように入り組んで呼び出されているということがありますが、話はそれだけでは全然終わらないのです。金融の注文といっても、株も債券もあり、それも外国株とか国債とか多岐にわたった種類があります。一つの商品の注文を扱うだけでも相当複雑なチェックが必要なのに、それが各商品ごとにまったく同じではないけれども少しずつ違う処理が存在しているということです。
金融にかかわらず製造や流通などさまざまな業務で、このような複雑な処理が必要なのですが、こうした複雑なドメインを上手に扱うときにこそ本来はオブジェクト指向の設計が威力を発揮するところだと思います。残念ながら私が見たほとんどの大規模基幹システムでは、Java言語を使っていても、オブジェクト指向ということは全くと言っていいほど考慮されておらず、商品ごとにほとんど同じようなロジックが何十か所にもわたってコピペされているという状況になっていました。
確かに、
- 画面部品
- データベースアクセス
- ワークフロー
- 通信
など、汎用的なところではオブジェクト指向のフレームワークが使われているのですが、肝心の業務ロジックの部分でオブジェクト指向が活用されているというケースが少なすぎるのではないかと思うのです。
エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)
- 作者: エリック・エヴァンス,今関剛,和智右桂,牧野祐子
- 出版社/メーカー: 翔泳社
- 発売日: 2011/04/09
- メディア: 大型本
- 購入: 19人 クリック: 1,360回
- この商品を含むブログ (131件) を見る
素人にもわかるフレームワークを作成してはならない。
設計ができるほどには賢くない人も開発者の中にいると想定したチーム分割は、失敗する可能性が高い。これはアプリケーション開発の難しさを過小評価しているからである。設計ができるほどには賢くないなら、ソフトウェア開発を担当させてはならない。逆に十分に賢いのであれば、大事に面倒を見ようとすると、かえって担当者が必要とするツールとの間に障壁を作り出すだけである。
これは、単価の低いPGを大量に雇って品質の低いプログラムを労働集約的に量産させると考えている従来のSIerの考え方とまったく逆の主張です。もちろん、オブジェクト指向設計のできる上級プログラマーの人数には限りがあるというところも考慮すべきですが、従来何万行のロジック+コピペで作成していたような複雑なドメインロジックの開発を大量の人月をかけて労働集約で開発するのではなく、少ない人数のコアのプログラマーが中心となって慎重に設計しながら開発するというモデルにシフトすれば、全体として開発費用を大幅に削減できるでしょう。そして、できたプログラムの機能拡張やバグ修正も容易となり、長期にわたったメンテナンスコストは大きく削減できると思うのです。
もちろん、3か月だけ稼動して後は捨てるといったシステムを短期間に作成するといった場合は、とにかく大人数で作るというモデルが適合しているケースもあるのですが、少なくとも何十年にもわたってメンテナンスするような基幹業務のシステムでは、もっと品質の高いプログラムを作成して維持するという方式にいいかげん切り替えていくべきなのではないでしょうか。
なお、同じような考え方はプログラム内部の設計より大きなシステムの設計や調達といった大きな粒度にも当てはまると思います。実際、DDDの4部では、本来のEA的、SOA的な考え方にもつながるような考え方が書かれています。こだわりのある職人プログラマーほど、無駄なコードを少なくしたいものという事実を理解してほしい - 達人プログラマーを目指してで書いたこととも関連しますが、既存のレガシーシステムを活用しながらも、戦略的・段階的に企業のシステムのアーキテクチャを発展させていくような考え方が説明されています。これなどは粒度は異なるところがあるとはいえ、プログラムの段階的なリファクタリングという考え方に通じるところがあります。EA、SOA、クラウドなどキーワード上はこうした考え方を取り入れている(つもりになっている)大企業はたくさんあると思いますが、実際にはベンダの言われるままにハードやミドルにお金をかけているだけで考え方が全然理解されていないのではないかというところがあります。これは、Javaを使っているけれどCOBOLと同じような設計になっているというところと似ています。
今後、あるべき姿に発展させていくためには、業界構造の変革を通して、SIer自身が変わっていかなくてはならないだけでなく、システムを調達するユーザー企業の側もプログラムやシステムの設計の品質に対する考え方*1を変革していくことが大切であると思います。
いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について
正しく意味を理解している方にとっては、まったく常識レベルの話であり、何をいまさらと思われる方々も多いかと思いますが、大規模案件のレガシーコードなど、私が仕事で見かけるJavaのコードを読むと、「このコードを書いたSEやPGの方々は、はたして継承の意味を正しく理解していないのではないか」と思われる設計のコードに出会うことが少なからずあります。現在では改良されましたが(Javaプログラミング能力認定試験の問題がかなり改善されていました - 達人プログラマーを目指して)、以前のJavaプログラム認定試験の問題は、そうした不適切な設計がされている典型的な例となっていたのですが、実際、SI業界ではあのような品質のコードのシステムが今でも現役で多数稼動しているというだけでなく、現在でも新たに生み出されているというのは残念ながら紛れもない事実のようなのです。
確かに新人研修で「哺乳類を継承して犬クラスと猫クラスができる」といったようなオブジェクト指向の説明を聞いただけで、簡単に理解できるものではありませんが、以降手続き型のレガシーコードしか相手にしていないPGやコードを目にすることもなくExcel方眼紙ばかり描いて(書いて)いるSEの方々は、継承の意味などをきちんと理解する前に忘れてしまっているという方も多いかもしれません。私の経験上、この業界ではこうした基本的な知識が理解されていないという現場が、むしろ、典型的なケースなのではないかという疑いすらあると思えてくるのです。(あの認定試験問題の品質をSI業界の代表的なプログラム品質と考えることの是非 - 達人プログラマーを目指して)
オブジェクト指向プログラミングの方法を理解して使いこなせるようになるには、通常はそれなりの努力と期間を要するものですし、きちんと指導してもらえる先輩に巡り合うということも大切ですが、ここでは、「継承」の意味に絞って、最低限知っておいてほしいポイントについて、今更ですがまとめてみたいと思います。あらかじめお断りしておくと、これを読んですぐに理解できるという保証もできないのですが、あと一歩のところで正しい理解に到達できない人にとってヒントになるところもあるかもしれません。そして、もちろん、継承はオブジェクト指向プログラミングのほんの一部分の要素でしかありませんが、その意味を理解することで、次のステップに進みやすくなるとも考えられるのです。(ここではJava言語を例として説明していますが、C#やVisual Basicなど、業務で利用する主流のオブジェクト指向プログラミング言語でも基本的なポイントはほぼ同じです。)
クラスの継承の文字通りの意味
Java言語ではクラスを継承するときにはextendsというキーワードを使ってクラスを宣言します。これは、文字通り「拡張する」という意味ですが、親クラスの定義を拡張して新しいクラスを定義するという働きがあります。以下のコード例をみてください。*1
class Parent { public String fieldA = "field A"; public String methodA() { return "method A"; } } class Child extends Parent { public String fieldB = "field B"; public String methodB() { return "method B"; } }
この場合、以下のようにChildクラスのインスタンスを生成して、フィールドやメソッドにアクセスすると以下のようになります。
public class Inheritance { public static void main(String[] args) { Child child = new Child(); System.out.println(child.fieldA); // field A System.out.println(child.fieldB); // field B System.out.println(child.methodA()); // method A System.out.println(child.methodB()); // method B } }
もともとの「継承する」「拡張する」という意味の通り、ChildクラスはParentクラスのフィールドやメソッドを継承元の親から引き継いで自分自身で定義しているような動作となっています。つまり、Childを以下のように定義した場合と同じ動作ということですね。
class Child { public String fieldA = "field A"; public String fieldB = "field B"; public String methodA() { return "method A"; } public String methodB() { return "method B"; } }
継承を活用することで、このように共通のデータ(フィールド)や処理(メソッド)を親クラスにまとめて定義することができるのです。
子(サブ)クラスで同一の形のメソッドをオーバーライドする
「なんだ、かんたんじゃないか」これで、一人前に継承を理解できたと考える人もいるかもしれません。ですが、残念ながら話はここで終わらないのです。むしろ、実は、ここまでの話はJava言語の継承の働きの中でも本当に20%というか、継承の本当に威力のあるポイントを見逃していることになるのです。話のクライマックスはまだこれからなのです。
話が面白くなるのは、子クラスが親クラスと同じ形(シグネチャ)のメソッドを定義している場合です。以下の定義を見てください。
class Parent { public String fieldA = "field A"; public String methodA() { return "method A"; } } class Child extends Parent { public String fieldB = "field B"; @Override public String methodA() { // 親クラスのメソッドをオーバーライドする。 return "method A in Child"; } public String methodB() { return "method B"; } }
ここでは、Childクラスにおいて親クラスで定義されているmethodA()と同じ形のメソッドを再度定義しなおしています。この形で、親クラスのメソッドを子クラスで再定義することをオーバーライドと呼んでいます。なお、Java5以降のバージョンでは、正しくオーバーライドしていることをコンパイラにチェックさせるために@Overrideというアノテーションを明示的につけることが推奨されています。(うっかりスペルミスをしたり、シグネチャが異なっていたりするとコンパイルエラーとなります。)
次に、これを実行してみましょう。
public class Inheritance { public static void main(String[] args) { Child child = new Child(); System.out.println(child.fieldA); // field A System.out.println(child.fieldB); // field B System.out.println(child.methodA()); // method A in Child System.out.println(child.methodB()); // method B } }
期待通り、child.methodA()の呼び出しはParentクラス中のメソッドでなく、Chlildクラス中のメソッド呼び出しに置き換えられています。これも、オーバーライドの機構を理解してしまえば、難しいところはないと思います。通常は親クラスのメソッドが継承されてくるのに、子クラスでオーバーライドすると、継承元の親クラスの側のメソッドでなく子クラスのメソッドが呼び出されるということです。共通部分を親クラスに定義しておき、必要に応じて、差分があれば子クラスでオーバーライドできるということで、便利ですね。
継承には型の継承というもう一つの重要な側面がある
実は、多くのJavaプログラマーの理解がこの段階で止まってしまっているのではないかと思われるのですが、Java言語の継承には型の継承というもう一つの重要な側面があります。つまり、今までの説明では「継承とは親クラスのフィールドやメソッドを子クラスで再利用するための便利な方法」という意味しかなかったのですが、それに加えて、子クラスのオブジェクトは親クラスの型と代入互換性があるという性質があるのです。Javaの変数は型をつけて宣言する必要があったことを思い出してください。
int a = 3; String b = "hello"; b = a; // コンパイルエラー
つまり、以上の例のように基本的には同じ型の変数にしか代入できないように、コンパイラがチェックしてくれます。一方、クラスに親子関係があると、親クラスの型の変数に子クラスの型のインスタンスを代入できるという重要な規則があります。
Child child = new Child(); Parent parent = child; // コンパイルOK
この規則は、冷静になって考えると自然なものであると納得ができます。今まで説明した継承のメカニズムによって、子クラスは親クラスのすべてのデータや振る舞いを保持しているのですから、場合によっては親クラスの型であると抽象化して考えても問題ないということですね。そして、以下の結果を見てください。
public class Inheritance { public static void main(String[] args) { Parent parent = new Child(); System.out.println(parent.fieldA); // field A System.out.println(parent.methodA()); // method A in Child } }
ここで、非常に大切なポイントはparent.methodA()の呼び出し結果がParentクラスのmethodA()でなく、ChildクラスのmethodA()にバインドされているという事実です。このポイントはレイトバインディングや仮想メソッド呼び出し*2などと説明されることがありますが、とにかく、宣言されている変数の型ではなく、実際に変数に代入されているインスタンスの型によって実行時の振る舞いが決まるということです。
つまり、継承によるメソッドのオーバーライドが、Java言語でポリモーフィズムを実現する手段となっているという事実を理解してください。これが、実はJava言語における継承の威力の中でも最も重要な働きをするポイントとなっているのです。
ポリモーフィズムについては、以前、ドラゴンボールで学ぶオブジェクト指向 改 - 達人プログラマーを目指してで、
共通のメソッド呼び出しで、対象とするオブジェクトの種類に応じてまったく異なるさまざまな処理を実行可能な性質をポリモーフィズム(多態性)と呼びます。
のように説明しています。なお、この説明だけだと、まだ何が嬉しいのかピンと来ないかもしれませんが、親クラスの型の変数に代入できるという性質は変数だけでなく、メソッドのパラメーターにも同様に成り立ちます。だから、
public void someMethod(Parent parent) { // parentを使って何らかの処理 }
のようなメソッドを一度定義しておくと、Parentクラスの任意の子クラスをパラメーターとして渡して処理をさせることができるというわけです。つまり、このポリモーフィズムの性質を使うことで、拡張性や再利用性の高いライブラリーを作成できるのです。実際、たとえば証券のドメインであれば、ある銘柄の注文を表すOrderクラスを入力として受け取るメソッドを定義しておくと、株式や債券など様々な種類のOrderの子クラスを処理するように拡張させるといった設計が可能になるのです。
型の継承によるポリモーフィズム的な側面にのみ着目したのが抽象メソッドとインターフェース
このようにJavaの継承の構文が持つ働きには
という二つの側面があるのです。特に、後者の概念はなかなか難しく、(私の説明のまずさもありますが)話を聞いただけですぐに理解できないかもしれません。しかし、コードを実際に写経*3するなどして、実際に試しながら、じっくりと理解することにしましょう。Javaプログラミングでは、この壁を越えらえるかどうかが非常に重要なポイントで、ここをいったんクリアできれば、デザインパターンやフレームワークなど、オブジェクト指向プログラミングの広大な世界を冒険する準備ができたことになります。
さて、この型の継承とポリモーフィズムということに着目すると、もともとの親クラスのメソッドの実装は不要になる場合があります。その場合は、以下のように親クラスをabstractクラスにして、オーバーライドするメソッドをabstractメソッドとして宣言することができます。
abstract class Parent { public String fieldA = "field A"; public abstract String methodA(); // 抽象メソッド }
この場合も、前回と同様にParent型の変数のChildのインスタンスを代入して実行すると、正しくChildのメソッドが実行されるのです。
public class Inheritance { public static void main(String[] args) { Parent parent = new Child(); System.out.println(parent.fieldA); // field A System.out.println(parent.methodA()); // method A in Child } }
Parentクラスの抽象メソッドは型としてmethodA()が呼び出せるということをコンパイラに知らせている働きがある一方で、実際にメソッドの実装はポリモーフィズムにより、Childクラスのメソッドにバインドされています。
さらに、Javaの場合、継承による型の継承とポリモーフィズムという点を究極に推し進めたものとしてインターフェースが定義できます。上記のParentをインターフェースに置き換えると以下のようになります。
interface Parent { String methodA(); } class Child implements Parent { public String fieldB = "field B"; @Override public String methodA() { return "method A in Child"; } public String methodB() { return "method B"; } } public class Inheritance { public static void main(String[] args) { Parent parent = new Child(); // Childのインスタンスをインターフェース型に代入 System.out.println(parent.methodA()); // method A in Child } }
(補足)Java言語ではフィールドはオーバーライドできない*4
本エントリに対して重要なコメントをいただきましたので、子クラスで親クラスと同一名のフィールドを定義した場合にどうなるかについて補足させていただきます。通常は、親クラスと同一名のフィールドを子クラスで定義すべきではなく、このセクションで書いた内容の理解は後回しでもよいと思いますが、うっかりバグの原因となることがあるので、注意が必要であるということは知っておく方がよいかもしれません。
実際、子クラスを以下のように定義して確認してみます。
class Parent { String fieldA = "field A"; public String methodA() { return "method A"; } } class Child extends Parent { String fieldA = "field A in Child"; //親クラスと同一名称のフィールドを定義 String fieldB = "field B"; @Override public String methodA() { return "method A in Child"; } public String methodB() { return "method B"; } } public class Inheritance { public static void main(String[] args) { Child child = new Child(); Parent parent = child; // フィールドは宣言されている変数の型で決まる System.out.println(parent.fieldA); // field A System.out.println(child.fieldA); // field A in Child // Child型の変数で隠ぺいされているParentのフィールドを参照するにはキャストすることも可 System.out.println(((Parent)child).fieldA); // field A // メソッドはオーバーライドされてポリモーフィックに呼び出される System.out.println(parent.methodA()); // method A in Child System.out.println(child.methodA()); // method A in Child } }
結果は以上のようになりました。メソッドの場合はすでに説明したように子クラスの実装で親クラスの実装がオーバーライドされており、変数の型によらずに、実際に生成されているオブジェクトの型に応じて子クラスのメソッドが呼び出されるのでした。しかし、同一名のフィールドを定義した場合は変数の型によって、親クラスか子クラスのどちらかのフィールドが参照されています。これはどういうことかというと、フィールドは同一名称であっても決してオーバーライドできないということを意味しています。つまり、たまたま名前が同じ変数が親と子に別々に含まれているだけであって(異なる名前のフィールドの継承とおなじく)子クラスのインスタンスにはどちらの変数も含まれているということになります。ただし、同一名のフィールドをこのように宣言してしまうと変数の型によってどちらかのフィールドが参照できなくなってしまうのです。これは、一般にグローバル変数とローカル変数で同じ名前のものがあると、ローカル変数しか見えなくなってしまうといったことと似ています。それぞれの変数の領域は存在しているのですが、単に名前が隠ぺいされて参照できなくなっているということです。
(補足)staticメソッドもオーバーライドできず、ポリモーフィズムが存在しない
つぎに、staticメソッドの継承について調べてみます。以下の例は、以前の例に対して、staticキーワードを各メソッドに追加しています。
class Parent { String fieldA = "field A"; public static String methodA() { return "method A"; } } class Child extends Parent { String fieldA = "field A in Child"; String fieldB = "field B"; public static String methodA() { // 親クラスと同一形のstaticメソッド。普通はやらない。 return "method A in Child"; } public static String methodB() { return "method B"; } } public class Inheritance { public static void main(String[] args) { Child child = new Child(); Parent parent = child; // staticメソッドはクラスごとに存在しており、変数の型で呼び出し対象が静的に決まる。 // つまり、ポリモーフィズムが存在しない。 System.out.println(parent.methodA()); // method A System.out.println(child.methodA()); // method A in Child System.out.println(((Parent)child).methodA()); // method A // 紛らわしいので、普通はインスタンスでなくて、クラスに対して呼び出す。 System.out.println(Parent.methodA()); // method A System.out.println(Child.methodA()); // method A in Child } }
staticでない普通のメソッドと違い、staticメソッドはクラスごとに別々の実装が独立して存在しており、子クラスで同一名のメソッドを定義してもオーバーライドできません。見かけ上インスタンス経由で呼び出した場合、インスタンスの型でなく宣言されている変数の型によって呼び出し先が決まります。前の節で説明したフィールドの場合と似たように解決されています。
このようにstaticメソッドにはポリモーフィズムが存在せず、コンパイル時に呼び出し先が一つに固定されます。staticメソッドの呼び出しにより、コンパイル時に実装が一つに固定されてしまうということです。(一方、普通のメソッドは、ポリモーフィズムにより、オーバーライドしている子クラスの数だけ無数に実装が存在している可能性があります。)staticメソッドが拡張性が低く、また、クラスを分離した単体試験が難しくなるということと関連しています。
(補足)コンストラクタ、初期化ブロック、static初期化ブロックについて
コメント欄にて川久保さんにご指摘いただきましたので、コンストラクタや初期化ブロックに関して簡単に補足させていただきます。初期化ブロックはともかく、コンストラクタに関する理解は重要だと思います。
コンストラクタは継承されない(しかし、子クラスのコンストラクタから呼び出される)
class Parent { public Parent() { System.out.println("Parent no args constructor"); } public Parent(String arg) { System.out.println("Parent one arg constructor arg = " + arg); } } class Child extends Parent { public Child() { // super(); コメントをはずしても同じ System.out.println("Child no args constructor"); } } public class Inheritance { public static void main(String[] args) { Parent parent = new Parent(); //Parent no args constructor Parent parent2 = new Parent("test"); //Parent one arg constractor arg = test Child child = new Child(); // Parent no args constructor, Child no args constructor // コメントをはずすとコンパイルが通らない。 // コンストラクタはメソッドのようには継承されない // Child child2 = new Child("test"); } }
ご指摘のとおり、Java言語の場合にはコンストラクタは通常のメソッドのように自動的に子クラスに継承されません。したがって、以上の例では文字列のパラメーターをとるコンストラクタは親クラスには存在しますが、子クラスには存在しません。*5ただし、子クラスのコンストラクタ中から親クラスのコンストラクタが呼び出されます。(呼び出しが省略された場合は親クラスのパラメーターのないコンストラクタの呼び出しが自動的に行われる仕様。)
インスタンス初期化ブロックはnew実行時に自動的に実行され、オーバーライドできない
存在自体知らない人も多いかもしれませんが、コンストラクタの代わりに初期化処理を行うための、インスタンス初期化ブロックという仕掛けがJDK1.1のころからあります。以下の例を実行してみると、各クラスのコンストラクタの実行前に呼び出されることがわかります。親クラスの初期化ブロックは自動的に呼び出されるため、子クラスでオーバーライドできません。
class Parent { // インスタンス初期化ブロック { System.out.println("Parent instance initilizer block"); } public Parent() { System.out.println("Parent no args constructor"); } } class Child extends Parent { // インスタンス初期化ブロック { System.out.println("Child instance initilizer block"); } public Child() { System.out.println("Child no args constructor"); } } public class Inheritance { public static void main(String[] args) { // Parent instance initilizer block // Parent no args constructor Parent parent = new Parent(); System.out.println(); // Parent instance initilizer block // Parent no args constructor // Child instance initilizer block // Child no args constructor Child child = new Child(); } }
この動作は、各フィールドの初期化子に似ています。
static初期化ブロックはクラスロード時に自動的に一度だけ実行され、オーバーライドできない
同様に、static初期化ブロックの例も示します。こちらはインスタンスの生成時ではなく、クラスのロード時に一回だけ実行される点が違います。いずれにしても、サブクラスでオーバーライドするということはできません。
class Parent { // static初期化ブロック static { System.out.println("Parent static initilizer block"); } public Parent() { System.out.println("Parent no args constructor"); } } class Child extends Parent { // static初期化ブロック static { System.out.println("Child static initilizer block"); } public Child() { System.out.println("Child no args constructor"); } } public class Inheritance { public static void main(String[] args) { // Parent static initilizer block // Parent no args constructor Parent parent = new Parent(); // Parent no args constructor Parent parent2 = new Parent(); System.out.println(); // Child static initilizer block // Parent no args constructor // Child no args constructor Child child = new Child(); // Parent no args constructor // Child no args constructor Child child2 = new Child(); } }
まとめ
通常、会社の研修や入門書ではオブジェクト指向の考え方と一緒に継承の説明を一気にされることが多いと思います。そのため、なかなか本質を理解しにくかったり、重要なポイントを見逃してしまうということもあると思います。ここでは、カプセル化や関連などオブジェクト指向に関する他の説明はいったん無視して、Java言語における継承の意味に絞って説明を試みてみました。
- 継承は親クラスの定義を子クラスで再利用するための手段
- 親クラス(インターフェース)の型に子クラスを代入することでポリモーフィズムを利用して拡張性の高いライブラリーを作成する手段
という二つの側面があるという点を理解することが大切です。特に、後者はなかなか分かりにくいところがあると思いますので、すぐに理解できなくても悲観することはありません。私も最初そうでしたが、ふとある時突然「そういうことだったのか」という瞬間が来るものです。
SI業界では、プログラミングの研修に十分な時間がとられないことも多く、こうしたごく基本的なことを理解しないまま、設計を行ったり、コードを書いたりするということも多いかもしれません。しかし、より高い生産性のプログラマーを目指すために、こうした基本的な知識を正しく理解しておくことは、プロフェッショナルのプログラマーとして最低限の義務なのではないでしょうか。
なお、今回は最短の説明でJava言語の継承の働きを説明するため、オブジェクト指向の説明はあえて省略してしまいましたが、オブジェクト指向については、以下もご参照ください。
ドラゴンボールで学ぶオブジェクト指向 改 - 達人プログラマーを目指して
(追記)
本エントリでは、オブジェクト指向の概念モデルやクラスライブラリーとして意味のないParentやChildといったクラスを説明に使いました。もちろん、入門書などでは普通は図形や実世界の物などを使って継承関係を説明することが多いと思います。今回の説明のアプローチに対して、非難もあるかと思いますが、オブジェクト指向の意味やモデリングということを同時に理解しようとすると、考えることが多すぎて、逆に理解を妨げるということもあると思うのです。また、クラス設計というのは実際にモデル化すべき対象となる問題やプログラム(すなわちコンテキスト)を考えないと、あまり意味がないということもあります。
したがって、まず、初心者の方はオブジェクト指向の考え方はいったん後回しにして、プログラミング言語の機能としての継承の使い方を形から理解するというところから入るというアプローチもあると思います。その後で、JDKやOSSのクラスライブラリーやフレームワークの使い方を理解しながら、デザインパターンやリファクタリングなどを学習し、その過程で自然にオブジェクト指向的なモデリングの考え方ができるようになれば、DDDなどの本を勉強して実際の問題を適切にクラスで設計できるようになるという流れもあると思います。
多くのオブジェクト指向の説明ではそういった段階を飛び越していきなり概念モデルに入ろうとするため、なかなか理解しにくいところがあるのではと思うのです。(自分のドラゴンボールの説明はそういうアプローチですが。)特に、インターフェースとポリモーフィズムの威力を理解するには、まずデザインパターンの中で、StrategyパターンかCommandパターンあたりから勉強することをお勧めします。以下の本はデザインパターンについて、初心者にもわかりやすく書かれていると思います。
Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本
- 作者: Eric Freeman,Elisabeth Freeman,Kathy Sierra,Bert Bates,佐藤直生,木下哲也,有限会社福龍興業
- 出版社/メーカー: オライリージャパン
- 発売日: 2005/12/02
- メディア: 大型本
- 購入: 14人 クリック: 362回
- この商品を含むブログ (98件) を見る
ブクマで
Parent parent = new Child(); の変数名はchildを使いたいなあ。
というコメントをいただいています。確かに、ポリモーフィズムによって呼び出し時のメソッドはオーバーライドされていればChildクラスとして振る舞うので気持ちがわからなくないのですが、あくまでも呼び出し元はParentという型として認識しているという点もポイントかと思います。ポリモーフィズムを説明する時に私がよく使うたとえとして「筆記用具で文字を書く」という文があります。筆記用具は抽象親クラスであり、文字を書く人はあくまでも筆記用具を使っていると考えているのですが、実際に使っているのは筆記用具の子クラスのオブジェクトである鉛筆だったりボールペンだったりするということがあります。文字の太さや色は実際に使っているオブジェクトによって決まるのですが、そのオブジェクトを使う人は特に両者の違いを意識しなくても文字を書けます。変数名にparentを使っているのはそういう気持ちからすれば、一般的に自然だと思います。たとえ話も危険な場合がありますが、ポリモーフィズムは本来は日常我々が行っている抽象化をプログラミングで実現する手段としてはすごく自然な考え方だと思います。
*1:ここではアクセス修飾子の問題はあえて考えなくて済むようにすべてpublicとしています。もちろん、通常、フィールドはprivateなどにしてカプセル化すべきです。
*2:コンパイル時に呼び出し先のメソッドが解決できないため、実行時に判断する必要があるということ。
*3:単に話を聞いたり本を読んだりするだけでなく、エディタやIDEを使って実際に例題を書き写し、動作を確認しながら理解すること。
*4:Java言語はC++など多くのオブジェクト指向言語と同様に、フィールドとメソッドを明確に区別しています。つまり、統一アクセスの原理が言語上はサポートされていません。よって、フィールドをpublicにせず、getterとsetterを定義するということが通常は行われます。C#やVBではプロパティというしくみがあり、また、Scalaではそもそもフィールドとメソッドの区別はよりあいまいです。
*5:この辺りはオブジェクト指向言語によって違いもあるところがあるので、注意が必要です。たとえば、Delphiなどではコンストラクタは継承されます。