攻撃的プログラミング:妄想的、攻撃的、攻撃的または無防備なプログラミング

コードを簡潔かつ従順にする方法







明らかに奇妙な振る舞いをするアプリケーションに会ったことがありますか? ボタンを押しても何も起こりません。 または、画面が突然黒くなります。 または、アプリケーションが「奇妙な状態」に陥り、アプリケーションを再起動して再度機能させる必要があります。



もしあなたがそのような経験を持っていたなら、おそらくあなたは特定の形の防御的プログラミングの犠牲になったでしょう。 ディフェンダーは慎重で合理的です。 妄想は恐怖を感じ、奇妙な行動をします。 この記事では、代替アプローチを提案します: 攻撃的プログラミング



用心深い読者



妄想的なプログラミングはどのようなものでしょうか? 典型的なJavaの例を次に示します



public String badlyImplementedGetData(String urlAsString) { // Convert the string URL into a real URL URL url = null; try { url = new URL(urlAsString); } catch (MalformedURLException e) { logger.error("Malformed URL", e); } // Open the connection to the server HttpURLConnection connection = null; try { connection = (HttpURLConnection) url.openConnection(); } catch (IOException e) { logger.error("Could not connect to " + url, e); } // Read the data from the connection StringBuilder builder = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { builder.append(line); } } catch (Exception e) { logger.error("Failed to read data from " + url, e); } return builder.toString(); }
      
      





このコードは、単にURLの内容を文字列として読み取ります。 非常に単純な仕事をするための予想外の量のコードですが、これはJavaです。



このコードの何が問題になっていますか? このコードは、発生する可能性のあるすべてのエラーに対処しているように見えますが、ひどい方法で処理します。単純にそれらを無視して作業を続けます。 このプラクティスは、Javaのチェック例外(無条件の発明)によって無条件にサポートされていますが、他の言語でも同様の動作が見られます。



エラーが発生するとどうなりますか:





一部の開発者は、アプリケーションがクラッシュしないため、このようなコードは良いと主張しています。 私は、クラッシュ以外にもアプリケーションに起こりうるより悪いことがあると主張します。 この場合、エラーは説明なしで間違った動作を引き起こすだけです。 たとえば、画面は空白のままになりますが、アプリケーションはエラーをスローしません。



無防備な方法で書かれたコードを見てみましょう。



 public String getData(String url) throws IOException { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); // Read the data from the connection StringBuilder builder = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { builder.append(line); } } return builder.toString(); }
      
      





throws IOExceptionステートメント(Javaには必要ですが、私に知られている他の言語には必要ありません)は、このメソッドが機能しない可能性があり、呼び出しメソッドがそれを処理する準備ができていることを示します。



このコードはより簡潔で、エラーが発生した場合、ユーザーとロガーは(おそらく)正しいエラーメッセージを受け取ります。



レッスン1 :例外をローカルで処理しないでください。



バリアフロー



それでは、この種のエラーはどのように処理されるべきでしょうか? エラー処理を適切に行うために、アプリケーションのアーキテクチャ全体を考慮する必要があります。 URLのコンテンツでUIを定期的に更新するアプリケーションがあると仮定しましょう。



 public static void startTimer() { Timer timer = new Timer(); timer.scheduleAtFixedRate(timerTask(SERVER_URL), 0, 1000); } private static TimerTask timerTask(final String url) { return new TimerTask() { @Override public void run() { try { String data = getData(url); updateUi(data); } catch (Exception e) { logger.error("Failed to execute task", e); } } }; }
      
      





これはまさに私たちが望む考え方です! 最も予期しないエラーのうち、回復する方法はありませんが、タイマーをこれから停止させたくはありませんか?



そして、私たちがしたい場合はどうなりますか? 最初に、RuntimeExceptionsでJavaのチェック済み例外をラップするよく知られた方法があります。



 public static String getData(String urlAsString) { try { URL url = new URL(urlAsString); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); // Read the data from the connection StringBuilder builder = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { builder.append(line); } } return builder.toString(); } catch (IOException e) { throw new RuntimeException(e.getMessage(), e); } }
      
      





実際、ライブラリは、Java言語のこのthisい機能を隠すよりも少しだけ感覚的に書かれています。



これでタイマーを単純化できます:



 public static void startTimer() { Timer timer = new Timer(); timer.scheduleAtFixedRate(timerTask(SERVER_URL), 0, 1000); } private static TimerTask timerTask(final String url) { return new TimerTask() { @Override public void run() { updateUi(getData(url)); } }; }
      
      





エラーのあるURLでこのコードを実行すると(またはサーバーにアクセスできない場合)、すべてが十分に悪くなります。標準エラー出力ストリームでエラーメッセージを受信し、タイマーが停止します。



この時点で、1つ明白なことがあります。このコードは、NullPointerExceptionを引き起こすバグであるかどうか、またはサーバーが現在利用できない場合に関係なく、アクションを繰り返します。



2番目の状況は私たちに適していますが、最初の状況はそれほど適切ではないかもしれません。コードが毎回クラッシュするバグにより、エラーログが散らばってしまいます。 たぶん、タイマーを殺した方がいいのでしょうか?



 public static void startTimer() // ... public static String getData(String urlAsString) // ... private static TimerTask timerTask(final String url) { return new TimerTask() { @Override public void run() { try { String data = getData(url); updateUi(data); } catch (IOException e) { logger.error("Failed to execute task", e); } } }; }
      
      





レッスン2 :回復は常に良いとは限りません。 ネットワークの問題など、環境に起因するエラーや、誰かがコードを修正するまで消えないバグに起因するエラーを考慮する必要があります。



本当にそこにいますか?



タスクを持つWorkOrders



クラスがあるとしWorkOrders



。 各タスクは誰かが実装します。 WorkOder



関与している人々を集めたいとWorkOder



ます。 同様のコードに出会ったに違いありません。



 public static Set findWorkers(WorkOrder workOrder) { Set people = new HashSet(); Jobs jobs = workOrder.getJobs(); if (jobs != null) { List jobList = jobs.getJobs(); if (jobList != null) { for (Job job : jobList) { Contact contact = job.getContact(); if (contact != null) { Email email = contact.getEmail(); if (email != null) { people.add(email.getText()); } } } } } return people; }
      
      





このコードでは、実際に何が起こっているかを信用していませんよね? 悪いデータが与えられたと仮定しましょう。 この場合、コードは喜んでデータを飲み込み、空のセットを返します。 データが期待を満たしていないことに本当に気付きません。



コードをきれいにしましょう:



 public static Set findWorkers(WorkOrder workOrder) { Set people = new HashSet(); for (Job job : workOrder.getJobs().getJobs()) { people.add(job.getContact().getEmail().getText()); } return people; }
      
      





おい! すべてのコードはどこに行きましたか? 突然、コードの議論と理解が再び簡単になりました。 また、処理中のタスクの構造に問題がある場合、コードは喜んで壊れて通知されます。



nullのチェックは、妄想的なプログラミングの最も陰湿なソースの1つであり、その数は急速に増加しています。 本番からバグレポートを受け取ったと想像してください。このコードでNullPointerExceptionが発生したため、コードが落ちました。



 public String getCustomerName() { return customer.getName(); }
      
      





人々はストレスにさらされています! そして何をすべきか? もちろん、別のnullチェックを追加しています。



 public String getCustomerName() { if (customer == null) return null; return customer.getName(); }
      
      





コードをコンパイルし、サーバーにアップロードします。 しばらくすると、別のレポートを取得します。次のコードのnullポインター例外。



 public String getOrderDescription() { return getOrderDate() + " " + getCustomerName().substring(0,10) + "..."; }
      
      





そして、それが始まります-コード全体にヌルのチェックを広げます。 最初から問題を止めるだけです。nullを受け入れないでください。



ところで、パーサーコードに強制的にnullを受け入れさせ、単純なままにしておくことができるかどうかに興味がある場合は、できます。 タスクの例で、XMLファイルからデータを取得するとします。 この場合、この問題を解決するための私のお気に入りの方法は次のとおりです。



 public static Set findWorkers(XmlElement workOrder) { Set people = new HashSet(); for (XmlElement email : workOrder.findRequiredChildren("jobs", "job", "contact", "email")) { people.add(email.text()); } return people; }
      
      





もちろん、これにはJavaが現在持っているものよりも価値のあるライブラリが必要です。



レッスン3 :Nullチェックはエラーを隠し、さらにNullチェックを生成します。



おわりに



セキュリティコードを記述しようとすると、プログラマはしばしば妄想に陥ります-根本的な原因を整理するのではなく、必然的に自分が見たすべての問題に自らを投げつけます。 コードを落とし、問題の原因を修正するための無防備な戦略により、コードがよりクリーンになり、エラーが発生しにくくなります。



エラーの隠蔽はバグの増殖につながります。 鼻のすぐ前でアプリケーションが爆発すると、本当の問題を解決できます。






Olga ChernopitskayaとEdisonに翻訳していただきありがとうございます。Elisonは従業員の時間を記録するための 内部スタッフテストシステムアプリケーションを開発してます。



All Articles