JavaRush /Java Blog /Random-JA /アンチパターンとは何ですか? 例を見てみましょう (パート 1)
Константин
レベル 36

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

Random-JA グループに公開済み
アンチパターンとは何ですか?  例を見てみましょう (パート 1) - 1全てにおいて良い日!先日インタビューを受け、アンチパターンについて質問されました。これはどのような種類の獣で、その種類や実際の例は何ですか。もちろん、私は質問に答えましたが、この問題についてはあまり深く調べていなかったため、非常に表面的に答えました。インタビューの後、私はインターネットを調べ始め、このトピックにますますのめり込むようになりました。今日は、最も一般的なアンチパターンとその例について簡単にレビューしたいと思います。これを読んで、この問題について必要な知識を得ることができます。始めましょう!したがって、アンチパターンとは何かについて説明する前に、パターンとは何かを思い出してみましょう。 パターンは、アプリケーションの設計時に発生する一般的な問題や状況を解決するための反復可能なアーキテクチャ設計です。しかし、今日私たちはそれらについて話しているのではなく、それらの反対であるアンチパターンについて話しています。 アンチパターンは、 効果的ではない、リスクが高い、または非生産的な、よく遭遇するクラスの問題を解決するための一般的なアプローチです。言い換えれば、それはエラー パターン (トラップとも呼ばれます) です。 アンチパターンとは何ですか?  例を見てみましょう (パート 1) - 2一般に、アンチパターンは次のタイプに分類されます。
  1. アーキテクチャー アンチパターン- システムの構造を設計するときに (通常はアーキテクトによって) 発生するアーキテクチャー アンチパターン。
  2. 管理アンチパターン- 管理分野におけるアンチパターン。通常、さまざまなマネージャー (またはマネージャーのグループ) が遭遇します。
  3. 開発アンチパターン- アンチパターンは、通常のプログラマーがシステムを作成するときに発生する開発上の問題です。
アンチパターンの異国情緒はさらに広範囲に及びますが、通常の開発者にとってこれは圧倒されるため、今日は考慮しません。まず、管理分野におけるアンチパターンの例を見てみましょう。

1. 分析麻痺

分析麻痺は古典的な組織のアンチパターンとみなされます。これには、計画を立てる際に状況を過度に分析して、決定や行動を行わず、本質的に開発を麻痺させることが含まれます。これは、完璧を達成し、分析期間を完全に完了することが目標である場合によく発生します。このアンチパターンの特徴は、循環 (一種の閉ループ) を歩き回り、詳細なモデルを修正および作成することで、ワークフローが妨げられることです。たとえば、次のようなことを予測しようとしています。ユーザーが突然、名前の 4 文字目と 5 文字目に基づいて従業員のリストを作成したいと考えた場合はどうなるでしょうか。そのリストには、従業員が 2015 年までに最も多くの労働時間を費やしたプロジェクトも含まれています。過去 4 年間では新年と 3 月 8 日でしたか? 本質的に、これは過剰な分析です。実例としては、分析麻痺がコダック社を破産に導いたことが挙げられます。分析麻痺に対処するための小さなヒントをいくつか紹介します。
  1. 意思決定の指標として長期的な目標を定義する必要があります。そうすることで、すべての決定が目標に近づくことができ、時間をマークする必要がなくなります。
  2. 些細なことに集中しないでください(なぜ人生で最後であるかのように小さなニュアンスで決断を下すのでしょうか?)
  3. 決定を下すための期限を設定します。
  4. タスクを完璧に実行しようとしないでください。非常にうまく実行する方が良いです。
ここでは、あまり深くは考えずに、他の管理のアンチパターンについて検討します。したがって、前置きはせずに、いくつかのアーキテクチャのアンチパターンに移りましょう。おそらく、この記事は管理者ではなく、将来の開発者が読むことになるからです。

2.神オブジェクト

Divine オブジェクトは、多量の多様なデータ (アプリケーションが中心となるオブジェクト) を保存する、あまりにも多くの異種機能の過剰な集中を表すアンチパターンです。小さな例を見てみましょう:
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 つだけ持つ必要があります。この例では、これを 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) - 3このテンプレートの欠点を見てみましょう。
  1. グローバルな状態。クラスのインスタンスにアクセスするとき、そのクラスの現在の状態や、誰がいつ変更したかがわかりません。また、その状態が期待どおりではない可能性があります。言い換えれば、シングルトンの操作の正しさはその呼び出し順序に依存し、これによりサブシステムが相互に依存することになり、その結果、開発の複雑さが大幅に増加します。

  2. Singleton は、SOLID 原則の 1 つである単一責任原則に違反しています。Singleton クラスは、その直接の責任を実行することに加えて、そのインスタンスの数も制御します。

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

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

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

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

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

    現在、このトピックに関しては多くの議論があるため、シングルトンがパターンであるかアンチパターンであるかを判断するのはあなた次第です。

    これにはこだわらずに、今日の最後のデザイン パターンであるポルターガイストに進みます。

4. ポルターガイスト

