🕌

実務で出会ったグレーな設計手法

2024/01/06に公開

はじめに

ソフトウェア開発の現場では、理想と現実の間で常にバランスを取る必要があります。特に BtoB 形式のプロダクト開発においては、顧客のニーズに迅速に対応するため、時には標準的な設計手法から逸脱する「グレーな設計手法」を採用することがあります。この記事では、一般には公開されていない BtoB プロダクトで遭遇した、伝統的な設計原則からは外れるが、特定の状況下で有効であった設計手法について考察します。

https://github.com/tonbiattack/gray-design-method

これらの手法は、一般的なソフトウェア開発のベストプラクティスとは異なる場合が多く、メリットとデメリットの両方を持ち合わせています。ここでは、それぞれの手法の背景、実装時の考慮点、そしてそれに伴うリスクについて詳しく掘り下げていきます。目的は、これらの「グレーな設計手法」が実務でどのように機能し、またどのような影響をもたらす可能性があるかを理解することです。

注意: ここで紹介する手法は、特定の文脈や要件に基づいて選択されたものです。これらの手法を適用する際には、常にプロジェクトの特性やリスクを十分に考慮し、適切な判断を行うことが重要です。


1. 永続化と更新処理の統合

新規作成と更新処理を一つの更新処理に統合し、HTTP メソッドとしては、Patch や PUT の代わりに Post を使用します。これにより、データが既に存在する場合はそのデータを更新し、存在しない場合は新規に作成する処理を一つの操作で行うことができます。

  • メリット:
    • 処理の単純化: 新規作成と更新の処理を一つのメソッドで行えるため、コードの複雑さが減少します。
    • API インターフェースの統一: すべてのデータ操作を Post メソッドで行うことで、API の使用が単純かつ直感的になります。
  • デメリット:
    • RESTful 原則からの逸脱: HTTP メソッドの意図的な使用(GET は取得、POST は新規作成、PUT/PATCH は更新)から逸脱し、API の意図が不明確になる可能性があります。
    • コンテキストに応じた操作の困難さ: 特に複雑なデータモデルを扱う場合、新規作成と更新の区別が重要になる場面で、このアプローチが問題を引き起こす可能性があります。

JavaEE の JPA を用いた実装例

JavaEE の JPA (Java Persistence API) を用いたシステムでは、通常 persist メソッドと merge メソッドを使用して、それぞれ新規エンティティの保存と既存エンティティの更新を行います。しかし、このアプローチでは両者を merge メソッドに統一します。

@RestController
@RequestMapping("/example")
public class ExampleController {

    @Autowired
    private ExampleService service;

    @PostMapping
    public ResponseEntity<ExampleEntity> saveOrUpdate(@RequestBody ExampleEntity entity) {
        ExampleEntity savedEntity = service.saveOrUpdate(entity);
        return ResponseEntity.ok(savedEntity);
    }
}

@Entity
public class ExampleEntity {
    @Id
    private Long id;
    // その他のフィールド
}

@Repository
public class ExampleRepository {

    @PersistenceContext
    private EntityManager entityManager;

    public ExampleEntity saveOrUpdate(ExampleEntity entity) {
        return entityManager.merge(entity);
    }
}

@Service
public class ExampleService {

    @Autowired
    private ExampleRepository repository;

    public ExampleEntity saveOrUpdate(ExampleEntity entity) {
        return repository.saveOrUpdate(entity);
    }
}

この例では、ExampleController クラスが HTTP POST リクエストを受け取り、ExampleService の saveOrUpdate メソッドを呼び出しています。saveOrUpdate メソッドでは、新規作成と更新の両方の処理を merge で行っています。

注意: 一部の ORM ツールでは、新規登録と更新処理が明確に分かれていないものもあります。例えば、Spring Data JPA の CrudRepository や JpaRepository は save メソッドを提供しており、これは内部的に新規登録か更新かを判断して適切な処理を行います。このような ORM ツールを使用する場合、アプリケーション開発者は新規作成と更新の区別を意識する必要がなくなります。

Spring Boot の Spring Data JPA を用いた実装例

Spring Boot でのデータ永続化において、Spring Data JPA の CrudRepositoryJpaRepository インターフェースの利用が一般的です。これらのインターフェースは save メソッドを提供しており、新規作成と更新の両方をこのメソッドで行います。この save メソッドは、エンティティの ID が null かどうかに基づいて内部的に persist または merge を適切に呼び出します。

@RestController
@RequestMapping("/example")
public class ExampleController {

    @Autowired
    private ExampleService exampleService;

    @PostMapping
    public ResponseEntity<ExampleEntity> createOrUpdate(@RequestBody ExampleEntity entity) {
        ExampleEntity savedEntity = exampleService.saveOrUpdate(entity);
        return new ResponseEntity<>(savedEntity, HttpStatus.OK);
    }
}

@Entity
public class ExampleEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    // その他のフィールド
}

public interface ExampleRepository extends JpaRepository<ExampleEntity, Long> {
    // 標準的なCRUD操作
}

@Service
public class ExampleService {

    @Autowired
    private ExampleRepository repository;

    public ExampleEntity saveOrUpdate(ExampleEntity entity) {
        return repository.save(entity);
    }
}

2. UUID の採番場所の変更

この設計手法では、UUID の採番をバックエンドではなくフロントエンドで行います。これにより、新規作成と更新処理を区別する必要がなくなります。

メリット

  1. 単純化されたアーキテクチャ:

    • アプリケーションのアーキテクチャが単純化され、開発者にとって理解しやすくなります。
  2. 開発スピードの向上:

    • 間接層を減らすことで、小規模なプロジェクトやプロトタイピングにおいて迅速な開発が可能になります。
  3. コード量の削減:

    • リポジトリ層の抽象化が不要になるため、全体的なコード量が減少します。
  4. 新規作成と更新処理の統合:

    • フロントエンドで ID を生成することにより、バックエンドで新規作成と更新の処理を分ける必要がなくなります。これにより、API の設計が単純化され、バックエンドの処理がより効率的になります。

デメリット

  1. セキュリティリスクの増加:

    • フロントエンドで ID を生成することは、予測可能な ID や衝突のリスクを増加させる可能性があり、セキュリティ上の問題を引き起こす可能性があります。
  2. データ整合性の問題:

    • 複数のクライアントが同時に ID を生成する場合、ID の衝突が起こる可能性があり、これがデータ整合性に影響を及ぼす可能性があります。
  3. バックエンドでの検証必要性:

    • フロントエンドで生成された ID は、バックエンドで再度検証する必要があります。これにより、バックエンドの複雑性が増します。

JavaScript での UUID 採番の実装例

JavaScript では、crypto オブジェクトの randomUUID メソッドを使用して簡単に UUID を生成できます。以下はその使用例です。

const id = crypto.randomUUID();
console.log(id);
// 例: 'd5ab28c1-3dce-c66b-7990-ce8f9a6a8c77'

3. DTO の非利用とエンティティの直接操作

  • 項目:
    • DTO を利用しない、またはトランザクションの分離。
    • エンティティクラスの詰め替えをせず、テーブルのエンティティクラスをそのまま使用。

メリット

  1. 開発の迅速化と処理の簡素化:

    • DTO の利用を省略することで、エンティティクラスの詰め替えが不要になり、開発プロセスが簡略化され、開発速度が向上します。

デメリット

  1. セキュリティリスクとメンテナンスの困難:

    • エンティティクラスを直接使用することは、セキュリティリスクを増大させる可能性があり、システムのメンテナンスもより複雑になります。
  2. データ整合性の確保が困難:

    • 複数のトランザクションに分けて処理すると、データの整合性を維持することが難しくなります。
  3. 再利用性の低下:

    • DTO を利用せずにエンティティを直接扱うと、ビジネスロジックとデータモデルが密接に結びつくため、コンポーネントの再利用性が低下する可能性があります。

4. サービスクラスでの直接的なデータベース操作

この設計手法では、リポジトリ層を省略し、サービスクラス内で直接データベースの操作を行います。JPQL(Java Persistence Query Language)や他のクエリ言語を用いて動的に SQL を生成する際に、この手法が特に検討されることがあります。

メリット

  1. 単純化されたアーキテクチャ:

    • アプリケーションのアーキテクチャが単純化され、開発者にとって理解しやすくなります。
  2. 開発スピードの向上:

    • 間接層を減らすことで、小規模なプロジェクトやプロトタイピングにおいて迅速な開発が可能になります。
  3. コード量の削減:

    • リポジトリ層の抽象化が不要になるため、全体的なコード量が減少します。

デメリット

  1. ビジネスロジックとデータアクセスの混在:

    • サービスクラス内でデータベース操作を行うことにより、ビジネスロジックとデータアクセスコードが混在し、コードの可読性と保守性が低下します。
  2. 再利用性の低下:

    • データアクセスロジックがサービスクラスに密結合しているため、他のサービスやコンポーネントでの再利用が難しくなります。
  3. テストの難易度増加:

    • ビジネスロジックとデータアクセスロジックが混在しているため、単体テストの作成が難しく、テストの品質が低下する可能性があります。
  4. スケーラビリティとメンテナンスの問題:

    • アプリケーションが成長するにつれて、サービスクラスが肥大化し、将来的な変更や拡張が困難になる可能性があります。

JPQL を用いた動的 SQL 生成の考慮点

  • JPQL などで動的に SQL を生成する場合、サービスクラスでの直接的な実装が単純かつ効率的に思えるかもしれません。しかし、ビジネスロジックとデータアクセスロジックの混在は、保守性や再利用性の低下を招く可能性があります。
  • クリーンなアーキテクチャを目指す場合は、データアクセスロジックをサービス層から分離し、リポジトリ層に配置するべきです。

この設計手法は、開発の迅速化と単純化を実現する一方で、長期的な視点ではアプリケーションの柔軟性、保守性、テスト容易性を損なう可能性があるため、特に小規模プロジェクトやプロトタイピングの段階での一時的な措置として採用することを検討するのが適切です。

JPQL を用いた動的 SQL 生成の Java コード例

import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import java.util.List;

@Service
public class ExampleService {

    @PersistenceContext
    private EntityManager entityManager;

    public List<ExampleEntity> findWithDynamicQuery(String name, Integer age) {
        StringBuilder queryBuilder = new StringBuilder("SELECT e FROM ExampleEntity e WHERE 1 = 1");

        if (name != null && !name.isEmpty()) {
            queryBuilder.append(" AND e.name = :name");
            entityManager.setParameter("name", name);
        }
        if (age != null) {
            queryBuilder.append(" AND e.age = :age");
            entityManager.setParameter("age", age);
        }

        TypedQuery<ExampleEntity> query = entityManager.createQuery(queryBuilder.toString(), ExampleEntity.class);
        return query.getResultList();
    }
}

@Entity
public class ExampleEntity {
    @Id
    private Long id;
    private String name;
    private Integer age;
    // getters and setters
}

5. DELETE 以外はパスパラメーターではなくクエリパラメーターを利用する

一意に特定できる項目であっても、DELETE 操作を除く他の API 操作において、パスパラメータではなくクエリパラメータを使用することを推奨します。

メリット

ルーティングの衝突回避:

  • クエリパラメータの使用により、パスパラメータによく見られるルーティングの衝突を回避できます。これにより、同じベース URL を使用しながら異なる操作を区別できるため、API の設計が単純化されます。

デメリット

RESTful な設計からの逸脱:

  • 通常、RESTful な設計では、リソースの一意性はパスパラメータを通じて表現されます。クエリパラメータのみを使用するアプローチは、RESsTful なパターンと異なるため、API の直感性が損なわれる可能性があります。

DELETE 操作を除く他の API 操作においてクエリパラメータの使用を検討する際は、これらのメリットとデメリットをバランスよく考慮することが重要です。特に、ルーティングの衝突を避けることができる利点は、API 設計において大きなメリットとなり得ますが、RESTful な設計原則との整合性も考慮する必要があります。

6.API レスポンスの共通化アプローチ

/**
 * 継承して仕様される前提のクラス
 */
public class BasicResponse {
    /** Errorがない場合でも明示的にfalseを返す */
    private boolean error = false;
    private String errorCode;
    private String errorMessage;

    public boolean isError() {
        return error;
    }

    public void setError(boolean error) {
        this.error = error;
    }

    public String getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }
}
/**
 * APIレスポンスとして返すクラス
 */
public class ObjectResponse<T> extends BasicResponse {
    private T obj;

    public ObjectResponse() {
    }

    public ObjectResponse(T obj) {
        this.obj = obj;
    }

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
}

API レスポンスを共通化するこのようなアプローチは、以下のような点で有効です。

メリット

  1. 再利用性:

    • ObjectResponse がジェネリック型 T を使用しているため、様々なタイプのレスポンスオブジェクトに再利用可能です。
  2. 拡張性:

    • BasicResponse クラスを継承しているため、共通のエラーハンドリング機能を提供しつつ、独自の機能を追加することが容易です。
  3. 一貫性:

    • 全ての API レスポンスが同じ基本構造(エラー情報など)を共有するため、API の利用者にとって理解しやすくなります。

デメリット

  1. 過度な抽象化:

    • すべてのレスポンスが同じ基本構造を持つ必要はない場合もあります。場合によっては、過度な抽象化が逆に複雑さを増すことがあります。
  2. パフォーマンスの影響:

    • ジェネリックスは型安全を提供しますが、ランタイム時に型消去が行われるため、パフォーマンスに微妙な影響を与える可能性があります。

全体としては、API の設計において共通のレスポンス型を定義することは、多くの場合でメリットが大きいです。ただし、特定のケースにおいては、よりカスタマイズされたアプローチが必要になることもあります。

OpenAPI/Swagger の利用

OpenAPI や Swagger を使用することで、API レスポンスの共通化アプローチは以下のメリットを享受できます。

メリット

  • 明確なドキュメント: API の仕様がより理解しやすくなります。
  • コード生成の利便性: 開発プロセスの迅速化に寄与します。
  • API の標準化: 一貫性と予測可能性が向上します。

デメリット

  • 過度な汎用化のリスク: 特定のエンドポイントの重要な情報が省略される可能性があります。
  • ドキュメントの複雑化: 新しい開発者が API を理解する際の障壁となる可能性があります。

これらのメリットとデメリットを考慮して、Swagger や OpenAPI を用いた API 設計を行う際には、共通のレスポンス型を適切に利用することが重要です。特に、API の明確なドキュメントと一貫性の維持は大きな利点ですが、各エンドポイントの独自性を適切に表現するためのバランスを取ることが必要になります。

まとめ

各グレーな設計手法の適切な使用についての考察、バランスの重要性とケースバイケースでの慎重な判断の必要性を強調します。

Discussion