じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

この世の中に溢れているので自分が発言する必要はないが「ソフトウェアは認知の限界まで複雑になる」を自分なりに再考する

人間が何もしないと病気になるのと同じように、ソフトウェアも何もしないと複雑になる。

はじめに

ソフトウェア開発の世界に飛び込んでから、「ソフトウェアは認知の限界まで複雑になる」という言葉を耳にしたとき、正直なところ、「ほへー」って思いながら何も理解していませんでした。しかし、大規模なシステムに携わるようになって、その言葉の重みを身をもって感じるようになりました。内部構造や相互作用が複雑化し、全体を把握するのが難しくなっていく。それは挑戦であると同時に、私たち開発者の存在意義を問いかけるものでもあります

この複雑性との格闘は、時に苦しいものです。でも、それを乗り越えたときの喜びは何物にも代えがたい。私たちの理解力の限界に挑戦し続けることで、成長の機会を得られるのかもしれません。また、絶対的な正解が存在しないことも認識することが重要です。それぞれの組織や開発チームにとっての最適解は異なるため、継続的に自分たちの状況を評価し、最適なアプローチを探り続ける必要があります。この過程では、チームメンバー間のオープンなコミュニケーションと実験的な姿勢が鍵となります。時には失敗することもありますが、そこから学びを得て前進することで、長期的には組織全体の能力向上につながるでしょう。

speakerdeck.com

なお、この概念は広く知られており、多くの議論がなされています。しかし、自分なりに再考することには大きな意義があります。なぜなら、個人の経験や視点を通じて理解を深めることで、この普遍的な課題に対する新たな洞察や独自のアプローチを見出せる可能性があるからです。また、自分の言葉で表現し直すことで、チーム内での議論を促進し、共通理解を深める機会にもなります。さらに、技術の進化や開発手法の変化に伴い、この概念の意味や影響も変化しているかもしれません。そのため、現代のコンテキストにおいてこの概念を再評価することは、ソフトウェア開発の未来を考える上で重要なのです。正直なところ、このブログに書いていることは完全に自己満足かもしれません。しかし、この自己満足的な行為を通じて、私自身の理解を深め、そして少しでも他の人の考えるきっかけになれば、それはそれで価値があるのではないでしょうか。

個人的には「Good Code, Bad Code ~持続可能な開発のためのソフトウェアエンジニア的思考」や「ルールズ・オブ・プログラミング ―より良いコードを書くための21のルール」も良かったのです。資料としては「複雑さに立ち向かうためのコードリーディング入門」や「オブジェクト指向のその前に-凝集度と結合度/Coheision-Coupling」も合わせてオススメです。

複雑性の源泉

ソフトウェアの複雑性は様々な要因から生まれます。

  1. 機能の増加:ユーザーの要求に応えるため、次々と新機能が追加されていく。
  2. レガシーコードの蓄積:古いコードが新しいコードと共存し、相互作用する。
  3. 技術的負債:短期的な解決策が長期的な複雑性を生み出す。
  4. 外部依存関係サードパーティライブラリやAPIの統合が複雑性を増す。
  5. スケーラビリティ要件:大規模なデータや高いトラフィックに対応するための設計が複雑さを増す。

これらの要因が相互に作用し合い、ソフトウェアシステムは徐々に、そして時には急激に複雑化していきます。

複雑性の影響

過度の複雑性は、ソフトウェア開発プロセス全体に深刻な影響を及ぼします。

  1. 開発速度の低下:新機能の実装や既存機能の修正に時間がかかるようになる。
  2. バグの増加:複雑なシステムほど、予期せぬ相互作用やエッジケースが発生しやすい。
  3. メンテナンス性の低下:コードベースの理解が困難になり、変更のリスクが高まる。
  4. オンボーディングの難化:新しいチームメンバーが全体を把握するまでの時間が長くなる。
  5. イノベーションの阻害:既存システムの制約が新しいアイデアの実現を妨げる。

複雑性との共存

完全に複雑性を排除することは不可能ですが、以下の戦略を通じて管理することは可能です。

  1. モジュール化:システムを独立した、理解しやすいコンポーネントに分割する。
  2. 抽象化:詳細を隠蔽し、高レベルの概念を通じてシステムを理解・操作できるようにする。
  3. 設計パターンの活用:一般的な問題に対する標準的な解決策を適用する。
  4. 継続的なリファクタリング:定期的にコードを見直し、改善する。
  5. 適切な文書化:システムの構造や意思決定の理由を明確に記録する。

マイクロサービスアーキテクチャの採用は、大規模なモノリシックシステムの複雑性を管理するための一つのアプローチです。しかし、これは単に複雑性の性質を変えるだけで、新たな形の複雑性(例えば、サービス間通信やデータ一貫性の管理)をもたらす可能性があります。そのため、アーキテクチャの選択は慎重に行い、トレードオフを十分に考慮する必要があります。

複雑性と認知負荷

ソフトウェアの複雑性は、開発者の認知負荷と密接に関連しています。人間の脳には情報処理能力の限界があり、この限界を超えると効率的な問題解決や創造的思考が困難になります。

複雑なソフトウェアシステムは、以下の方法で開発者の認知負荷を増大させます。

  1. 同時に考慮すべき要素の増加
  2. 複雑な相互依存関係の理解
  3. 抽象化レベルの頻繁な切り替え
  4. 長期記憶と作業記憶の継続的な活用

これらの要因により、開発者は「認知の限界」に達し、それ以上の複雑性を効果的に管理することが困難になります。

以下は、複雑性が増大したコードの例です。

// ComplexSystem は、システム全体の複雑性を体現する構造体です。
// 複雑性の要因:多数の依存関係、状態管理、イベント処理、設定管理の組み合わせ
type ComplexSystem struct {
    components     map[string]Component            // 動的に管理される多数のコンポーネント
    interactions   map[string][]string             // コンポーネント間の複雑な相互作用を表現
    stateManager   *StateManager                   // 全体の状態を管理する複雑なロジック
    eventBus       *EventBus                       // 非同期イベント処理による複雑性
    configProvider ConfigProvider                  // 動的な設定変更による複雑性
    logger         Logger                          // 複数の場所でのロギングによる情報の分散
    cache          *Cache                          // パフォーマンス最適化のための追加レイヤー
    metrics        *MetricsCollector               // システム監視のための追加の複雑性
    errorHandler   ErrorHandler                    // カスタムエラーハンドリングによる複雑性
    scheduler      *Scheduler                      // 非同期タスクスケジューリングによる複雑性
}

// ProcessEvent は、イベント処理の複雑性を示す関数です。
// 複雑性の要因:多段階の処理、エラーハンドリング、状態更新、非同期処理の組み合わせ
func (cs *ComplexSystem) ProcessEvent(event Event) error {
    cs.metrics.IncrementEventCounter(event.Type)  // メトリクス収集による複雑性
    cs.logger.Log("Processing event: " + event.Name)
    
    // キャッシュチェックによる条件分岐の増加
    if cachedResult, found := cs.cache.Get(event.ID); found {
        cs.logger.Log("Cache hit for event: " + event.ID)
        return cs.handleCachedResult(cachedResult)
    }

    // 複雑な依存関係の解決
    affectedComponents := cs.resolveAffectedComponents(event)
    
    // ゴルーチンを使用した並行処理による複雑性の増加
    resultChan := make(chan ComponentResult, len(affectedComponents))
    for _, componentID := range affectedComponents {
        go cs.processComponentAsync(componentID, event, resultChan)
    }

    // 非同期処理結果の収集と統合
    for i := 0; i < len(affectedComponents); i++ {
        result := <-resultChan
        if result.Error != nil {
            cs.errorHandler.HandleError(result.Error)
            return result.Error
        }
        cs.updateSystemState(result)
    }

    // 動的設定に基づく条件付き処理
    config := cs.configProvider.GetConfig()
    if config.EnablePostProcessing {
        if err := cs.performPostProcessing(event); err != nil {
            cs.logger.Error("Error in post-processing: " + err.Error())
            return cs.errorHandler.WrapError(err, "PostProcessingFailed")
        }
    }

    // イベントバスを使用した非同期通知
    cs.eventBus.Publish(NewStateChangedEvent(event.ID, cs.stateManager.GetCurrentState()))

    // 次のスケジュールされたタスクのトリガー
    cs.scheduler.TriggerNextTask()

    cs.logger.Log("Event processed successfully")
    return nil
}

// processComponentAsync は、個別のコンポーネント処理を非同期で行う関数です。
// 複雑性の要因:ゴルーチン内での処理、エラーハンドリング、状態更新の組み合わせ
func (cs *ComplexSystem) processComponentAsync(componentID string, event Event, resultChan chan<- ComponentResult) {
    component, exists := cs.components[componentID]
    if !exists {
        resultChan <- ComponentResult{Error: fmt.Errorf("component not found: %s", componentID)}
        return
    }

    newState, err := component.HandleEvent(event)
    if err != nil {
        resultChan <- ComponentResult{Error: cs.errorHandler.WrapError(err, "ComponentProcessingFailed")}
        return
    }

    cs.stateManager.UpdateState(componentID, newState)
    resultChan <- ComponentResult{ID: componentID, State: newState}
}

// performPostProcessing は、イベント処理後の追加処理を行う関数です。
// 複雑性の要因:条件分岐、エラーハンドリング、外部サービス呼び出しの組み合わせ
func (cs *ComplexSystem) performPostProcessing(event Event) error {
    // 複雑な条件分岐
    switch event.Type {
    case "TypeA":
        // 外部サービス呼び出し
        if err := cs.externalServiceA.Process(event); err != nil {
            return cs.errorHandler.WrapError(err, "ExternalServiceAFailed")
        }
    case "TypeB":
        // データ変換と検証
        transformedData, err := cs.dataTransformer.Transform(event.Data)
        if err != nil {
            return cs.errorHandler.WrapError(err, "DataTransformationFailed")
        }
        if !cs.dataValidator.Validate(transformedData) {
            return cs.errorHandler.NewError("InvalidTransformedData")
        }
        // さらなる処理...
    default:
        // デフォルトの複雑な処理ロジック
        // ...
    }

    // メトリクス更新
    cs.metrics.IncrementPostProcessingCounter(event.Type)

    return nil
}

このコードは、多層の依存関係、複雑な状態管理、非同期イベント処理、動的設定、並行処理、多重エラーハンドリング、クロスカッティングコンサーンなどを含む極度に複雑なシステムを表現しており、その全体を理解し効果的に管理するには開発者の認知能力を大きく超える負荷が必要となります。

この複雑性に対処するため、システムを小さな独立したサービスに分割し、各コンポーネントの責務を明確に定義することで、全体の理解と管理を容易にすることができます。以下は、そのアプローチを示す簡略化したサンプルです。

// EventProcessor は、イベント処理の主要なインターフェースを定義します。
type EventProcessor interface {
    ProcessEvent(event Event) error
}

// SimpleEventProcessor は、EventProcessor の基本的な実装です。
type SimpleEventProcessor struct {
    logger     Logger
    repository Repository
    publisher  EventPublisher
}

// NewSimpleEventProcessor は、SimpleEventProcessor の新しいインスタンスを作成します。
func NewSimpleEventProcessor(logger Logger, repository Repository, publisher EventPublisher) *SimpleEventProcessor {
    return &SimpleEventProcessor{
        logger:     logger,
        repository: repository,
        publisher:  publisher,
    }
}

// ProcessEvent は、単一のイベントを処理します。
func (p *SimpleEventProcessor) ProcessEvent(event Event) error {
    p.logger.Info("Processing event", "id", event.ID, "type", event.Type)

    if err := event.Validate(); err != nil {
        return fmt.Errorf("invalid event: %w", err)
    }

    result, err := p.repository.Store(event)
    if err != nil {
        return fmt.Errorf("failed to store event: %w", err)
    }

    if err := p.publisher.Publish(result); err != nil {
        p.logger.Error("Failed to publish result", "error", err)
    }

    p.logger.Info("Event processed successfully", "id", event.ID)
    return nil
}

このアプローチにより、システムの複雑性が大幅に低減され、各コンポーネントの役割が明確になり、開発者の認知負荷が軽減されます。結果として、(組織や人によっては)コードの理解、保守、拡張が容易になり、長期的なシステムの健全性が向上します。

複雑性のパラドックス

興味深いことに、ソフトウェアの複雑性には一種のパラドックスのような構造が存在します。それはシステムを単純化しようとする試みが、かえって複雑性を増大させることがあるのです。

例えば:

  1. 抽象化の過剰:過度に抽象化されたシステムは、具体的な実装の理解を困難にする。
  2. 過度な一般化:あらゆるケースに対応しようとすることで、システムが不必要に複雑になる。
  3. 新技術の導入:複雑性を減らすために導入された新技術が、学習コストや統合の複雑さを増す。

以下は、過度な抽象化の例です:

type AbstractFactory interface {
    CreateProduct() Product
    ConfigureProduct(Product) error
    ValidateProduct(Product) bool
}

type ConcreteFactory struct {
    config     Config
    validator  Validator
    decorator  Decorator
}

func (f *ConcreteFactory) CreateProduct() Product {
    // Complex creation logic
    return nil
}

func (f *ConcreteFactory) ConfigureProduct(p Product) error {
    // Complex configuration logic
    return nil
}

func (f *ConcreteFactory) ValidateProduct(p Product) bool {
    // Complex validation logic
    return true
}

// Usage
func UseFactory(factory AbstractFactory) {
    product := factory.CreateProduct()
    err := factory.ConfigureProduct(product)
    if err != nil {
        // Error handling
    }
    if !factory.ValidateProduct(product) {
        // Validation failed
    }
    // Use the product
}

このコードは柔軟性を目指していますが、実際の使用時には理解と実装が困難になる可能性があります。

このような複雑性のパラドックスに対処するには、適度な抽象化と具体的な実装のバランスを取ることが重要です。以下は、シンプルさと柔軟性のバランスを取った改善例です。

type Product struct {
    // Product fields
}

type ProductFactory struct {
    config Config
}

func NewProductFactory(config Config) *ProductFactory {
    return &ProductFactory{config: config}
}

func (f *ProductFactory) CreateProduct() (*Product, error) {
    product := &Product{}
    if err := f.configureProduct(product); err != nil {
        return nil, fmt.Errorf("failed to configure product: %w", err)
    }
    if !f.validateProduct(product) {
        return nil, errors.New("product validation failed")
    }
    return product, nil
}

func (f *ProductFactory) configureProduct(p *Product) error {
    // Configuration logic
    return nil
}

func (f *ProductFactory) validateProduct(p *Product) bool {
    // Validation logic
    return true
}

// Usage
func UseFactory(factory *ProductFactory) {
    product, err := factory.CreateProduct()
    if err != nil {
        // Error handling
        return
    }
    // Use the product
}

この改善したコードは、単一責任の原則に基づいた ProductFactory の特化、一箇所でのエラーハンドリング、具体的な型の使用による理解のしやすさ、そして内部メソッドの非公開化によるカプセル化を特徴とし、これらの要素が複合的に作用することで、コードの複雑性を軽減しつつ必要な機能性を維持しています。

このアプローチにより、コードの複雑性を減らしつつ、必要な柔軟性を維持することができます。適度な抽象化と具体的な実装のバランスを取ることで、(組織や人によっては)開発者の理解を促進し、長期的なメンテナンス性を向上させることができます。

おわりに

ソフトウェアの複雑性は、諸刃の剣のようなものだと気づきました。それは私たちの能力を押し上げる原動力になる一方で、管理を怠れば混沌を招く危険性も秘めています。完全に複雑性を排除することは不可能かもしれません。しかし、それと向き合い、うまく付き合っていく術を見つけることは可能だと信じています。

病気になってから健康に気を使い始めるのが辛いように、限界まで複雑化したソフトウェアをリファクタリングしていく作業も非常に困難です。そのため、早い段階から複雑性を管理する習慣を身につけることが重要です。ただし、この過程で過度に最適化やリファクタリング固執すると、本来の目的を見失い、それ自体が目的化してしまう危険性があります。これは趣味が手段から目的にすり替わる現象に似ており、行き過ぎた最適化はまた別の問題を引き起こす可能性があります。

したがって、ビジネス側の要求や理想を実現するために、様々な手法やアプローチを積極的に検証していく姿勢も必要です。技術的な観点だけでなく、ビジネスゴールを常に意識し、両者のバランスを取りながら最適な解決策を模索することが、持続可能なソフトウェア開発につながります。過度な最適化や複雑性の管理に陥ることなく、ビジネス価値の創出と技術的な健全性のバランスを保つことが重要です。

日々の開発の中で、継続的な管理プロセスの重要性を実感しています。適切なトレードオフを見極め、チーム内での知識共有や学習を大切にすること。これらは複雑性と付き合っていく上で欠かせない要素です。さらに、ビジネス部門との緊密なコミュニケーションを通じて、技術的な制約や可能性について相互理解を深めることも重要です。

ツールやプラクティスは確かに助けになりますが、それらだけでは根本的な解決にはなりません。結局のところ、私たち人間の認知能力と技術の限界との絶え間ない闘いが続くのです。この挑戦に立ち向かい、バランスを取りながら進化し続けること。そして、ビジネスとテクノロジーの両面から問題にアプローチする柔軟性を持つことが、ソフトウェア開発者としての真の成長につながるのではないでしょうか。知らんけど…

近年の大規模言語モデル(LLM)の急速な発展により、ソフトウェアの複雑性管理に新たな要素がもたらされつつあり、LLMが人間の認知能力を超える可能性が現実味を帯びてきている中、これはソフトウェア開発者にとってチャンスと挑戦の両面を意味します。例えばLLMが複雑なコードベースを瞬時に解析して最適化の提案を行ったり、人間には把握しきれない複雑な相互作用を予測して潜在的な問題を事前に指摘したりする可能性があります。

github.com

一方で、LLMの判断をどのように検証し、人間の意図や倫理的考慮をどのように組み込んでいくか、またLLMと人間の協働をどのように設計し、それぞれの強みを最大限に活かすかといった新たな課題に対する明確な解答や確立された手法はまだ見つかっていません。このような状況下で、エンジニアとして、LLMの進化とその影響について継続的かつ慎重に情報収集を行い、批判的に分析する姿勢が不可欠です。単に新技術を受け入れるのではなく、その長所と短所を十分に理解し、既存のソフトウェア開発プラクティスとの整合性を慎重に評価する必要があります

speakerdeck.com

今後はLLMの能力を活用しつつ、人間ならではの創造性や突っ込めないコンテキスト、直感、倫理的判断を組み合わせた新しいソフトウェア開発のアプローチを模索し、技術の進歩に適応しながらも人間中心の開発哲学を失わないバランスを取ることが求められるのではないでしょうか?

運用者目線でどうなのか?みたいなことを喋る機会があるので喋っていきたい。

event2024.cloudopsdays.com