Poltergeist は、別のクラスのメソッドを呼び出すために使用されたり、単に不必要な抽象化層を追加したりする、役に立たないクラス アンチパターンです。アンチパターンは、状態を持たない短命なオブジェクトの形で現れます。これらのオブジェクトは、他のより耐久性のあるオブジェクトを初期化するためによく使用されます。
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. ボートアンカー

アンチパターンの文脈におけるボート アンカーとは、最適化またはリファクタリングの後に残ったシステムの未使用部分を保存することを意味します。また、再度使用する必要がある場合に備えて、コードの一部を「将来のために」残しておくこともできます。これにより、コードは実質的にゴミ箱となります。 アンチパターンとは何ですか?  例を見てみましょう (パート 1) - 4例:
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());
}
データベースからのユーザーと更新に来たユーザーのデータをマージする別のメソッドを使用する更新メソッドがあります (更新に来たユーザーのフィールドが空の場合、古いものとして書き込まれます)データベースより)。たとえば、空のフィールドがある場合でも、レコードを古いレコードとマージせず、上書きする必要があるという要件がありました。
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 つのコピーで埋められます。そのようなオブジェクトが取得されると、そのオブジェクトはプールから削除され (存在しない場合は、作成されてすぐに与えられます)、最後にオブジェクトを元に戻すメソッドがあります。オブジェクトはResource次のようになります。
public class Resource {
   private Map<String, String> patterns;
   public Resource() {
       patterns = new HashMap<>();
       patterns.put("заместитель", "https://studfile.net/preview/3676297/page:3/");
       patterns.put("мост", "https://studfile.net/preview/3676297/page:4/");
       patterns.put("фасад", "https://studfile.net/preview/3676297/page:5/");
       patterns.put("строитель", "https://studfile.net/preview/3676297/page:6/#16");
   }
   public Map<String, String> getPatterns() {
       return patterns;
   }
   public void setPatterns(Map<String, String> patterns) {
       this.patterns = patterns;
   }
}
ここには、パターンの名前をキーとして、パターンへのリンクを値として含むマップと、マップにアクセスするためのメソッドを含む小さなオブジェクトがあります。見てみようmain
class SomeMain {
   public static void main(String[] args) {
       ReusablePool pool = ReusablePool.getInstance();

       Resource firstResource = pool.acquireResource();
       Map<String, String> firstPatterns = firstResource.getPatterns();
       // ......Howим-то образом используем нашу мапу.....
       pool.releaseResource(firstResource);

       Resource secondResource = pool.acquireResource();
       Map<String, String> secondPatterns = firstResource.getPatterns();
       // ......Howим-то образом используем нашу мапу.....
       pool.releaseResource(secondResource);

       Resource thirdResource = pool.acquireResource();
       Map<String, String> thirdPatterns = firstResource.getPatterns();
       // ......Howим-то образом используем нашу мапу.....
       pool.releaseResource(thirdResource);
   }
}
ここでもすべてが明確です。プール オブジェクトを取得し、そこからリソースを含むオブジェクトを取り出し、そこからマップを取得し、それを使って何かを行い、さらに再利用できるようにすべてをプールに戻します。ほら、これでオブジェクト プール パターンが完成しました。でも、私たちはアンチパターンについて話していましたよね?このケースを見てみましょうmain:
Resource fourthResource = pool.acquireResource();
   Map<String, String> fourthPatterns = firstResource.getPatterns();
// ......Howим-то образом используем нашу мапу.....
fourthPatterns.clear();
firstPatterns.put("first","blablabla");
firstPatterns.put("second","blablabla");
firstPatterns.put("third","blablabla");
firstPatterns.put("fourth","blablabla");
pool.releaseResource(fourthResource);
ここでも、リソース オブジェクトが取得され、パターンを含むそのマップが取得され、それに対して何らかの処理が行われますが、オブジェクト プールに保存し直す前に、マップがクリーンアップされ、この Resource オブジェクトを再利用に適さなくする理解できないデータで埋められます。オブジェクト プールの主なニュアンスの 1 つは、オブジェクトが返された後、さらなる再利用に適した状態に戻す必要があることです。オブジェクトがプールに返された後、不正な状態または未定義の状態にある場合、この構造はオブジェクト セスプールと呼ばれます。再利用できないオブジェクトを保存することに何の意味があるのでしょうか? この状況では、コンストラクターで内部マップを不変にすることができます。
public Resource() {
   patterns = new HashMap<>();
   patterns.put("заместитель", "https://studfile.net/preview/3676297/page:3/");
   patterns.put("мост", "https://studfile.net/preview/3676297/page:4/");
   patterns.put("фасад", "https://studfile.net/preview/3676297/page:5/");
   patterns.put("строитель", "https://studfile.net/preview/3676297/page:6/#16");
   patterns = Collections.unmodifiableMap(patterns);
}
(コンテンツを変更しようとすると、UnsupportedOperationException が発生します)。 アンチパターンは、深刻な時間不足、不注意、経験不足、またはマネージャーからの非難によって開発者が陥りやすい罠です。通常、時間が足りず急いでいると、将来アプリケーションに大きな問題が発生する可能性があるため、これらの間違いを事前に認識して回避する必要があります。 アンチパターンとは何ですか?  例を見てみましょう (パート 1) - 6これで、この記事の前半は終了です。続きます
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION