CodeGym /Java Blog /ランダム /アンチパターンとは何ですか? いくつかの例を見てみましょう (パート 1)
John Squirrels
レベル 41
San Francisco

アンチパターンとは何ですか? いくつかの例を見てみましょう (パート 1)

ランダム グループに公開済み
全てにおいて良い日!先日就職面接を受けたのですが、アンチパターンとは何なのか、どんな種類があるのか​​、具体的な例は何があるのか​​など、アンチパターンについていくつか質問されました。もちろん、私は質問に答えましたが、これまでこのトピックについて深く掘り下げたことがなかったため、非常に表面的なものでした。インタビューの後、私はインターネットを調べ始め、この話題にどんどん没頭していきました。 アンチパターンとは何ですか?  いくつかの例を見てみましょう (パート 1) - 1 今日は、最も一般的なアンチパターンの概要を説明し、いくつかの例を確認したいと思います。これを読むことで、この分野で必要な知識が得られることを願っています。始めましょう!アンチパターンとは何かについて説明する前に、デザイン パターンとは何かを思い出してください。デザインパターンは、アプリケーションの設計時に発生する一般的な問題や状況に対する再現可能なアーキテクチャ ソリューションです。しかし、今日私たちはそれらについてではなく、むしろその反対、つまりアンチパターンについて話します。アンチパターンは、一般的な問題を解決するために広く普及しているものの、効果がなく、リスクがあり、非生産的なアプローチです。言い換えれば、これは間違いのパターンです(罠とも呼ばれます)。一般に、アンチパターンは次のタイプに分類されます。
  1. アーキテクチャのアンチパターン— これらのアンチパターンは、システムの構造が (通常はアーキテクトによって) 設計されるときに発生します。
  2. 管理/組織のアンチパターン— これらはプロジェクト管理におけるアンチパターンで、通常はさまざまなマネージャー (またはマネージャーのグループ) が遭遇します。
  3. 開発アンチパターン— これらのアンチパターンは、システムが通常のプログラマによって実装されるときに発生します。
あらゆる種類のアンチパターンはさらに風変わりなものですが、今日はそれらすべてを検討しません。普通の開発者にとって、それは多すぎるでしょう。まず、管理者のアンチパターンを例として考えてみましょう。

1. 分析麻痺

分析麻痺典型的な管理のアンチパターンとみなされます。これには、計画中に状況を過剰に分析することが含まれ、その結果、決定やアクションが実行されず、基本的に開発プロセスが麻痺します。これは、分析期間中に完璧を達成し、すべてを完全に考慮することが目標である場合によく発生します。このアンチパターンの特徴は、堂々巡り (ありきたりな閉ループ) を繰り返し、詳細なモデルを修正および作成することで、ワークフローが妨げられることです。たとえば、あるレベルで物事を予測しようとしているとします。しかし、ユーザーが突然、名前の 4 文字目と 5 文字目に基づいて、最も多くの労働時間を費やしたプロジェクトのリストを含む従業員のリストを作成したい場合はどうなるでしょうか。過去 4 年間、新年と国際女性デーの間には何がありましたか? 要するに、それは」分析しすぎです。分析麻痺と戦うためのヒントをいくつか紹介します。
  1. 意思決定の指標として長期的な目標を定義し、それぞれの決定によって停滞するのではなく目標に近づける必要があります。
  2. 些細なことに集中しないでください(なぜ、取るに足らない詳細について、あたかも人生で最も重要な決断であるかのように決断するのでしょうか?)
  3. 決定の期限を設定します。
  4. タスクを完璧に完了しようとしないでください。非常にうまく完了することをお勧めします。
ここではあまり深く考える必要はないので、他の管理上のアンチパターンについては考慮しません。したがって、前置きはせずに、いくつかのアーキテクチャ上のアンチパターンに進みます。この記事は、管理者ではなく将来の開発者が読む可能性が最も高いためです。

2.神オブジェクト

God オブジェクトは、あらゆる種類の機能と大量の異種データ (アプリケーションが中心となるオブジェクト) が過剰に集中していることを表すアンチパターンです。小さな例を挙げてみましょう。

public class SomeUserGodObject {
   private static final String FIND_ALL_USERS_EN = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users;
   private static final String FIND_BY_ID = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users WHERE id = ?";
   private static final String FIND_ALL_CUSTOMERS = "SELECT id, u.email, u.phone, u.first_name_en, u.middle_name_en, u.last_name_en, u.created_date" +
           "  WHERE u.id IN (SELECT up.user_id FROM user_permissions up WHERE up.permission_id = ?)";
   private static final String FIND_BY_EMAIL = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_dateFROM users WHERE email = ?";
   private static final String LIMIT_OFFSET = " LIMIT ? OFFSET ?";
   private static final String ORDER = " ORDER BY ISNULL(last_name_en), last_name_en, ISNULL(first_name_en), first_name_en, ISNULL(last_name_ru), " +
           "last_name_ru, ISNULL(first_name_ru), first_name_ru";
   private static final String CREATE_USER_EN = "INSERT INTO users(id, phone, email, first_name_en, middle_name_en, last_name_en, created_date) " +
           "VALUES (?, ?, ?, ?, ?, ?, ?)";
   private static final String FIND_ID_BY_LANG_CODE = "SELECT id FROM languages WHERE lang_code = ?";
                                  ........
   private final JdbcTemplate jdbcTemplate;
   private Map<String, String> firstName;
   private Map<String, String> middleName;
   private Map<String, String> lastName;
   private List<Long> permission;
                                   ........
   @Override
   public List<User> findAllEnCustomers(Long permissionId) {
       return jdbcTemplate.query( FIND_ALL_CUSTOMERS + ORDER, userRowMapper(), permissionId);
   }
   @Override
   public List<User> findAllEn() {
       return jdbcTemplate.query(FIND_ALL_USERS_EN + ORDER, userRowMapper());
   }
   @Override
   public Optional<List<User>> findAllEnByEmail(String email) {
       var query = FIND_ALL_USERS_EN + FIND_BY_EMAIL + ORDER;
       return Optional.ofNullable(jdbcTemplate.query(query, userRowMapper(), email));
   }
                              .............
   private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
       switch (type) {
           case USERS:
               return findAllEnUsers(permissionId);
           case CUSTOMERS:
               return findAllEnCustomers(permissionId);
           default:
               return findAllEn();
       }
   }
                              ..............…

   private RowMapper<User> userRowMapperEn() {
       return (rs, rowNum) ->
               User.builder()
                       .id(rs.getLong("id"))
                       .email(rs.getString("email"))
                       .accessFailed(rs.getInt("access_counter"))
                       .createdDate(rs.getObject("created_date", LocalDateTime.class))
                       .firstName(rs.getString("first_name_en"))
                       .middleName(rs.getString("middle_name_en"))
                       .lastName(rs.getString("last_name_en"))
                       .phone(rs.getString("phone"))
                       .build();
   }
}
ここには、あらゆることを行う巨大なクラスが表示されます。これには、データベース クエリといくつかのデータが含まれています。ビジネス ロジックを含む findAllWithoutPageEn ファサード メソッドも表示されます。このような神のオブジェクトは巨大になり、適切に維持するのが困難になります。コードのすべての部分でこれをいじる必要があります。多くのシステム コンポーネントはこれに依存しており、密接に結合されています。このようなコードを保守するのはますます難しくなります。このような場合、コードを別のクラスに分割し、それぞれの目的を 1 つだけにする必要があります。この例では、God オブジェクトを Dao クラスに分割できます。

public class UserDaoImpl {
   private static final String FIND_ALL_USERS_EN = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users;
   private static final String FIND_BY_ID = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users WHERE id = ?";
  
                                   ........
   private final JdbcTemplate jdbcTemplate;
                                                        
                                   ........
   @Override
   public List<User> findAllEnCustomers(Long permissionId) {
       return jdbcTemplate.query(FIND_ALL_CUSTOMERS + ORDER, userRowMapper(), permissionId);
   }
   @Override
   public List<User> findAllEn() {
       return jdbcTemplate.query(FIND_ALL_USERS_EN + ORDER, userRowMapper());
   }
  
                               ........
}
データとそのデータにアクセスするためのメソッドを含むクラス:

public class UserInfo {
   private Map<String, String> firstName;
                     …..
   public Map<String, String> getFirstName() {
       return firstName;
   }
   public void setFirstName(Map<String, String> firstName) {
       this.firstName = firstName;
   }
                    ....
そして、ビジネス ロジックを含むメソッドをサービスに移動する方が適切です。

private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
   switch (type) {
       case USERS:
           return findAllEnUsers(permissionId);
       case CUSTOMERS:
           return findAllEnCustomers(permissionId);
       default:
           return findAllEn();
   }
}

3. シングルトン

シングルトンは最も単純なパターンです。これにより、シングルスレッド アプリケーション内にクラスのインスタンスが 1 つ存在することが保証され、このオブジェクトへのグローバル アクセス ポイントが提供されます。しかし、それはパターンでしょうか、それともアンチパターンでしょうか? このパターンの欠点を見てみましょう。
  1. グローバル状態 クラスのインスタンスにアクセスするとき、このクラスの現在の状態はわかりません。誰がいつ変更したかはわかりません。状態は私たちが期待しているものとは異なる可能性があります。言い換えれば、シングルトンの操作の正確さは、シングルトンへのアクセス順序に依存します。これは、サブシステムが相互に依存していることを意味し、その結果、設計が非常に複雑になります。

  2. シングルトンは、SOLID 原則 (単一責任原則) に違反します。シングルトン クラスは、その直接の役割に加えて、インスタンスの数も制御します。

  3. 通常のクラスのシングルトンへの依存は、クラスのインターフェイスには表示されません。シングルトン インスタンスは通常、メソッドの引数として渡されるのではなく、getInstance() を通じて直接取得されるため、クラスのシングルトンへの依存関係を特定するには、各メソッドの実装に入る必要があります。クラスの public インスタンスを確認するだけです。契約だけでは不十分です。

    シングルトンが存在すると、アプリケーション全体、特にシングルトンを使用するクラスのテスト容易性が低下します。まず、シングルトンをモック オブジェクトに置き換えることはできません。次に、シングルトンに状態を変更するためのインターフェイスがある場合、テストは相互に依存します。

    言い換えれば、シングルトンは結合を増加させ、上記のすべては結合の増加の結果にすぎません。

    そしてよく考えれば、シングルトンの使用を避けることができます。たとえば、さまざまな種類のファクトリを使用してオブジェクトのインスタンスの数を制御することは非常に可能です (そして実際に必要です)。

    最大の危険は、シングルトンに基づいてアプリケーション アーキテクチャ全体を構築しようとする試みにあります。このアプローチに代わる素晴らしい選択肢がたくさんあります。最も重要な例は Spring、つまりその IoC コンテナです。これらは実際には「強化された工場」であるため、サービスの作成を制御する問題に対する自然な解決策です。

    この主題に関しては、現在、多くの果てしなく和解の余地のない議論が激怒しています。シングルトンがパターンであるかアンチパターンであるかを決定するのはあなた次第です。

    私たちはそれを長引かせるつもりはありません。代わりに、今日の最後のデザイン パターン、ポルターガイストに進みます。

4. ポルターガイスト

ポルターガイストは、別のクラスのメソッドを呼び出すために使用される、または単純に不必要な抽象化層を追加する、無意味なクラスに関係するアンチパターンです。このアンチパターンは、状態を持たない、存続期間の短いオブジェクトとして現れます。これらのオブジェクトは、他のより永続的なオブジェクトを初期化するためによく使用されます。

public class UserManager {
   private UserService service;
   public UserManager(UserService userService) {
       service = userService;
   }
   User createUser(User user) {
       return service.create(user);
   }
   Long findAllUsers(){
       return service.findAll().size();
   }
   String findEmailById(Long id) {
       return service.findById(id).getEmail();}
   User findUserByEmail(String email) {
       return service.findByEmail(email);
   }
   User deleteUserById(Long id) {
       return service.delete(id);
   }
}
単なる仲介者であり、その作業を他の人に委任するオブジェクトがなぜ必要なのでしょうか? 私たちはそれを排除し、それが持っていた小さな機能を長寿命のオブジェクトに移します。次に、(一般の開発者として) 私たちにとって最も興味深いパターン、つまり開発アンチパターンに進みます。

5. ハードコーディング

そこで私たちは、ハードコーディングという恐ろしい言葉にたどり着きました。このアンチパターンの本質は、コードが特定のハードウェア構成やシステム環境に強く結びついていることです。これにより、コードを他の構成に移植することが非常に複雑になります。このアンチパターンはマジックナンバーと密接に関連しています (これらのアンチパターンはしばしば絡み合っています)。例:

public Connection buildConnection() throws Exception {
   Class.forName("com.mysql.cj.jdbc.Driver");
   connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8&characterSetResults=UTF-8&serverTimezone=UTC", "user01", "12345qwert");
   return connection;
}
痛いですよね?ここでは、接続設定をハードコーディングします。その結果、コードは MySQL でのみ正しく動作します。データベースを変更するには、コードを調べてすべてを手動で変更する必要があります。良い解決策は、構成を別のファイルに置くことです。

spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
もう 1 つのオプションは、定数を使用することです。

6. ボートアンカー

アンチパターンの文脈では、ボート アンカーとは、最適化またはリファクタリングを実行した後に使用されなくなったシステムの部分を保持することを意味します。また、コードの一部は、突然必要になった場合に備えて「将来の使用のために」保存しておくこともできます。基本的に、これによりコードがゴミ箱に変わります。例:

public User update(Long id, User request) {
   User user = mergeUser(findById(id), request);
   return userDAO.update(user);
}
private User mergeUser(User findUser, User requestUser) {
   return new User(
           findUser.getId(),
           requestUser.getEmail() != null ? requestUser.getEmail() : findUser.getEmail(),
           requestUser.getFirstName() != null ? requestUser.getFirstName() : findUser.getFirstNameRu(),
           requestUser.getMiddleName() != null ? requestUser.getMiddleName() : findUser.getMiddleNameRu(),
           requestUser.getLastName() != null ? requestUser.getLastName() : findUser.getLastNameEn(),
           requestUser.getPhone() != null ? requestUser.getPhone() : findUser.getPhone());
}
データベースからのユーザー データとメソッドに渡されたユーザー データをマージする別のメソッドを使用する更新メソッドがあります (更新メソッドに渡されたユーザーに null フィールドがある場合、古いフィールド値がデータベースから取得されます)。 。次に、レコードを古いレコードとマージしてはならないという新しい要件があるとします。代わりに、null フィールドがある場合でも、古いレコードを上書きするために使用されるとします。

public User update(Long id, User request) {
   return userDAO.update(user);
}
これは、mergeUser がもう使用されないことを意味しますが、削除するのは残念です。このメソッド (またはこのメソッドのアイデア) がいつか役立つかもしれないとしたらどうでしょうか? このようなコードはシステムを複雑にし、混乱を招くだけであり、実質的に実用的な価値はありません。このような「死片」を含むコードは、別のプロジェクトに移るときに同僚に渡すのが難しいことを忘れてはなりません。ボートのアンカーに対処する最善の方法は、コードをリファクタリングすることです。つまり、コードのセクションを削除することです (悲痛なことはわかっています)。さらに、開発スケジュールを作成する際には、このようなアンカーを考慮する(後片付けに時間を割り当てる)必要があります。

7. オブジェクトのセスプール

このアンチパターンを説明するには、まずオブジェクト プールパターンを理解する必要があります。オブジェクトプール(リソース プール) は、作成用のデザイン パターンであり、初期化されてすぐに使用できるオブジェクトのセットです。アプリケーションがオブジェクトを必要とする場合、オブジェクトは再作成されるのではなく、このプールから取得されます。オブジェクトが不要になった場合でも、オブジェクトは破棄されません。代わりに、プールに戻されます。このパターンは通常、データベースに接続するときなど、必要になるたびに作成するのに時間がかかる重いオブジェクトに使用されます。小さくて単純な例を見てみましょう。このパターンを表すクラスは次のとおりです。

class ReusablePool {
   private static ReusablePool pool;
   private List<Resource> list = new LinkedList<>();
   private ReusablePool() {
       for (int i = 0; i < 3; i++)
           list.add(new Resource());
   }
   public static ReusablePool getInstance() {
       if (pool == null) {
           pool = new ReusablePool();
       }
       return pool;
   }
   public Resource acquireResource() {
       if (list.size() == 0) {
           return new Resource();
       } else {
           Resource r = list.get(0);
           list.remove(r);
           return r;
       }
   }
   public void releaseResource(Resource r) {
       list.add(r);
   }
}
このクラスは、上記のシングルトンパターン/アンチパターン の形式で表されます。つまり、このタイプのオブジェクトは 1 つだけ存在できます。特定のオブジェクトを使用しますResource。デフォルトでは、コンストラクターはプールに 4 つのインスタンスを埋めます。オブジェクトを取得すると、そのオブジェクトはプールから削除されます (使用可能なオブジェクトがない場合は、オブジェクトが作成され、すぐに返されます)。そして最後に、オブジェクトを元に戻すメソッドがあります。リソース オブジェクトは次のようになります。

public class Resource {
   private Map<String, String> patterns;
   public Resource() {
       patterns = new HashMap<>();
       patterns.put("proxy", "https://en.wikipedia.org/wiki/Proxy_pattern");
       patterns.put("bridge", "https://en.wikipedia.org/wiki/Bridge_pattern");
       patterns.put("facade", "https://en.wikipedia.org/wiki/Facade_pattern");
       patterns.put("builder", "https://en.wikipedia.org/wiki/Builder_pattern");
   }
   public Map<String, String> getPatterns() {
       return patterns;
   }
   public void setPatterns(Map<String, String> patterns) {
       this.patterns = patterns;
   }
}
ここには、デザイン パターン名をキーとして、対応する Wikipedia リンクを値として持つマップと、マップにアクセスするメソッドを含む小さなオブジェクトがあります。メインを見てみましょう:

class SomeMain {
   public static void main(String[] args) {
       ReusablePool pool = ReusablePool.getInstance();

       Resource firstResource = pool.acquireResource();
       Map<String, String> firstPatterns = firstResource.getPatterns();
       // use our map somehow...
       pool.releaseResource(firstResource);

       Resource secondResource = pool.acquireResource();
       Map<String, String> secondPatterns = firstResource.getPatterns();
       // use our map somehow...
       pool.releaseResource(secondResource);

       Resource thirdResource = pool.acquireResource();
       Map<String, String> thirdPatterns = firstResource.getPatterns();
       // use our map somehow...
       pool.releaseResource(thirdResource);
   }
}
ここでのことはすべて明確です。プール オブジェクトを取得し、プールからリソースを含むオブジェクトを取得し、リソース オブジェクトからマップを取得し、それを使って何かを行い、これらすべてをさらに再利用できるようにプール内の所定の位置に置きます。ほら、これがオブジェクト プールの設計パターンです。でも、私たちはアンチパターンについて話していましたよね? main メソッドで次のケースを考えてみましょう。

Resource fourthResource = pool.acquireResource();
   Map<String, String> fourthPatterns = firstResource.getPatterns();
// use our map somehow...
fourthPatterns.clear();
firstPatterns.put("first","blablabla");
firstPatterns.put("second","blablabla");
firstPatterns.put("third","blablabla");
firstPatterns.put("fourth","blablabla");
pool.releaseResource(fourthResource);
ここでも、Resource オブジェクトを取得し、そのパターンのマップを取得し、そのマップで何かを実行します。ただし、マップをオブジェクトのプールに保存し直す前にマップがクリアされ、破損したデータが追加されるため、Resource オブジェクトは再利用に適さなくなります。オブジェクト プールの主な詳細の 1 つは、オブジェクトが返されたときに、さらなる再利用に適した状態に復元する必要があることです。プールに返されたオブジェクトが誤った状態または未定義の状態のままである場合、その設計はオブジェクト セスプールと呼ばれます。再利用に適さないオブジェクトを保管することに意味はあるのでしょうか? この状況では、コンストラクターで内部マップを不変にすることができます。

public Resource() {
   patterns = new HashMap<>();
   patterns.put("proxy", "https://en.wikipedia.org/wiki/Proxy_pattern");
   patterns.put("bridge", "https://en.wikipedia.org/wiki/Bridge_pattern");
   patterns.put("facade", "https://en.wikipedia.org/wiki/Facade_pattern");
   patterns.put("builder", "https://en.wikipedia.org/wiki/Builder_pattern");
   patterns = Collections.unmodifiableMap(patterns);
}
マップのコンテンツを変更しようとする試みやその願望は、生成される UnsupportedOperationException のおかげで消え去ります。 アンチパターンは、深刻な時間不足、不注意、経験不足、またはプロジェクト マネージャーからのプレッシャーによって開発者が頻繁に遭遇するトラップです。よくあるラッシュは、将来アプリケーションに大きな問題を引き起こす可能性があるため、これらのエラーについて理解し、事前に回避する必要があります。これで記事の最初の部分は終わりです。つづく...
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION